modules/svg/data.js (393 lines of code) (raw):
import { geoBounds as d3_geoBounds, geoPath as d3_geoPath } from 'd3-geo';
import { text as d3_text } from 'd3-fetch';
import { select as d3_select } from 'd3-selection';
import _throttle from 'lodash-es/throttle';
import stringify from 'fast-json-stable-stringify';
import { gpx, kml } from '@tmcw/togeojson';
import { Extent, geomPolygonIntersectsPolygon } from '@id-sdk/math';
import { utilArrayFlatten, utilArrayUnion, utilHashcode } from '@id-sdk/util';
import { services } from '../services';
import { svgPath } from './helpers';
import { utilDetect } from '../util/detect';
var _initialized = false;
var _enabled = false;
var _geojson;
export function svgData(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var _showLabels = true;
var detected = utilDetect();
var layer = d3_select(null);
var _vtService;
var _fileList;
var _template;
var _src;
function init() {
if (_initialized) return; // run once
_geojson = {};
_enabled = true;
function over(d3_event) {
d3_event.stopPropagation();
d3_event.preventDefault();
d3_event.dataTransfer.dropEffect = 'copy';
}
context.container()
.attr('dropzone', 'copy')
.on('drop.svgData', function(d3_event) {
d3_event.stopPropagation();
d3_event.preventDefault();
if (!detected.filedrop) return;
drawData.fileList(d3_event.dataTransfer.files);
})
.on('dragenter.svgData', over)
.on('dragexit.svgData', over)
.on('dragover.svgData', over);
_initialized = true;
}
function getService() {
if (services.vectorTile && !_vtService) {
_vtService = services.vectorTile;
_vtService.event.on('loadedData', throttledRedraw);
} else if (!services.vectorTile && _vtService) {
_vtService = null;
}
return _vtService;
}
function showLayer() {
layerOn();
layer
.style('opacity', 0)
.transition()
.duration(250)
.style('opacity', 1)
.on('end', function () { dispatch.call('change'); });
}
function hideLayer() {
throttledRedraw.cancel();
layer
.transition()
.duration(250)
.style('opacity', 0)
.on('end', layerOff);
}
function layerOn() {
layer.style('display', 'block');
}
function layerOff() {
layer.selectAll('.viewfield-group').remove();
layer.style('display', 'none');
}
// ensure that all geojson features in a collection have IDs
function ensureIDs(gj) {
if (!gj) return null;
if (gj.type === 'FeatureCollection') {
for (var i = 0; i < gj.features.length; i++) {
ensureFeatureID(gj.features[i]);
}
} else {
ensureFeatureID(gj);
}
return gj;
}
// ensure that each single Feature object has a unique ID
function ensureFeatureID(feature) {
if (!feature) return;
feature.__featurehash__ = utilHashcode(stringify(feature));
return feature;
}
// Prefer an array of Features instead of a FeatureCollection
function getFeatures(gj) {
if (!gj) return [];
if (gj.type === 'FeatureCollection') {
return gj.features;
} else {
return [gj];
}
}
function featureKey(d) {
return d.__featurehash__;
}
function isPolygon(d) {
return d.geometry.type === 'Polygon' || d.geometry.type === 'MultiPolygon';
}
function clipPathID(d) {
return 'ideditor-data-' + d.__featurehash__ + '-clippath';
}
function featureClasses(d) {
return [
'data' + d.__featurehash__,
d.geometry.type,
isPolygon(d) ? 'area' : '',
d.__layerID__ || ''
].filter(Boolean).join(' ');
}
function drawData(selection) {
var vtService = getService();
var getPath = svgPath(projection).geojson;
var getAreaPath = svgPath(projection, null, true).geojson;
var hasData = drawData.hasData();
layer = selection.selectAll('.layer-mapdata')
.data(_enabled && hasData ? [0] : []);
layer.exit()
.remove();
layer = layer.enter()
.append('g')
.attr('class', 'layer-mapdata')
.merge(layer);
var surface = context.surface();
if (!surface || surface.empty()) return; // not ready to draw yet, starting up
// Gather data
var geoData, polygonData;
if (_template && vtService) { // fetch data from vector tile service
var sourceID = _template;
vtService.loadTiles(sourceID, _template, projection);
geoData = vtService.data(sourceID, projection);
} else {
geoData = getFeatures(_geojson);
}
geoData = geoData.filter(getPath);
polygonData = geoData.filter(isPolygon);
// Draw clip paths for polygons
var clipPaths = surface.selectAll('defs').selectAll('.clipPath-data')
.data(polygonData, featureKey);
clipPaths.exit()
.remove();
var clipPathsEnter = clipPaths.enter()
.append('clipPath')
.attr('class', 'clipPath-data')
.attr('id', clipPathID);
clipPathsEnter
.append('path');
clipPaths.merge(clipPathsEnter)
.selectAll('path')
.attr('d', getAreaPath);
// Draw fill, shadow, stroke layers
var datagroups = layer
.selectAll('g.datagroup')
.data(['fill', 'shadow', 'stroke']);
datagroups = datagroups.enter()
.append('g')
.attr('class', function(d) { return 'datagroup datagroup-' + d; })
.merge(datagroups);
// Draw paths
var pathData = {
fill: polygonData,
shadow: geoData,
stroke: geoData
};
var paths = datagroups
.selectAll('path')
.data(function(layer) { return pathData[layer]; }, featureKey);
// exit
paths.exit()
.remove();
// enter/update
paths = paths.enter()
.append('path')
.attr('class', function(d) {
var datagroup = this.parentNode.__data__;
return 'pathdata ' + datagroup + ' ' + featureClasses(d);
})
.attr('clip-path', function(d) {
var datagroup = this.parentNode.__data__;
return datagroup === 'fill' ? ('url(#' + clipPathID(d) + ')') : null;
})
.merge(paths)
.attr('d', function(d) {
var datagroup = this.parentNode.__data__;
return datagroup === 'fill' ? getAreaPath(d) : getPath(d);
});
// Draw labels
layer
.call(drawLabels, 'label-halo', geoData)
.call(drawLabels, 'label', geoData);
function drawLabels(selection, textClass, data) {
var labelPath = d3_geoPath(projection);
var labelData = data.filter(function(d) {
return _showLabels && d.properties && (d.properties.desc || d.properties.name);
});
var labels = selection.selectAll('text.' + textClass)
.data(labelData, featureKey);
// exit
labels.exit()
.remove();
// enter/update
labels = labels.enter()
.append('text')
.attr('class', function(d) { return textClass + ' ' + featureClasses(d); })
.merge(labels)
.text(function(d) {
return d.properties.desc || d.properties.name;
})
.attr('x', function(d) {
var centroid = labelPath.centroid(d);
return centroid[0] + 11;
})
.attr('y', function(d) {
var centroid = labelPath.centroid(d);
return centroid[1];
});
}
}
function getExtension(fileName) {
if (!fileName) return;
var re = /\.(gpx|kml|(geo)?json)$/i;
var match = fileName.toLowerCase().match(re);
return match && match.length && match[0];
}
function xmlToDom(textdata) {
return (new DOMParser()).parseFromString(textdata, 'text/xml');
}
drawData.setFile = function(extension, data) {
_template = null;
_fileList = null;
_geojson = null;
_src = null;
var gj;
switch (extension) {
case '.gpx':
gj = gpx(xmlToDom(data));
break;
case '.kml':
gj = kml(xmlToDom(data));
break;
case '.geojson':
case '.json':
gj = JSON.parse(data);
break;
}
gj = gj || {};
if (Object.keys(gj).length) {
_geojson = ensureIDs(gj);
_src = extension + ' data file';
this.fitZoom();
}
dispatch.call('change');
return this;
};
drawData.showLabels = function(val) {
if (!arguments.length) return _showLabels;
_showLabels = val;
return this;
};
drawData.enabled = function(val) {
if (!arguments.length) return _enabled;
_enabled = val;
if (_enabled) {
showLayer();
} else {
hideLayer();
}
dispatch.call('change');
return this;
};
drawData.hasData = function() {
var gj = _geojson || {};
return !!(_template || Object.keys(gj).length);
};
drawData.template = function(val, src) {
if (!arguments.length) return _template;
// test source against OSM imagery blocklists..
var osm = context.connection();
if (osm) {
var blocklists = osm.imageryBlocklists();
var fail = false;
var tested = 0;
var regex;
for (var i = 0; i < blocklists.length; i++) {
regex = blocklists[i];
fail = regex.test(val);
tested++;
if (fail) break;
}
// ensure at least one test was run.
if (!tested) {
regex = /.*\.google(apis)?\..*\/(vt|kh)[\?\/].*([xyz]=.*){3}.*/;
fail = regex.test(val);
}
}
_template = val;
_fileList = null;
_geojson = null;
// strip off the querystring/hash from the template,
// it often includes the access token
_src = src || ('vectortile:' + val.split(/[?#]/)[0]);
dispatch.call('change');
return this;
};
drawData.geojson = function(gj, src) {
if (!arguments.length) return _geojson;
_template = null;
_fileList = null;
_geojson = null;
_src = null;
gj = gj || {};
if (Object.keys(gj).length) {
_geojson = ensureIDs(gj);
_src = src || 'unknown.geojson';
}
dispatch.call('change');
return this;
};
drawData.fileList = function(fileList) {
if (!arguments.length) return _fileList;
_template = null;
_fileList = fileList;
_geojson = null;
_src = null;
if (!fileList || !fileList.length) return this;
var f = fileList[0];
var extension = getExtension(f.name);
var reader = new FileReader();
reader.onload = (function() {
return function(e) {
drawData.setFile(extension, e.target.result);
};
})(f);
reader.readAsText(f);
return this;
};
drawData.url = function(url, defaultExtension) {
_template = null;
_fileList = null;
_geojson = null;
_src = null;
// strip off any querystring/hash from the url before checking extension
var testUrl = url.split(/[?#]/)[0];
var extension = getExtension(testUrl) || defaultExtension;
if (extension) {
_template = null;
d3_text(url)
.then(function(data) {
drawData.setFile(extension, data);
var isTaskBoundsUrl = extension === '.gpx' && url.indexOf('project') > 0 && url.indexOf('task') > 0;
if (isTaskBoundsUrl) {
context.rapidContext().setTaskExtentByGpxData(data);
}
})
.catch(function() {
/* ignore */
});
} else {
drawData.template(url);
}
return this;
};
drawData.getSrc = function() {
return _src || '';
};
drawData.fitZoom = function() {
var features = getFeatures(_geojson);
if (!features.length) return;
var map = context.map();
var viewport = map.trimmedExtent().polygon();
var coords = features.reduce(function(coords, feature) {
var geom = feature.geometry;
if (!geom) return coords;
var c = geom.coordinates;
/* eslint-disable no-fallthrough */
switch (geom.type) {
case 'Point':
c = [c];
case 'MultiPoint':
case 'LineString':
break;
case 'MultiPolygon':
c = utilArrayFlatten(c);
case 'Polygon':
case 'MultiLineString':
c = utilArrayFlatten(c);
break;
}
/* eslint-enable no-fallthrough */
return utilArrayUnion(coords, c);
}, []);
if (!geomPolygonIntersectsPolygon(viewport, coords, true)) {
var bounds = d3_geoBounds({ type: 'LineString', coordinates: coords });
var extent = new Extent(bounds[0], bounds[1]);
map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));
}
return this;
};
init();
return drawData;
}