terriajs
Version:
Geospatial data visualization platform.
323 lines • 14.5 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { action, observable, runInAction, makeObservable } from "mobx";
import DeveloperError from "terriajs-cesium/Source/Core/DeveloperError";
import isDefined from "../../Core/isDefined";
import loadJson from "../../Core/loadJson";
export default class RegionProvider {
corsProxy;
regionType;
regionProp;
nameProp;
description;
layerName;
server;
serverSubdomains;
serverMinZoom;
serverMaxZoom;
serverMaxNativeZoom;
bbox;
aliases;
serverReplacements;
dataReplacements;
disambigProp;
uniqueIdProp;
textCodes;
regionIdsFile;
regionDisambigIdsFile;
disambigDataReplacements;
disambigServerReplacements;
disambigAliases;
_appliedReplacements = {
serverReplacements: {},
disambigServerReplacements: {},
dataReplacements: {},
disambigDataReplacements: {}
};
/**
* Array of attributes of each region, once retrieved from the server.
*/
_regions = [];
get regions() {
return this._regions;
}
/**
* Look-up table of attributes, for speed.
*/
_idIndex = {};
/** Cache the loadRegionID promises so they are not regenerated each time until this._regions is defined. */
_loadRegionIDsPromises = undefined;
/** Flag to indicate if loadRegionID has finished */
_loaded = false;
get loaded() {
return this._loaded;
}
constructor(regionType, properties, corsProxy) {
makeObservable(this);
this.regionType = regionType;
this.corsProxy = corsProxy;
this.regionProp = properties.regionProp;
this.nameProp = properties.nameProp;
this.description = properties.description;
this.layerName = properties.layerName;
this.server = properties.server;
this.serverSubdomains = properties.serverSubdomains;
this.serverMinZoom = properties.serverMinZoom ?? 0;
this.serverMaxZoom = properties.serverMaxZoom ?? Infinity;
this.serverMaxNativeZoom =
properties.serverMaxNativeZoom ?? this.serverMaxZoom;
this.bbox = properties.bbox;
this.aliases = properties.aliases ?? [this.regionType];
this.serverReplacements =
properties.serverReplacements instanceof Array
? properties.serverReplacements.map(function (r) {
return [
r[0],
r[1].toLowerCase(),
new RegExp(r[0].toLowerCase(), "gi")
];
})
: [];
this.dataReplacements =
properties.dataReplacements instanceof Array
? properties.dataReplacements.map(function (r) {
return [
r[0],
r[1].toLowerCase(),
new RegExp(r[0].toLowerCase(), "gi")
];
})
: [];
this.disambigProp = properties.disambigProp;
this.uniqueIdProp = properties.uniqueIdProp ?? "FID";
this.textCodes = properties.textCodes ?? false; // yes, it's singular...
this.regionIdsFile = properties.regionIdsFile;
this.regionDisambigIdsFile = properties.regionDisambigIdsFile;
}
setDisambigProperties(dp) {
this.disambigDataReplacements = dp?.dataReplacements;
this.disambigServerReplacements = dp?.serverReplacements;
this.disambigAliases = dp?.aliases;
}
/**
* Given an entry from the region mapping config, load the IDs that correspond to it, and possibly the disambiguation properties.
*/
async loadRegionIDs() {
try {
if (this._regions.length > 0) {
return; // already loaded, so return insta-promise.
}
if (this.server === undefined) {
// technically this may not be a problem yet, but it will be when we want to actually fetch tiles.
throw new DeveloperError("No server for region mapping defined: " + this.regionType);
}
// Check for a pre-calculated promise (which may not have resolved yet), and returned that if it exists.
if (!isDefined(this._loadRegionIDsPromises)) {
const fetchAndProcess = async (idListFile, disambig) => {
if (!isDefined(idListFile)) {
return;
}
this.processRegionIds((await loadJson(idListFile)).values, disambig);
};
this._loadRegionIDsPromises = [
fetchAndProcess(this.regionIdsFile, false),
fetchAndProcess(this.regionDisambigIdsFile, true)
];
}
await Promise.all(this._loadRegionIDsPromises);
}
catch (_e) {
console.log(`Failed to load region IDS for ${this.regionType}`);
}
finally {
runInAction(() => (this._loaded = true));
}
}
/**
* Returns the region variable of the given name, matching against the aliases provided.
*
* @param {string[]} varNames Array of variable names.
* @returns {string} The name of the first column that matches any of the given aliases.
*/
findRegionVariable(varNames) {
return findVariableForAliases(varNames, [this.regionType, ...this.aliases]);
}
/**
* If a disambiguation column is known for this provider, return a column matching its description.
*
* @param {string[]} varNames Array of variable names.
* @returns {string} The name of the first column that matches any of the given disambiguation aliases.
*/
findDisambigVariable(varNames) {
if (!isDefined(this.disambigAliases) || this.disambigAliases.length === 0) {
return undefined;
}
return findVariableForAliases(varNames, this.disambigAliases);
}
/**
* Given a list of region IDs in feature ID order, apply server replacements if needed, and build the this._regions array.
* If no propertyName is supplied, also builds this._idIndex (a lookup by attribute for performance).
* @param {Array} values An array of string or numeric region IDs, eg. [10050, 10110, 10150, ...] or ['2060', '2061', '2062', ...]
* @param {boolean} disambig True if processing region IDs for disambiguation
*/
processRegionIds(values, disambig) {
// There is also generally a `layer` and `property` property in this file, which we ignore for now.
values.forEach((value, index) => {
if (!isDefined(this._regions[index])) {
this._regions[index] = {};
}
let valueAfterReplacement = value;
if (typeof valueAfterReplacement === "string") {
// we apply server-side replacements while loading. If it ever turns out we need
// to store the un-regexed version, we should add a line here.
valueAfterReplacement = this.applyReplacements(valueAfterReplacement.toLowerCase(), disambig ? "disambigServerReplacements" : "serverReplacements");
}
// If disambig IDS - only set this._regions properties - not this._index properties
if (disambig) {
this._regions[index].disambigProp = value;
this._regions[index].disambigPropWithServerReplacement =
valueAfterReplacement;
}
else {
this._regions[index].regionProp = value;
this._regions[index].regionPropWithServerReplacement =
valueAfterReplacement;
// store a lookup by attribute, for performance.
// This is only used for region prop (not disambig prop)
if (isDefined(value) && isDefined(valueAfterReplacement)) {
// If value is different after replacement, then also add original value for _index
if (value !== valueAfterReplacement) {
this._idIndex[value] = index;
}
if (!isDefined(this._idIndex[valueAfterReplacement])) {
this._idIndex[valueAfterReplacement] = index;
}
else {
// if we have already seen this value before, store an array of values, not one value.
if (Array.isArray(this._idIndex[valueAfterReplacement])) {
this._idIndex[valueAfterReplacement].push(index);
}
else {
this._idIndex[valueAfterReplacement] = [
this._idIndex[valueAfterReplacement],
index
];
}
}
// Here we make a big assumption that every region has a unique identifier (probably called FID), that it counts from zero,
// and that regions are provided in sorted order from FID 0. We do this to avoid having to explicitly request
// the FID column, which would double the amount of traffic per region dataset.
// It is needed to simplify reverse lookups from complex matches (regexes and disambigs)
this._regions[index].fid = index;
}
}
});
}
/**
* Apply an array of regular expression replacements to a string. Also caches the applied replacements in regionProvider._appliedReplacements.
* @param {String} s The string.
* @param {String} replacementsProp Name of a property containing [ [ regex, replacement], ... ], where replacement is a string which can contain '$1' etc.
*/
applyReplacements(s, replacementsProp) {
let r;
if (typeof s === "number") {
r = String(s);
}
else {
r = s.toLowerCase().trim();
}
const replacements = this[replacementsProp];
if (replacements === undefined || replacements.length === 0) {
return r;
}
if (this._appliedReplacements[replacementsProp][r] !== undefined) {
return this._appliedReplacements[replacementsProp][r];
}
replacements.forEach(function (rep) {
r = r.replace(rep[2], rep[1]);
});
this._appliedReplacements[replacementsProp][s] = r;
return r;
}
/**
* Given a region code, try to find a region that matches it, using replacements, disambiguation, indexes and other wizardry.
* @param {string | number} code Code to search for. Falsy codes return -1.
* @param {string | number | undefined} disambigCode Code to use if disambiguation is necessary
* @returns {Number} Zero-based index in list of regions if successful, or -1.
*/
findRegionIndex(code, disambigCode) {
if (!isDefined(code) || code === "") {
// Note a code of 0 is ok
return -1;
}
const codeAfterReplacement = this.applyReplacements(code, "dataReplacements");
const id = this._idIndex[code];
const idAfterReplacement = this._idIndex[codeAfterReplacement];
if (!isDefined(id) && !isDefined(idAfterReplacement)) {
return -1;
}
if (typeof id === "number") {
// found an unambiguous match (without replacement)
return id;
}
else if (typeof idAfterReplacement === "number") {
// found an unambiguous match (with replacement)
return idAfterReplacement;
}
else {
const ids = id ?? idAfterReplacement; // found an ambiguous match
if (!isDefined(disambigCode)) {
// we have an ambiguous value, but nothing with which to disambiguate. We pick the first, warn.
console.warn("Ambiguous value found in region mapping: " +
(codeAfterReplacement || code));
return ids[0];
}
if (this.disambigProp) {
const processedDisambigCode = this.applyReplacements(disambigCode, "disambigDataReplacements");
// Check out each of the matching IDs to see if the disambiguation field matches the one we have.
for (let i = 0; i < ids.length; i++) {
if (this._regions[ids[i]].disambigProp === processedDisambigCode ||
this._regions[ids[i]].disambigPropWithServerReplacement ===
processedDisambigCode) {
return ids[i];
}
}
}
}
return -1;
}
}
__decorate([
observable
], RegionProvider.prototype, "_loaded", void 0);
__decorate([
action
], RegionProvider.prototype, "loadRegionIDs", null);
function findVariableForAliases(varNames, aliases) {
// Try first with no transformation (but case-insensitive)
for (let j = 0; j < aliases.length; j++) {
const re = new RegExp("^" + aliases[j] + "$", "i");
for (let i = 0; i < varNames.length; i++) {
if (re.test(varNames[i])) {
return varNames[i];
}
}
}
// Now try without whitespace, hyphens and underscores
for (let j = 0; j < aliases.length; j++) {
const aliasNoWhiteSpace = aliases[j].replace(/[-_\s]/g, "");
const re = new RegExp("^" + aliasNoWhiteSpace + "$", "i");
for (let i = 0; i < varNames.length; i++) {
const varNameNoWhiteSpace = varNames[i].replace(/[-_\s]/g, "");
if (re.test(varNameNoWhiteSpace)) {
return varNames[i];
}
}
}
return undefined;
}
//# sourceMappingURL=RegionProvider.js.map