mirror of
https://github.com/FoggedLens/iD.git
synced 2026-03-19 09:33:32 +00:00
1587 lines
52 KiB
JavaScript
1587 lines
52 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 || {})));
|
|
}(this, function (exports) { 'use strict';
|
|
|
|
function BackgroundSource(data) {
|
|
var source = _.clone(data),
|
|
offset = [0, 0],
|
|
name = source.name,
|
|
best = !!source.best;
|
|
|
|
source.scaleExtent = data.scaleExtent || [0, 20];
|
|
source.overzoom = data.overzoom !== false;
|
|
|
|
source.offset = function(_) {
|
|
if (!arguments.length) return offset;
|
|
offset = _;
|
|
return source;
|
|
};
|
|
|
|
source.nudge = function(_, zoomlevel) {
|
|
offset[0] += _[0] / Math.pow(2, zoomlevel);
|
|
offset[1] += _[1] / Math.pow(2, zoomlevel);
|
|
return source;
|
|
};
|
|
|
|
source.name = function() {
|
|
return name;
|
|
};
|
|
|
|
source.best = function() {
|
|
return best;
|
|
};
|
|
|
|
source.area = function() {
|
|
if (!data.polygon) return Number.MAX_VALUE; // worldwide
|
|
var area = d3.geo.area({ type: 'MultiPolygon', coordinates: [ data.polygon ] });
|
|
return isNaN(area) ? 0 : area;
|
|
};
|
|
|
|
source.imageryUsed = function() {
|
|
return source.id || name;
|
|
};
|
|
|
|
source.url = function(coord) {
|
|
return data.template
|
|
.replace('{x}', coord[0])
|
|
.replace('{y}', coord[1])
|
|
// TMS-flipped y coordinate
|
|
.replace(/\{[t-]y\}/, Math.pow(2, coord[2]) - coord[1] - 1)
|
|
.replace(/\{z(oom)?\}/, coord[2])
|
|
.replace(/\{switch:([^}]+)\}/, function(s, r) {
|
|
var subdomains = r.split(',');
|
|
return subdomains[(coord[0] + coord[1]) % subdomains.length];
|
|
})
|
|
.replace('{u}', function() {
|
|
var u = '';
|
|
for (var zoom = coord[2]; zoom > 0; zoom--) {
|
|
var b = 0;
|
|
var mask = 1 << (zoom - 1);
|
|
if ((coord[0] & mask) !== 0) b++;
|
|
if ((coord[1] & mask) !== 0) b += 2;
|
|
u += b.toString();
|
|
}
|
|
return u;
|
|
});
|
|
};
|
|
|
|
source.intersects = function(extent) {
|
|
extent = extent.polygon();
|
|
return !data.polygon || data.polygon.some(function(polygon) {
|
|
return iD.geo.polygonIntersectsPolygon(polygon, extent, true);
|
|
});
|
|
};
|
|
|
|
source.validZoom = function(z) {
|
|
return source.scaleExtent[0] <= z &&
|
|
(source.overzoom || source.scaleExtent[1] > z);
|
|
};
|
|
|
|
source.isLocatorOverlay = function() {
|
|
return name === 'Locator Overlay';
|
|
};
|
|
|
|
source.copyrightNotices = function() {};
|
|
|
|
return source;
|
|
}
|
|
|
|
BackgroundSource.Bing = function(data, dispatch) {
|
|
// http://msdn.microsoft.com/en-us/library/ff701716.aspx
|
|
// http://msdn.microsoft.com/en-us/library/ff701701.aspx
|
|
|
|
data.template = 'https://ecn.t{switch:0,1,2,3}.tiles.virtualearth.net/tiles/a{u}.jpeg?g=587&mkt=en-gb&n=z';
|
|
|
|
var bing = BackgroundSource(data),
|
|
key = 'Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU', // Same as P2 and JOSM
|
|
url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&key=' +
|
|
key + '&jsonp={callback}',
|
|
providers = [];
|
|
|
|
d3.jsonp(url, function(json) {
|
|
providers = json.resourceSets[0].resources[0].imageryProviders.map(function(provider) {
|
|
return {
|
|
attribution: provider.attribution,
|
|
areas: provider.coverageAreas.map(function(area) {
|
|
return {
|
|
zoom: [area.zoomMin, area.zoomMax],
|
|
extent: iD.geo.Extent([area.bbox[1], area.bbox[0]], [area.bbox[3], area.bbox[2]])
|
|
};
|
|
})
|
|
};
|
|
});
|
|
dispatch.change();
|
|
});
|
|
|
|
bing.copyrightNotices = function(zoom, extent) {
|
|
zoom = Math.min(zoom, 21);
|
|
return providers.filter(function(provider) {
|
|
return _.some(provider.areas, function(area) {
|
|
return extent.intersects(area.extent) &&
|
|
area.zoom[0] <= zoom &&
|
|
area.zoom[1] >= zoom;
|
|
});
|
|
}).map(function(provider) {
|
|
return provider.attribution;
|
|
}).join(', ');
|
|
};
|
|
|
|
bing.logo = 'bing_maps.png';
|
|
bing.terms_url = 'https://blog.openstreetmap.org/2010/11/30/microsoft-imagery-details';
|
|
|
|
return bing;
|
|
};
|
|
|
|
BackgroundSource.None = function() {
|
|
var source = BackgroundSource({id: 'none', template: ''});
|
|
|
|
source.name = function() {
|
|
return t('background.none');
|
|
};
|
|
|
|
source.imageryUsed = function() {
|
|
return 'None';
|
|
};
|
|
|
|
source.area = function() {
|
|
return -1;
|
|
};
|
|
|
|
return source;
|
|
};
|
|
|
|
BackgroundSource.Custom = function(template) {
|
|
var source = BackgroundSource({id: 'custom', template: template});
|
|
|
|
source.name = function() {
|
|
return t('background.custom');
|
|
};
|
|
|
|
source.imageryUsed = function() {
|
|
return 'Custom (' + template + ')';
|
|
};
|
|
|
|
source.area = function() {
|
|
return -2;
|
|
};
|
|
|
|
return source;
|
|
};
|
|
|
|
function TileLayer(context) {
|
|
var tileSize = 256,
|
|
tile = d3.geo.tile(),
|
|
projection,
|
|
cache = {},
|
|
tileOrigin,
|
|
z,
|
|
transformProp = iD.util.prefixCSSProperty('Transform'),
|
|
source = d3.functor('');
|
|
|
|
|
|
// blacklist overlay tiles around Null Island..
|
|
function nearNullIsland(x, y, z) {
|
|
if (z >= 7) {
|
|
var center = Math.pow(2, z - 1),
|
|
width = Math.pow(2, z - 6),
|
|
min = center - (width / 2),
|
|
max = center + (width / 2) - 1;
|
|
return x >= min && x <= max && y >= min && y <= max;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function tileSizeAtZoom(d, z) {
|
|
var epsilon = 0.002;
|
|
return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + epsilon;
|
|
}
|
|
|
|
function atZoom(t, distance) {
|
|
var power = Math.pow(2, distance);
|
|
return [
|
|
Math.floor(t[0] * power),
|
|
Math.floor(t[1] * power),
|
|
t[2] + distance];
|
|
}
|
|
|
|
function lookUp(d) {
|
|
for (var up = -1; up > -d[2]; up--) {
|
|
var tile = atZoom(d, up);
|
|
if (cache[source.url(tile)] !== false) {
|
|
return tile;
|
|
}
|
|
}
|
|
}
|
|
|
|
function uniqueBy(a, n) {
|
|
var o = [], seen = {};
|
|
for (var i = 0; i < a.length; i++) {
|
|
if (seen[a[i][n]] === undefined) {
|
|
o.push(a[i]);
|
|
seen[a[i][n]] = true;
|
|
}
|
|
}
|
|
return o;
|
|
}
|
|
|
|
function addSource(d) {
|
|
d.push(source.url(d));
|
|
return d;
|
|
}
|
|
|
|
// Update tiles based on current state of `projection`.
|
|
function background(selection) {
|
|
tile.scale(projection.scale() * 2 * Math.PI)
|
|
.translate(projection.translate());
|
|
|
|
tileOrigin = [
|
|
projection.scale() * Math.PI - projection.translate()[0],
|
|
projection.scale() * Math.PI - projection.translate()[1]];
|
|
|
|
z = Math.max(Math.log(projection.scale() * 2 * Math.PI) / Math.log(2) - 8, 0);
|
|
|
|
render(selection);
|
|
}
|
|
|
|
// Derive the tiles onscreen, remove those offscreen and position them.
|
|
// Important that this part not depend on `projection` because it's
|
|
// rentered when tiles load/error (see #644).
|
|
function render(selection) {
|
|
var requests = [];
|
|
var showDebug = context.getDebug('tile') && !source.overlay;
|
|
|
|
if (source.validZoom(z)) {
|
|
tile().forEach(function(d) {
|
|
addSource(d);
|
|
if (d[3] === '') return;
|
|
if (typeof d[3] !== 'string') return; // Workaround for chrome crash https://github.com/openstreetmap/iD/issues/2295
|
|
requests.push(d);
|
|
if (cache[d[3]] === false && lookUp(d)) {
|
|
requests.push(addSource(lookUp(d)));
|
|
}
|
|
});
|
|
|
|
requests = uniqueBy(requests, 3).filter(function(r) {
|
|
if (!!source.overlay && nearNullIsland(r[0], r[1], r[2])) {
|
|
return false;
|
|
}
|
|
// don't re-request tiles which have failed in the past
|
|
return cache[r[3]] !== false;
|
|
});
|
|
}
|
|
|
|
var pixelOffset = [
|
|
source.offset()[0] * Math.pow(2, z),
|
|
source.offset()[1] * Math.pow(2, z)
|
|
];
|
|
|
|
function load(d) {
|
|
cache[d[3]] = true;
|
|
d3.select(this)
|
|
.on('error', null)
|
|
.on('load', null)
|
|
.classed('tile-loaded', true);
|
|
render(selection);
|
|
}
|
|
|
|
function error(d) {
|
|
cache[d[3]] = false;
|
|
d3.select(this)
|
|
.on('error', null)
|
|
.on('load', null)
|
|
.remove();
|
|
render(selection);
|
|
}
|
|
|
|
function imageTransform(d) {
|
|
var _ts = tileSize * Math.pow(2, z - d[2]);
|
|
var scale = tileSizeAtZoom(d, z);
|
|
return 'translate(' +
|
|
((d[0] * _ts) - tileOrigin[0] + pixelOffset[0]) + 'px,' +
|
|
((d[1] * _ts) - tileOrigin[1] + pixelOffset[1]) + 'px)' +
|
|
'scale(' + scale + ',' + scale + ')';
|
|
}
|
|
|
|
function debugTransform(d) {
|
|
var _ts = tileSize * Math.pow(2, z - d[2]);
|
|
var scale = tileSizeAtZoom(d, z);
|
|
return 'translate(' +
|
|
((d[0] * _ts) - tileOrigin[0] + pixelOffset[0] + scale * (tileSize / 4)) + 'px,' +
|
|
((d[1] * _ts) - tileOrigin[1] + pixelOffset[1] + scale * (tileSize / 2)) + 'px)';
|
|
}
|
|
|
|
var image = selection
|
|
.selectAll('img')
|
|
.data(requests, function(d) { return d[3]; });
|
|
|
|
image.exit()
|
|
.style(transformProp, imageTransform)
|
|
.classed('tile-removing', true)
|
|
.each(function() {
|
|
var tile = d3.select(this);
|
|
window.setTimeout(function() {
|
|
if (tile.classed('tile-removing')) {
|
|
tile.remove();
|
|
}
|
|
}, 300);
|
|
});
|
|
|
|
image.enter().append('img')
|
|
.attr('class', 'tile')
|
|
.attr('src', function(d) { return d[3]; })
|
|
.on('error', error)
|
|
.on('load', load);
|
|
|
|
image
|
|
.style(transformProp, imageTransform)
|
|
.classed('tile-debug', showDebug)
|
|
.classed('tile-removing', false);
|
|
|
|
|
|
var debug = selection.selectAll('.tile-label-debug')
|
|
.data(showDebug ? requests : [], function(d) { return d[3]; });
|
|
|
|
debug.exit()
|
|
.remove();
|
|
|
|
debug.enter()
|
|
.append('div')
|
|
.attr('class', 'tile-label-debug');
|
|
|
|
debug
|
|
.text(function(d) { return d[2] + ' / ' + d[0] + ' / ' + d[1]; })
|
|
.style(transformProp, debugTransform);
|
|
}
|
|
|
|
background.projection = function(_) {
|
|
if (!arguments.length) return projection;
|
|
projection = _;
|
|
return background;
|
|
};
|
|
|
|
background.dimensions = function(_) {
|
|
if (!arguments.length) return tile.size();
|
|
tile.size(_);
|
|
return background;
|
|
};
|
|
|
|
background.source = function(_) {
|
|
if (!arguments.length) return source;
|
|
source = _;
|
|
cache = {};
|
|
tile.scaleExtent(source.scaleExtent);
|
|
return background;
|
|
};
|
|
|
|
return background;
|
|
}
|
|
|
|
function Background(context) {
|
|
var dispatch = d3.dispatch('change'),
|
|
baseLayer = TileLayer(context).projection(context.projection),
|
|
overlayLayers = [],
|
|
backgroundSources;
|
|
|
|
|
|
function findSource(id) {
|
|
return _.find(backgroundSources, function(d) {
|
|
return d.id && d.id === id;
|
|
});
|
|
}
|
|
|
|
|
|
function background(selection) {
|
|
var base = selection.selectAll('.layer-background')
|
|
.data([0]);
|
|
|
|
base.enter()
|
|
.insert('div', '.layer-data')
|
|
.attr('class', 'layer layer-background');
|
|
|
|
base.call(baseLayer);
|
|
|
|
var overlays = selection.selectAll('.layer-overlay')
|
|
.data(overlayLayers, function(d) { return d.source().name(); });
|
|
|
|
overlays.enter()
|
|
.insert('div', '.layer-data')
|
|
.attr('class', 'layer layer-overlay');
|
|
|
|
overlays.each(function(layer) {
|
|
d3.select(this).call(layer);
|
|
});
|
|
|
|
overlays.exit()
|
|
.remove();
|
|
}
|
|
|
|
|
|
background.updateImagery = function() {
|
|
var b = background.baseLayerSource(),
|
|
o = overlayLayers.map(function (d) { return d.source().id; }).join(','),
|
|
meters = iD.geo.offsetToMeters(b.offset()),
|
|
epsilon = 0.01,
|
|
x = +meters[0].toFixed(2),
|
|
y = +meters[1].toFixed(2),
|
|
q = iD.util.stringQs(location.hash.substring(1));
|
|
|
|
var id = b.id;
|
|
if (id === 'custom') {
|
|
id = 'custom:' + b.template;
|
|
}
|
|
|
|
if (id) {
|
|
q.background = id;
|
|
} else {
|
|
delete q.background;
|
|
}
|
|
|
|
if (o) {
|
|
q.overlays = o;
|
|
} else {
|
|
delete q.overlays;
|
|
}
|
|
|
|
if (Math.abs(x) > epsilon || Math.abs(y) > epsilon) {
|
|
q.offset = x + ',' + y;
|
|
} else {
|
|
delete q.offset;
|
|
}
|
|
|
|
location.replace('#' + iD.util.qsString(q, true));
|
|
|
|
var imageryUsed = [b.imageryUsed()];
|
|
|
|
overlayLayers.forEach(function (d) {
|
|
var source = d.source();
|
|
if (!source.isLocatorOverlay()) {
|
|
imageryUsed.push(source.imageryUsed());
|
|
}
|
|
});
|
|
|
|
var gpx = context.layers().layer('gpx');
|
|
if (gpx && gpx.enabled() && gpx.hasGpx()) {
|
|
imageryUsed.push('Local GPX');
|
|
}
|
|
|
|
var mapillary_images = context.layers().layer('mapillary-images');
|
|
if (mapillary_images && mapillary_images.enabled()) {
|
|
imageryUsed.push('Mapillary Images');
|
|
}
|
|
|
|
var mapillary_signs = context.layers().layer('mapillary-signs');
|
|
if (mapillary_signs && mapillary_signs.enabled()) {
|
|
imageryUsed.push('Mapillary Signs');
|
|
}
|
|
|
|
context.history().imageryUsed(imageryUsed);
|
|
};
|
|
|
|
background.sources = function(extent) {
|
|
return backgroundSources.filter(function(source) {
|
|
return source.intersects(extent);
|
|
});
|
|
};
|
|
|
|
background.dimensions = function(_) {
|
|
baseLayer.dimensions(_);
|
|
|
|
overlayLayers.forEach(function(layer) {
|
|
layer.dimensions(_);
|
|
});
|
|
};
|
|
|
|
background.baseLayerSource = function(d) {
|
|
if (!arguments.length) return baseLayer.source();
|
|
baseLayer.source(d);
|
|
dispatch.change();
|
|
background.updateImagery();
|
|
return background;
|
|
};
|
|
|
|
background.bing = function() {
|
|
background.baseLayerSource(findSource('Bing'));
|
|
};
|
|
|
|
background.showsLayer = function(d) {
|
|
return d === baseLayer.source() ||
|
|
(d.id === 'custom' && baseLayer.source().id === 'custom') ||
|
|
overlayLayers.some(function(l) { return l.source() === d; });
|
|
};
|
|
|
|
background.overlayLayerSources = function() {
|
|
return overlayLayers.map(function (l) { return l.source(); });
|
|
};
|
|
|
|
background.toggleOverlayLayer = function(d) {
|
|
var layer;
|
|
|
|
for (var i = 0; i < overlayLayers.length; i++) {
|
|
layer = overlayLayers[i];
|
|
if (layer.source() === d) {
|
|
overlayLayers.splice(i, 1);
|
|
dispatch.change();
|
|
background.updateImagery();
|
|
return;
|
|
}
|
|
}
|
|
|
|
layer = TileLayer(context)
|
|
.source(d)
|
|
.projection(context.projection)
|
|
.dimensions(baseLayer.dimensions());
|
|
|
|
overlayLayers.push(layer);
|
|
dispatch.change();
|
|
background.updateImagery();
|
|
};
|
|
|
|
background.nudge = function(d, zoom) {
|
|
baseLayer.source().nudge(d, zoom);
|
|
dispatch.change();
|
|
background.updateImagery();
|
|
return background;
|
|
};
|
|
|
|
background.offset = function(d) {
|
|
if (!arguments.length) return baseLayer.source().offset();
|
|
baseLayer.source().offset(d);
|
|
dispatch.change();
|
|
background.updateImagery();
|
|
return background;
|
|
};
|
|
|
|
background.load = function(imagery) {
|
|
function parseMap(qmap) {
|
|
if (!qmap) return false;
|
|
var args = qmap.split('/').map(Number);
|
|
if (args.length < 3 || args.some(isNaN)) return false;
|
|
return iD.geo.Extent([args[1], args[2]]);
|
|
}
|
|
|
|
var q = iD.util.stringQs(location.hash.substring(1)),
|
|
chosen = q.background || q.layer,
|
|
extent = parseMap(q.map),
|
|
best;
|
|
|
|
backgroundSources = imagery.map(function(source) {
|
|
if (source.type === 'bing') {
|
|
return BackgroundSource.Bing(source, dispatch);
|
|
} else {
|
|
return BackgroundSource(source);
|
|
}
|
|
});
|
|
|
|
backgroundSources.unshift(BackgroundSource.None());
|
|
|
|
if (!chosen && extent) {
|
|
best = _.find(this.sources(extent), function(s) { return s.best(); });
|
|
}
|
|
|
|
if (chosen && chosen.indexOf('custom:') === 0) {
|
|
background.baseLayerSource(BackgroundSource.Custom(chosen.replace(/^custom:/, '')));
|
|
} else {
|
|
background.baseLayerSource(findSource(chosen) || best || findSource('Bing') || backgroundSources[1] || backgroundSources[0]);
|
|
}
|
|
|
|
var locator = _.find(backgroundSources, function(d) {
|
|
return d.overlay && d.default;
|
|
});
|
|
|
|
if (locator) {
|
|
background.toggleOverlayLayer(locator);
|
|
}
|
|
|
|
var overlays = (q.overlays || '').split(',');
|
|
overlays.forEach(function(overlay) {
|
|
overlay = findSource(overlay);
|
|
if (overlay) {
|
|
background.toggleOverlayLayer(overlay);
|
|
}
|
|
});
|
|
|
|
if (q.gpx) {
|
|
var gpx = context.layers().layer('gpx');
|
|
if (gpx) {
|
|
gpx.url(q.gpx);
|
|
}
|
|
}
|
|
|
|
if (q.offset) {
|
|
var offset = q.offset.replace(/;/g, ',').split(',').map(function(n) {
|
|
return !isNaN(n) && n;
|
|
});
|
|
|
|
if (offset.length === 2) {
|
|
background.offset(iD.geo.metersToOffset(offset));
|
|
}
|
|
}
|
|
};
|
|
|
|
return d3.rebind(background, dispatch, 'on');
|
|
}
|
|
|
|
function Features(context) {
|
|
var traffic_roads = {
|
|
'motorway': true,
|
|
'motorway_link': true,
|
|
'trunk': true,
|
|
'trunk_link': true,
|
|
'primary': true,
|
|
'primary_link': true,
|
|
'secondary': true,
|
|
'secondary_link': true,
|
|
'tertiary': true,
|
|
'tertiary_link': true,
|
|
'residential': true,
|
|
'unclassified': true,
|
|
'living_street': true
|
|
};
|
|
|
|
var service_roads = {
|
|
'service': true,
|
|
'road': true,
|
|
'track': true
|
|
};
|
|
|
|
var paths = {
|
|
'path': true,
|
|
'footway': true,
|
|
'cycleway': true,
|
|
'bridleway': true,
|
|
'steps': true,
|
|
'pedestrian': true,
|
|
'corridor': true
|
|
};
|
|
|
|
var past_futures = {
|
|
'proposed': true,
|
|
'construction': true,
|
|
'abandoned': true,
|
|
'dismantled': true,
|
|
'disused': true,
|
|
'razed': true,
|
|
'demolished': true,
|
|
'obliterated': true
|
|
};
|
|
|
|
var dispatch = d3.dispatch('change', 'redraw'),
|
|
_cullFactor = 1,
|
|
_cache = {},
|
|
_features = {},
|
|
_stats = {},
|
|
_keys = [],
|
|
_hidden = [];
|
|
|
|
function update() {
|
|
_hidden = features.hidden();
|
|
dispatch.change();
|
|
dispatch.redraw();
|
|
}
|
|
|
|
function defineFeature(k, filter, max) {
|
|
_keys.push(k);
|
|
_features[k] = {
|
|
filter: filter,
|
|
enabled: true, // whether the user wants it enabled..
|
|
count: 0,
|
|
currentMax: (max || Infinity),
|
|
defaultMax: (max || Infinity),
|
|
enable: function() { this.enabled = true; this.currentMax = this.defaultMax; },
|
|
disable: function() { this.enabled = false; this.currentMax = 0; },
|
|
hidden: function() { return !context.editable() || this.count > this.currentMax * _cullFactor; },
|
|
autoHidden: function() { return this.hidden() && this.currentMax > 0; }
|
|
};
|
|
}
|
|
|
|
|
|
defineFeature('points', function isPoint(entity, resolver, geometry) {
|
|
return geometry === 'point';
|
|
}, 200);
|
|
|
|
defineFeature('traffic_roads', function isTrafficRoad(entity) {
|
|
return traffic_roads[entity.tags.highway];
|
|
});
|
|
|
|
defineFeature('service_roads', function isServiceRoad(entity) {
|
|
return service_roads[entity.tags.highway];
|
|
});
|
|
|
|
defineFeature('paths', function isPath(entity) {
|
|
return paths[entity.tags.highway];
|
|
});
|
|
|
|
defineFeature('buildings', function isBuilding(entity) {
|
|
return (
|
|
!!entity.tags['building:part'] ||
|
|
(!!entity.tags.building && entity.tags.building !== 'no') ||
|
|
entity.tags.amenity === 'shelter' ||
|
|
entity.tags.parking === 'multi-storey' ||
|
|
entity.tags.parking === 'sheds' ||
|
|
entity.tags.parking === 'carports' ||
|
|
entity.tags.parking === 'garage_boxes'
|
|
);
|
|
}, 250);
|
|
|
|
defineFeature('landuse', function isLanduse(entity, resolver, geometry) {
|
|
return geometry === 'area' &&
|
|
!_features.buildings.filter(entity) &&
|
|
!_features.water.filter(entity);
|
|
});
|
|
|
|
defineFeature('boundaries', function isBoundary(entity) {
|
|
return !!entity.tags.boundary;
|
|
});
|
|
|
|
defineFeature('water', function isWater(entity) {
|
|
return (
|
|
!!entity.tags.waterway ||
|
|
entity.tags.natural === 'water' ||
|
|
entity.tags.natural === 'coastline' ||
|
|
entity.tags.natural === 'bay' ||
|
|
entity.tags.landuse === 'pond' ||
|
|
entity.tags.landuse === 'basin' ||
|
|
entity.tags.landuse === 'reservoir' ||
|
|
entity.tags.landuse === 'salt_pond'
|
|
);
|
|
});
|
|
|
|
defineFeature('rail', function isRail(entity) {
|
|
return (
|
|
!!entity.tags.railway ||
|
|
entity.tags.landuse === 'railway'
|
|
) && !(
|
|
traffic_roads[entity.tags.highway] ||
|
|
service_roads[entity.tags.highway] ||
|
|
paths[entity.tags.highway]
|
|
);
|
|
});
|
|
|
|
defineFeature('power', function isPower(entity) {
|
|
return !!entity.tags.power;
|
|
});
|
|
|
|
// contains a past/future tag, but not in active use as a road/path/cycleway/etc..
|
|
defineFeature('past_future', function isPastFuture(entity) {
|
|
if (
|
|
traffic_roads[entity.tags.highway] ||
|
|
service_roads[entity.tags.highway] ||
|
|
paths[entity.tags.highway]
|
|
) { return false; }
|
|
|
|
var strings = Object.keys(entity.tags);
|
|
|
|
for (var i = 0; i < strings.length; i++) {
|
|
var s = strings[i];
|
|
if (past_futures[s] || past_futures[entity.tags[s]]) { return true; }
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Lines or areas that don't match another feature filter.
|
|
// IMPORTANT: The 'others' feature must be the last one defined,
|
|
// so that code in getMatches can skip this test if `hasMatch = true`
|
|
defineFeature('others', function isOther(entity, resolver, geometry) {
|
|
return (geometry === 'line' || geometry === 'area');
|
|
});
|
|
|
|
|
|
function features() {}
|
|
|
|
features.features = function() {
|
|
return _features;
|
|
};
|
|
|
|
features.keys = function() {
|
|
return _keys;
|
|
};
|
|
|
|
features.enabled = function(k) {
|
|
if (!arguments.length) {
|
|
return _.filter(_keys, function(k) { return _features[k].enabled; });
|
|
}
|
|
return _features[k] && _features[k].enabled;
|
|
};
|
|
|
|
features.disabled = function(k) {
|
|
if (!arguments.length) {
|
|
return _.reject(_keys, function(k) { return _features[k].enabled; });
|
|
}
|
|
return _features[k] && !_features[k].enabled;
|
|
};
|
|
|
|
features.hidden = function(k) {
|
|
if (!arguments.length) {
|
|
return _.filter(_keys, function(k) { return _features[k].hidden(); });
|
|
}
|
|
return _features[k] && _features[k].hidden();
|
|
};
|
|
|
|
features.autoHidden = function(k) {
|
|
if (!arguments.length) {
|
|
return _.filter(_keys, function(k) { return _features[k].autoHidden(); });
|
|
}
|
|
return _features[k] && _features[k].autoHidden();
|
|
};
|
|
|
|
features.enable = function(k) {
|
|
if (_features[k] && !_features[k].enabled) {
|
|
_features[k].enable();
|
|
update();
|
|
}
|
|
};
|
|
|
|
features.disable = function(k) {
|
|
if (_features[k] && _features[k].enabled) {
|
|
_features[k].disable();
|
|
update();
|
|
}
|
|
};
|
|
|
|
features.toggle = function(k) {
|
|
if (_features[k]) {
|
|
(function(f) { return f.enabled ? f.disable() : f.enable(); }(_features[k]));
|
|
update();
|
|
}
|
|
};
|
|
|
|
features.resetStats = function() {
|
|
_.each(_features, function(f) { f.count = 0; });
|
|
dispatch.change();
|
|
};
|
|
|
|
features.gatherStats = function(d, resolver, dimensions) {
|
|
var needsRedraw = false,
|
|
type = _.groupBy(d, function(ent) { return ent.type; }),
|
|
entities = [].concat(type.relation || [], type.way || [], type.node || []),
|
|
currHidden, geometry, matches;
|
|
|
|
_.each(_features, function(f) { f.count = 0; });
|
|
|
|
// adjust the threshold for point/building culling based on viewport size..
|
|
// a _cullFactor of 1 corresponds to a 1000x1000px viewport..
|
|
_cullFactor = dimensions[0] * dimensions[1] / 1000000;
|
|
|
|
for (var i = 0; i < entities.length; i++) {
|
|
geometry = entities[i].geometry(resolver);
|
|
if (!(geometry === 'vertex' || geometry === 'relation')) {
|
|
matches = Object.keys(features.getMatches(entities[i], resolver, geometry));
|
|
for (var j = 0; j < matches.length; j++) {
|
|
_features[matches[j]].count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
currHidden = features.hidden();
|
|
if (currHidden !== _hidden) {
|
|
_hidden = currHidden;
|
|
needsRedraw = true;
|
|
dispatch.change();
|
|
}
|
|
|
|
return needsRedraw;
|
|
};
|
|
|
|
features.stats = function() {
|
|
_.each(_keys, function(k) { _stats[k] = _features[k].count; });
|
|
return _stats;
|
|
};
|
|
|
|
features.clear = function(d) {
|
|
for (var i = 0; i < d.length; i++) {
|
|
features.clearEntity(d[i]);
|
|
}
|
|
};
|
|
|
|
features.clearEntity = function(entity) {
|
|
delete _cache[iD.Entity.key(entity)];
|
|
};
|
|
|
|
features.reset = function() {
|
|
_cache = {};
|
|
};
|
|
|
|
features.getMatches = function(entity, resolver, geometry) {
|
|
if (geometry === 'vertex' || geometry === 'relation') return {};
|
|
|
|
var ent = iD.Entity.key(entity);
|
|
if (!_cache[ent]) {
|
|
_cache[ent] = {};
|
|
}
|
|
|
|
if (!_cache[ent].matches) {
|
|
var matches = {},
|
|
hasMatch = false;
|
|
|
|
for (var i = 0; i < _keys.length; i++) {
|
|
if (_keys[i] === 'others') {
|
|
if (hasMatch) continue;
|
|
|
|
// Multipolygon members:
|
|
// If an entity...
|
|
// 1. is a way that hasn't matched other "interesting" feature rules,
|
|
// 2. and it belongs to a single parent multipolygon relation
|
|
// ...then match whatever feature rules the parent multipolygon has matched.
|
|
// see #2548, #2887
|
|
//
|
|
// IMPORTANT:
|
|
// For this to work, getMatches must be called on relations before ways.
|
|
//
|
|
if (entity.type === 'way') {
|
|
var parents = features.getParents(entity, resolver, geometry);
|
|
if (parents.length === 1 && parents[0].isMultipolygon()) {
|
|
var pkey = iD.Entity.key(parents[0]);
|
|
if (_cache[pkey] && _cache[pkey].matches) {
|
|
matches = _.clone(_cache[pkey].matches);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_features[_keys[i]].filter(entity, resolver, geometry)) {
|
|
matches[_keys[i]] = hasMatch = true;
|
|
}
|
|
}
|
|
_cache[ent].matches = matches;
|
|
}
|
|
|
|
return _cache[ent].matches;
|
|
};
|
|
|
|
features.getParents = function(entity, resolver, geometry) {
|
|
if (geometry === 'point') return [];
|
|
|
|
var ent = iD.Entity.key(entity);
|
|
if (!_cache[ent]) {
|
|
_cache[ent] = {};
|
|
}
|
|
|
|
if (!_cache[ent].parents) {
|
|
var parents = [];
|
|
if (geometry === 'vertex') {
|
|
parents = resolver.parentWays(entity);
|
|
} else { // 'line', 'area', 'relation'
|
|
parents = resolver.parentRelations(entity);
|
|
}
|
|
_cache[ent].parents = parents;
|
|
}
|
|
return _cache[ent].parents;
|
|
};
|
|
|
|
features.isHiddenFeature = function(entity, resolver, geometry) {
|
|
if (!_hidden.length) return false;
|
|
if (!entity.version) return false;
|
|
|
|
var matches = features.getMatches(entity, resolver, geometry);
|
|
|
|
for (var i = 0; i < _hidden.length; i++) {
|
|
if (matches[_hidden[i]]) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
features.isHiddenChild = function(entity, resolver, geometry) {
|
|
if (!_hidden.length) return false;
|
|
if (!entity.version || geometry === 'point') return false;
|
|
|
|
var parents = features.getParents(entity, resolver, geometry);
|
|
if (!parents.length) return false;
|
|
|
|
for (var i = 0; i < parents.length; i++) {
|
|
if (!features.isHidden(parents[i], resolver, parents[i].geometry(resolver))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
features.hasHiddenConnections = function(entity, resolver) {
|
|
if (!_hidden.length) return false;
|
|
var childNodes, connections;
|
|
|
|
if (entity.type === 'midpoint') {
|
|
childNodes = [resolver.entity(entity.edge[0]), resolver.entity(entity.edge[1])];
|
|
connections = [];
|
|
} else {
|
|
childNodes = entity.nodes ? resolver.childNodes(entity) : [];
|
|
connections = features.getParents(entity, resolver, entity.geometry(resolver));
|
|
}
|
|
|
|
// gather ways connected to child nodes..
|
|
connections = _.reduce(childNodes, function(result, e) {
|
|
return resolver.isShared(e) ? _.union(result, resolver.parentWays(e)) : result;
|
|
}, connections);
|
|
|
|
return connections.length ? _.some(connections, function(e) {
|
|
return features.isHidden(e, resolver, e.geometry(resolver));
|
|
}) : false;
|
|
};
|
|
|
|
features.isHidden = function(entity, resolver, geometry) {
|
|
if (!_hidden.length) return false;
|
|
if (!entity.version) return false;
|
|
|
|
var fn = (geometry === 'vertex' ? features.isHiddenChild : features.isHiddenFeature);
|
|
return fn(entity, resolver, geometry);
|
|
};
|
|
|
|
features.filter = function(d, resolver) {
|
|
if (!_hidden.length) return d;
|
|
|
|
var result = [];
|
|
for (var i = 0; i < d.length; i++) {
|
|
var entity = d[i];
|
|
if (!features.isHidden(entity, resolver, entity.geometry(resolver))) {
|
|
result.push(entity);
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return d3.rebind(features, dispatch, 'on');
|
|
}
|
|
|
|
function Map(context) {
|
|
var dimensions = [1, 1],
|
|
dispatch = d3.dispatch('move', 'drawn'),
|
|
projection = context.projection,
|
|
zoom = d3.behavior.zoom()
|
|
.translate(projection.translate())
|
|
.scale(projection.scale() * 2 * Math.PI)
|
|
.scaleExtent([1024, 256 * Math.pow(2, 24)])
|
|
.on('zoom', zoomPan),
|
|
dblclickEnabled = true,
|
|
redrawEnabled = true,
|
|
transformStart,
|
|
transformed = false,
|
|
easing = false,
|
|
minzoom = 0,
|
|
drawLayers = iD.svg.Layers(projection, context),
|
|
drawPoints = iD.svg.Points(projection, context),
|
|
drawVertices = iD.svg.Vertices(projection, context),
|
|
drawLines = iD.svg.Lines(projection),
|
|
drawAreas = iD.svg.Areas(projection),
|
|
drawMidpoints = iD.svg.Midpoints(projection, context),
|
|
drawLabels = iD.svg.Labels(projection, context),
|
|
supersurface,
|
|
wrapper,
|
|
surface,
|
|
mouse,
|
|
mousemove;
|
|
|
|
function map(selection) {
|
|
context
|
|
.on('change.map', redraw);
|
|
context.history()
|
|
.on('change.map', redraw);
|
|
context.background()
|
|
.on('change.map', redraw);
|
|
context.features()
|
|
.on('redraw.map', redraw);
|
|
drawLayers
|
|
.on('change.map', function() {
|
|
context.background().updateImagery();
|
|
redraw();
|
|
});
|
|
|
|
selection
|
|
.on('dblclick.map', dblClick)
|
|
.call(zoom);
|
|
|
|
supersurface = selection.append('div')
|
|
.attr('id', 'supersurface')
|
|
.call(iD.util.setTransform, 0, 0);
|
|
|
|
// Need a wrapper div because Opera can't cope with an absolutely positioned
|
|
// SVG element: http://bl.ocks.org/jfirebaugh/6fbfbd922552bf776c16
|
|
wrapper = supersurface
|
|
.append('div')
|
|
.attr('class', 'layer layer-data');
|
|
|
|
map.surface = surface = wrapper
|
|
.call(drawLayers)
|
|
.selectAll('.surface')
|
|
.attr('id', 'surface');
|
|
|
|
surface
|
|
.on('mousedown.zoom', function() {
|
|
if (d3.event.button === 2) {
|
|
d3.event.stopPropagation();
|
|
}
|
|
}, true)
|
|
.on('mouseup.zoom', function() {
|
|
if (resetTransform()) redraw();
|
|
})
|
|
.on('mousemove.map', function() {
|
|
mousemove = d3.event;
|
|
})
|
|
.on('mouseover.vertices', function() {
|
|
if (map.editable() && !transformed) {
|
|
var hover = d3.event.target.__data__;
|
|
surface.call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom());
|
|
dispatch.drawn({full: false});
|
|
}
|
|
})
|
|
.on('mouseout.vertices', function() {
|
|
if (map.editable() && !transformed) {
|
|
var hover = d3.event.relatedTarget && d3.event.relatedTarget.__data__;
|
|
surface.call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom());
|
|
dispatch.drawn({full: false});
|
|
}
|
|
});
|
|
|
|
|
|
supersurface
|
|
.call(context.background());
|
|
|
|
|
|
context.on('enter.map', function() {
|
|
if (map.editable() && !transformed) {
|
|
var all = context.intersects(map.extent()),
|
|
filter = d3.functor(true),
|
|
graph = context.graph();
|
|
|
|
all = context.features().filter(all, graph);
|
|
surface
|
|
.call(drawVertices, graph, all, filter, map.extent(), map.zoom())
|
|
.call(drawMidpoints, graph, all, filter, map.trimmedExtent());
|
|
dispatch.drawn({full: false});
|
|
}
|
|
});
|
|
|
|
map.dimensions(selection.dimensions());
|
|
|
|
drawLabels.supersurface(supersurface);
|
|
}
|
|
|
|
function pxCenter() {
|
|
return [dimensions[0] / 2, dimensions[1] / 2];
|
|
}
|
|
|
|
function drawVector(difference, extent) {
|
|
var graph = context.graph(),
|
|
features = context.features(),
|
|
all = context.intersects(map.extent()),
|
|
data, filter;
|
|
|
|
if (difference) {
|
|
var complete = difference.complete(map.extent());
|
|
data = _.compact(_.values(complete));
|
|
filter = function(d) { return d.id in complete; };
|
|
features.clear(data);
|
|
|
|
} else {
|
|
// force a full redraw if gatherStats detects that a feature
|
|
// should be auto-hidden (e.g. points or buildings)..
|
|
if (features.gatherStats(all, graph, dimensions)) {
|
|
extent = undefined;
|
|
}
|
|
|
|
if (extent) {
|
|
data = context.intersects(map.extent().intersection(extent));
|
|
var set = d3.set(_.map(data, 'id'));
|
|
filter = function(d) { return set.has(d.id); };
|
|
|
|
} else {
|
|
data = all;
|
|
filter = d3.functor(true);
|
|
}
|
|
}
|
|
|
|
data = features.filter(data, graph);
|
|
|
|
surface
|
|
.call(drawVertices, graph, data, filter, map.extent(), map.zoom())
|
|
.call(drawLines, graph, data, filter)
|
|
.call(drawAreas, graph, data, filter)
|
|
.call(drawMidpoints, graph, data, filter, map.trimmedExtent())
|
|
.call(drawLabels, graph, data, filter, dimensions, !difference && !extent)
|
|
.call(drawPoints, graph, data, filter);
|
|
|
|
dispatch.drawn({full: true});
|
|
}
|
|
|
|
function editOff() {
|
|
context.features().resetStats();
|
|
surface.selectAll('.layer-osm *').remove();
|
|
dispatch.drawn({full: true});
|
|
}
|
|
|
|
function dblClick() {
|
|
if (!dblclickEnabled) {
|
|
d3.event.preventDefault();
|
|
d3.event.stopImmediatePropagation();
|
|
}
|
|
}
|
|
|
|
function zoomPan() {
|
|
if (Math.log(d3.event.scale) / Math.LN2 - 8 < minzoom) {
|
|
surface.interrupt();
|
|
iD.ui.flash(context.container())
|
|
.select('.content')
|
|
.text(t('cannot_zoom'));
|
|
setZoom(context.minEditableZoom(), true);
|
|
queueRedraw();
|
|
dispatch.move(map);
|
|
return;
|
|
}
|
|
|
|
projection
|
|
.translate(d3.event.translate)
|
|
.scale(d3.event.scale / (2 * Math.PI));
|
|
|
|
var scale = d3.event.scale / transformStart[0],
|
|
tX = (d3.event.translate[0] / scale - transformStart[1][0]) * scale,
|
|
tY = (d3.event.translate[1] / scale - transformStart[1][1]) * scale;
|
|
|
|
transformed = true;
|
|
iD.util.setTransform(supersurface, tX, tY, scale);
|
|
queueRedraw();
|
|
|
|
dispatch.move(map);
|
|
}
|
|
|
|
function resetTransform() {
|
|
if (!transformed) return false;
|
|
|
|
surface.selectAll('.radial-menu').interrupt().remove();
|
|
iD.util.setTransform(supersurface, 0, 0);
|
|
transformed = false;
|
|
return true;
|
|
}
|
|
|
|
function redraw(difference, extent) {
|
|
if (!surface || !redrawEnabled) return;
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
// If we are in the middle of a zoom/pan, we can't do differenced redraws.
|
|
// It would result in artifacts where differenced entities are redrawn with
|
|
// one transform and unchanged entities with another.
|
|
if (resetTransform()) {
|
|
difference = extent = undefined;
|
|
}
|
|
|
|
var zoom = String(~~map.zoom());
|
|
if (surface.attr('data-zoom') !== zoom) {
|
|
surface.attr('data-zoom', zoom)
|
|
.classed('low-zoom', zoom <= 16);
|
|
}
|
|
|
|
if (!difference) {
|
|
supersurface.call(context.background());
|
|
}
|
|
|
|
// OSM
|
|
if (map.editable()) {
|
|
context.loadTiles(projection, dimensions);
|
|
drawVector(difference, extent);
|
|
} else {
|
|
editOff();
|
|
}
|
|
|
|
wrapper
|
|
.call(drawLayers);
|
|
|
|
transformStart = [
|
|
projection.scale() * 2 * Math.PI,
|
|
projection.translate().slice()];
|
|
|
|
return map;
|
|
}
|
|
|
|
var timeoutId;
|
|
function queueRedraw() {
|
|
timeoutId = setTimeout(function() { redraw(); }, 750);
|
|
}
|
|
|
|
function pointLocation(p) {
|
|
var translate = projection.translate(),
|
|
scale = projection.scale() * 2 * Math.PI;
|
|
return [(p[0] - translate[0]) / scale, (p[1] - translate[1]) / scale];
|
|
}
|
|
|
|
function locationPoint(l) {
|
|
var translate = projection.translate(),
|
|
scale = projection.scale() * 2 * Math.PI;
|
|
return [l[0] * scale + translate[0], l[1] * scale + translate[1]];
|
|
}
|
|
|
|
map.mouse = function() {
|
|
var e = mousemove || d3.event, s;
|
|
while ((s = e.sourceEvent)) e = s;
|
|
return mouse(e);
|
|
};
|
|
|
|
map.mouseCoordinates = function() {
|
|
return projection.invert(map.mouse());
|
|
};
|
|
|
|
map.dblclickEnable = function(_) {
|
|
if (!arguments.length) return dblclickEnabled;
|
|
dblclickEnabled = _;
|
|
return map;
|
|
};
|
|
|
|
map.redrawEnable = function(_) {
|
|
if (!arguments.length) return redrawEnabled;
|
|
redrawEnabled = _;
|
|
return map;
|
|
};
|
|
|
|
function interpolateZoom(_) {
|
|
var k = projection.scale(),
|
|
t = projection.translate();
|
|
|
|
surface.node().__chart__ = {
|
|
x: t[0],
|
|
y: t[1],
|
|
k: k * 2 * Math.PI
|
|
};
|
|
|
|
setZoom(_);
|
|
projection.scale(k).translate(t); // undo setZoom projection changes
|
|
|
|
zoom.event(surface.transition());
|
|
}
|
|
|
|
function setZoom(_, force) {
|
|
if (_ === map.zoom() && !force)
|
|
return false;
|
|
var scale = 256 * Math.pow(2, _),
|
|
center = pxCenter(),
|
|
l = pointLocation(center);
|
|
scale = Math.max(1024, Math.min(256 * Math.pow(2, 24), scale));
|
|
projection.scale(scale / (2 * Math.PI));
|
|
zoom.scale(scale);
|
|
var t = projection.translate();
|
|
l = locationPoint(l);
|
|
t[0] += center[0] - l[0];
|
|
t[1] += center[1] - l[1];
|
|
projection.translate(t);
|
|
zoom.translate(projection.translate());
|
|
return true;
|
|
}
|
|
|
|
function setCenter(_) {
|
|
var c = map.center();
|
|
if (_[0] === c[0] && _[1] === c[1])
|
|
return false;
|
|
var t = projection.translate(),
|
|
pxC = pxCenter(),
|
|
ll = projection(_);
|
|
projection.translate([
|
|
t[0] - ll[0] + pxC[0],
|
|
t[1] - ll[1] + pxC[1]]);
|
|
zoom.translate(projection.translate());
|
|
return true;
|
|
}
|
|
|
|
map.pan = function(d) {
|
|
var t = projection.translate();
|
|
t[0] += d[0];
|
|
t[1] += d[1];
|
|
projection.translate(t);
|
|
zoom.translate(projection.translate());
|
|
dispatch.move(map);
|
|
return redraw();
|
|
};
|
|
|
|
map.dimensions = function(_) {
|
|
if (!arguments.length) return dimensions;
|
|
var center = map.center();
|
|
dimensions = _;
|
|
drawLayers.dimensions(dimensions);
|
|
context.background().dimensions(dimensions);
|
|
projection.clipExtent([[0, 0], dimensions]);
|
|
mouse = iD.util.fastMouse(supersurface.node());
|
|
setCenter(center);
|
|
return redraw();
|
|
};
|
|
|
|
function zoomIn(integer) {
|
|
interpolateZoom(~~map.zoom() + integer);
|
|
}
|
|
|
|
function zoomOut(integer) {
|
|
interpolateZoom(~~map.zoom() - integer);
|
|
}
|
|
|
|
map.zoomIn = function() { zoomIn(1); };
|
|
map.zoomInFurther = function() { zoomIn(4); };
|
|
|
|
map.zoomOut = function() { zoomOut(1); };
|
|
map.zoomOutFurther = function() { zoomOut(4); };
|
|
|
|
map.center = function(loc) {
|
|
if (!arguments.length) {
|
|
return projection.invert(pxCenter());
|
|
}
|
|
|
|
if (setCenter(loc)) {
|
|
dispatch.move(map);
|
|
}
|
|
|
|
return redraw();
|
|
};
|
|
|
|
map.zoom = function(z) {
|
|
if (!arguments.length) {
|
|
return Math.max(Math.log(projection.scale() * 2 * Math.PI) / Math.LN2 - 8, 0);
|
|
}
|
|
|
|
if (z < minzoom) {
|
|
surface.interrupt();
|
|
iD.ui.flash(context.container())
|
|
.select('.content')
|
|
.text(t('cannot_zoom'));
|
|
z = context.minEditableZoom();
|
|
}
|
|
|
|
if (setZoom(z)) {
|
|
dispatch.move(map);
|
|
}
|
|
|
|
return redraw();
|
|
};
|
|
|
|
map.zoomTo = function(entity, zoomLimits) {
|
|
var extent = entity.extent(context.graph());
|
|
if (!isFinite(extent.area())) return;
|
|
|
|
var zoom = map.trimmedExtentZoom(extent);
|
|
zoomLimits = zoomLimits || [context.minEditableZoom(), 20];
|
|
map.centerZoom(extent.center(), Math.min(Math.max(zoom, zoomLimits[0]), zoomLimits[1]));
|
|
};
|
|
|
|
map.centerZoom = function(loc, z) {
|
|
var centered = setCenter(loc),
|
|
zoomed = setZoom(z);
|
|
|
|
if (centered || zoomed) {
|
|
dispatch.move(map);
|
|
}
|
|
|
|
return redraw();
|
|
};
|
|
|
|
map.centerEase = function(loc2, duration) {
|
|
duration = duration || 250;
|
|
|
|
surface.one('mousedown.ease', function() {
|
|
map.cancelEase();
|
|
});
|
|
|
|
if (easing) {
|
|
map.cancelEase();
|
|
}
|
|
|
|
var t1 = Date.now(),
|
|
t2 = t1 + duration,
|
|
loc1 = map.center(),
|
|
ease = d3.ease('cubic-in-out');
|
|
|
|
easing = true;
|
|
|
|
d3.timer(function() {
|
|
if (!easing) return true; // cancelled ease
|
|
|
|
var tNow = Date.now();
|
|
if (tNow > t2) {
|
|
tNow = t2;
|
|
easing = false;
|
|
}
|
|
|
|
var locNow = iD.geo.interp(loc1, loc2, ease((tNow - t1) / duration));
|
|
setCenter(locNow);
|
|
|
|
d3.event = {
|
|
scale: zoom.scale(),
|
|
translate: zoom.translate()
|
|
};
|
|
|
|
zoomPan();
|
|
return !easing;
|
|
});
|
|
|
|
return map;
|
|
};
|
|
|
|
map.cancelEase = function() {
|
|
easing = false;
|
|
d3.timer.flush();
|
|
return map;
|
|
};
|
|
|
|
map.extent = function(_) {
|
|
if (!arguments.length) {
|
|
return new iD.geo.Extent(projection.invert([0, dimensions[1]]),
|
|
projection.invert([dimensions[0], 0]));
|
|
} else {
|
|
var extent = iD.geo.Extent(_);
|
|
map.centerZoom(extent.center(), map.extentZoom(extent));
|
|
}
|
|
};
|
|
|
|
map.trimmedExtent = function(_) {
|
|
if (!arguments.length) {
|
|
var headerY = 60, footerY = 30, pad = 10;
|
|
return new iD.geo.Extent(projection.invert([pad, dimensions[1] - footerY - pad]),
|
|
projection.invert([dimensions[0] - pad, headerY + pad]));
|
|
} else {
|
|
var extent = iD.geo.Extent(_);
|
|
map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));
|
|
}
|
|
};
|
|
|
|
function calcZoom(extent, dim) {
|
|
var tl = projection([extent[0][0], extent[1][1]]),
|
|
br = projection([extent[1][0], extent[0][1]]);
|
|
|
|
// Calculate maximum zoom that fits extent
|
|
var hFactor = (br[0] - tl[0]) / dim[0],
|
|
vFactor = (br[1] - tl[1]) / dim[1],
|
|
hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2,
|
|
vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2,
|
|
newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff);
|
|
|
|
return newZoom;
|
|
}
|
|
|
|
map.extentZoom = function(_) {
|
|
return calcZoom(iD.geo.Extent(_), dimensions);
|
|
};
|
|
|
|
map.trimmedExtentZoom = function(_) {
|
|
var trimY = 120, trimX = 40,
|
|
trimmed = [dimensions[0] - trimX, dimensions[1] - trimY];
|
|
return calcZoom(iD.geo.Extent(_), trimmed);
|
|
};
|
|
|
|
map.editable = function() {
|
|
return map.zoom() >= context.minEditableZoom();
|
|
};
|
|
|
|
map.minzoom = function(_) {
|
|
if (!arguments.length) return minzoom;
|
|
minzoom = _;
|
|
return map;
|
|
};
|
|
|
|
map.layers = drawLayers;
|
|
|
|
return d3.rebind(map, dispatch, 'on');
|
|
}
|
|
|
|
exports.BackgroundSource = BackgroundSource;
|
|
exports.Background = Background;
|
|
exports.Features = Features;
|
|
exports.Map = Map;
|
|
exports.TileLayer = TileLayer;
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
})); |