mirror of
https://github.com/FoggedLens/iD.git
synced 2026-03-21 18:43:23 +00:00
3227 lines
104 KiB
JavaScript
3227 lines
104 KiB
JavaScript
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
(factory((global.iD = global.iD || {}, global.iD.svg = global.iD.svg || {})));
|
|
}(this, function (exports) { 'use strict';
|
|
|
|
function Areas(projection) {
|
|
// Patterns only work in Firefox when set directly on element.
|
|
// (This is not a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=750632)
|
|
var patterns = {
|
|
wetland: 'wetland',
|
|
beach: 'beach',
|
|
scrub: 'scrub',
|
|
construction: 'construction',
|
|
military: 'construction',
|
|
cemetery: 'cemetery',
|
|
grave_yard: 'cemetery',
|
|
meadow: 'meadow',
|
|
farm: 'farmland',
|
|
farmland: 'farmland',
|
|
orchard: 'orchard'
|
|
};
|
|
|
|
var patternKeys = ['landuse', 'natural', 'amenity'];
|
|
|
|
function setPattern(d) {
|
|
for (var i = 0; i < patternKeys.length; i++) {
|
|
if (patterns.hasOwnProperty(d.tags[patternKeys[i]])) {
|
|
this.style.fill = this.style.stroke = 'url("#pattern-' + patterns[d.tags[patternKeys[i]]] + '")';
|
|
return;
|
|
}
|
|
}
|
|
this.style.fill = this.style.stroke = '';
|
|
}
|
|
|
|
return function drawAreas(surface, graph, entities, filter) {
|
|
var path = iD.svg.Path(projection, graph, true),
|
|
areas = {},
|
|
multipolygon;
|
|
|
|
for (var i = 0; i < entities.length; i++) {
|
|
var entity = entities[i];
|
|
if (entity.geometry(graph) !== 'area') continue;
|
|
|
|
multipolygon = iD.geo.isSimpleMultipolygonOuterMember(entity, graph);
|
|
if (multipolygon) {
|
|
areas[multipolygon.id] = {
|
|
entity: multipolygon.mergeTags(entity.tags),
|
|
area: Math.abs(entity.area(graph))
|
|
};
|
|
} else if (!areas[entity.id]) {
|
|
areas[entity.id] = {
|
|
entity: entity,
|
|
area: Math.abs(entity.area(graph))
|
|
};
|
|
}
|
|
}
|
|
|
|
areas = d3.values(areas).filter(function hasPath(a) { return path(a.entity); });
|
|
areas.sort(function areaSort(a, b) { return b.area - a.area; });
|
|
areas = _.map(areas, 'entity');
|
|
|
|
var strokes = areas.filter(function(area) {
|
|
return area.type === 'way';
|
|
});
|
|
|
|
var data = {
|
|
clip: areas,
|
|
shadow: strokes,
|
|
stroke: strokes,
|
|
fill: areas
|
|
};
|
|
|
|
var clipPaths = surface.selectAll('defs').selectAll('.clipPath')
|
|
.filter(filter)
|
|
.data(data.clip, iD.Entity.key);
|
|
|
|
clipPaths.enter()
|
|
.append('clipPath')
|
|
.attr('class', 'clipPath')
|
|
.attr('id', function(entity) { return entity.id + '-clippath'; })
|
|
.append('path');
|
|
|
|
clipPaths.selectAll('path')
|
|
.attr('d', path);
|
|
|
|
clipPaths.exit()
|
|
.remove();
|
|
|
|
var areagroup = surface
|
|
.selectAll('.layer-areas')
|
|
.selectAll('g.areagroup')
|
|
.data(['fill', 'shadow', 'stroke']);
|
|
|
|
areagroup.enter()
|
|
.append('g')
|
|
.attr('class', function(d) { return 'layer areagroup area-' + d; });
|
|
|
|
var paths = areagroup
|
|
.selectAll('path')
|
|
.filter(filter)
|
|
.data(function(layer) { return data[layer]; }, iD.Entity.key);
|
|
|
|
// Remove exiting areas first, so they aren't included in the `fills`
|
|
// array used for sorting below (https://github.com/openstreetmap/iD/issues/1903).
|
|
paths.exit()
|
|
.remove();
|
|
|
|
var fills = surface.selectAll('.area-fill path.area')[0];
|
|
|
|
var bisect = d3.bisector(function(node) {
|
|
return -node.__data__.area(graph);
|
|
}).left;
|
|
|
|
function sortedByArea(entity) {
|
|
if (this.__data__ === 'fill') {
|
|
return fills[bisect(fills, -entity.area(graph))];
|
|
}
|
|
}
|
|
|
|
paths.enter()
|
|
.insert('path', sortedByArea)
|
|
.each(function(entity) {
|
|
var layer = this.parentNode.__data__;
|
|
|
|
this.setAttribute('class', entity.type + ' area ' + layer + ' ' + entity.id);
|
|
|
|
if (layer === 'fill') {
|
|
this.setAttribute('clip-path', 'url(#' + entity.id + '-clippath)');
|
|
setPattern.apply(this, arguments);
|
|
}
|
|
})
|
|
.call(iD.svg.TagClasses());
|
|
|
|
paths
|
|
.attr('d', path);
|
|
};
|
|
}
|
|
|
|
function Debug(projection, context) {
|
|
|
|
function multipolygons(imagery) {
|
|
return imagery.map(function(data) {
|
|
return {
|
|
type: 'MultiPolygon',
|
|
coordinates: [ data.polygon ]
|
|
};
|
|
});
|
|
}
|
|
|
|
function drawDebug(surface) {
|
|
var showsTile = context.getDebug('tile'),
|
|
showsCollision = context.getDebug('collision'),
|
|
showsImagery = context.getDebug('imagery'),
|
|
showsImperial = context.getDebug('imperial'),
|
|
showsDriveLeft = context.getDebug('driveLeft'),
|
|
path = d3.geo.path().projection(projection);
|
|
|
|
|
|
var debugData = [];
|
|
if (showsTile) {
|
|
debugData.push({ class: 'red', label: 'tile' });
|
|
}
|
|
if (showsCollision) {
|
|
debugData.push({ class: 'yellow', label: 'collision' });
|
|
}
|
|
if (showsImagery) {
|
|
debugData.push({ class: 'orange', label: 'imagery' });
|
|
}
|
|
if (showsImperial) {
|
|
debugData.push({ class: 'cyan', label: 'imperial' });
|
|
}
|
|
if (showsDriveLeft) {
|
|
debugData.push({ class: 'green', label: 'driveLeft' });
|
|
}
|
|
|
|
|
|
var legend = d3.select('#content')
|
|
.selectAll('.debug-legend')
|
|
.data(debugData.length ? [0] : []);
|
|
|
|
legend.enter()
|
|
.append('div')
|
|
.attr('class', 'fillD debug-legend');
|
|
|
|
legend.exit()
|
|
.remove();
|
|
|
|
|
|
var legendItems = legend.selectAll('.debug-legend-item')
|
|
.data(debugData, function(d) { return d.label; });
|
|
|
|
legendItems.enter()
|
|
.append('span')
|
|
.attr('class', function(d) { return 'debug-legend-item ' + d.class; })
|
|
.text(function(d) { return d.label; });
|
|
|
|
legendItems.exit()
|
|
.remove();
|
|
|
|
|
|
var layer = surface.selectAll('.layer-debug')
|
|
.data(showsImagery || showsImperial || showsDriveLeft ? [0] : []);
|
|
|
|
layer.enter()
|
|
.append('g')
|
|
.attr('class', 'layer-debug');
|
|
|
|
layer.exit()
|
|
.remove();
|
|
|
|
|
|
var extent = context.map().extent(),
|
|
availableImagery = showsImagery && multipolygons(iD.data.imagery.filter(function(source) {
|
|
if (!source.polygon) return false;
|
|
return source.polygon.some(function(polygon) {
|
|
return iD.geo.polygonIntersectsPolygon(polygon, extent, true);
|
|
});
|
|
}));
|
|
|
|
var imagery = layer.selectAll('path.debug-imagery')
|
|
.data(showsImagery ? availableImagery : []);
|
|
|
|
imagery.enter()
|
|
.append('path')
|
|
.attr('class', 'debug-imagery debug orange');
|
|
|
|
imagery.exit()
|
|
.remove();
|
|
|
|
|
|
var imperial = layer
|
|
.selectAll('path.debug-imperial')
|
|
.data(showsImperial ? [iD.data.imperial] : []);
|
|
|
|
imperial.enter()
|
|
.append('path')
|
|
.attr('class', 'debug-imperial debug cyan');
|
|
|
|
imperial.exit()
|
|
.remove();
|
|
|
|
|
|
var driveLeft = layer
|
|
.selectAll('path.debug-drive-left')
|
|
.data(showsDriveLeft ? [iD.data.driveLeft] : []);
|
|
|
|
driveLeft.enter()
|
|
.append('path')
|
|
.attr('class', 'debug-drive-left debug green');
|
|
|
|
driveLeft.exit()
|
|
.remove();
|
|
|
|
|
|
// update
|
|
layer.selectAll('path')
|
|
.attr('d', path);
|
|
}
|
|
|
|
// This looks strange because `enabled` methods on other layers are
|
|
// chainable getter/setters, and this one is just a getter.
|
|
drawDebug.enabled = function() {
|
|
if (!arguments.length) {
|
|
return context.getDebug('tile') ||
|
|
context.getDebug('collision') ||
|
|
context.getDebug('imagery') ||
|
|
context.getDebug('imperial') ||
|
|
context.getDebug('driveLeft');
|
|
} else {
|
|
return this;
|
|
}
|
|
};
|
|
|
|
return drawDebug;
|
|
}
|
|
|
|
/*
|
|
A standalone SVG element that contains only a `defs` sub-element. To be
|
|
used once globally, since defs IDs must be unique within a document.
|
|
*/
|
|
function Defs(context) {
|
|
|
|
function SVGSpriteDefinition(id, href) {
|
|
return function(defs) {
|
|
d3.xml(href, 'image/svg+xml', function(err, svg) {
|
|
if (err) return;
|
|
defs.node().appendChild(
|
|
d3.select(svg.documentElement).attr('id', id).node()
|
|
);
|
|
});
|
|
};
|
|
}
|
|
|
|
return function drawDefs(selection) {
|
|
var defs = selection.append('defs');
|
|
|
|
// marker
|
|
defs.append('marker')
|
|
.attr({
|
|
id: 'oneway-marker',
|
|
viewBox: '0 0 10 10',
|
|
refY: 2.5,
|
|
refX: 5,
|
|
markerWidth: 2,
|
|
markerHeight: 2,
|
|
markerUnits: 'strokeWidth',
|
|
orient: 'auto'
|
|
})
|
|
.append('path')
|
|
.attr('class', 'oneway')
|
|
.attr('d', 'M 5 3 L 0 3 L 0 2 L 5 2 L 5 0 L 10 2.5 L 5 5 z')
|
|
.attr('stroke', 'none')
|
|
.attr('fill', '#000')
|
|
.attr('opacity', '0.5');
|
|
|
|
// patterns
|
|
var patterns = defs.selectAll('pattern')
|
|
.data([
|
|
// pattern name, pattern image name
|
|
['wetland', 'wetland'],
|
|
['construction', 'construction'],
|
|
['cemetery', 'cemetery'],
|
|
['orchard', 'orchard'],
|
|
['farmland', 'farmland'],
|
|
['beach', 'dots'],
|
|
['scrub', 'dots'],
|
|
['meadow', 'dots']
|
|
])
|
|
.enter()
|
|
.append('pattern')
|
|
.attr({
|
|
id: function (d) {
|
|
return 'pattern-' + d[0];
|
|
},
|
|
width: 32,
|
|
height: 32,
|
|
patternUnits: 'userSpaceOnUse'
|
|
});
|
|
|
|
patterns.append('rect')
|
|
.attr({
|
|
x: 0,
|
|
y: 0,
|
|
width: 32,
|
|
height: 32,
|
|
'class': function (d) {
|
|
return 'pattern-color-' + d[0];
|
|
}
|
|
});
|
|
|
|
patterns.append('image')
|
|
.attr({
|
|
x: 0,
|
|
y: 0,
|
|
width: 32,
|
|
height: 32
|
|
})
|
|
.attr('xlink:href', function (d) {
|
|
return context.imagePath('pattern/' + d[1] + '.png');
|
|
});
|
|
|
|
// clip paths
|
|
defs.selectAll()
|
|
.data([12, 18, 20, 32, 45])
|
|
.enter().append('clipPath')
|
|
.attr('id', function (d) {
|
|
return 'clip-square-' + d;
|
|
})
|
|
.append('rect')
|
|
.attr('x', 0)
|
|
.attr('y', 0)
|
|
.attr('width', function (d) {
|
|
return d;
|
|
})
|
|
.attr('height', function (d) {
|
|
return d;
|
|
});
|
|
|
|
defs.call(SVGSpriteDefinition(
|
|
'iD-sprite',
|
|
context.imagePath('iD-sprite.svg')));
|
|
|
|
defs.call(SVGSpriteDefinition(
|
|
'maki-sprite',
|
|
context.imagePath('maki-sprite.svg')));
|
|
};
|
|
}
|
|
|
|
function createCommonjsModule(fn, module) {
|
|
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
|
}
|
|
|
|
var require$$0 = {};
|
|
|
|
var togeojson = createCommonjsModule(function (module, exports) {
|
|
var toGeoJSON = (function() {
|
|
'use strict';
|
|
|
|
var removeSpace = (/\s*/g),
|
|
trimSpace = (/^\s*|\s*$/g),
|
|
splitSpace = (/\s+/);
|
|
// generate a short, numeric hash of a string
|
|
function okhash(x) {
|
|
if (!x || !x.length) return 0;
|
|
for (var i = 0, h = 0; i < x.length; i++) {
|
|
h = ((h << 5) - h) + x.charCodeAt(i) | 0;
|
|
} return h;
|
|
}
|
|
// all Y children of X
|
|
function get(x, y) { return x.getElementsByTagName(y); }
|
|
function attr(x, y) { return x.getAttribute(y); }
|
|
function attrf(x, y) { return parseFloat(attr(x, y)); }
|
|
// one Y child of X, if any, otherwise null
|
|
function get1(x, y) { var n = get(x, y); return n.length ? n[0] : null; }
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Node.normalize
|
|
function norm(el) { if (el.normalize) { el.normalize(); } return el; }
|
|
// cast array x into numbers
|
|
function numarray(x) {
|
|
for (var j = 0, o = []; j < x.length; j++) { o[j] = parseFloat(x[j]); }
|
|
return o;
|
|
}
|
|
function clean(x) {
|
|
var o = {};
|
|
for (var i in x) { if (x[i]) { o[i] = x[i]; } }
|
|
return o;
|
|
}
|
|
// get the content of a text node, if any
|
|
function nodeVal(x) {
|
|
if (x) { norm(x); }
|
|
return (x && x.textContent) || '';
|
|
}
|
|
// get one coordinate from a coordinate array, if any
|
|
function coord1(v) { return numarray(v.replace(removeSpace, '').split(',')); }
|
|
// get all coordinates from a coordinate array as [[],[]]
|
|
function coord(v) {
|
|
var coords = v.replace(trimSpace, '').split(splitSpace),
|
|
o = [];
|
|
for (var i = 0; i < coords.length; i++) {
|
|
o.push(coord1(coords[i]));
|
|
}
|
|
return o;
|
|
}
|
|
function coordPair(x) {
|
|
var ll = [attrf(x, 'lon'), attrf(x, 'lat')],
|
|
ele = get1(x, 'ele'),
|
|
// handle namespaced attribute in browser
|
|
heartRate = get1(x, 'gpxtpx:hr') || get1(x, 'hr'),
|
|
time = get1(x, 'time'),
|
|
e;
|
|
if (ele) {
|
|
e = parseFloat(nodeVal(ele));
|
|
if (!isNaN(e)) {
|
|
ll.push(e);
|
|
}
|
|
}
|
|
return {
|
|
coordinates: ll,
|
|
time: time ? nodeVal(time) : null,
|
|
heartRate: heartRate ? parseFloat(nodeVal(heartRate)) : null
|
|
};
|
|
}
|
|
|
|
// create a new feature collection parent object
|
|
function fc() {
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: []
|
|
};
|
|
}
|
|
|
|
var serializer;
|
|
if (typeof XMLSerializer !== 'undefined') {
|
|
/* istanbul ignore next */
|
|
serializer = new XMLSerializer();
|
|
// only require xmldom in a node environment
|
|
} else if (typeof exports === 'object' && typeof process === 'object' && !process.browser) {
|
|
serializer = new (require$$0.XMLSerializer)();
|
|
}
|
|
function xml2str(str) {
|
|
// IE9 will create a new XMLSerializer but it'll crash immediately.
|
|
// This line is ignored because we don't run coverage tests in IE9
|
|
/* istanbul ignore next */
|
|
if (str.xml !== undefined) return str.xml;
|
|
return serializer.serializeToString(str);
|
|
}
|
|
|
|
var t = {
|
|
kml: function(doc) {
|
|
|
|
var gj = fc(),
|
|
// styleindex keeps track of hashed styles in order to match features
|
|
styleIndex = {},
|
|
// atomic geospatial types supported by KML - MultiGeometry is
|
|
// handled separately
|
|
geotypes = ['Polygon', 'LineString', 'Point', 'Track', 'gx:Track'],
|
|
// all root placemarks in the file
|
|
placemarks = get(doc, 'Placemark'),
|
|
styles = get(doc, 'Style'),
|
|
styleMaps = get(doc, 'StyleMap');
|
|
|
|
for (var k = 0; k < styles.length; k++) {
|
|
styleIndex['#' + attr(styles[k], 'id')] = okhash(xml2str(styles[k])).toString(16);
|
|
}
|
|
for (var l = 0; l < styleMaps.length; l++) {
|
|
styleIndex['#' + attr(styleMaps[l], 'id')] = okhash(xml2str(styleMaps[l])).toString(16);
|
|
}
|
|
for (var j = 0; j < placemarks.length; j++) {
|
|
gj.features = gj.features.concat(getPlacemark(placemarks[j]));
|
|
}
|
|
function kmlColor(v) {
|
|
var color, opacity;
|
|
v = v || '';
|
|
if (v.substr(0, 1) === '#') { v = v.substr(1); }
|
|
if (v.length === 6 || v.length === 3) { color = v; }
|
|
if (v.length === 8) {
|
|
opacity = parseInt(v.substr(0, 2), 16) / 255;
|
|
color = '#'+v.substr(2);
|
|
}
|
|
return [color, isNaN(opacity) ? undefined : opacity];
|
|
}
|
|
function gxCoord(v) { return numarray(v.split(' ')); }
|
|
function gxCoords(root) {
|
|
var elems = get(root, 'coord', 'gx'), coords = [], times = [];
|
|
if (elems.length === 0) elems = get(root, 'gx:coord');
|
|
for (var i = 0; i < elems.length; i++) coords.push(gxCoord(nodeVal(elems[i])));
|
|
var timeElems = get(root, 'when');
|
|
for (var j = 0; j < timeElems.length; j++) times.push(nodeVal(timeElems[j]));
|
|
return {
|
|
coords: coords,
|
|
times: times
|
|
};
|
|
}
|
|
function getGeometry(root) {
|
|
var geomNode, geomNodes, i, j, k, geoms = [], coordTimes = [];
|
|
if (get1(root, 'MultiGeometry')) { return getGeometry(get1(root, 'MultiGeometry')); }
|
|
if (get1(root, 'MultiTrack')) { return getGeometry(get1(root, 'MultiTrack')); }
|
|
if (get1(root, 'gx:MultiTrack')) { return getGeometry(get1(root, 'gx:MultiTrack')); }
|
|
for (i = 0; i < geotypes.length; i++) {
|
|
geomNodes = get(root, geotypes[i]);
|
|
if (geomNodes) {
|
|
for (j = 0; j < geomNodes.length; j++) {
|
|
geomNode = geomNodes[j];
|
|
if (geotypes[i] === 'Point') {
|
|
geoms.push({
|
|
type: 'Point',
|
|
coordinates: coord1(nodeVal(get1(geomNode, 'coordinates')))
|
|
});
|
|
} else if (geotypes[i] === 'LineString') {
|
|
geoms.push({
|
|
type: 'LineString',
|
|
coordinates: coord(nodeVal(get1(geomNode, 'coordinates')))
|
|
});
|
|
} else if (geotypes[i] === 'Polygon') {
|
|
var rings = get(geomNode, 'LinearRing'),
|
|
coords = [];
|
|
for (k = 0; k < rings.length; k++) {
|
|
coords.push(coord(nodeVal(get1(rings[k], 'coordinates'))));
|
|
}
|
|
geoms.push({
|
|
type: 'Polygon',
|
|
coordinates: coords
|
|
});
|
|
} else if (geotypes[i] === 'Track' ||
|
|
geotypes[i] === 'gx:Track') {
|
|
var track = gxCoords(geomNode);
|
|
geoms.push({
|
|
type: 'LineString',
|
|
coordinates: track.coords
|
|
});
|
|
if (track.times.length) coordTimes.push(track.times);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
geoms: geoms,
|
|
coordTimes: coordTimes
|
|
};
|
|
}
|
|
function getPlacemark(root) {
|
|
var geomsAndTimes = getGeometry(root), i, properties = {},
|
|
name = nodeVal(get1(root, 'name')),
|
|
styleUrl = nodeVal(get1(root, 'styleUrl')),
|
|
description = nodeVal(get1(root, 'description')),
|
|
timeSpan = get1(root, 'TimeSpan'),
|
|
extendedData = get1(root, 'ExtendedData'),
|
|
lineStyle = get1(root, 'LineStyle'),
|
|
polyStyle = get1(root, 'PolyStyle');
|
|
|
|
if (!geomsAndTimes.geoms.length) return [];
|
|
if (name) properties.name = name;
|
|
if (styleUrl[0] !== '#') {
|
|
styleUrl = '#' + styleUrl;
|
|
}
|
|
if (styleUrl && styleIndex[styleUrl]) {
|
|
properties.styleUrl = styleUrl;
|
|
properties.styleHash = styleIndex[styleUrl];
|
|
}
|
|
if (description) properties.description = description;
|
|
if (timeSpan) {
|
|
var begin = nodeVal(get1(timeSpan, 'begin'));
|
|
var end = nodeVal(get1(timeSpan, 'end'));
|
|
properties.timespan = { begin: begin, end: end };
|
|
}
|
|
if (lineStyle) {
|
|
var linestyles = kmlColor(nodeVal(get1(lineStyle, 'color'))),
|
|
color = linestyles[0],
|
|
opacity = linestyles[1],
|
|
width = parseFloat(nodeVal(get1(lineStyle, 'width')));
|
|
if (color) properties.stroke = color;
|
|
if (!isNaN(opacity)) properties['stroke-opacity'] = opacity;
|
|
if (!isNaN(width)) properties['stroke-width'] = width;
|
|
}
|
|
if (polyStyle) {
|
|
var polystyles = kmlColor(nodeVal(get1(polyStyle, 'color'))),
|
|
pcolor = polystyles[0],
|
|
popacity = polystyles[1],
|
|
fill = nodeVal(get1(polyStyle, 'fill')),
|
|
outline = nodeVal(get1(polyStyle, 'outline'));
|
|
if (pcolor) properties.fill = pcolor;
|
|
if (!isNaN(popacity)) properties['fill-opacity'] = popacity;
|
|
if (fill) properties['fill-opacity'] = fill === '1' ? 1 : 0;
|
|
if (outline) properties['stroke-opacity'] = outline === '1' ? 1 : 0;
|
|
}
|
|
if (extendedData) {
|
|
var datas = get(extendedData, 'Data'),
|
|
simpleDatas = get(extendedData, 'SimpleData');
|
|
|
|
for (i = 0; i < datas.length; i++) {
|
|
properties[datas[i].getAttribute('name')] = nodeVal(get1(datas[i], 'value'));
|
|
}
|
|
for (i = 0; i < simpleDatas.length; i++) {
|
|
properties[simpleDatas[i].getAttribute('name')] = nodeVal(simpleDatas[i]);
|
|
}
|
|
}
|
|
if (geomsAndTimes.coordTimes.length) {
|
|
properties.coordTimes = (geomsAndTimes.coordTimes.length === 1) ?
|
|
geomsAndTimes.coordTimes[0] : geomsAndTimes.coordTimes;
|
|
}
|
|
var feature = {
|
|
type: 'Feature',
|
|
geometry: (geomsAndTimes.geoms.length === 1) ? geomsAndTimes.geoms[0] : {
|
|
type: 'GeometryCollection',
|
|
geometries: geomsAndTimes.geoms
|
|
},
|
|
properties: properties
|
|
};
|
|
if (attr(root, 'id')) feature.id = attr(root, 'id');
|
|
return [feature];
|
|
}
|
|
return gj;
|
|
},
|
|
gpx: function(doc) {
|
|
var i,
|
|
tracks = get(doc, 'trk'),
|
|
routes = get(doc, 'rte'),
|
|
waypoints = get(doc, 'wpt'),
|
|
// a feature collection
|
|
gj = fc(),
|
|
feature;
|
|
for (i = 0; i < tracks.length; i++) {
|
|
feature = getTrack(tracks[i]);
|
|
if (feature) gj.features.push(feature);
|
|
}
|
|
for (i = 0; i < routes.length; i++) {
|
|
feature = getRoute(routes[i]);
|
|
if (feature) gj.features.push(feature);
|
|
}
|
|
for (i = 0; i < waypoints.length; i++) {
|
|
gj.features.push(getPoint(waypoints[i]));
|
|
}
|
|
function getPoints(node, pointname) {
|
|
var pts = get(node, pointname),
|
|
line = [],
|
|
times = [],
|
|
heartRates = [],
|
|
l = pts.length;
|
|
if (l < 2) return {}; // Invalid line in GeoJSON
|
|
for (var i = 0; i < l; i++) {
|
|
var c = coordPair(pts[i]);
|
|
line.push(c.coordinates);
|
|
if (c.time) times.push(c.time);
|
|
if (c.heartRate) heartRates.push(c.heartRate);
|
|
}
|
|
return {
|
|
line: line,
|
|
times: times,
|
|
heartRates: heartRates
|
|
};
|
|
}
|
|
function getTrack(node) {
|
|
var segments = get(node, 'trkseg'),
|
|
track = [],
|
|
times = [],
|
|
heartRates = [],
|
|
line;
|
|
for (var i = 0; i < segments.length; i++) {
|
|
line = getPoints(segments[i], 'trkpt');
|
|
if (line.line) track.push(line.line);
|
|
if (line.times && line.times.length) times.push(line.times);
|
|
if (line.heartRates && line.heartRates.length) heartRates.push(line.heartRates);
|
|
}
|
|
if (track.length === 0) return;
|
|
var properties = getProperties(node);
|
|
if (times.length) properties.coordTimes = track.length === 1 ? times[0] : times;
|
|
if (heartRates.length) properties.heartRates = track.length === 1 ? heartRates[0] : heartRates;
|
|
return {
|
|
type: 'Feature',
|
|
properties: properties,
|
|
geometry: {
|
|
type: track.length === 1 ? 'LineString' : 'MultiLineString',
|
|
coordinates: track.length === 1 ? track[0] : track
|
|
}
|
|
};
|
|
}
|
|
function getRoute(node) {
|
|
var line = getPoints(node, 'rtept');
|
|
if (!line.line) return;
|
|
var routeObj = {
|
|
type: 'Feature',
|
|
properties: getProperties(node),
|
|
geometry: {
|
|
type: 'LineString',
|
|
coordinates: line.line
|
|
}
|
|
};
|
|
return routeObj;
|
|
}
|
|
function getPoint(node) {
|
|
var prop = getProperties(node);
|
|
prop.sym = nodeVal(get1(node, 'sym'));
|
|
return {
|
|
type: 'Feature',
|
|
properties: prop,
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: coordPair(node).coordinates
|
|
}
|
|
};
|
|
}
|
|
function getProperties(node) {
|
|
var meta = ['name', 'desc', 'author', 'copyright', 'link',
|
|
'time', 'keywords'],
|
|
prop = {},
|
|
k;
|
|
for (k = 0; k < meta.length; k++) {
|
|
prop[meta[k]] = nodeVal(get1(node, meta[k]));
|
|
}
|
|
return clean(prop);
|
|
}
|
|
return gj;
|
|
}
|
|
};
|
|
return t;
|
|
})();
|
|
|
|
if (typeof module !== 'undefined') module.exports = toGeoJSON;
|
|
});
|
|
|
|
var toGeoJSON = (togeojson && typeof togeojson === 'object' && 'default' in togeojson ? togeojson['default'] : togeojson);
|
|
|
|
function Gpx(projection, context, dispatch) {
|
|
var showLabels = true,
|
|
layer;
|
|
|
|
function init() {
|
|
if (iD.svg.Gpx.initialized) return; // run once
|
|
|
|
iD.svg.Gpx.geojson = {};
|
|
iD.svg.Gpx.enabled = true;
|
|
|
|
function over() {
|
|
d3.event.stopPropagation();
|
|
d3.event.preventDefault();
|
|
d3.event.dataTransfer.dropEffect = 'copy';
|
|
}
|
|
|
|
d3.select('body')
|
|
.attr('dropzone', 'copy')
|
|
.on('drop.localgpx', function() {
|
|
d3.event.stopPropagation();
|
|
d3.event.preventDefault();
|
|
if (!iD.detect().filedrop) return;
|
|
drawGpx.files(d3.event.dataTransfer.files);
|
|
})
|
|
.on('dragenter.localgpx', over)
|
|
.on('dragexit.localgpx', over)
|
|
.on('dragover.localgpx', over);
|
|
|
|
iD.svg.Gpx.initialized = true;
|
|
}
|
|
|
|
|
|
function drawGpx(surface) {
|
|
var geojson = iD.svg.Gpx.geojson,
|
|
enabled = iD.svg.Gpx.enabled;
|
|
|
|
layer = surface.selectAll('.layer-gpx')
|
|
.data(enabled ? [0] : []);
|
|
|
|
layer.enter()
|
|
.append('g')
|
|
.attr('class', 'layer-gpx');
|
|
|
|
layer.exit()
|
|
.remove();
|
|
|
|
|
|
var paths = layer
|
|
.selectAll('path')
|
|
.data([geojson]);
|
|
|
|
paths.enter()
|
|
.append('path')
|
|
.attr('class', 'gpx');
|
|
|
|
paths.exit()
|
|
.remove();
|
|
|
|
var path = d3.geo.path()
|
|
.projection(projection);
|
|
|
|
paths
|
|
.attr('d', path);
|
|
|
|
|
|
var labels = layer.selectAll('text')
|
|
.data(showLabels && geojson.features ? geojson.features : []);
|
|
|
|
labels.enter()
|
|
.append('text')
|
|
.attr('class', 'gpx');
|
|
|
|
labels.exit()
|
|
.remove();
|
|
|
|
labels
|
|
.text(function(d) {
|
|
return d.properties.desc || d.properties.name;
|
|
})
|
|
.attr('x', function(d) {
|
|
var centroid = path.centroid(d);
|
|
return centroid[0] + 7;
|
|
})
|
|
.attr('y', function(d) {
|
|
var centroid = path.centroid(d);
|
|
return centroid[1];
|
|
});
|
|
|
|
}
|
|
|
|
function toDom(x) {
|
|
return (new DOMParser()).parseFromString(x, 'text/xml');
|
|
}
|
|
|
|
drawGpx.showLabels = function(_) {
|
|
if (!arguments.length) return showLabels;
|
|
showLabels = _;
|
|
return this;
|
|
};
|
|
|
|
drawGpx.enabled = function(_) {
|
|
if (!arguments.length) return iD.svg.Gpx.enabled;
|
|
iD.svg.Gpx.enabled = _;
|
|
dispatch.change();
|
|
return this;
|
|
};
|
|
|
|
drawGpx.hasGpx = function() {
|
|
var geojson = iD.svg.Gpx.geojson;
|
|
return (!(_.isEmpty(geojson) || _.isEmpty(geojson.features)));
|
|
};
|
|
|
|
drawGpx.geojson = function(gj) {
|
|
if (!arguments.length) return iD.svg.Gpx.geojson;
|
|
if (_.isEmpty(gj) || _.isEmpty(gj.features)) return this;
|
|
iD.svg.Gpx.geojson = gj;
|
|
dispatch.change();
|
|
return this;
|
|
};
|
|
|
|
drawGpx.url = function(url) {
|
|
d3.text(url, function(err, data) {
|
|
if (!err) {
|
|
drawGpx.geojson(toGeoJSON.gpx(toDom(data)));
|
|
}
|
|
});
|
|
return this;
|
|
};
|
|
|
|
drawGpx.files = function(fileList) {
|
|
if (!fileList.length) return this;
|
|
var f = fileList[0],
|
|
reader = new FileReader();
|
|
|
|
reader.onload = function(e) {
|
|
drawGpx.geojson(toGeoJSON.gpx(toDom(e.target.result))).fitZoom();
|
|
};
|
|
|
|
reader.readAsText(f);
|
|
return this;
|
|
};
|
|
|
|
drawGpx.fitZoom = function() {
|
|
if (!this.hasGpx()) return this;
|
|
var geojson = iD.svg.Gpx.geojson;
|
|
|
|
var map = context.map(),
|
|
viewport = map.trimmedExtent().polygon(),
|
|
coords = _.reduce(geojson.features, function(coords, feature) {
|
|
var c = feature.geometry.coordinates;
|
|
return _.union(coords, feature.geometry.type === 'Point' ? [c] : c);
|
|
}, []);
|
|
|
|
if (!iD.geo.polygonIntersectsPolygon(viewport, coords, true)) {
|
|
var extent = iD.geo.Extent(d3.geo.bounds(geojson));
|
|
map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
init();
|
|
return drawGpx;
|
|
}
|
|
|
|
function Icon(name, svgklass, useklass) {
|
|
return function drawIcon(selection) {
|
|
selection.selectAll('svg')
|
|
.data([0])
|
|
.enter()
|
|
.append('svg')
|
|
.attr('class', 'icon ' + (svgklass || ''))
|
|
.append('use')
|
|
.attr('xlink:href', name)
|
|
.attr('class', useklass);
|
|
};
|
|
}
|
|
|
|
var index$1 = createCommonjsModule(function (module) {
|
|
'use strict';
|
|
|
|
module.exports = partialSort;
|
|
|
|
// Floyd-Rivest selection algorithm:
|
|
// Rearrange items so that all items in the [left, k] range are smaller than all items in (k, right];
|
|
// The k-th element will have the (k - left + 1)th smallest value in [left, right]
|
|
|
|
function partialSort(arr, k, left, right, compare) {
|
|
left = left || 0;
|
|
right = right || (arr.length - 1);
|
|
compare = compare || defaultCompare;
|
|
|
|
while (right > left) {
|
|
if (right - left > 600) {
|
|
var n = right - left + 1;
|
|
var m = k - left + 1;
|
|
var z = Math.log(n);
|
|
var s = 0.5 * Math.exp(2 * z / 3);
|
|
var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
|
|
var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
|
|
var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
|
|
partialSort(arr, k, newLeft, newRight, compare);
|
|
}
|
|
|
|
var t = arr[k];
|
|
var i = left;
|
|
var j = right;
|
|
|
|
swap(arr, left, k);
|
|
if (compare(arr[right], t) > 0) swap(arr, left, right);
|
|
|
|
while (i < j) {
|
|
swap(arr, i, j);
|
|
i++;
|
|
j--;
|
|
while (compare(arr[i], t) < 0) i++;
|
|
while (compare(arr[j], t) > 0) j--;
|
|
}
|
|
|
|
if (compare(arr[left], t) === 0) swap(arr, left, j);
|
|
else {
|
|
j++;
|
|
swap(arr, j, right);
|
|
}
|
|
|
|
if (j <= k) left = j + 1;
|
|
if (k <= j) right = j - 1;
|
|
}
|
|
}
|
|
|
|
function swap(arr, i, j) {
|
|
var tmp = arr[i];
|
|
arr[i] = arr[j];
|
|
arr[j] = tmp;
|
|
}
|
|
|
|
function defaultCompare(a, b) {
|
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
}
|
|
});
|
|
|
|
var require$$0$1 = (index$1 && typeof index$1 === 'object' && 'default' in index$1 ? index$1['default'] : index$1);
|
|
|
|
var index = createCommonjsModule(function (module) {
|
|
'use strict';
|
|
|
|
module.exports = rbush;
|
|
|
|
var quickselect = require$$0$1;
|
|
|
|
function rbush(maxEntries, format) {
|
|
if (!(this instanceof rbush)) return new rbush(maxEntries, format);
|
|
|
|
// max entries in a node is 9 by default; min node fill is 40% for best performance
|
|
this._maxEntries = Math.max(4, maxEntries || 9);
|
|
this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
|
|
|
|
if (format) {
|
|
this._initFormat(format);
|
|
}
|
|
|
|
this.clear();
|
|
}
|
|
|
|
rbush.prototype = {
|
|
|
|
all: function () {
|
|
return this._all(this.data, []);
|
|
},
|
|
|
|
search: function (bbox) {
|
|
|
|
var node = this.data,
|
|
result = [],
|
|
toBBox = this.toBBox;
|
|
|
|
if (!intersects(bbox, node)) return result;
|
|
|
|
var nodesToSearch = [],
|
|
i, len, child, childBBox;
|
|
|
|
while (node) {
|
|
for (i = 0, len = node.children.length; i < len; i++) {
|
|
|
|
child = node.children[i];
|
|
childBBox = node.leaf ? toBBox(child) : child;
|
|
|
|
if (intersects(bbox, childBBox)) {
|
|
if (node.leaf) result.push(child);
|
|
else if (contains(bbox, childBBox)) this._all(child, result);
|
|
else nodesToSearch.push(child);
|
|
}
|
|
}
|
|
node = nodesToSearch.pop();
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
collides: function (bbox) {
|
|
|
|
var node = this.data,
|
|
toBBox = this.toBBox;
|
|
|
|
if (!intersects(bbox, node)) return false;
|
|
|
|
var nodesToSearch = [],
|
|
i, len, child, childBBox;
|
|
|
|
while (node) {
|
|
for (i = 0, len = node.children.length; i < len; i++) {
|
|
|
|
child = node.children[i];
|
|
childBBox = node.leaf ? toBBox(child) : child;
|
|
|
|
if (intersects(bbox, childBBox)) {
|
|
if (node.leaf || contains(bbox, childBBox)) return true;
|
|
nodesToSearch.push(child);
|
|
}
|
|
}
|
|
node = nodesToSearch.pop();
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
load: function (data) {
|
|
if (!(data && data.length)) return this;
|
|
|
|
if (data.length < this._minEntries) {
|
|
for (var i = 0, len = data.length; i < len; i++) {
|
|
this.insert(data[i]);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
// recursively build the tree with the given data from stratch using OMT algorithm
|
|
var node = this._build(data.slice(), 0, data.length - 1, 0);
|
|
|
|
if (!this.data.children.length) {
|
|
// save as is if tree is empty
|
|
this.data = node;
|
|
|
|
} else if (this.data.height === node.height) {
|
|
// split root if trees have the same height
|
|
this._splitRoot(this.data, node);
|
|
|
|
} else {
|
|
if (this.data.height < node.height) {
|
|
// swap trees if inserted one is bigger
|
|
var tmpNode = this.data;
|
|
this.data = node;
|
|
node = tmpNode;
|
|
}
|
|
|
|
// insert the small tree into the large tree at appropriate level
|
|
this._insert(node, this.data.height - node.height - 1, true);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
insert: function (item) {
|
|
if (item) this._insert(item, this.data.height - 1);
|
|
return this;
|
|
},
|
|
|
|
clear: function () {
|
|
this.data = createNode([]);
|
|
return this;
|
|
},
|
|
|
|
remove: function (item, equalsFn) {
|
|
if (!item) return this;
|
|
|
|
var node = this.data,
|
|
bbox = this.toBBox(item),
|
|
path = [],
|
|
indexes = [],
|
|
i, parent, index, goingUp;
|
|
|
|
// depth-first iterative tree traversal
|
|
while (node || path.length) {
|
|
|
|
if (!node) { // go up
|
|
node = path.pop();
|
|
parent = path[path.length - 1];
|
|
i = indexes.pop();
|
|
goingUp = true;
|
|
}
|
|
|
|
if (node.leaf) { // check current node
|
|
index = findItem(item, node.children, equalsFn);
|
|
|
|
if (index !== -1) {
|
|
// item found, remove the item and condense tree upwards
|
|
node.children.splice(index, 1);
|
|
path.push(node);
|
|
this._condense(path);
|
|
return this;
|
|
}
|
|
}
|
|
|
|
if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
|
|
path.push(node);
|
|
indexes.push(i);
|
|
i = 0;
|
|
parent = node;
|
|
node = node.children[0];
|
|
|
|
} else if (parent) { // go right
|
|
i++;
|
|
node = parent.children[i];
|
|
goingUp = false;
|
|
|
|
} else node = null; // nothing found
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
toBBox: function (item) { return item; },
|
|
|
|
compareMinX: compareNodeMinX,
|
|
compareMinY: compareNodeMinY,
|
|
|
|
toJSON: function () { return this.data; },
|
|
|
|
fromJSON: function (data) {
|
|
this.data = data;
|
|
return this;
|
|
},
|
|
|
|
_all: function (node, result) {
|
|
var nodesToSearch = [];
|
|
while (node) {
|
|
if (node.leaf) result.push.apply(result, node.children);
|
|
else nodesToSearch.push.apply(nodesToSearch, node.children);
|
|
|
|
node = nodesToSearch.pop();
|
|
}
|
|
return result;
|
|
},
|
|
|
|
_build: function (items, left, right, height) {
|
|
|
|
var N = right - left + 1,
|
|
M = this._maxEntries,
|
|
node;
|
|
|
|
if (N <= M) {
|
|
// reached leaf level; return leaf
|
|
node = createNode(items.slice(left, right + 1));
|
|
calcBBox(node, this.toBBox);
|
|
return node;
|
|
}
|
|
|
|
if (!height) {
|
|
// target height of the bulk-loaded tree
|
|
height = Math.ceil(Math.log(N) / Math.log(M));
|
|
|
|
// target number of root entries to maximize storage utilization
|
|
M = Math.ceil(N / Math.pow(M, height - 1));
|
|
}
|
|
|
|
node = createNode([]);
|
|
node.leaf = false;
|
|
node.height = height;
|
|
|
|
// split the items into M mostly square tiles
|
|
|
|
var N2 = Math.ceil(N / M),
|
|
N1 = N2 * Math.ceil(Math.sqrt(M)),
|
|
i, j, right2, right3;
|
|
|
|
multiSelect(items, left, right, N1, this.compareMinX);
|
|
|
|
for (i = left; i <= right; i += N1) {
|
|
|
|
right2 = Math.min(i + N1 - 1, right);
|
|
|
|
multiSelect(items, i, right2, N2, this.compareMinY);
|
|
|
|
for (j = i; j <= right2; j += N2) {
|
|
|
|
right3 = Math.min(j + N2 - 1, right2);
|
|
|
|
// pack each entry recursively
|
|
node.children.push(this._build(items, j, right3, height - 1));
|
|
}
|
|
}
|
|
|
|
calcBBox(node, this.toBBox);
|
|
|
|
return node;
|
|
},
|
|
|
|
_chooseSubtree: function (bbox, node, level, path) {
|
|
|
|
var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;
|
|
|
|
while (true) {
|
|
path.push(node);
|
|
|
|
if (node.leaf || path.length - 1 === level) break;
|
|
|
|
minArea = minEnlargement = Infinity;
|
|
|
|
for (i = 0, len = node.children.length; i < len; i++) {
|
|
child = node.children[i];
|
|
area = bboxArea(child);
|
|
enlargement = enlargedArea(bbox, child) - area;
|
|
|
|
// choose entry with the least area enlargement
|
|
if (enlargement < minEnlargement) {
|
|
minEnlargement = enlargement;
|
|
minArea = area < minArea ? area : minArea;
|
|
targetNode = child;
|
|
|
|
} else if (enlargement === minEnlargement) {
|
|
// otherwise choose one with the smallest area
|
|
if (area < minArea) {
|
|
minArea = area;
|
|
targetNode = child;
|
|
}
|
|
}
|
|
}
|
|
|
|
node = targetNode || node.children[0];
|
|
}
|
|
|
|
return node;
|
|
},
|
|
|
|
_insert: function (item, level, isNode) {
|
|
|
|
var toBBox = this.toBBox,
|
|
bbox = isNode ? item : toBBox(item),
|
|
insertPath = [];
|
|
|
|
// find the best node for accommodating the item, saving all nodes along the path too
|
|
var node = this._chooseSubtree(bbox, this.data, level, insertPath);
|
|
|
|
// put the item into the node
|
|
node.children.push(item);
|
|
extend(node, bbox);
|
|
|
|
// split on node overflow; propagate upwards if necessary
|
|
while (level >= 0) {
|
|
if (insertPath[level].children.length > this._maxEntries) {
|
|
this._split(insertPath, level);
|
|
level--;
|
|
} else break;
|
|
}
|
|
|
|
// adjust bboxes along the insertion path
|
|
this._adjustParentBBoxes(bbox, insertPath, level);
|
|
},
|
|
|
|
// split overflowed node into two
|
|
_split: function (insertPath, level) {
|
|
|
|
var node = insertPath[level],
|
|
M = node.children.length,
|
|
m = this._minEntries;
|
|
|
|
this._chooseSplitAxis(node, m, M);
|
|
|
|
var splitIndex = this._chooseSplitIndex(node, m, M);
|
|
|
|
var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
|
|
newNode.height = node.height;
|
|
newNode.leaf = node.leaf;
|
|
|
|
calcBBox(node, this.toBBox);
|
|
calcBBox(newNode, this.toBBox);
|
|
|
|
if (level) insertPath[level - 1].children.push(newNode);
|
|
else this._splitRoot(node, newNode);
|
|
},
|
|
|
|
_splitRoot: function (node, newNode) {
|
|
// split root node
|
|
this.data = createNode([node, newNode]);
|
|
this.data.height = node.height + 1;
|
|
this.data.leaf = false;
|
|
calcBBox(this.data, this.toBBox);
|
|
},
|
|
|
|
_chooseSplitIndex: function (node, m, M) {
|
|
|
|
var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;
|
|
|
|
minOverlap = minArea = Infinity;
|
|
|
|
for (i = m; i <= M - m; i++) {
|
|
bbox1 = distBBox(node, 0, i, this.toBBox);
|
|
bbox2 = distBBox(node, i, M, this.toBBox);
|
|
|
|
overlap = intersectionArea(bbox1, bbox2);
|
|
area = bboxArea(bbox1) + bboxArea(bbox2);
|
|
|
|
// choose distribution with minimum overlap
|
|
if (overlap < minOverlap) {
|
|
minOverlap = overlap;
|
|
index = i;
|
|
|
|
minArea = area < minArea ? area : minArea;
|
|
|
|
} else if (overlap === minOverlap) {
|
|
// otherwise choose distribution with minimum area
|
|
if (area < minArea) {
|
|
minArea = area;
|
|
index = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return index;
|
|
},
|
|
|
|
// sorts node children by the best axis for split
|
|
_chooseSplitAxis: function (node, m, M) {
|
|
|
|
var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,
|
|
compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,
|
|
xMargin = this._allDistMargin(node, m, M, compareMinX),
|
|
yMargin = this._allDistMargin(node, m, M, compareMinY);
|
|
|
|
// if total distributions margin value is minimal for x, sort by minX,
|
|
// otherwise it's already sorted by minY
|
|
if (xMargin < yMargin) node.children.sort(compareMinX);
|
|
},
|
|
|
|
// total margin of all possible split distributions where each node is at least m full
|
|
_allDistMargin: function (node, m, M, compare) {
|
|
|
|
node.children.sort(compare);
|
|
|
|
var toBBox = this.toBBox,
|
|
leftBBox = distBBox(node, 0, m, toBBox),
|
|
rightBBox = distBBox(node, M - m, M, toBBox),
|
|
margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),
|
|
i, child;
|
|
|
|
for (i = m; i < M - m; i++) {
|
|
child = node.children[i];
|
|
extend(leftBBox, node.leaf ? toBBox(child) : child);
|
|
margin += bboxMargin(leftBBox);
|
|
}
|
|
|
|
for (i = M - m - 1; i >= m; i--) {
|
|
child = node.children[i];
|
|
extend(rightBBox, node.leaf ? toBBox(child) : child);
|
|
margin += bboxMargin(rightBBox);
|
|
}
|
|
|
|
return margin;
|
|
},
|
|
|
|
_adjustParentBBoxes: function (bbox, path, level) {
|
|
// adjust bboxes along the given tree path
|
|
for (var i = level; i >= 0; i--) {
|
|
extend(path[i], bbox);
|
|
}
|
|
},
|
|
|
|
_condense: function (path) {
|
|
// go through the path, removing empty nodes and updating bboxes
|
|
for (var i = path.length - 1, siblings; i >= 0; i--) {
|
|
if (path[i].children.length === 0) {
|
|
if (i > 0) {
|
|
siblings = path[i - 1].children;
|
|
siblings.splice(siblings.indexOf(path[i]), 1);
|
|
|
|
} else this.clear();
|
|
|
|
} else calcBBox(path[i], this.toBBox);
|
|
}
|
|
},
|
|
|
|
_initFormat: function (format) {
|
|
// data format (minX, minY, maxX, maxY accessors)
|
|
|
|
// uses eval-type function compilation instead of just accepting a toBBox function
|
|
// because the algorithms are very sensitive to sorting functions performance,
|
|
// so they should be dead simple and without inner calls
|
|
|
|
var compareArr = ['return a', ' - b', ';'];
|
|
|
|
this.compareMinX = new Function('a', 'b', compareArr.join(format[0]));
|
|
this.compareMinY = new Function('a', 'b', compareArr.join(format[1]));
|
|
|
|
this.toBBox = new Function('a',
|
|
'return {minX: a' + format[0] +
|
|
', minY: a' + format[1] +
|
|
', maxX: a' + format[2] +
|
|
', maxY: a' + format[3] + '};');
|
|
}
|
|
};
|
|
|
|
function findItem(item, items, equalsFn) {
|
|
if (!equalsFn) return items.indexOf(item);
|
|
|
|
for (var i = 0; i < items.length; i++) {
|
|
if (equalsFn(item, items[i])) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// calculate node's bbox from bboxes of its children
|
|
function calcBBox(node, toBBox) {
|
|
distBBox(node, 0, node.children.length, toBBox, node);
|
|
}
|
|
|
|
// min bounding rectangle of node children from k to p-1
|
|
function distBBox(node, k, p, toBBox, destNode) {
|
|
if (!destNode) destNode = createNode(null);
|
|
destNode.minX = Infinity;
|
|
destNode.minY = Infinity;
|
|
destNode.maxX = -Infinity;
|
|
destNode.maxY = -Infinity;
|
|
|
|
for (var i = k, child; i < p; i++) {
|
|
child = node.children[i];
|
|
extend(destNode, node.leaf ? toBBox(child) : child);
|
|
}
|
|
|
|
return destNode;
|
|
}
|
|
|
|
function extend(a, b) {
|
|
a.minX = Math.min(a.minX, b.minX);
|
|
a.minY = Math.min(a.minY, b.minY);
|
|
a.maxX = Math.max(a.maxX, b.maxX);
|
|
a.maxY = Math.max(a.maxY, b.maxY);
|
|
return a;
|
|
}
|
|
|
|
function compareNodeMinX(a, b) { return a.minX - b.minX; }
|
|
function compareNodeMinY(a, b) { return a.minY - b.minY; }
|
|
|
|
function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
|
|
function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
|
|
|
|
function enlargedArea(a, b) {
|
|
return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
|
|
(Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
|
|
}
|
|
|
|
function intersectionArea(a, b) {
|
|
var minX = Math.max(a.minX, b.minX),
|
|
minY = Math.max(a.minY, b.minY),
|
|
maxX = Math.min(a.maxX, b.maxX),
|
|
maxY = Math.min(a.maxY, b.maxY);
|
|
|
|
return Math.max(0, maxX - minX) *
|
|
Math.max(0, maxY - minY);
|
|
}
|
|
|
|
function contains(a, b) {
|
|
return a.minX <= b.minX &&
|
|
a.minY <= b.minY &&
|
|
b.maxX <= a.maxX &&
|
|
b.maxY <= a.maxY;
|
|
}
|
|
|
|
function intersects(a, b) {
|
|
return b.minX <= a.maxX &&
|
|
b.minY <= a.maxY &&
|
|
b.maxX >= a.minX &&
|
|
b.maxY >= a.minY;
|
|
}
|
|
|
|
function createNode(children) {
|
|
return {
|
|
children: children,
|
|
height: 1,
|
|
leaf: true,
|
|
minX: Infinity,
|
|
minY: Infinity,
|
|
maxX: -Infinity,
|
|
maxY: -Infinity
|
|
};
|
|
}
|
|
|
|
// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
|
|
// combines selection algorithm with binary divide & conquer approach
|
|
|
|
function multiSelect(arr, left, right, n, compare) {
|
|
var stack = [left, right],
|
|
mid;
|
|
|
|
while (stack.length) {
|
|
right = stack.pop();
|
|
left = stack.pop();
|
|
|
|
if (right - left <= n) continue;
|
|
|
|
mid = left + Math.ceil((right - left) / n / 2) * n;
|
|
quickselect(arr, mid, left, right, compare);
|
|
|
|
stack.push(left, mid, mid, right);
|
|
}
|
|
}
|
|
});
|
|
|
|
var rbush = (index && typeof index === 'object' && 'default' in index ? index['default'] : index);
|
|
|
|
function Labels(projection, context) {
|
|
var path = d3.geo.path().projection(projection);
|
|
|
|
// Replace with dict and iterate over entities tags instead?
|
|
var label_stack = [
|
|
['line', 'aeroway'],
|
|
['line', 'highway'],
|
|
['line', 'railway'],
|
|
['line', 'waterway'],
|
|
['area', 'aeroway'],
|
|
['area', 'amenity'],
|
|
['area', 'building'],
|
|
['area', 'historic'],
|
|
['area', 'leisure'],
|
|
['area', 'man_made'],
|
|
['area', 'natural'],
|
|
['area', 'shop'],
|
|
['area', 'tourism'],
|
|
['point', 'aeroway'],
|
|
['point', 'amenity'],
|
|
['point', 'building'],
|
|
['point', 'historic'],
|
|
['point', 'leisure'],
|
|
['point', 'man_made'],
|
|
['point', 'natural'],
|
|
['point', 'shop'],
|
|
['point', 'tourism'],
|
|
['line', 'name'],
|
|
['area', 'name'],
|
|
['point', 'name']
|
|
];
|
|
|
|
var default_size = 12;
|
|
|
|
var font_sizes = label_stack.map(function(d) {
|
|
var style = iD.util.getStyle('text.' + d[0] + '.tag-' + d[1]),
|
|
m = style && style.cssText.match('font-size: ([0-9]{1,2})px;');
|
|
if (m) return parseInt(m[1], 10);
|
|
|
|
style = iD.util.getStyle('text.' + d[0]);
|
|
m = style && style.cssText.match('font-size: ([0-9]{1,2})px;');
|
|
if (m) return parseInt(m[1], 10);
|
|
|
|
return default_size;
|
|
});
|
|
|
|
var iconSize = 18;
|
|
|
|
var pointOffsets = [
|
|
[15, -11, 'start'], // right
|
|
[10, -11, 'start'], // unused right now
|
|
[-15, -11, 'end']
|
|
];
|
|
|
|
var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70, 25,
|
|
75, 20, 80, 15, 95, 10, 90, 5, 95];
|
|
|
|
|
|
var noIcons = ['building', 'landuse', 'natural'];
|
|
function blacklisted(preset) {
|
|
return _.some(noIcons, function(s) {
|
|
return preset.id.indexOf(s) >= 0;
|
|
});
|
|
}
|
|
|
|
function get(array, prop) {
|
|
return function(d, i) { return array[i][prop]; };
|
|
}
|
|
|
|
var textWidthCache = {};
|
|
|
|
function textWidth(text, size, elem) {
|
|
var c = textWidthCache[size];
|
|
if (!c) c = textWidthCache[size] = {};
|
|
|
|
if (c[text]) {
|
|
return c[text];
|
|
|
|
} else if (elem) {
|
|
c[text] = elem.getComputedTextLength();
|
|
return c[text];
|
|
|
|
} else {
|
|
var str = encodeURIComponent(text).match(/%[CDEFcdef]/g);
|
|
if (str === null) {
|
|
return size / 3 * 2 * text.length;
|
|
} else {
|
|
return size / 3 * (2 * text.length + str.length);
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawLineLabels(group, entities, filter, classes, labels) {
|
|
var texts = group.selectAll('text.' + classes)
|
|
.filter(filter)
|
|
.data(entities, iD.Entity.key);
|
|
|
|
texts.enter()
|
|
.append('text')
|
|
.attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; })
|
|
.append('textPath')
|
|
.attr('class', 'textpath');
|
|
|
|
|
|
texts.selectAll('.textpath')
|
|
.filter(filter)
|
|
.data(entities, iD.Entity.key)
|
|
.attr({
|
|
'startOffset': '50%',
|
|
'xlink:href': function(d) { return '#labelpath-' + d.id; }
|
|
})
|
|
.text(iD.util.displayName);
|
|
|
|
texts.exit().remove();
|
|
}
|
|
|
|
function drawLinePaths(group, entities, filter, classes, labels) {
|
|
var halos = group.selectAll('path')
|
|
.filter(filter)
|
|
.data(entities, iD.Entity.key);
|
|
|
|
halos.enter()
|
|
.append('path')
|
|
.style('stroke-width', get(labels, 'font-size'))
|
|
.attr('id', function(d) { return 'labelpath-' + d.id; })
|
|
.attr('class', classes);
|
|
|
|
halos.attr('d', get(labels, 'lineString'));
|
|
|
|
halos.exit().remove();
|
|
}
|
|
|
|
function drawPointLabels(group, entities, filter, classes, labels) {
|
|
var texts = group.selectAll('text.' + classes)
|
|
.filter(filter)
|
|
.data(entities, iD.Entity.key);
|
|
|
|
texts.enter()
|
|
.append('text')
|
|
.attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; });
|
|
|
|
texts.attr('x', get(labels, 'x'))
|
|
.attr('y', get(labels, 'y'))
|
|
.style('text-anchor', get(labels, 'textAnchor'))
|
|
.text(iD.util.displayName)
|
|
.each(function(d, i) { textWidth(iD.util.displayName(d), labels[i].height, this); });
|
|
|
|
texts.exit().remove();
|
|
return texts;
|
|
}
|
|
|
|
function drawAreaLabels(group, entities, filter, classes, labels) {
|
|
entities = entities.filter(hasText);
|
|
labels = labels.filter(hasText);
|
|
return drawPointLabels(group, entities, filter, classes, labels);
|
|
|
|
function hasText(d, i) {
|
|
return labels[i].hasOwnProperty('x') && labels[i].hasOwnProperty('y');
|
|
}
|
|
}
|
|
|
|
function drawAreaIcons(group, entities, filter, classes, labels) {
|
|
var icons = group.selectAll('use')
|
|
.filter(filter)
|
|
.data(entities, iD.Entity.key);
|
|
|
|
icons.enter()
|
|
.append('use')
|
|
.attr('class', 'icon areaicon')
|
|
.attr('width', '18px')
|
|
.attr('height', '18px');
|
|
|
|
icons.attr('transform', get(labels, 'transform'))
|
|
.attr('xlink:href', function(d) {
|
|
var icon = context.presets().match(d, context.graph()).icon;
|
|
return '#' + icon + (icon === 'hairdresser' ? '-24': '-18'); // workaround: maki hairdresser-18 broken?
|
|
});
|
|
|
|
|
|
icons.exit().remove();
|
|
}
|
|
|
|
function reverse(p) {
|
|
var angle = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]);
|
|
return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2);
|
|
}
|
|
|
|
function lineString(nodes) {
|
|
return 'M' + nodes.join('L');
|
|
}
|
|
|
|
function subpath(nodes, from, to) {
|
|
function segmentLength(i) {
|
|
var dx = nodes[i][0] - nodes[i + 1][0];
|
|
var dy = nodes[i][1] - nodes[i + 1][1];
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
var sofar = 0,
|
|
start, end, i0, i1;
|
|
for (var i = 0; i < nodes.length - 1; i++) {
|
|
var current = segmentLength(i);
|
|
var portion;
|
|
if (!start && sofar + current >= from) {
|
|
portion = (from - sofar) / current;
|
|
start = [
|
|
nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]),
|
|
nodes[i][1] + portion * (nodes[i + 1][1] - nodes[i][1])
|
|
];
|
|
i0 = i + 1;
|
|
}
|
|
if (!end && sofar + current >= to) {
|
|
portion = (to - sofar) / current;
|
|
end = [
|
|
nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]),
|
|
nodes[i][1] + portion * (nodes[i + 1][1] - nodes[i][1])
|
|
];
|
|
i1 = i + 1;
|
|
}
|
|
sofar += current;
|
|
|
|
}
|
|
var ret = nodes.slice(i0, i1);
|
|
ret.unshift(start);
|
|
ret.push(end);
|
|
return ret;
|
|
|
|
}
|
|
|
|
function hideOnMouseover() {
|
|
var layers = d3.select(this)
|
|
.selectAll('.layer-label, .layer-halo');
|
|
|
|
layers.selectAll('.proximate')
|
|
.classed('proximate', false);
|
|
|
|
var mouse = context.mouse(),
|
|
pad = 50,
|
|
bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad },
|
|
ids = _.map(rtree.search(bbox), 'id');
|
|
|
|
if (!ids.length) return;
|
|
layers.selectAll('.' + ids.join(', .'))
|
|
.classed('proximate', true);
|
|
}
|
|
|
|
var rtree = rbush(),
|
|
bboxes = {};
|
|
|
|
function drawLabels(surface, graph, entities, filter, dimensions, fullRedraw) {
|
|
var hidePoints = !surface.selectAll('.node.point').node();
|
|
|
|
var labelable = [], i, k, entity;
|
|
for (i = 0; i < label_stack.length; i++) labelable.push([]);
|
|
|
|
if (fullRedraw) {
|
|
rtree.clear();
|
|
bboxes = {};
|
|
} else {
|
|
for (i = 0; i < entities.length; i++) {
|
|
rtree.remove(bboxes[entities[i].id]);
|
|
}
|
|
}
|
|
|
|
// Split entities into groups specified by label_stack
|
|
for (i = 0; i < entities.length; i++) {
|
|
entity = entities[i];
|
|
var geometry = entity.geometry(graph);
|
|
|
|
if (geometry === 'vertex')
|
|
continue;
|
|
if (hidePoints && geometry === 'point')
|
|
continue;
|
|
|
|
var preset = geometry === 'area' && context.presets().match(entity, graph),
|
|
icon = preset && !blacklisted(preset) && preset.icon;
|
|
|
|
if (!icon && !iD.util.displayName(entity))
|
|
continue;
|
|
|
|
for (k = 0; k < label_stack.length; k++) {
|
|
if (geometry === label_stack[k][0] && entity.tags[label_stack[k][1]]) {
|
|
labelable[k].push(entity);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var positions = {
|
|
point: [],
|
|
line: [],
|
|
area: []
|
|
};
|
|
|
|
var labelled = {
|
|
point: [],
|
|
line: [],
|
|
area: []
|
|
};
|
|
|
|
// Try and find a valid label for labellable entities
|
|
for (k = 0; k < labelable.length; k++) {
|
|
var font_size = font_sizes[k];
|
|
for (i = 0; i < labelable[k].length; i++) {
|
|
entity = labelable[k][i];
|
|
var name = iD.util.displayName(entity),
|
|
width = name && textWidth(name, font_size),
|
|
p;
|
|
if (entity.geometry(graph) === 'point') {
|
|
p = getPointLabel(entity, width, font_size);
|
|
} else if (entity.geometry(graph) === 'line') {
|
|
p = getLineLabel(entity, width, font_size);
|
|
} else if (entity.geometry(graph) === 'area') {
|
|
p = getAreaLabel(entity, width, font_size);
|
|
}
|
|
if (p) {
|
|
p.classes = entity.geometry(graph) + ' tag-' + label_stack[k][1];
|
|
positions[entity.geometry(graph)].push(p);
|
|
labelled[entity.geometry(graph)].push(entity);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getPointLabel(entity, width, height) {
|
|
var coord = projection(entity.loc),
|
|
m = 5, // margin
|
|
offset = pointOffsets[0],
|
|
p = {
|
|
height: height,
|
|
width: width,
|
|
x: coord[0] + offset[0],
|
|
y: coord[1] + offset[1],
|
|
textAnchor: offset[2]
|
|
};
|
|
var bbox = { minX: p.x - m, minY: p.y - m, maxX: p.x + width + m, maxY: p.y + height + m };
|
|
if (tryInsert(bbox, entity.id)) return p;
|
|
}
|
|
|
|
|
|
function getLineLabel(entity, width, height) {
|
|
var nodes = _.map(graph.childNodes(entity), 'loc').map(projection),
|
|
length = iD.geo.pathLength(nodes);
|
|
if (length < width + 20) return;
|
|
|
|
for (var i = 0; i < lineOffsets.length; i++) {
|
|
var offset = lineOffsets[i],
|
|
middle = offset / 100 * length,
|
|
start = middle - width/2;
|
|
if (start < 0 || start + width > length) continue;
|
|
var sub = subpath(nodes, start, start + width),
|
|
rev = reverse(sub),
|
|
bbox = {
|
|
minX: Math.min(sub[0][0], sub[sub.length - 1][0]) - 10,
|
|
minY: Math.min(sub[0][1], sub[sub.length - 1][1]) - 10,
|
|
maxX: Math.max(sub[0][0], sub[sub.length - 1][0]) + 20,
|
|
maxY: Math.max(sub[0][1], sub[sub.length - 1][1]) + 30
|
|
};
|
|
if (rev) sub = sub.reverse();
|
|
if (tryInsert(bbox, entity.id)) return {
|
|
'font-size': height + 2,
|
|
lineString: lineString(sub),
|
|
startOffset: offset + '%'
|
|
};
|
|
}
|
|
}
|
|
|
|
function getAreaLabel(entity, width, height) {
|
|
var centroid = path.centroid(entity.asGeoJSON(graph, true)),
|
|
extent = entity.extent(graph),
|
|
entitywidth = projection(extent[1])[0] - projection(extent[0])[0],
|
|
bbox;
|
|
|
|
if (isNaN(centroid[0]) || entitywidth < 20) return;
|
|
|
|
var iconX = centroid[0] - (iconSize/2),
|
|
iconY = centroid[1] - (iconSize/2),
|
|
textOffset = iconSize + 5;
|
|
|
|
var p = {
|
|
transform: 'translate(' + iconX + ',' + iconY + ')'
|
|
};
|
|
|
|
if (width && entitywidth >= width + 20) {
|
|
p.x = centroid[0];
|
|
p.y = centroid[1] + textOffset;
|
|
p.textAnchor = 'middle';
|
|
p.height = height;
|
|
bbox = { minX: p.x - width/2, minY: p.y, maxX: p.x + width/2, maxY: p.y + height + textOffset };
|
|
} else {
|
|
bbox = { minX: iconX, minY: iconY, maxX: iconX + iconSize, maxY: iconY + iconSize };
|
|
}
|
|
|
|
if (tryInsert(bbox, entity.id)) return p;
|
|
|
|
}
|
|
|
|
function tryInsert(bbox, id) {
|
|
// Check that label is visible
|
|
if (bbox.minX < 0 || bbox.minY < 0 || bbox.maxX > dimensions[0] || bbox.maxY > dimensions[1]) return false;
|
|
var v = rtree.search(bbox).length === 0;
|
|
if (v) {
|
|
bbox.id = id;
|
|
rtree.insert(bbox);
|
|
bboxes[id] = bbox;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
var label = surface.selectAll('.layer-label'),
|
|
halo = surface.selectAll('.layer-halo');
|
|
|
|
// points
|
|
drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point);
|
|
drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo', positions.point);
|
|
|
|
// lines
|
|
drawLinePaths(halo, labelled.line, filter, '', positions.line);
|
|
drawLineLabels(label, labelled.line, filter, 'linelabel', positions.line);
|
|
drawLineLabels(halo, labelled.line, filter, 'linelabel-halo', positions.line);
|
|
|
|
// areas
|
|
drawAreaLabels(label, labelled.area, filter, 'arealabel', positions.area);
|
|
drawAreaLabels(halo, labelled.area, filter, 'arealabel-halo', positions.area);
|
|
drawAreaIcons(label, labelled.area, filter, 'arealabel-icon', positions.area);
|
|
|
|
// debug
|
|
var showDebug = context.getDebug('collision');
|
|
var debug = label.selectAll('.layer-label-debug')
|
|
.data(showDebug ? [true] : []);
|
|
|
|
debug.enter()
|
|
.append('g')
|
|
.attr('class', 'layer-label-debug');
|
|
|
|
debug.exit()
|
|
.remove();
|
|
|
|
if (showDebug) {
|
|
var gj = rtree.all().map(function(d) {
|
|
return { type: 'Polygon', coordinates: [[
|
|
[d.minX, d.minY],
|
|
[d.maxX, d.minY],
|
|
[d.maxX, d.maxY],
|
|
[d.minX, d.maxY],
|
|
[d.minX, d.minY]
|
|
]]};
|
|
});
|
|
|
|
var debugboxes = debug.selectAll('.debug').data(gj);
|
|
|
|
debugboxes.enter()
|
|
.append('path')
|
|
.attr('class', 'debug yellow');
|
|
|
|
debugboxes.exit()
|
|
.remove();
|
|
|
|
debugboxes
|
|
.attr('d', d3.geo.path().projection(null));
|
|
}
|
|
}
|
|
|
|
drawLabels.supersurface = function(supersurface) {
|
|
supersurface
|
|
.on('mousemove.hidelabels', hideOnMouseover)
|
|
.on('mousedown.hidelabels', function () {
|
|
supersurface.on('mousemove.hidelabels', null);
|
|
})
|
|
.on('mouseup.hidelabels', function () {
|
|
supersurface.on('mousemove.hidelabels', hideOnMouseover);
|
|
});
|
|
};
|
|
|
|
return drawLabels;
|
|
}
|
|
|
|
function Layers(projection, context) {
|
|
var dispatch = d3.dispatch('change'),
|
|
svg = d3.select(null),
|
|
layers = [
|
|
{ id: 'osm', layer: iD.svg.Osm(projection, context, dispatch) },
|
|
{ id: 'gpx', layer: iD.svg.Gpx(projection, context, dispatch) },
|
|
{ id: 'mapillary-images', layer: iD.svg.MapillaryImages(projection, context, dispatch) },
|
|
{ id: 'mapillary-signs', layer: iD.svg.MapillarySigns(projection, context, dispatch) },
|
|
{ id: 'debug', layer: iD.svg.Debug(projection, context, dispatch) }
|
|
];
|
|
|
|
|
|
function drawLayers(selection) {
|
|
svg = selection.selectAll('.surface')
|
|
.data([0]);
|
|
|
|
svg.enter()
|
|
.append('svg')
|
|
.attr('class', 'surface')
|
|
.append('defs');
|
|
|
|
var groups = svg.selectAll('.data-layer')
|
|
.data(layers);
|
|
|
|
groups.enter()
|
|
.append('g')
|
|
.attr('class', function(d) { return 'data-layer data-layer-' + d.id; });
|
|
|
|
groups
|
|
.each(function(d) { d3.select(this).call(d.layer); });
|
|
|
|
groups.exit()
|
|
.remove();
|
|
}
|
|
|
|
drawLayers.all = function() {
|
|
return layers;
|
|
};
|
|
|
|
drawLayers.layer = function(id) {
|
|
var obj = _.find(layers, function(o) {return o.id === id;});
|
|
return obj && obj.layer;
|
|
};
|
|
|
|
drawLayers.only = function(what) {
|
|
var arr = [].concat(what);
|
|
drawLayers.remove(_.difference(_.map(layers, 'id'), arr));
|
|
return this;
|
|
};
|
|
|
|
drawLayers.remove = function(what) {
|
|
var arr = [].concat(what);
|
|
arr.forEach(function(id) {
|
|
layers = _.reject(layers, function(o) {return o.id === id;});
|
|
});
|
|
dispatch.change();
|
|
return this;
|
|
};
|
|
|
|
drawLayers.add = function(what) {
|
|
var arr = [].concat(what);
|
|
arr.forEach(function(obj) {
|
|
if ('id' in obj && 'layer' in obj) {
|
|
layers.push(obj);
|
|
}
|
|
});
|
|
dispatch.change();
|
|
return this;
|
|
};
|
|
|
|
drawLayers.dimensions = function(_) {
|
|
if (!arguments.length) return svg.dimensions();
|
|
svg.dimensions(_);
|
|
layers.forEach(function(obj) {
|
|
if (obj.layer.dimensions) {
|
|
obj.layer.dimensions(_);
|
|
}
|
|
});
|
|
return this;
|
|
};
|
|
|
|
|
|
return d3.rebind(drawLayers, dispatch, 'on');
|
|
}
|
|
|
|
function Lines(projection) {
|
|
|
|
var highway_stack = {
|
|
motorway: 0,
|
|
motorway_link: 1,
|
|
trunk: 2,
|
|
trunk_link: 3,
|
|
primary: 4,
|
|
primary_link: 5,
|
|
secondary: 6,
|
|
tertiary: 7,
|
|
unclassified: 8,
|
|
residential: 9,
|
|
service: 10,
|
|
footway: 11
|
|
};
|
|
|
|
function waystack(a, b) {
|
|
var as = 0, bs = 0;
|
|
|
|
if (a.tags.highway) { as -= highway_stack[a.tags.highway]; }
|
|
if (b.tags.highway) { bs -= highway_stack[b.tags.highway]; }
|
|
return as - bs;
|
|
}
|
|
|
|
return function drawLines(surface, graph, entities, filter) {
|
|
var ways = [], pathdata = {}, onewaydata = {},
|
|
getPath = iD.svg.Path(projection, graph);
|
|
|
|
for (var i = 0; i < entities.length; i++) {
|
|
var entity = entities[i],
|
|
outer = iD.geo.simpleMultipolygonOuterMember(entity, graph);
|
|
if (outer) {
|
|
ways.push(entity.mergeTags(outer.tags));
|
|
} else if (entity.geometry(graph) === 'line') {
|
|
ways.push(entity);
|
|
}
|
|
}
|
|
|
|
ways = ways.filter(getPath);
|
|
|
|
pathdata = _.groupBy(ways, function(way) { return way.layer(); });
|
|
|
|
_.forOwn(pathdata, function(v, k) {
|
|
onewaydata[k] = _(v)
|
|
.filter(function(d) { return d.isOneWay(); })
|
|
.map(iD.svg.OneWaySegments(projection, graph, 35))
|
|
.flatten()
|
|
.valueOf();
|
|
});
|
|
|
|
var layergroup = surface
|
|
.selectAll('.layer-lines')
|
|
.selectAll('g.layergroup')
|
|
.data(d3.range(-10, 11));
|
|
|
|
layergroup.enter()
|
|
.append('g')
|
|
.attr('class', function(d) { return 'layer layergroup layer' + String(d); });
|
|
|
|
|
|
var linegroup = layergroup
|
|
.selectAll('g.linegroup')
|
|
.data(['shadow', 'casing', 'stroke']);
|
|
|
|
linegroup.enter()
|
|
.append('g')
|
|
.attr('class', function(d) { return 'layer linegroup line-' + d; });
|
|
|
|
|
|
var lines = linegroup
|
|
.selectAll('path')
|
|
.filter(filter)
|
|
.data(
|
|
function() { return pathdata[this.parentNode.parentNode.__data__] || []; },
|
|
iD.Entity.key
|
|
);
|
|
|
|
// Optimization: call simple TagClasses only on enter selection. This
|
|
// works because iD.Entity.key is defined to include the entity v attribute.
|
|
lines.enter()
|
|
.append('path')
|
|
.attr('class', function(d) { return 'way line ' + this.parentNode.__data__ + ' ' + d.id; })
|
|
.call(iD.svg.TagClasses());
|
|
|
|
lines
|
|
.sort(waystack)
|
|
.attr('d', getPath)
|
|
.call(iD.svg.TagClasses().tags(iD.svg.RelationMemberTags(graph)));
|
|
|
|
lines.exit()
|
|
.remove();
|
|
|
|
|
|
var onewaygroup = layergroup
|
|
.selectAll('g.onewaygroup')
|
|
.data(['oneway']);
|
|
|
|
onewaygroup.enter()
|
|
.append('g')
|
|
.attr('class', 'layer onewaygroup');
|
|
|
|
|
|
var oneways = onewaygroup
|
|
.selectAll('path')
|
|
.filter(filter)
|
|
.data(
|
|
function() { return onewaydata[this.parentNode.parentNode.__data__] || []; },
|
|
function(d) { return [d.id, d.index]; }
|
|
);
|
|
|
|
oneways.enter()
|
|
.append('path')
|
|
.attr('class', 'oneway')
|
|
.attr('marker-mid', 'url(#oneway-marker)');
|
|
|
|
oneways
|
|
.attr('d', function(d) { return d.d; });
|
|
|
|
if (iD.detect().ie) {
|
|
oneways.each(function() { this.parentNode.insertBefore(this, this); });
|
|
}
|
|
|
|
oneways.exit()
|
|
.remove();
|
|
|
|
};
|
|
}
|
|
|
|
function MapillaryImages(projection, context, dispatch) {
|
|
var debouncedRedraw = _.debounce(function () { dispatch.change(); }, 1000),
|
|
minZoom = 12,
|
|
layer = d3.select(null),
|
|
_mapillary;
|
|
|
|
|
|
function init() {
|
|
if (iD.svg.MapillaryImages.initialized) return; // run once
|
|
iD.svg.MapillaryImages.enabled = false;
|
|
iD.svg.MapillaryImages.initialized = true;
|
|
}
|
|
|
|
function getMapillary() {
|
|
if (iD.services.mapillary && !_mapillary) {
|
|
_mapillary = iD.services.mapillary();
|
|
_mapillary.on('loadedImages', debouncedRedraw);
|
|
} else if (!iD.services.mapillary && _mapillary) {
|
|
_mapillary = null;
|
|
}
|
|
|
|
return _mapillary;
|
|
}
|
|
|
|
function showLayer() {
|
|
var mapillary = getMapillary();
|
|
if (!mapillary) return;
|
|
|
|
mapillary.loadViewer();
|
|
editOn();
|
|
|
|
layer
|
|
.style('opacity', 0)
|
|
.transition()
|
|
.duration(500)
|
|
.style('opacity', 1)
|
|
.each('end', debouncedRedraw);
|
|
}
|
|
|
|
function hideLayer() {
|
|
var mapillary = getMapillary();
|
|
if (mapillary) {
|
|
mapillary.hideViewer();
|
|
}
|
|
|
|
debouncedRedraw.cancel();
|
|
|
|
layer
|
|
.transition()
|
|
.duration(500)
|
|
.style('opacity', 0)
|
|
.each('end', editOff);
|
|
}
|
|
|
|
function editOn() {
|
|
layer.style('display', 'block');
|
|
}
|
|
|
|
function editOff() {
|
|
layer.selectAll('.viewfield-group').remove();
|
|
layer.style('display', 'none');
|
|
}
|
|
|
|
function click(d) {
|
|
var mapillary = getMapillary();
|
|
if (!mapillary) return;
|
|
|
|
context.map().centerEase(d.loc);
|
|
|
|
mapillary
|
|
.setSelectedImage(d.key, true)
|
|
.updateViewer(d.key, context)
|
|
.showViewer();
|
|
}
|
|
|
|
function transform(d) {
|
|
var t = iD.svg.PointTransform(projection)(d);
|
|
if (d.ca) t += ' rotate(' + Math.floor(d.ca) + ',0,0)';
|
|
return t;
|
|
}
|
|
|
|
function update() {
|
|
var mapillary = getMapillary(),
|
|
data = (mapillary ? mapillary.images(projection, layer.dimensions()) : []),
|
|
imageKey = mapillary ? mapillary.getSelectedImage() : null;
|
|
|
|
var markers = layer.selectAll('.viewfield-group')
|
|
.data(data, function(d) { return d.key; });
|
|
|
|
// Enter
|
|
var enter = markers.enter()
|
|
.append('g')
|
|
.attr('class', 'viewfield-group')
|
|
.classed('selected', function(d) { return d.key === imageKey; })
|
|
.on('click', click);
|
|
|
|
enter.append('path')
|
|
.attr('class', 'viewfield')
|
|
.attr('transform', 'scale(1.5,1.5),translate(-8, -13)')
|
|
.attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');
|
|
|
|
enter.append('circle')
|
|
.attr('dx', '0')
|
|
.attr('dy', '0')
|
|
.attr('r', '6');
|
|
|
|
// Exit
|
|
markers.exit()
|
|
.remove();
|
|
|
|
// Update
|
|
markers
|
|
.attr('transform', transform);
|
|
}
|
|
|
|
function drawImages(selection) {
|
|
var enabled = iD.svg.MapillaryImages.enabled,
|
|
mapillary = getMapillary();
|
|
|
|
layer = selection.selectAll('.layer-mapillary-images')
|
|
.data(mapillary ? [0] : []);
|
|
|
|
layer.enter()
|
|
.append('g')
|
|
.attr('class', 'layer-mapillary-images')
|
|
.style('display', enabled ? 'block' : 'none');
|
|
|
|
layer.exit()
|
|
.remove();
|
|
|
|
if (enabled) {
|
|
if (mapillary && ~~context.map().zoom() >= minZoom) {
|
|
editOn();
|
|
update();
|
|
mapillary.loadImages(projection, layer.dimensions());
|
|
} else {
|
|
editOff();
|
|
}
|
|
}
|
|
}
|
|
|
|
drawImages.enabled = function(_) {
|
|
if (!arguments.length) return iD.svg.MapillaryImages.enabled;
|
|
iD.svg.MapillaryImages.enabled = _;
|
|
if (iD.svg.MapillaryImages.enabled) {
|
|
showLayer();
|
|
} else {
|
|
hideLayer();
|
|
}
|
|
dispatch.change();
|
|
return this;
|
|
};
|
|
|
|
drawImages.supported = function() {
|
|
return !!getMapillary();
|
|
};
|
|
|
|
drawImages.dimensions = function(_) {
|
|
if (!arguments.length) return layer.dimensions();
|
|
layer.dimensions(_);
|
|
return this;
|
|
};
|
|
|
|
init();
|
|
return drawImages;
|
|
}
|
|
|
|
function MapillarySigns(projection, context, dispatch) {
|
|
var debouncedRedraw = _.debounce(function () { dispatch.change(); }, 1000),
|
|
minZoom = 12,
|
|
layer = d3.select(null),
|
|
_mapillary;
|
|
|
|
|
|
function init() {
|
|
if (iD.svg.MapillarySigns.initialized) return; // run once
|
|
iD.svg.MapillarySigns.enabled = false;
|
|
iD.svg.MapillarySigns.initialized = true;
|
|
}
|
|
|
|
function getMapillary() {
|
|
if (iD.services.mapillary && !_mapillary) {
|
|
_mapillary = iD.services.mapillary().on('loadedSigns', debouncedRedraw);
|
|
} else if (!iD.services.mapillary && _mapillary) {
|
|
_mapillary = null;
|
|
}
|
|
return _mapillary;
|
|
}
|
|
|
|
function showLayer() {
|
|
editOn();
|
|
debouncedRedraw();
|
|
}
|
|
|
|
function hideLayer() {
|
|
debouncedRedraw.cancel();
|
|
editOff();
|
|
}
|
|
|
|
function editOn() {
|
|
layer.style('display', 'block');
|
|
}
|
|
|
|
function editOff() {
|
|
layer.selectAll('.icon-sign').remove();
|
|
layer.style('display', 'none');
|
|
}
|
|
|
|
function click(d) {
|
|
var mapillary = getMapillary();
|
|
if (!mapillary) return;
|
|
|
|
context.map().centerEase(d.loc);
|
|
|
|
mapillary
|
|
.setSelectedImage(d.key, true)
|
|
.updateViewer(d.key, context)
|
|
.showViewer();
|
|
}
|
|
|
|
function update() {
|
|
var mapillary = getMapillary(),
|
|
data = (mapillary ? mapillary.signs(projection, layer.dimensions()) : []),
|
|
imageKey = mapillary ? mapillary.getSelectedImage() : null;
|
|
|
|
var signs = layer.selectAll('.icon-sign')
|
|
.data(data, function(d) { return d.key; });
|
|
|
|
// Enter
|
|
var enter = signs.enter()
|
|
.append('foreignObject')
|
|
.attr('class', 'icon-sign')
|
|
.attr('width', '32px') // for Firefox
|
|
.attr('height', '32px') // for Firefox
|
|
.classed('selected', function(d) { return d.key === imageKey; })
|
|
.on('click', click);
|
|
|
|
enter
|
|
.append('xhtml:body')
|
|
.html(mapillary.signHTML);
|
|
|
|
// Exit
|
|
signs.exit()
|
|
.remove();
|
|
|
|
// Update
|
|
signs
|
|
.attr('transform', iD.svg.PointTransform(projection));
|
|
}
|
|
|
|
function drawSigns(selection) {
|
|
var enabled = iD.svg.MapillarySigns.enabled,
|
|
mapillary = getMapillary();
|
|
|
|
layer = selection.selectAll('.layer-mapillary-signs')
|
|
.data(mapillary ? [0] : []);
|
|
|
|
layer.enter()
|
|
.append('g')
|
|
.attr('class', 'layer-mapillary-signs')
|
|
.style('display', enabled ? 'block' : 'none')
|
|
.attr('transform', 'translate(-16, -16)'); // center signs on loc
|
|
|
|
layer.exit()
|
|
.remove();
|
|
|
|
if (enabled) {
|
|
if (mapillary && ~~context.map().zoom() >= minZoom) {
|
|
editOn();
|
|
update();
|
|
mapillary.loadSigns(context, projection, layer.dimensions());
|
|
} else {
|
|
editOff();
|
|
}
|
|
}
|
|
}
|
|
|
|
drawSigns.enabled = function(_) {
|
|
if (!arguments.length) return iD.svg.MapillarySigns.enabled;
|
|
iD.svg.MapillarySigns.enabled = _;
|
|
if (iD.svg.MapillarySigns.enabled) {
|
|
showLayer();
|
|
} else {
|
|
hideLayer();
|
|
}
|
|
dispatch.change();
|
|
return this;
|
|
};
|
|
|
|
drawSigns.supported = function() {
|
|
var mapillary = getMapillary();
|
|
return (mapillary && mapillary.signsSupported());
|
|
};
|
|
|
|
drawSigns.dimensions = function(_) {
|
|
if (!arguments.length) return layer.dimensions();
|
|
layer.dimensions(_);
|
|
return this;
|
|
};
|
|
|
|
init();
|
|
return drawSigns;
|
|
}
|
|
|
|
function Midpoints(projection, context) {
|
|
return function drawMidpoints(surface, graph, entities, filter, extent) {
|
|
var poly = extent.polygon(),
|
|
midpoints = {};
|
|
|
|
for (var i = 0; i < entities.length; i++) {
|
|
var entity = entities[i];
|
|
|
|
if (entity.type !== 'way')
|
|
continue;
|
|
if (!filter(entity))
|
|
continue;
|
|
if (context.selectedIDs().indexOf(entity.id) < 0)
|
|
continue;
|
|
|
|
var nodes = graph.childNodes(entity);
|
|
for (var j = 0; j < nodes.length - 1; j++) {
|
|
|
|
var a = nodes[j],
|
|
b = nodes[j + 1],
|
|
id = [a.id, b.id].sort().join('-');
|
|
|
|
if (midpoints[id]) {
|
|
midpoints[id].parents.push(entity);
|
|
} else {
|
|
if (iD.geo.euclideanDistance(projection(a.loc), projection(b.loc)) > 40) {
|
|
var point = iD.geo.interp(a.loc, b.loc, 0.5),
|
|
loc = null;
|
|
|
|
if (extent.intersects(point)) {
|
|
loc = point;
|
|
} else {
|
|
for (var k = 0; k < 4; k++) {
|
|
point = iD.geo.lineIntersection([a.loc, b.loc], [poly[k], poly[k+1]]);
|
|
if (point &&
|
|
iD.geo.euclideanDistance(projection(a.loc), projection(point)) > 20 &&
|
|
iD.geo.euclideanDistance(projection(b.loc), projection(point)) > 20)
|
|
{
|
|
loc = point;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (loc) {
|
|
midpoints[id] = {
|
|
type: 'midpoint',
|
|
id: id,
|
|
loc: loc,
|
|
edge: [a.id, b.id],
|
|
parents: [entity]
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function midpointFilter(d) {
|
|
if (midpoints[d.id])
|
|
return true;
|
|
|
|
for (var i = 0; i < d.parents.length; i++)
|
|
if (filter(d.parents[i]))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
var groups = surface.selectAll('.layer-hit').selectAll('g.midpoint')
|
|
.filter(midpointFilter)
|
|
.data(_.values(midpoints), function(d) { return d.id; });
|
|
|
|
var enter = groups.enter()
|
|
.insert('g', ':first-child')
|
|
.attr('class', 'midpoint');
|
|
|
|
enter.append('polygon')
|
|
.attr('points', '-6,8 10,0 -6,-8')
|
|
.attr('class', 'shadow');
|
|
|
|
enter.append('polygon')
|
|
.attr('points', '-3,4 5,0 -3,-4')
|
|
.attr('class', 'fill');
|
|
|
|
groups
|
|
.attr('transform', function(d) {
|
|
var translate = iD.svg.PointTransform(projection),
|
|
a = context.entity(d.edge[0]),
|
|
b = context.entity(d.edge[1]),
|
|
angle = Math.round(iD.geo.angle(a, b, projection) * (180 / Math.PI));
|
|
return translate(d) + ' rotate(' + angle + ')';
|
|
})
|
|
.call(iD.svg.TagClasses().tags(
|
|
function(d) { return d.parents[0].tags; }
|
|
));
|
|
|
|
// Propagate data bindings.
|
|
groups.select('polygon.shadow');
|
|
groups.select('polygon.fill');
|
|
|
|
groups.exit()
|
|
.remove();
|
|
};
|
|
}
|
|
|
|
function OneWaySegments(projection, graph, dt) {
|
|
return function(entity) {
|
|
var a,
|
|
b,
|
|
i = 0,
|
|
offset = dt,
|
|
segments = [],
|
|
clip = d3.geo.clipExtent().extent(projection.clipExtent()).stream,
|
|
coordinates = graph.childNodes(entity).map(function(n) {
|
|
return n.loc;
|
|
});
|
|
|
|
if (entity.tags.oneway === '-1') coordinates.reverse();
|
|
|
|
d3.geo.stream({
|
|
type: 'LineString',
|
|
coordinates: coordinates
|
|
}, projection.stream(clip({
|
|
lineStart: function() {},
|
|
lineEnd: function() {
|
|
a = null;
|
|
},
|
|
point: function(x, y) {
|
|
b = [x, y];
|
|
|
|
if (a) {
|
|
var span = iD.geo.euclideanDistance(a, b) - offset;
|
|
|
|
if (span >= 0) {
|
|
var angle = Math.atan2(b[1] - a[1], b[0] - a[0]),
|
|
dx = dt * Math.cos(angle),
|
|
dy = dt * Math.sin(angle),
|
|
p = [a[0] + offset * Math.cos(angle),
|
|
a[1] + offset * Math.sin(angle)];
|
|
|
|
var segment = 'M' + a[0] + ',' + a[1] +
|
|
'L' + p[0] + ',' + p[1];
|
|
|
|
for (span -= dt; span >= 0; span -= dt) {
|
|
p[0] += dx;
|
|
p[1] += dy;
|
|
segment += 'L' + p[0] + ',' + p[1];
|
|
}
|
|
|
|
segment += 'L' + b[0] + ',' + b[1];
|
|
segments.push({id: entity.id, index: i, d: segment});
|
|
}
|
|
|
|
offset = -span;
|
|
i++;
|
|
}
|
|
|
|
a = b;
|
|
}
|
|
})));
|
|
|
|
return segments;
|
|
};
|
|
}
|
|
|
|
function Osm() {
|
|
return function drawOsm(selection) {
|
|
var layers = selection.selectAll('.layer-osm')
|
|
.data(['areas', 'lines', 'hit', 'halo', 'label']);
|
|
|
|
layers.enter().append('g')
|
|
.attr('class', function(d) { return 'layer-osm layer-' + d; });
|
|
};
|
|
}
|
|
|
|
function Path(projection, graph, polygon) {
|
|
var cache = {},
|
|
clip = d3.geo.clipExtent().extent(projection.clipExtent()).stream,
|
|
project = projection.stream,
|
|
path = d3.geo.path()
|
|
.projection({stream: function(output) { return polygon ? project(output) : project(clip(output)); }});
|
|
|
|
return function(entity) {
|
|
if (entity.id in cache) {
|
|
return cache[entity.id];
|
|
} else {
|
|
return cache[entity.id] = path(entity.asGeoJSON(graph));
|
|
}
|
|
};
|
|
}
|
|
|
|
function PointTransform(projection) {
|
|
return function(entity) {
|
|
// http://jsperf.com/short-array-join
|
|
var pt = projection(entity.loc);
|
|
return 'translate(' + pt[0] + ',' + pt[1] + ')';
|
|
};
|
|
}
|
|
|
|
function Points(projection, context) {
|
|
function markerPath(selection, klass) {
|
|
selection
|
|
.attr('class', klass)
|
|
.attr('transform', 'translate(-8, -23)')
|
|
.attr('d', 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z');
|
|
}
|
|
|
|
function sortY(a, b) {
|
|
return b.loc[1] - a.loc[1];
|
|
}
|
|
|
|
return function drawPoints(surface, graph, entities, filter) {
|
|
var wireframe = surface.classed('fill-wireframe'),
|
|
points = wireframe ? [] : _.filter(entities, function(e) {
|
|
return e.geometry(graph) === 'point';
|
|
});
|
|
|
|
points.sort(sortY);
|
|
|
|
var groups = surface.selectAll('.layer-hit').selectAll('g.point')
|
|
.filter(filter)
|
|
.data(points, iD.Entity.key);
|
|
|
|
var group = groups.enter()
|
|
.append('g')
|
|
.attr('class', function(d) { return 'node point ' + d.id; })
|
|
.order();
|
|
|
|
group.append('path')
|
|
.call(markerPath, 'shadow');
|
|
|
|
group.append('path')
|
|
.call(markerPath, 'stroke');
|
|
|
|
group.append('use')
|
|
.attr('transform', 'translate(-6, -20)')
|
|
.attr('class', 'icon')
|
|
.attr('width', '12px')
|
|
.attr('height', '12px');
|
|
|
|
groups.attr('transform', iD.svg.PointTransform(projection))
|
|
.call(iD.svg.TagClasses());
|
|
|
|
// Selecting the following implicitly
|
|
// sets the data (point entity) on the element
|
|
groups.select('.shadow');
|
|
groups.select('.stroke');
|
|
groups.select('.icon')
|
|
.attr('xlink:href', function(entity) {
|
|
var preset = context.presets().match(entity, graph);
|
|
return preset.icon ? '#' + preset.icon + '-12' : '';
|
|
});
|
|
|
|
groups.exit()
|
|
.remove();
|
|
};
|
|
}
|
|
|
|
function RelationMemberTags(graph) {
|
|
return function(entity) {
|
|
var tags = entity.tags;
|
|
graph.parentRelations(entity).forEach(function(relation) {
|
|
var type = relation.tags.type;
|
|
if (type === 'multipolygon' || type === 'boundary') {
|
|
tags = _.extend({}, relation.tags, tags);
|
|
}
|
|
});
|
|
return tags;
|
|
};
|
|
}
|
|
|
|
function TagClasses() {
|
|
var primaries = [
|
|
'building', 'highway', 'railway', 'waterway', 'aeroway',
|
|
'motorway', 'boundary', 'power', 'amenity', 'natural', 'landuse',
|
|
'leisure', 'place'
|
|
],
|
|
statuses = [
|
|
'proposed', 'construction', 'disused', 'abandoned', 'dismantled',
|
|
'razed', 'demolished', 'obliterated'
|
|
],
|
|
secondaries = [
|
|
'oneway', 'bridge', 'tunnel', 'embankment', 'cutting', 'barrier',
|
|
'surface', 'tracktype', 'crossing'
|
|
],
|
|
tagClassRe = /^tag-/,
|
|
tags = function(entity) { return entity.tags; };
|
|
|
|
|
|
var tagClasses = function(selection) {
|
|
selection.each(function tagClassesEach(entity) {
|
|
var value = this.className,
|
|
classes, primary, status;
|
|
|
|
if (value.baseVal !== undefined) value = value.baseVal;
|
|
|
|
classes = value.trim().split(/\s+/).filter(function(name) {
|
|
return name.length && !tagClassRe.test(name);
|
|
}).join(' ');
|
|
|
|
var t = tags(entity), i, k, v;
|
|
|
|
// pick at most one primary classification tag..
|
|
for (i = 0; i < primaries.length; i++) {
|
|
k = primaries[i];
|
|
v = t[k];
|
|
if (!v || v === 'no') continue;
|
|
|
|
primary = k;
|
|
if (statuses.indexOf(v) !== -1) { // e.g. `railway=abandoned`
|
|
status = v;
|
|
classes += ' tag-' + k;
|
|
} else {
|
|
classes += ' tag-' + k + ' tag-' + k + '-' + v;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// add at most one status tag, only if relates to primary tag..
|
|
if (!status) {
|
|
for (i = 0; i < statuses.length; i++) {
|
|
k = statuses[i];
|
|
v = t[k];
|
|
if (!v || v === 'no') continue;
|
|
|
|
if (v === 'yes') { // e.g. `railway=rail + abandoned=yes`
|
|
status = k;
|
|
}
|
|
else if (primary && primary === v) { // e.g. `railway=rail + abandoned=railway`
|
|
status = k;
|
|
} else if (!primary && primaries.indexOf(v) !== -1) { // e.g. `abandoned=railway`
|
|
status = k;
|
|
primary = v;
|
|
classes += ' tag-' + v;
|
|
} // else ignore e.g. `highway=path + abandoned=railway`
|
|
|
|
if (status) break;
|
|
}
|
|
}
|
|
|
|
if (status) {
|
|
classes += ' tag-status tag-status-' + status;
|
|
}
|
|
|
|
// add any secondary (structure) tags
|
|
for (i = 0; i < secondaries.length; i++) {
|
|
k = secondaries[i];
|
|
v = t[k];
|
|
if (!v || v === 'no') continue;
|
|
classes += ' tag-' + k + ' tag-' + k + '-' + v;
|
|
}
|
|
|
|
// For highways, look for surface tagging..
|
|
if (primary === 'highway') {
|
|
var paved = (t.highway !== 'track');
|
|
for (k in t) {
|
|
v = t[k];
|
|
if (k in iD.pavedTags) {
|
|
paved = !!iD.pavedTags[k][v];
|
|
break;
|
|
}
|
|
}
|
|
if (!paved) {
|
|
classes += ' tag-unpaved';
|
|
}
|
|
}
|
|
|
|
classes = classes.trim();
|
|
|
|
if (classes !== value) {
|
|
d3.select(this).attr('class', classes);
|
|
}
|
|
});
|
|
};
|
|
|
|
tagClasses.tags = function(_) {
|
|
if (!arguments.length) return tags;
|
|
tags = _;
|
|
return tagClasses;
|
|
};
|
|
|
|
return tagClasses;
|
|
}
|
|
|
|
function Turns(projection) {
|
|
return function drawTurns(surface, graph, turns) {
|
|
function key(turn) {
|
|
return [turn.from.node + turn.via.node + turn.to.node].join('-');
|
|
}
|
|
|
|
function icon(turn) {
|
|
var u = turn.u ? '-u' : '';
|
|
if (!turn.restriction)
|
|
return '#turn-yes' + u;
|
|
var restriction = graph.entity(turn.restriction).tags.restriction;
|
|
return '#turn-' +
|
|
(!turn.indirect_restriction && /^only_/.test(restriction) ? 'only' : 'no') + u;
|
|
}
|
|
|
|
var groups = surface.selectAll('.layer-hit').selectAll('g.turn')
|
|
.data(turns, key);
|
|
|
|
// Enter
|
|
var enter = groups.enter().append('g')
|
|
.attr('class', 'turn');
|
|
|
|
var nEnter = enter.filter(function (turn) { return !turn.u; });
|
|
|
|
nEnter.append('rect')
|
|
.attr('transform', 'translate(-22, -12)')
|
|
.attr('width', '44')
|
|
.attr('height', '24');
|
|
|
|
nEnter.append('use')
|
|
.attr('transform', 'translate(-22, -12)')
|
|
.attr('width', '44')
|
|
.attr('height', '24');
|
|
|
|
|
|
var uEnter = enter.filter(function (turn) { return turn.u; });
|
|
|
|
uEnter.append('circle')
|
|
.attr('r', '16');
|
|
|
|
uEnter.append('use')
|
|
.attr('transform', 'translate(-16, -16)')
|
|
.attr('width', '32')
|
|
.attr('height', '32');
|
|
|
|
|
|
// Update
|
|
groups
|
|
.attr('transform', function (turn) {
|
|
var v = graph.entity(turn.via.node),
|
|
t = graph.entity(turn.to.node),
|
|
a = iD.geo.angle(v, t, projection),
|
|
p = projection(v.loc),
|
|
r = turn.u ? 0 : 60;
|
|
|
|
return 'translate(' + (r * Math.cos(a) + p[0]) + ',' + (r * Math.sin(a) + p[1]) + ') ' +
|
|
'rotate(' + a * 180 / Math.PI + ')';
|
|
});
|
|
|
|
groups.select('use')
|
|
.attr('xlink:href', icon);
|
|
|
|
groups.select('rect');
|
|
groups.select('circle');
|
|
|
|
|
|
// Exit
|
|
groups.exit()
|
|
.remove();
|
|
|
|
return this;
|
|
};
|
|
}
|
|
|
|
function Vertices(projection, context) {
|
|
var radiuses = {
|
|
// z16-, z17, z18+, tagged
|
|
shadow: [6, 7.5, 7.5, 11.5],
|
|
stroke: [2.5, 3.5, 3.5, 7],
|
|
fill: [1, 1.5, 1.5, 1.5]
|
|
};
|
|
|
|
var hover;
|
|
|
|
function siblingAndChildVertices(ids, graph, extent) {
|
|
var vertices = {};
|
|
|
|
function addChildVertices(entity) {
|
|
if (!context.features().isHiddenFeature(entity, graph, entity.geometry(graph))) {
|
|
var i;
|
|
if (entity.type === 'way') {
|
|
for (i = 0; i < entity.nodes.length; i++) {
|
|
addChildVertices(graph.entity(entity.nodes[i]));
|
|
}
|
|
} else if (entity.type === 'relation') {
|
|
for (i = 0; i < entity.members.length; i++) {
|
|
var member = context.hasEntity(entity.members[i].id);
|
|
if (member) {
|
|
addChildVertices(member);
|
|
}
|
|
}
|
|
} else if (entity.intersects(extent, graph)) {
|
|
vertices[entity.id] = entity;
|
|
}
|
|
}
|
|
}
|
|
|
|
ids.forEach(function(id) {
|
|
var entity = context.hasEntity(id);
|
|
if (entity && entity.type === 'node') {
|
|
vertices[entity.id] = entity;
|
|
context.graph().parentWays(entity).forEach(function(entity) {
|
|
addChildVertices(entity);
|
|
});
|
|
} else if (entity) {
|
|
addChildVertices(entity);
|
|
}
|
|
});
|
|
|
|
return vertices;
|
|
}
|
|
|
|
function draw(selection, vertices, klass, graph, zoom) {
|
|
var icons = {},
|
|
z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2);
|
|
|
|
var groups = selection
|
|
.data(vertices, iD.Entity.key);
|
|
|
|
function icon(entity) {
|
|
if (entity.id in icons) return icons[entity.id];
|
|
icons[entity.id] =
|
|
entity.hasInterestingTags() &&
|
|
context.presets().match(entity, graph).icon;
|
|
return icons[entity.id];
|
|
}
|
|
|
|
function setClass(klass) {
|
|
return function(entity) {
|
|
this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id);
|
|
};
|
|
}
|
|
|
|
function setAttributes(selection) {
|
|
['shadow','stroke','fill'].forEach(function(klass) {
|
|
var rads = radiuses[klass];
|
|
selection.selectAll('.' + klass)
|
|
.each(function(entity) {
|
|
var i = z && icon(entity),
|
|
c = i ? 0.5 : 0,
|
|
r = rads[i ? 3 : z];
|
|
this.setAttribute('cx', c);
|
|
this.setAttribute('cy', -c);
|
|
this.setAttribute('r', r);
|
|
if (i && klass === 'fill') {
|
|
this.setAttribute('visibility', 'hidden');
|
|
} else {
|
|
this.removeAttribute('visibility');
|
|
}
|
|
});
|
|
});
|
|
|
|
selection.selectAll('use')
|
|
.each(function() {
|
|
if (z) {
|
|
this.removeAttribute('visibility');
|
|
} else {
|
|
this.setAttribute('visibility', 'hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
var enter = groups.enter()
|
|
.append('g')
|
|
.attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; });
|
|
|
|
enter.append('circle')
|
|
.each(setClass('shadow'));
|
|
|
|
enter.append('circle')
|
|
.each(setClass('stroke'));
|
|
|
|
// Vertices with icons get a `use`.
|
|
enter.filter(function(d) { return icon(d); })
|
|
.append('use')
|
|
.attr('transform', 'translate(-6, -6)')
|
|
.attr('xlink:href', function(d) { return '#' + icon(d) + '-12'; })
|
|
.attr('width', '12px')
|
|
.attr('height', '12px')
|
|
.each(setClass('icon'));
|
|
|
|
// Vertices with tags get a fill.
|
|
enter.filter(function(d) { return d.hasInterestingTags(); })
|
|
.append('circle')
|
|
.each(setClass('fill'));
|
|
|
|
groups
|
|
.attr('transform', iD.svg.PointTransform(projection))
|
|
.classed('shared', function(entity) { return graph.isShared(entity); })
|
|
.call(setAttributes);
|
|
|
|
groups.exit()
|
|
.remove();
|
|
}
|
|
|
|
function drawVertices(surface, graph, entities, filter, extent, zoom) {
|
|
var selected = siblingAndChildVertices(context.selectedIDs(), graph, extent),
|
|
wireframe = surface.classed('fill-wireframe'),
|
|
vertices = [];
|
|
|
|
for (var i = 0; i < entities.length; i++) {
|
|
var entity = entities[i],
|
|
geometry = entity.geometry(graph);
|
|
|
|
if (wireframe && geometry === 'point') {
|
|
vertices.push(entity);
|
|
continue;
|
|
}
|
|
|
|
if (geometry !== 'vertex')
|
|
continue;
|
|
|
|
if (entity.id in selected ||
|
|
entity.hasInterestingTags() ||
|
|
entity.isIntersection(graph)) {
|
|
vertices.push(entity);
|
|
}
|
|
}
|
|
|
|
surface.selectAll('.layer-hit').selectAll('g.vertex.vertex-persistent')
|
|
.filter(filter)
|
|
.call(draw, vertices, 'vertex-persistent', graph, zoom);
|
|
|
|
drawHover(surface, graph, extent, zoom);
|
|
}
|
|
|
|
function drawHover(surface, graph, extent, zoom) {
|
|
var hovered = hover ? siblingAndChildVertices([hover.id], graph, extent) : {};
|
|
|
|
surface.selectAll('.layer-hit').selectAll('g.vertex.vertex-hover')
|
|
.call(draw, d3.values(hovered), 'vertex-hover', graph, zoom);
|
|
}
|
|
|
|
drawVertices.drawHover = function(surface, graph, target, extent, zoom) {
|
|
if (target === hover) return;
|
|
hover = target;
|
|
drawHover(surface, graph, extent, zoom);
|
|
};
|
|
|
|
return drawVertices;
|
|
}
|
|
|
|
exports.Areas = Areas;
|
|
exports.Debug = Debug;
|
|
exports.Defs = Defs;
|
|
exports.Gpx = Gpx;
|
|
exports.Icon = Icon;
|
|
exports.Labels = Labels;
|
|
exports.Layers = Layers;
|
|
exports.Lines = Lines;
|
|
exports.MapillaryImages = MapillaryImages;
|
|
exports.MapillarySigns = MapillarySigns;
|
|
exports.Midpoints = Midpoints;
|
|
exports.OneWaySegments = OneWaySegments;
|
|
exports.Osm = Osm;
|
|
exports.Path = Path;
|
|
exports.PointTransform = PointTransform;
|
|
exports.Points = Points;
|
|
exports.RelationMemberTags = RelationMemberTags;
|
|
exports.TagClasses = TagClasses;
|
|
exports.Turns = Turns;
|
|
exports.Vertices = Vertices;
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
})); |