Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7731_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Update USA location lookup to use both location names and abbreviations [[7731](https://github.com/plotly/plotly.js/pull/7731)]
182 changes: 97 additions & 85 deletions src/lib/geo_location_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,64 +11,69 @@ var loggers = require('./loggers');
var isPlainObject = require('./is_plain_object');
var nestedProperty = require('./nested_property');
var polygon = require('./polygon');
const { usaLocationAbbreviations, usaLocationList } = require('./usa_location_names');

// make list of all country iso3 ids from at runtime
var countryIds = Object.keys(countryRegex);

var locationmodeToIdFinder = {
'ISO-3': identity,
'USA-states': identity,
'USA-states': usaLocationToAbbreviation,
'country names': countryNameToISO3
};

function countryNameToISO3(countryName) {
for(var i = 0; i < countryIds.length; i++) {
for (var i = 0; i < countryIds.length; i++) {
var iso3 = countryIds[i];
var regex = new RegExp(countryRegex[iso3]);

if(regex.test(countryName.trim().toLowerCase())) return iso3;
if (regex.test(countryName.trim().toLowerCase())) return iso3;
}

loggers.log('Unrecognized country name: ' + countryName + '.');

return false;
}

function locationToFeature(locationmode, location, features) {
if(!location || typeof location !== 'string') return false;
function usaLocationToAbbreviation(loc) {
loc = loc.trim();
const abbreviation = usaLocationAbbreviations.has(loc.toUpperCase())
? loc.toUpperCase()
: usaLocationList[loc.toLowerCase()];

if (abbreviation) return abbreviation;

loggers.log('Unrecognized US location: ' + loc + '.');

return false;
}

var locationId = locationmodeToIdFinder[locationmode](location);
var filteredFeatures;
var f, i;
function locationToFeature(locationmode, location, features) {
if (!location || typeof location !== 'string') return false;

if(locationId) {
if(locationmode === 'USA-states') {
const locationId = locationmodeToIdFinder[locationmode](location);
if (locationId) {
let filteredFeatures;
if (locationmode === 'USA-states') {
// Filter out features out in USA
//
// This is important as the Natural Earth files
// include state/provinces from USA, Canada, Australia and Brazil
// which have some overlay in their two-letter ids. For example,
// 'WA' is used for both Washington state and Western Australia.
filteredFeatures = [];
for(i = 0; i < features.length; i++) {
f = features[i];
if(f.properties && f.properties.gu && f.properties.gu === 'USA') {
filteredFeatures.push(f);
}
for (const f of features) {
if (f?.properties?.gu === 'USA') filteredFeatures.push(f);
}
} else {
filteredFeatures = features;
}

for(i = 0; i < filteredFeatures.length; i++) {
f = filteredFeatures[i];
if(f.id === locationId) return f;
for (const f of filteredFeatures) {
if (f.id === locationId) return f;
}

loggers.log([
'Location with id', locationId,
'does not have a matching topojson feature at this resolution.'
].join(' '));
loggers.log(`Location with id ${locationId} does not have a matching topojson feature at this resolution.`);
}

return false;
Expand All @@ -83,46 +88,43 @@ function feature2polygons(feature) {
var appendPolygon, j, k, m;

function doesCrossAntiMerdian(pts) {
for(var l = 0; l < pts.length - 1; l++) {
if(pts[l][0] > 0 && pts[l + 1][0] < 0) return l;
for (var l = 0; l < pts.length - 1; l++) {
if (pts[l][0] > 0 && pts[l + 1][0] < 0) return l;
}
return null;
}

if(loc === 'RUS' || loc === 'FJI') {
if (loc === 'RUS' || loc === 'FJI') {
// Russia and Fiji have landmasses that cross the antimeridian,
// we need to add +360 to their longitude coordinates, so that
// polygon 'contains' doesn't get confused when crossing the antimeridian.
//
// Note that other countries have polygons on either side of the antimeridian
// (e.g. some Aleutian island for the USA), but those don't confuse
// the 'contains' method; these are skipped here.
appendPolygon = function(_pts) {
appendPolygon = function (_pts) {
var pts;

if(doesCrossAntiMerdian(_pts) === null) {
if (doesCrossAntiMerdian(_pts) === null) {
pts = _pts;
} else {
pts = new Array(_pts.length);
for(m = 0; m < _pts.length; m++) {
for (m = 0; m < _pts.length; m++) {
// do not mutate calcdata[i][j].geojson !!
pts[m] = [
_pts[m][0] < 0 ? _pts[m][0] + 360 : _pts[m][0],
_pts[m][1]
];
pts[m] = [_pts[m][0] < 0 ? _pts[m][0] + 360 : _pts[m][0], _pts[m][1]];
}
}

polygons.push(polygon.tester(pts));
};
} else if(loc === 'ATA') {
} else if (loc === 'ATA') {
// Antarctica has a landmass that wraps around every longitudes which
// confuses the 'contains' methods.
appendPolygon = function(pts) {
appendPolygon = function (pts) {
var crossAntiMeridianIndex = doesCrossAntiMerdian(pts);

// polygon that do not cross anti-meridian need no special handling
if(crossAntiMeridianIndex === null) {
if (crossAntiMeridianIndex === null) {
return polygons.push(polygon.tester(pts));
}

Expand All @@ -135,10 +137,10 @@ function feature2polygons(feature) {
var stitch = new Array(pts.length + 1);
var si = 0;

for(m = 0; m < pts.length; m++) {
if(m > crossAntiMeridianIndex) {
for (m = 0; m < pts.length; m++) {
if (m > crossAntiMeridianIndex) {
stitch[si++] = [pts[m][0] + 360, pts[m][1]];
} else if(m === crossAntiMeridianIndex) {
} else if (m === crossAntiMeridianIndex) {
stitch[si++] = pts[m];
stitch[si++] = [pts[m][0], -90];
} else {
Expand All @@ -155,21 +157,21 @@ function feature2polygons(feature) {
};
} else {
// otherwise using same array ref is fine
appendPolygon = function(pts) {
appendPolygon = function (pts) {
polygons.push(polygon.tester(pts));
};
}

switch(geometry.type) {
switch (geometry.type) {
case 'MultiPolygon':
for(j = 0; j < coords.length; j++) {
for(k = 0; k < coords[j].length; k++) {
for (j = 0; j < coords.length; j++) {
for (k = 0; k < coords[j].length; k++) {
appendPolygon(coords[j][k]);
}
}
break;
case 'Polygon':
for(j = 0; j < coords.length; j++) {
for (j = 0; j < coords.length; j++) {
appendPolygon(coords[j]);
}
break;
Expand All @@ -185,7 +187,7 @@ function getTraceGeojson(trace) {

// This should not happen, but just in case something goes
// really wrong when fetching the GeoJSON
if(!isPlainObject(geojsonIn)) {
if (!isPlainObject(geojsonIn)) {
loggers.error('Oops ... something went wrong when fetching ' + g);
return false;
}
Expand All @@ -197,15 +199,15 @@ function extractTraceFeature(calcTrace) {
var trace = calcTrace[0].trace;

var geojsonIn = getTraceGeojson(trace);
if(!geojsonIn) return false;
if (!geojsonIn) return false;

var lookup = {};
var featuresOut = [];
var i;

for(i = 0; i < trace._length; i++) {
for (i = 0; i < trace._length; i++) {
var cdi = calcTrace[i];
if(cdi.loc || cdi.loc === 0) {
if (cdi.loc || cdi.loc === 0) {
lookup[cdi.loc] = cdi;
}
}
Expand All @@ -214,10 +216,10 @@ function extractTraceFeature(calcTrace) {
var id = nestedProperty(fIn, trace.featureidkey || 'id').get();
var cdi = lookup[id];

if(cdi) {
if (cdi) {
var geometry = fIn.geometry;

if(geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
var fOut = {
type: 'Feature',
id: id,
Expand All @@ -238,11 +240,15 @@ function extractTraceFeature(calcTrace) {

featuresOut.push(fOut);
} else {
loggers.log([
'Location', cdi.loc, 'does not have a valid GeoJSON geometry.',
'Traces with locationmode *geojson-id* only support',
'*Polygon* and *MultiPolygon* geometries.'
].join(' '));
loggers.log(
[
'Location',
cdi.loc,
'does not have a valid GeoJSON geometry.',
'Traces with locationmode *geojson-id* only support',
'*Polygon* and *MultiPolygon* geometries.'
].join(' ')
);
}
}

Expand All @@ -251,31 +257,36 @@ function extractTraceFeature(calcTrace) {
delete lookup[id];
}

switch(geojsonIn.type) {
switch (geojsonIn.type) {
case 'FeatureCollection':
var featuresIn = geojsonIn.features;
for(i = 0; i < featuresIn.length; i++) {
for (i = 0; i < featuresIn.length; i++) {
appendFeature(featuresIn[i]);
}
break;
case 'Feature':
appendFeature(geojsonIn);
break;
default:
loggers.warn([
'Invalid GeoJSON type', (geojsonIn.type || 'none') + '.',
'Traces with locationmode *geojson-id* only support',
'*FeatureCollection* and *Feature* types.'
].join(' '));
loggers.warn(
[
'Invalid GeoJSON type',
(geojsonIn.type || 'none') + '.',
'Traces with locationmode *geojson-id* only support',
'*FeatureCollection* and *Feature* types.'
].join(' ')
);
return false;
}

for(var loc in lookup) {
loggers.log([
'Location *' + loc + '*',
'does not have a matching feature with id-key',
'*' + trace.featureidkey + '*.'
].join(' '));
for (var loc in lookup) {
loggers.log(
[
'Location *' + loc + '*',
'does not have a matching feature with id-key',
'*' + trace.featureidkey + '*.'
].join(' ')
);
}

return featuresOut;
Expand All @@ -289,14 +300,14 @@ function findCentroid(feature) {
var geometry = feature.geometry;
var poly;

if(geometry.type === 'MultiPolygon') {
if (geometry.type === 'MultiPolygon') {
var coords = geometry.coordinates;
var maxArea = 0;

for(var i = 0; i < coords.length; i++) {
var polyi = {type: 'Polygon', coordinates: coords[i]};
for (var i = 0; i < coords.length; i++) {
var polyi = { type: 'Polygon', coordinates: coords[i] };
var area = turfArea(polyi);
if(area > maxArea) {
if (area > maxArea) {
maxArea = area;
poly = polyi;
}
Expand All @@ -313,13 +324,14 @@ function fetchTraceGeoData(calcData) {
var promises = [];

function fetch(url) {
return new Promise(function(resolve, reject) {
d3.json(url, function(err, d) {
if(err) {
return new Promise(function (resolve, reject) {
d3.json(url, function (err, d) {
if (err) {
delete PlotlyGeoAssets[url];
var msg = err.status === 404 ?
('GeoJSON at URL "' + url + '" does not exist.') :
('Unexpected error while fetching from ' + url);
var msg =
err.status === 404
? 'GeoJSON at URL "' + url + '" does not exist.'
: 'Unexpected error while fetching from ' + url;
return reject(new Error(msg));
}

Expand All @@ -330,14 +342,14 @@ function fetchTraceGeoData(calcData) {
}

function wait(url) {
return new Promise(function(resolve, reject) {
return new Promise(function (resolve, reject) {
var cnt = 0;
var interval = setInterval(function() {
if(PlotlyGeoAssets[url] && PlotlyGeoAssets[url] !== 'pending') {
var interval = setInterval(function () {
if (PlotlyGeoAssets[url] && PlotlyGeoAssets[url] !== 'pending') {
clearInterval(interval);
return resolve(PlotlyGeoAssets[url]);
}
if(cnt > 100) {
if (cnt > 100) {
clearInterval(interval);
return reject('Unexpected error while fetching from ' + url);
}
Expand All @@ -346,15 +358,15 @@ function fetchTraceGeoData(calcData) {
});
}

for(var i = 0; i < calcData.length; i++) {
for (var i = 0; i < calcData.length; i++) {
var trace = calcData[i][0].trace;
var url = trace.geojson;

if(typeof url === 'string') {
if(!PlotlyGeoAssets[url]) {
if (typeof url === 'string') {
if (!PlotlyGeoAssets[url]) {
PlotlyGeoAssets[url] = 'pending';
promises.push(fetch(url));
} else if(PlotlyGeoAssets[url] === 'pending') {
} else if (PlotlyGeoAssets[url] === 'pending') {
promises.push(wait(url));
}
}
Expand Down
Loading
Loading