8
8
9
9
'use strict' ;
10
10
11
+ var d3 = require ( 'd3' ) ;
11
12
var countryRegex = require ( 'country-regex' ) ;
12
- var Lib = require ( '../lib' ) ;
13
+ var turfArea = require ( '@turf/area' ) ;
14
+ var turfCentroid = require ( '@turf/centroid' ) ;
15
+
16
+ var identity = require ( './identity' ) ;
17
+ var loggers = require ( './loggers' ) ;
18
+ var isPlainObject = require ( './is_plain_object' ) ;
19
+ var polygon = require ( './polygon' ) ;
13
20
14
21
// make list of all country iso3 ids from at runtime
15
22
var countryIds = Object . keys ( countryRegex ) ;
16
23
17
24
var locationmodeToIdFinder = {
18
- 'ISO-3' : Lib . identity ,
19
- 'USA-states' : Lib . identity ,
25
+ 'ISO-3' : identity ,
26
+ 'USA-states' : identity ,
20
27
'country names' : countryNameToISO3
21
28
} ;
22
29
@@ -28,7 +35,7 @@ function countryNameToISO3(countryName) {
28
35
if ( regex . test ( countryName . trim ( ) . toLowerCase ( ) ) ) return iso3 ;
29
36
}
30
37
31
- Lib . log ( 'Unrecognized country name: ' + countryName + '.' ) ;
38
+ loggers . log ( 'Unrecognized country name: ' + countryName + '.' ) ;
32
39
33
40
return false ;
34
41
}
@@ -64,7 +71,7 @@ function locationToFeature(locationmode, location, features) {
64
71
if ( f . id === locationId ) return f ;
65
72
}
66
73
67
- Lib . log ( [
74
+ loggers . log ( [
68
75
'Location with id' , locationId ,
69
76
'does not have a matching topojson feature at this resolution.'
70
77
] . join ( ' ' ) ) ;
@@ -73,6 +80,256 @@ function locationToFeature(locationmode, location, features) {
73
80
return false ;
74
81
}
75
82
83
+ function feature2polygons ( feature ) {
84
+ var geometry = feature . geometry ;
85
+ var coords = geometry . coordinates ;
86
+ var loc = feature . id ;
87
+
88
+ var polygons = [ ] ;
89
+ var appendPolygon , j , k , m ;
90
+
91
+ function doesCrossAntiMerdian ( pts ) {
92
+ for ( var l = 0 ; l < pts . length - 1 ; l ++ ) {
93
+ if ( pts [ l ] [ 0 ] > 0 && pts [ l + 1 ] [ 0 ] < 0 ) return l ;
94
+ }
95
+ return null ;
96
+ }
97
+
98
+ if ( loc === 'RUS' || loc === 'FJI' ) {
99
+ // Russia and Fiji have landmasses that cross the antimeridian,
100
+ // we need to add +360 to their longitude coordinates, so that
101
+ // polygon 'contains' doesn't get confused when crossing the antimeridian.
102
+ //
103
+ // Note that other countries have polygons on either side of the antimeridian
104
+ // (e.g. some Aleutian island for the USA), but those don't confuse
105
+ // the 'contains' method; these are skipped here.
106
+ appendPolygon = function ( _pts ) {
107
+ var pts ;
108
+
109
+ if ( doesCrossAntiMerdian ( _pts ) === null ) {
110
+ pts = _pts ;
111
+ } else {
112
+ pts = new Array ( _pts . length ) ;
113
+ for ( m = 0 ; m < _pts . length ; m ++ ) {
114
+ // do not mutate calcdata[i][j].geojson !!
115
+ pts [ m ] = [
116
+ _pts [ m ] [ 0 ] < 0 ? _pts [ m ] [ 0 ] + 360 : _pts [ m ] [ 0 ] ,
117
+ _pts [ m ] [ 1 ]
118
+ ] ;
119
+ }
120
+ }
121
+
122
+ polygons . push ( polygon . tester ( pts ) ) ;
123
+ } ;
124
+ } else if ( loc === 'ATA' ) {
125
+ // Antarctica has a landmass that wraps around every longitudes which
126
+ // confuses the 'contains' methods.
127
+ appendPolygon = function ( pts ) {
128
+ var crossAntiMeridianIndex = doesCrossAntiMerdian ( pts ) ;
129
+
130
+ // polygon that do not cross anti-meridian need no special handling
131
+ if ( crossAntiMeridianIndex === null ) {
132
+ return polygons . push ( polygon . tester ( pts ) ) ;
133
+ }
134
+
135
+ // stitch polygon by adding pt over South Pole,
136
+ // so that it covers the projected region covers all latitudes
137
+ //
138
+ // Note that the algorithm below only works for polygons that
139
+ // start and end on longitude -180 (like the ones built by
140
+ // https://github.com/etpinard/sane-topojson).
141
+ var stitch = new Array ( pts . length + 1 ) ;
142
+ var si = 0 ;
143
+
144
+ for ( m = 0 ; m < pts . length ; m ++ ) {
145
+ if ( m > crossAntiMeridianIndex ) {
146
+ stitch [ si ++ ] = [ pts [ m ] [ 0 ] + 360 , pts [ m ] [ 1 ] ] ;
147
+ } else if ( m === crossAntiMeridianIndex ) {
148
+ stitch [ si ++ ] = pts [ m ] ;
149
+ stitch [ si ++ ] = [ pts [ m ] [ 0 ] , - 90 ] ;
150
+ } else {
151
+ stitch [ si ++ ] = pts [ m ] ;
152
+ }
153
+ }
154
+
155
+ // polygon.tester by default appends pt[0] to the points list,
156
+ // we must remove it here, to avoid a jump in longitude from 180 to -180,
157
+ // that would confuse the 'contains' method
158
+ var tester = polygon . tester ( stitch ) ;
159
+ tester . pts . pop ( ) ;
160
+ polygons . push ( tester ) ;
161
+ } ;
162
+ } else {
163
+ // otherwise using same array ref is fine
164
+ appendPolygon = function ( pts ) {
165
+ polygons . push ( polygon . tester ( pts ) ) ;
166
+ } ;
167
+ }
168
+
169
+ switch ( geometry . type ) {
170
+ case 'MultiPolygon' :
171
+ for ( j = 0 ; j < coords . length ; j ++ ) {
172
+ for ( k = 0 ; k < coords [ j ] . length ; k ++ ) {
173
+ appendPolygon ( coords [ j ] [ k ] ) ;
174
+ }
175
+ }
176
+ break ;
177
+ case 'Polygon' :
178
+ for ( j = 0 ; j < coords . length ; j ++ ) {
179
+ appendPolygon ( coords [ j ] ) ;
180
+ }
181
+ break ;
182
+ }
183
+
184
+ return polygons ;
185
+ }
186
+
187
+ function extractTraceFeature ( calcTrace ) {
188
+ var trace = calcTrace [ 0 ] . trace ;
189
+
190
+ var geojsonIn = typeof trace . geojson === 'string' ?
191
+ ( window . PlotlyGeoAssets || { } ) [ trace . geojson ] :
192
+ trace . geojson ;
193
+
194
+ // This should not happen, but just in case something goes
195
+ // really wrong when fetching the GeoJSON
196
+ if ( ! isPlainObject ( geojsonIn ) ) {
197
+ loggers . error ( 'Oops ... something when wrong when fetching ' + trace . geojson ) ;
198
+ return false ;
199
+ }
200
+
201
+ var lookup = { } ;
202
+ var featuresOut = [ ] ;
203
+ var i ;
204
+
205
+ for ( i = 0 ; i < trace . _length ; i ++ ) {
206
+ var cdi = calcTrace [ i ] ;
207
+ if ( cdi . loc ) lookup [ cdi . loc ] = cdi ;
208
+ }
209
+
210
+ function appendFeature ( fIn ) {
211
+ var cdi = lookup [ fIn . id ] ;
212
+
213
+ if ( cdi ) {
214
+ var geometry = fIn . geometry ;
215
+
216
+ if ( geometry . type === 'Polygon' || geometry . type === 'MultiPolygon' ) {
217
+ var fOut = {
218
+ type : 'Feature' ,
219
+ geometry : geometry ,
220
+ properties : { }
221
+ } ;
222
+
223
+ // Compute centroid, add it to the properties
224
+ fOut . properties . ct = findCentroid ( fOut ) ;
225
+
226
+ // Mutate in in/out features into calcdata
227
+ cdi . fIn = fIn ;
228
+ cdi . fOut = fOut ;
229
+
230
+ featuresOut . push ( fOut ) ;
231
+ } else {
232
+ loggers . log ( [
233
+ 'Location with id' , cdi . loc , 'does not have a valid GeoJSON geometry,' ,
234
+ 'choroplethmapbox traces only support *Polygon* and *MultiPolygon* geometries.'
235
+ ] . join ( ' ' ) ) ;
236
+ }
237
+ }
238
+
239
+ // remove key from lookup, so that we can track (if any)
240
+ // the locations that did not have a corresponding GeoJSON feature
241
+ delete lookup [ fIn . id ] ;
242
+ }
243
+
244
+ switch ( geojsonIn . type ) {
245
+ case 'FeatureCollection' :
246
+ var featuresIn = geojsonIn . features ;
247
+ for ( i = 0 ; i < featuresIn . length ; i ++ ) {
248
+ appendFeature ( featuresIn [ i ] ) ;
249
+ }
250
+ break ;
251
+ case 'Feature' :
252
+ appendFeature ( geojsonIn ) ;
253
+ break ;
254
+ default :
255
+ loggers . warn ( [
256
+ 'Invalid GeoJSON type' , ( geojsonIn . type || 'none' ) + ',' ,
257
+ 'choroplethmapbox traces only support *FeatureCollection* and *Feature* types.'
258
+ ] . join ( ' ' ) ) ;
259
+ return false ;
260
+ }
261
+
262
+ for ( var loc in lookup ) {
263
+ loggers . log ( 'Location with id ' + loc + ' does not have a matching feature' ) ;
264
+ }
265
+
266
+ return featuresOut ;
267
+ }
268
+
269
+ // TODO this find the centroid of the polygon of maxArea
270
+ // (just like we currently do for geo choropleth polygons),
271
+ // maybe instead it would make more sense to compute the centroid
272
+ // of each polygon and consider those on hover/select
273
+ function findCentroid ( feature ) {
274
+ var geometry = feature . geometry ;
275
+ var poly ;
276
+
277
+ if ( geometry . type === 'MultiPolygon' ) {
278
+ var coords = geometry . coordinates ;
279
+ var maxArea = 0 ;
280
+
281
+ for ( var i = 0 ; i < coords . length ; i ++ ) {
282
+ var polyi = { type : 'Polygon' , coordinates : coords [ i ] } ;
283
+ var area = turfArea . default ( polyi ) ;
284
+ if ( area > maxArea ) {
285
+ maxArea = area ;
286
+ poly = polyi ;
287
+ }
288
+ }
289
+ } else {
290
+ poly = geometry ;
291
+ }
292
+
293
+ return turfCentroid . default ( poly ) . geometry . coordinates ;
294
+ }
295
+
296
+ function fetchTraceGeoData ( calcData ) {
297
+ var PlotlyGeoAssets = window . PlotlyGeoAssets || { } ;
298
+ var promises = [ ] ;
299
+
300
+ function fetch ( url ) {
301
+ return new Promise ( function ( resolve , reject ) {
302
+ d3 . json ( url , function ( err , d ) {
303
+ if ( err ) {
304
+ delete PlotlyGeoAssets [ url ] ;
305
+ var msg = err . status === 404 ?
306
+ ( 'GeoJSON at URL "' + url + '" does not exist.' ) :
307
+ ( 'Unexpected error while fetching from ' + url ) ;
308
+ return reject ( new Error ( msg ) ) ;
309
+ }
310
+
311
+ PlotlyGeoAssets [ url ] = d ;
312
+ resolve ( d ) ;
313
+ } ) ;
314
+ } ) ;
315
+ }
316
+
317
+ for ( var i = 0 ; i < calcData . length ; i ++ ) {
318
+ var trace = calcData [ i ] [ 0 ] . trace ;
319
+ var url = trace . geojson ;
320
+
321
+ if ( typeof url === 'string' && ! PlotlyGeoAssets [ url ] ) {
322
+ PlotlyGeoAssets [ url ] = 'pending' ;
323
+ promises . push ( fetch ( url ) ) ;
324
+ }
325
+ }
326
+
327
+ return promises ;
328
+ }
329
+
76
330
module . exports = {
77
- locationToFeature : locationToFeature
331
+ locationToFeature : locationToFeature ,
332
+ feature2polygons : feature2polygons ,
333
+ extractTraceFeature : extractTraceFeature ,
334
+ fetchTraceGeoData : fetchTraceGeoData
78
335
} ;
0 commit comments