modules/services/fb_ai_features.js (258 lines of code) (raw):
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { xml as d3_xml } from 'd3-fetch';
import { Projection, Tiler } from '@id-sdk/math';
import { utilStringQs } from '@id-sdk/util';
import { coreGraph, coreTree } from '../core';
import { osmEntity, osmNode, osmWay } from '../osm';
import { utilRebind } from '../util';
// constants
var APIROOT = 'https://mapwith.ai/maps/ml_roads';
var TILEZOOM = 16;
var tiler = new Tiler().zoomRange(TILEZOOM);
var dispatch = d3_dispatch('loadedData');
var _datasets = {};
var _deferredAiFeaturesParsing = new Set();
var _off;
function abortRequest(i) {
i.abort();
}
function tileURL(dataset, extent, taskExtent) {
// Conflated datasets have a different ID, so they get stored in their own graph/tree
var isConflated = /-conflated$/.test(dataset.id);
var datasetID = dataset.id.replace('-conflated', '');
var qs = {
conflate_with_osm: isConflated,
theme: 'ml_road_vector',
collaborator: 'fbid',
token: 'ASZUVdYpCkd3M6ZrzjXdQzHulqRMnxdlkeBJWEKOeTUoY_Gwm9fuEd2YObLrClgDB_xfavizBsh0oDfTWTF7Zb4C',
hash: 'ASYM8LPNy8k1XoJiI7A'
};
if (datasetID === 'fbRoads') {
qs.result_type = 'road_vector_xml';
} else if (datasetID === 'msBuildings') {
qs.result_type = 'road_building_vector_xml';
qs.building_source = 'microsoft';
} else {
qs.result_type = 'osm_xml';
qs.sources = `esri_building.${datasetID}`;
}
qs.bbox = extent.toParam();
if (taskExtent) qs.crop_bbox = taskExtent.toParam();
// Note: we are not sure whether the `fb_ml_road_url` and `fb_ml_road_tags` query params are used anymore.
var customUrlRoot = utilStringQs(window.location.hash).fb_ml_road_url;
var customRoadTags = utilStringQs(window.location.hash).fb_ml_road_tags;
var urlRoot = customUrlRoot || APIROOT;
var url = urlRoot + '?' + fbmlQsString(qs, true); // true = noencode
if (customRoadTags) {
customRoadTags.split(',').forEach(function (tag) {
url += '&allow_tags[]=' + tag;
});
}
return url;
// This utilQsString does not sort the keys, because the fbml service needs them to be ordered a certain way.
function fbmlQsString(obj, noencode) {
// encode everything except special characters used in certain hash parameters:
// "/" in map states, ":", ",", {" and "}" in background
function softEncode(s) {
return encodeURIComponent(s).replace(/(%2F|%3A|%2C|%7B|%7D)/g, decodeURIComponent);
}
return Object.keys(obj).map(function(key) { // NO SORT
return encodeURIComponent(key) + '=' + (
noencode ? softEncode(obj[key]) : encodeURIComponent(obj[key]));
}).join('&');
}
}
function getLoc(attrs) {
var lon = attrs.lon && attrs.lon.value;
var lat = attrs.lat && attrs.lat.value;
return [parseFloat(lon), parseFloat(lat)];
}
function getNodes(obj) {
var elems = obj.getElementsByTagName('nd');
var nodes = new Array(elems.length);
for (var i = 0, l = elems.length; i < l; i++) {
nodes[i] = 'n' + elems[i].attributes.ref.value;
}
return nodes;
}
function getTags(obj) {
var elems = obj.getElementsByTagName('tag');
var tags = {};
for (var i = 0, l = elems.length; i < l; i++) {
var attrs = elems[i].attributes;
var k = (attrs.k.value || '').trim();
var v = (attrs.v.value || '').trim();
if (k && v) {
tags[k] = v;
}
}
return tags;
}
function getVisible(attrs) {
return (!attrs.visible || attrs.visible.value !== 'false');
}
var parsers = {
node: function nodeData(obj, uid) {
var attrs = obj.attributes;
return new osmNode({
id: uid,
visible: getVisible(attrs),
loc: getLoc(attrs),
tags: getTags(obj)
});
},
way: function wayData(obj, uid) {
var attrs = obj.attributes;
return new osmWay({
id: uid,
visible: getVisible(attrs),
tags: getTags(obj),
nodes: getNodes(obj),
});
}
};
function parseXML(dataset, xml, tile, callback, options) {
options = Object.assign({ skipSeen: true }, options);
if (!xml || !xml.childNodes) {
return callback({ message: 'No XML', status: -1 });
}
var graph = dataset.graph;
var cache = dataset.cache;
var root = xml.childNodes[0];
var children = root.childNodes;
var handle = window.requestIdleCallback(function() {
_deferredAiFeaturesParsing.delete(handle);
var results = [];
for (var i = 0; i < children.length; i++) {
var result = parseChild(children[i]);
if (result) results.push(result);
}
callback(null, results);
});
_deferredAiFeaturesParsing.add(handle);
function parseChild(child) {
var parser = parsers[child.nodeName];
if (!parser) return null;
var uid = osmEntity.id.fromOSM(child.nodeName, child.attributes.id.value);
if (options.skipSeen) {
if (cache.seen[uid]) return null; // avoid reparsing a "seen" entity
if (cache.origIdTile[uid]) return null; // avoid double-parsing a split way
cache.seen[uid] = true;
}
// Handle non-deterministic way splitting from Roads Service. Splits
// are consistent within a single request.
var origUid;
if (child.attributes.orig_id) {
origUid = osmEntity.id.fromOSM(child.nodeName, child.attributes.orig_id.value);
if (graph.entities[origUid] ||
(cache.origIdTile[origUid] && cache.origIdTile[origUid] !== tile.id)) {
return null;
}
cache.origIdTile[origUid] = tile.id;
}
var entity = parser(child, uid);
var meta = {
__fbid__: child.attributes.id.value,
__origid__: origUid,
__service__: 'fbml',
__datasetid__: dataset.id
};
return Object.assign(entity, meta);
}
}
export default {
init: function() {
this.event = utilRebind(this, dispatch, 'on');
// allocate a special dataset for the rapid intro graph.
var datasetID = 'rapid_intro_graph';
var graph = coreGraph();
var tree = coreTree(graph);
var cache = { inflight: {}, loaded: {}, seen: {}, origIdTile: {} };
var ds = { id: datasetID, graph: graph, tree: tree, cache: cache };
_datasets[datasetID] = ds;
},
reset: function() {
Array.from(_deferredAiFeaturesParsing).forEach(function(handle) {
window.cancelIdleCallback(handle);
_deferredAiFeaturesParsing.delete(handle);
});
Object.values(_datasets).forEach(function(ds) {
if (ds.cache.inflight) {
Object.values(ds.cache.inflight).forEach(abortRequest);
}
ds.graph = coreGraph();
ds.tree = coreTree(ds.graph);
ds.cache = { inflight: {}, loaded: {}, seen: {}, origIdTile: {} };
});
return this;
},
graph: function (datasetID) {
var ds = _datasets[datasetID];
return ds && ds.graph;
},
intersects: function (datasetID, extent) {
var ds = _datasets[datasetID];
if (!ds || !ds.tree || !ds.graph) return [];
return ds.tree.intersects(extent, ds.graph);
},
merge: function(datasetID, entities) {
var ds = _datasets[datasetID];
if (!ds || !ds.tree || !ds.graph) return;
ds.graph.rebase(entities, [ds.graph], false);
ds.tree.rebase(entities, false);
},
cache: function (datasetID, obj) {
var ds = _datasets[datasetID];
if (!ds || !ds.cache) return;
function cloneDeep(source) {
return JSON.parse(JSON.stringify(source));
}
if (!arguments.length) {
return {
tile: cloneDeep(ds.cache)
};
}
// access cache directly for testing
if (obj === 'get') {
return ds.cache;
}
ds.cache = obj;
},
toggle: function(val) {
_off = !val;
return this;
},
loadTiles: function(datasetID, projection, taskExtent) {
if (_off) return;
var ds = _datasets[datasetID];
var graph, tree, cache;
if (ds) {
graph = ds.graph;
tree = ds.tree;
cache = ds.cache;
} else {
// as tile requests arrive, setup the resources needed to hold the results
graph = coreGraph();
tree = coreTree(graph);
cache = { inflight: {}, loaded: {}, seen: {}, origIdTile: {} };
ds = { id: datasetID, graph: graph, tree: tree, cache: cache };
_datasets[datasetID] = ds;
}
var proj = new Projection().transform(projection.transform()).dimensions(projection.clipExtent());
var tiles = tiler.getTiles(proj).tiles;
// abort inflight requests that are no longer needed
Object.keys(cache.inflight).forEach(k => {
var wanted = tiles.find(function(tile) { return k === tile.id; });
if (!wanted) {
abortRequest(cache.inflight[k]);
delete cache.inflight[k];
}
});
tiles.forEach(function(tile) {
if (cache.loaded[tile.id] || cache.inflight[tile.id]) return;
var controller = new AbortController();
d3_xml(tileURL(ds, tile.wgs84Extent, taskExtent), { signal: controller.signal })
.then(function (dom) {
delete cache.inflight[tile.id];
if (!dom) return;
parseXML(ds, dom, tile, function(err, results) {
if (err) return;
graph.rebase(results, [graph], true);
tree.rebase(results, true);
cache.loaded[tile.id] = true;
dispatch.call('loadedData');
});
})
.catch(function() {});
cache.inflight[tile.id] = controller;
});
}
};