@rapideditor/location-conflation
Version:
Define complex geographic regions by including and excluding country codes and geojson shapes
309 lines (308 loc) • 11.6 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var index_exports = {};
__export(index_exports, {
LocationConflation: () => LocationConflation,
default: () => index_default
});
module.exports = __toCommonJS(index_exports);
var CountryCoder = __toESM(require("@rapideditor/country-coder"), 1);
var Polyclip = __toESM(require("polyclip-ts"), 1);
var import_geojson_area = __toESM(require("@mapbox/geojson-area"), 1);
var import_circle_to_polygon = __toESM(require("circle-to-polygon"), 1);
var import_geojson_precision = __toESM(require("geojson-precision"), 1);
var import_json_stringify_pretty_compact = __toESM(require("@aitodotai/json-stringify-pretty-compact"), 1);
class LocationConflation {
// constructor
//
// `fc` Optional FeatureCollection of known features
//
// Optionally pass a GeoJSON FeatureCollection of known features which we can refer to later.
// Each feature must have a filename-like `id`, for example: `something.geojson`
//
// {
// "type": "FeatureCollection"
// "features": [
// {
// "type": "Feature",
// "id": "philly_metro.geojson",
// "properties": { … },
// "geometry": { … }
// }
// ]
// }
constructor(fc) {
this._cache = {};
this.strict = true;
if (fc && fc.type === "FeatureCollection" && Array.isArray(fc.features)) {
fc.features.forEach((feature) => {
feature.properties = feature.properties || {};
let props = feature.properties;
let id = feature.id || props.id;
if (!id || !/^\S+\.geojson$/i.test(id)) return;
id = id.toLowerCase();
feature.id = id;
props.id = id;
if (!props.area) {
const area = import_geojson_area.default.geometry(feature.geometry) / 1e6;
props.area = Number(area.toFixed(2));
}
this._cache[id] = feature;
});
}
let world = _cloneDeep(CountryCoder.feature("Q2"));
world.geometry = {
type: "Polygon",
coordinates: [[[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]]
};
world.id = "Q2";
world.properties.id = "Q2";
world.properties.area = import_geojson_area.default.geometry(world.geometry) / 1e6;
this._cache.Q2 = world;
}
// validateLocation
// `location` The location to validate
//
// Pass a `location` value to validate
//
// Returns a result like:
// {
// type: 'point', 'geojson', or 'countrycoder'
// location: the queried location
// id: the stable identifier for the feature
// }
// or `null` if the location is invalid
//
validateLocation(location) {
if (Array.isArray(location) && (location.length === 2 || location.length === 3)) {
const lon = location[0];
const lat = location[1];
const radius = location[2];
if (Number.isFinite(lon) && lon >= -180 && lon <= 180 && Number.isFinite(lat) && lat >= -90 && lat <= 90 && (location.length === 2 || Number.isFinite(radius) && radius > 0)) {
const id = "[" + location.toString() + "]";
return { type: "point", location, id };
}
} else if (typeof location === "string" && /^\S+\.geojson$/i.test(location)) {
const id = location.toLowerCase();
if (this._cache[id]) {
return { type: "geojson", location, id };
}
} else if (typeof location === "string" || typeof location === "number") {
const feature = CountryCoder.feature(location);
if (feature) {
const id = feature.properties.wikidata;
return { type: "countrycoder", location, id };
}
}
if (this.strict) {
throw new Error(`validateLocation: Invalid location: "${location}".`);
} else {
return null;
}
}
// resolveLocation
// `location` The location to resolve
//
// Pass a `location` value to resolve
//
// Returns a result like:
// {
// type: 'point', 'geojson', or 'countrycoder'
// location: the queried location
// id: a stable identifier for the feature
// feature: the resolved GeoJSON feature
// }
// or `null` if the location is invalid
//
resolveLocation(location) {
const valid = this.validateLocation(location);
if (!valid) return null;
const id = valid.id;
if (this._cache[id]) {
return Object.assign(valid, { feature: this._cache[id] });
}
if (valid.type === "point") {
const lon = location[0];
const lat = location[1];
const radius = location[2] || 25;
const EDGES = 10;
const PRECISION = 3;
const area = Math.PI * radius * radius;
const feature = this._cache[id] = (0, import_geojson_precision.default)({
type: "Feature",
id,
properties: { id, area: Number(area.toFixed(2)) },
geometry: (0, import_circle_to_polygon.default)([lon, lat], radius * 1e3, EDGES)
// km to m
}, PRECISION);
return Object.assign(valid, { feature });
} else if (valid.type === "geojson") {
} else if (valid.type === "countrycoder") {
let feature = _cloneDeep(CountryCoder.feature(id));
let props = feature.properties;
if (Array.isArray(props.members)) {
let aggregate = CountryCoder.aggregateFeature(id);
aggregate.geometry.coordinates = _clip([aggregate], "UNION").geometry.coordinates;
feature.geometry = aggregate.geometry;
}
if (!props.area) {
const area = import_geojson_area.default.geometry(feature.geometry) / 1e6;
props.area = Number(area.toFixed(2));
}
feature.id = id;
props.id = id;
this._cache[id] = feature;
return Object.assign(valid, { feature });
}
if (this.strict) {
throw new Error(`resolveLocation: Couldn't resolve location "${location}".`);
} else {
return null;
}
}
// validateLocationSet
// `locationSet` the locationSet to validate
//
// Pass a locationSet Object to validate like:
// {
// include: [ Array of locations ],
// exclude: [ Array of locations ]
// }
//
// Returns a result like:
// {
// type: 'locationset'
// locationSet: the queried locationSet
// id: the stable identifier for the feature
// }
// or `null` if the locationSet is invalid
//
validateLocationSet(locationSet) {
locationSet = locationSet || {};
const validator = this.validateLocation.bind(this);
let include = (locationSet.include || []).map(validator).filter(Boolean);
let exclude = (locationSet.exclude || []).map(validator).filter(Boolean);
if (!include.length) {
if (this.strict) {
throw new Error(`validateLocationSet: LocationSet includes nothing.`);
} else {
locationSet.include = ["Q2"];
include = [{ type: "countrycoder", location: "Q2", id: "Q2" }];
}
}
include.sort(_sortLocations);
let id = "+[" + include.map((d) => d.id).join(",") + "]";
if (exclude.length) {
exclude.sort(_sortLocations);
id += "-[" + exclude.map((d) => d.id).join(",") + "]";
}
return { type: "locationset", locationSet, id };
}
// resolveLocationSet
// `locationSet` the locationSet to resolve
//
// Pass a locationSet Object to validate like:
// {
// include: [ Array of locations ],
// exclude: [ Array of locations ]
// }
//
// Returns a result like:
// {
// type: 'locationset'
// locationSet: the queried locationSet
// id: the stable identifier for the feature
// feature: the resolved GeoJSON feature
// }
// or `null` if the locationSet is invalid
//
resolveLocationSet(locationSet) {
locationSet = locationSet || {};
const valid = this.validateLocationSet(locationSet);
if (!valid) return null;
const id = valid.id;
if (this._cache[id]) {
return Object.assign(valid, { feature: this._cache[id] });
}
const resolver = this.resolveLocation.bind(this);
const includes = (locationSet.include || []).map(resolver).filter(Boolean);
const excludes = (locationSet.exclude || []).map(resolver).filter(Boolean);
if (includes.length === 1 && excludes.length === 0) {
return Object.assign(valid, { feature: includes[0].feature });
}
const includeGeoJSON = _clip(includes.map((d) => d.feature), "UNION");
const excludeGeoJSON = _clip(excludes.map((d) => d.feature), "UNION");
let resultGeoJSON = excludeGeoJSON ? _clip([includeGeoJSON, excludeGeoJSON], "DIFFERENCE") : includeGeoJSON;
const area = import_geojson_area.default.geometry(resultGeoJSON.geometry) / 1e6;
resultGeoJSON.id = id;
resultGeoJSON.properties = { id, area: Number(area.toFixed(2)) };
this._cache[id] = resultGeoJSON;
return Object.assign(valid, { feature: resultGeoJSON });
}
// stringify
// convenience method to prettyStringify the given object
stringify(obj, options) {
return (0, import_json_stringify_pretty_compact.default)(obj, options);
}
}
function _clip(features, which) {
if (!Array.isArray(features) || !features.length) return null;
const fn = { UNION: Polyclip.union, DIFFERENCE: Polyclip.difference }[which];
const args = features.map((feature) => feature.geometry.coordinates);
const coords = fn.apply(null, args);
return {
type: "Feature",
properties: {},
geometry: {
type: whichType(coords),
coordinates: coords
}
};
function whichType(coords2) {
const a = Array.isArray(coords2);
const b = a && Array.isArray(coords2[0]);
const c = b && Array.isArray(coords2[0][0]);
const d = c && Array.isArray(coords2[0][0][0]);
return d ? "MultiPolygon" : "Polygon";
}
}
function _cloneDeep(obj) {
return JSON.parse(JSON.stringify(obj));
}
function _sortLocations(a, b) {
const rank = { countrycoder: 1, geojson: 2, point: 3 };
const aRank = rank[a.type];
const bRank = rank[b.type];
return aRank > bRank ? 1 : aRank < bRank ? -1 : a.id.localeCompare(b.id);
}
var index_default = LocationConflation;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
LocationConflation
});
//# sourceMappingURL=location-conflation.cjs.map