88
99'use strict' ;
1010
11+ var d3 = require ( 'd3' ) ;
1112var 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' ) ;
1320
1421// make list of all country iso3 ids from at runtime
1522var countryIds = Object . keys ( countryRegex ) ;
1623
1724var locationmodeToIdFinder = {
18- 'ISO-3' : Lib . identity ,
19- 'USA-states' : Lib . identity ,
25+ 'ISO-3' : identity ,
26+ 'USA-states' : identity ,
2027 'country names' : countryNameToISO3
2128} ;
2229
@@ -28,7 +35,7 @@ function countryNameToISO3(countryName) {
2835 if ( regex . test ( countryName . trim ( ) . toLowerCase ( ) ) ) return iso3 ;
2936 }
3037
31- Lib . log ( 'Unrecognized country name: ' + countryName + '.' ) ;
38+ loggers . log ( 'Unrecognized country name: ' + countryName + '.' ) ;
3239
3340 return false ;
3441}
@@ -64,7 +71,7 @@ function locationToFeature(locationmode, location, features) {
6471 if ( f . id === locationId ) return f ;
6572 }
6673
67- Lib . log ( [
74+ loggers . log ( [
6875 'Location with id' , locationId ,
6976 'does not have a matching topojson feature at this resolution.'
7077 ] . join ( ' ' ) ) ;
@@ -73,6 +80,256 @@ function locationToFeature(locationmode, location, features) {
7380 return false ;
7481}
7582
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+
76330module . exports = {
77- locationToFeature : locationToFeature
331+ locationToFeature : locationToFeature ,
332+ feature2polygons : feature2polygons ,
333+ extractTraceFeature : extractTraceFeature ,
334+ fetchTraceGeoData : fetchTraceGeoData
78335} ;
0 commit comments