Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
quincylvania committed Oct 22, 2019
0 parents commit 1751931
Show file tree
Hide file tree
Showing 13 changed files with 803 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .gitignore
@@ -0,0 +1,10 @@
.DS_Store
.esm-cache
.vscode
.coverage

node_modules/
built/

npm-debug.log
package-lock.json
15 changes: 15 additions & 0 deletions LICENSE.md
@@ -0,0 +1,15 @@
## ISC License

Copyright (c) iD Contributors

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
3 changes: 3 additions & 0 deletions babel-jest-wrapper.js
@@ -0,0 +1,3 @@
module.exports = require('babel-jest').createTransformer({
rootMode: 'upward'
});
3 changes: 3 additions & 0 deletions babel.config.js
@@ -0,0 +1,3 @@
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
};
10 changes: 10 additions & 0 deletions jest.config.js
@@ -0,0 +1,10 @@
module.exports = {
collectCoverage: true,
collectCoverageFrom: ['**/built/cjs/*.js', '!**/node_modules/**'],
coverageDirectory: '<rootDir>/.coverage',
transform: {
'^.+\\.jsx?$': '<rootDir>/babel-jest-wrapper.js'
},
setupFilesAfterEnv: ['jest-extended'],
verbose: true
};
46 changes: 46 additions & 0 deletions package.json
@@ -0,0 +1,46 @@
{
"name": "country-coder",
"version": "1.0.0-pre.0",
"description": "Convert longitude-latitude pairs to ISO 3166-1 codes quickly and locally",
"repository": "ideditor/country-coder",
"main": "built/cjs/country-coder.js",
"module": "built/es6/country-coder.js",
"license": "ISC",
"contributors": [
"Quincy Morgan <@quincylvania>"
],
"keywords": [
"reverse geocoder",
"country codes",
"ISO 3166-1",
"geolocation"
],
"devDependencies": {
"@babel/core": "^7.5.4",
"@babel/preset-env": "^7.5.4",
"@types/node": "^12.6.0",
"babel-jest": "^24.8.0",
"jest": "^24.8.0",
"jest-extended": "^0.11.2",
"npm-run-all": "^4.0.0",
"prettier": "^1.18.2",
"shx": "^0.3.0",
"typescript": "^3.5.2"
},
"dependencies": {
"which-polygon": "^2.2.0"
},
"scripts": {
"all": "npm-run-all -s clean build pretty",
"clean": "shx rm -rf ./built/*",
"build": "npm-run-all -s build:**",
"build:es6": "tsc --project ./tsconfig.es6.json",
"build:cjs": "tsc --project ./tsconfig.cjs.json",
"test": "jest --config jest.config.js tests/*.js",
"pretty": "prettier --write \"./**/*.ts\""
},
"engines": {
"node": ">=8.15.0",
"npm": ">=5.0.0"
}
}
6 changes: 6 additions & 0 deletions prettier.config.js
@@ -0,0 +1,6 @@
module.exports = {
arrowParens: 'always',
printWidth: 100,
singleQuote: true,
trailingComma: 'none'
};
151 changes: 151 additions & 0 deletions src/country-coder.ts
@@ -0,0 +1,151 @@
import whichPolygon from 'which-polygon';
import * as geojson from './data/borders.json';

type FeatureProperties = {
// ISO 3166-1 alpha-2 code
iso1A2: string;
// ISO 3166-1 alpha-3 code
iso1A3: string | undefined;
// ISO 3166-1 numeric-3 code
iso1N3: string | undefined;
// Wikidata QID
wikidata: string | undefined;
// for features entirely within a country, the ISO 3166-1 alpha-2 code for that country
country: string | undefined;
// the ISO 3166-1 alpha-2 codes of other features this feature is entirely within, other than its country
groups: Array<string> | undefined;
// additional differentiator for some features which aren't countries
// - `intGroup`: an international organization
type: string | undefined;
// the status of this feature's ISO 3166-1 code(s) if they are not officially-assigned
// - `excRes`: exceptionally-reserved
// - `usrAssn`: user-assigned
isoStatus: string | undefined;
};
type Feature = { type: string; geometry: any; properties: FeatureProperties };
type FeatureCollection = { type: string; features: Array<Feature> };
type Vec2 = [number, number]; // [lon, lat]

export default class CountryCoder {
private featureQuery: any = {};
private featuresByCode: any = {};

// Constructs a new CountryCoder
constructor() {
let borders: FeatureCollection = (<any>geojson).default;
let geometryFeatures: Array<Feature> = [];
for (let i in borders.features) {
let feature = borders.features[i];
this.featuresByCode[feature.properties.iso1A2] = feature;
if (feature.geometry) {
geometryFeatures.push(feature);
}
}
// whichPolygon doesn't support null geometry even though GeoJSON does
let geometryOnlyCollection: FeatureCollection = {
type: 'FeatureCollection',
features: geometryFeatures
};
this.featureQuery = whichPolygon(geometryOnlyCollection);
}

// Returns the smallest feature of any code status containing `loc`, if any
smallestFeature(loc: Vec2): Feature | null {
let featureProperties: FeatureProperties = this.featureQuery(loc);
if (!featureProperties) return null;
return this.featuresByCode[featureProperties.iso1A2];
}

// Returns all the features containing `loc`, if any
features(loc: Vec2): Array<Feature> {
let feature = this.smallestFeature(loc);
if (!feature) return [];

let features: Array<Feature> = [];
let featuresByCode = this.featuresByCode;
function addAndCheckCode(code) {
let codeFeature = featuresByCode[code];
if (features.indexOf(codeFeature) !== -1) return;
features.push(codeFeature);
let properties = codeFeature.properties;
if (properties.groups) {
for (let i in properties.groups) {
addAndCheckCode(properties.groups[i]);
}
}
if (properties.country) {
addAndCheckCode(properties.country);
}
}
addAndCheckCode(feature.properties.iso1A2);
return features;
}

// Returns the ISO 3166-1 alpha-2 code for the country containing `loc`, if any
// e.g. a location in Puerto Rico will return US
countryIso1A2Code(loc: Vec2): string | null {
let feature = this.smallestFeature(loc);
if (!feature) return null;
// `country` can be explicit;
// a feature without `country` but with geometry is itself a country
return feature.properties.country || feature.properties.iso1A2;
}

// Returns the country feature containing `loc`, if any
countryFeature(loc: Vec2): Feature | null {
let countryCode = this.countryIso1A2Code(loc);
if (!countryCode) return null;
return this.featuresByCode[countryCode];
}

// Returns the ISO 3166-1 alpha-3 code for the country containing `loc`, if any
// e.g. a location in Puerto Rico will return USA
countryIso1A3Code(loc: Vec2): string | null {
let feature = this.countryFeature(loc);
if (!feature) return null;
return feature.properties.iso1A3 || null;
}

// Returns the ISO 3166-1 numeric-3 code for the country containing `loc`, if any
countryIso1N3Code(loc: Vec2): string | null {
let feature = this.countryFeature(loc);
if (!feature) return null;
return feature.properties.iso1N3 || null;
}

// Returns the Wikidata QID code for the country containing `loc`, if any
countryWikidataQID(loc: Vec2): string | null {
let feature = this.countryFeature(loc);
if (!feature) return null;
return feature.properties.wikidata || null;
}

// Returns the smallest feature containing `loc` to have an officially-assigned code, if any
smallestOfficialIsoFeature(loc: Vec2): Feature | null {
return (
this.features(loc).find(function(feature) {
return !feature.properties.isoStatus; // features without an explicit status are officially-assigned
}) || null
);
}

// Returns the ISO 3166-1 alpha-2 code of the smallest feature containing `loc` to have an officially-assigned code, if any
// e.g. a location in Puerto Rico will return PR
officialIso1A2Code(loc: Vec2): string | null {
let feature = this.smallestOfficialIsoFeature(loc);
if (!feature) return null;
return feature.properties.iso1A2;
}

// Returns the ISO 3166-1 alpha-2 codes for all features containing `loc`, if any
iso1A2Codes(loc: Vec2): Array<string> {
return this.features(loc).map(function(feature) {
return feature.properties.iso1A2;
});
}

// Returns true if `loc` is in an EU member state
isInEuropeanUnion(loc: Vec2): boolean {
return this.iso1A2Codes(loc).indexOf('EU') !== -1;
}
}

0 comments on commit 1751931

Please sign in to comment.