From 46d68bca879d64f4a3c603711d221b478a99fc29 Mon Sep 17 00:00:00 2001
From: Kushan Joshi <0o3ko0@gmail.com>
Date: Fri, 17 Jun 2016 12:20:33 +0530
Subject: [PATCH 1/2] modularize iD.renderer
---
Makefile | 11 +-
index.html | 7 +-
js/lib/id/renderer.js | 1587 +++++++++++++++++
{js/id => modules}/renderer/background.js | 19 +-
.../renderer/background_source.js | 16 +-
{js/id => modules}/renderer/features.js | 4 +-
modules/renderer/index.js | 5 +
{js/id => modules}/renderer/map.js | 4 +-
{js/id => modules}/renderer/tile_layer.js | 4 +-
test/index.html | 7 +-
10 files changed, 1624 insertions(+), 40 deletions(-)
create mode 100644 js/lib/id/renderer.js
rename {js/id => modules}/renderer/background.js (92%)
rename {js/id => modules}/renderer/background_source.js (92%)
rename {js/id => modules}/renderer/features.js (99%)
create mode 100644 modules/renderer/index.js
rename {js/id => modules}/renderer/map.js (99%)
rename {js/id => modules}/renderer/tile_layer.js (99%)
diff --git a/Makefile b/Makefile
index b9daafcdc..46b51f914 100644
--- a/Makefile
+++ b/Makefile
@@ -49,6 +49,7 @@ MODULE_TARGETS = \
js/lib/id/modes.js \
js/lib/id/operations.js \
js/lib/id/presets.js \
+ js/lib/id/renderer.js \
js/lib/id/services.js \
js/lib/id/svg.js \
js/lib/id/util.js \
@@ -78,6 +79,10 @@ js/lib/id/presets.js: $(shell find modules/presets -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD.presets modules/presets/index.js --no-strict -o $@
+js/lib/id/renderer.js: $(shell find modules/renderer -type f)
+ @rm -f $@
+ node_modules/.bin/rollup -f umd -n iD modules/renderer/index.js --no-strict -o $@
+
js/lib/id/services.js: $(shell find modules/services -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD.services modules/services/index.js --no-strict -o $@
@@ -94,7 +99,6 @@ js/lib/id/validations.js: $(shell find modules/validations -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD.validations modules/validations/index.js --no-strict -o $@
-
dist/iD.js: \
js/lib/bootstrap-tooltip.js \
js/lib/d3.v3.js \
@@ -132,11 +136,6 @@ dist/iD.js: \
js/id/behavior/paste.js \
js/id/behavior/select.js \
js/id/behavior/tail.js \
- js/id/renderer/background.js \
- js/id/renderer/background_source.js \
- js/id/renderer/features.js \
- js/id/renderer/map.js \
- js/id/renderer/tile_layer.js \
js/id/ui.js \
js/id/ui/account.js \
js/id/ui/attribution.js \
diff --git a/index.html b/index.html
index 002aad4e5..0a6b1b8e6 100644
--- a/index.html
+++ b/index.html
@@ -40,6 +40,7 @@
+
@@ -47,12 +48,6 @@
-
-
-
-
-
-
diff --git a/js/lib/id/renderer.js b/js/lib/id/renderer.js
new file mode 100644
index 000000000..7e7f836d5
--- /dev/null
+++ b/js/lib/id/renderer.js
@@ -0,0 +1,1587 @@
+(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 });
+
+}));
\ No newline at end of file
diff --git a/js/id/renderer/background.js b/modules/renderer/background.js
similarity index 92%
rename from js/id/renderer/background.js
rename to modules/renderer/background.js
index 4984df1d4..53d470f21 100644
--- a/js/id/renderer/background.js
+++ b/modules/renderer/background.js
@@ -1,6 +1,9 @@
-iD.Background = function(context) {
+import { BackgroundSource } from './background_source';
+import { TileLayer } from './tile_layer';
+
+export function Background(context) {
var dispatch = d3.dispatch('change'),
- baseLayer = iD.TileLayer(context).projection(context.projection),
+ baseLayer = TileLayer(context).projection(context.projection),
overlayLayers = [],
backgroundSources;
@@ -148,7 +151,7 @@ iD.Background = function(context) {
}
}
- layer = iD.TileLayer(context)
+ layer = TileLayer(context)
.source(d)
.projection(context.projection)
.dimensions(baseLayer.dimensions());
@@ -188,20 +191,20 @@ iD.Background = function(context) {
backgroundSources = imagery.map(function(source) {
if (source.type === 'bing') {
- return iD.BackgroundSource.Bing(source, dispatch);
+ return BackgroundSource.Bing(source, dispatch);
} else {
- return iD.BackgroundSource(source);
+ return BackgroundSource(source);
}
});
- backgroundSources.unshift(iD.BackgroundSource.None());
+ 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(iD.BackgroundSource.Custom(chosen.replace(/^custom:/, '')));
+ background.baseLayerSource(BackgroundSource.Custom(chosen.replace(/^custom:/, '')));
} else {
background.baseLayerSource(findSource(chosen) || best || findSource('Bing') || backgroundSources[1] || backgroundSources[0]);
}
@@ -241,4 +244,4 @@ iD.Background = function(context) {
};
return d3.rebind(background, dispatch, 'on');
-};
+}
diff --git a/js/id/renderer/background_source.js b/modules/renderer/background_source.js
similarity index 92%
rename from js/id/renderer/background_source.js
rename to modules/renderer/background_source.js
index c2b51d9a1..be63323b1 100644
--- a/js/id/renderer/background_source.js
+++ b/modules/renderer/background_source.js
@@ -1,4 +1,4 @@
-iD.BackgroundSource = function(data) {
+export function BackgroundSource(data) {
var source = _.clone(data),
offset = [0, 0],
name = source.name,
@@ -80,15 +80,15 @@ iD.BackgroundSource = function(data) {
source.copyrightNotices = function() {};
return source;
-};
+}
-iD.BackgroundSource.Bing = function(data, dispatch) {
+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 = iD.BackgroundSource(data),
+ 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}',
@@ -128,8 +128,8 @@ iD.BackgroundSource.Bing = function(data, dispatch) {
return bing;
};
-iD.BackgroundSource.None = function() {
- var source = iD.BackgroundSource({id: 'none', template: ''});
+BackgroundSource.None = function() {
+ var source = BackgroundSource({id: 'none', template: ''});
source.name = function() {
return t('background.none');
@@ -146,8 +146,8 @@ iD.BackgroundSource.None = function() {
return source;
};
-iD.BackgroundSource.Custom = function(template) {
- var source = iD.BackgroundSource({id: 'custom', template: template});
+BackgroundSource.Custom = function(template) {
+ var source = BackgroundSource({id: 'custom', template: template});
source.name = function() {
return t('background.custom');
diff --git a/js/id/renderer/features.js b/modules/renderer/features.js
similarity index 99%
rename from js/id/renderer/features.js
rename to modules/renderer/features.js
index 51306ea12..504b99a4e 100644
--- a/js/id/renderer/features.js
+++ b/modules/renderer/features.js
@@ -1,4 +1,4 @@
-iD.Features = function(context) {
+export function Features(context) {
var traffic_roads = {
'motorway': true,
'motorway_link': true,
@@ -417,4 +417,4 @@ iD.Features = function(context) {
};
return d3.rebind(features, dispatch, 'on');
-};
+}
diff --git a/modules/renderer/index.js b/modules/renderer/index.js
new file mode 100644
index 000000000..42ef8e0fb
--- /dev/null
+++ b/modules/renderer/index.js
@@ -0,0 +1,5 @@
+export { BackgroundSource } from './background_source';
+export { Background } from './background';
+export { Features } from './features';
+export { Map } from './map';
+export { TileLayer } from './tile_layer';
diff --git a/js/id/renderer/map.js b/modules/renderer/map.js
similarity index 99%
rename from js/id/renderer/map.js
rename to modules/renderer/map.js
index 72bee6a3e..564f1518a 100644
--- a/js/id/renderer/map.js
+++ b/modules/renderer/map.js
@@ -1,4 +1,4 @@
-iD.Map = function(context) {
+export function Map(context) {
var dimensions = [1, 1],
dispatch = d3.dispatch('move', 'drawn'),
projection = context.projection,
@@ -528,4 +528,4 @@ iD.Map = function(context) {
map.layers = drawLayers;
return d3.rebind(map, dispatch, 'on');
-};
+}
diff --git a/js/id/renderer/tile_layer.js b/modules/renderer/tile_layer.js
similarity index 99%
rename from js/id/renderer/tile_layer.js
rename to modules/renderer/tile_layer.js
index 8ac685979..d671bbe0a 100644
--- a/js/id/renderer/tile_layer.js
+++ b/modules/renderer/tile_layer.js
@@ -1,4 +1,4 @@
-iD.TileLayer = function(context) {
+export function TileLayer(context) {
var tileSize = 256,
tile = d3.geo.tile(),
projection,
@@ -204,4 +204,4 @@ iD.TileLayer = function(context) {
};
return background;
-};
+}
diff --git a/test/index.html b/test/index.html
index e20e512da..8032f5595 100644
--- a/test/index.html
+++ b/test/index.html
@@ -47,17 +47,12 @@
+
-
-
-
-
-
-
From 14f0d95e9451546f14a5f502c2daba6ec69b023b Mon Sep 17 00:00:00 2001
From: Kushan Joshi <0o3ko0@gmail.com>
Date: Fri, 17 Jun 2016 12:52:54 +0530
Subject: [PATCH 2/2] modularize iD.behavior
---
Makefile | 19 +-
index.html | 16 +-
js/id/behavior.js | 1 -
js/lib/id/behavior.js | 1394 +++++++++++++++++++++++
{js/id => modules}/behavior/add_way.js | 8 +-
{js/id => modules}/behavior/breathe.js | 4 +-
{js/id => modules}/behavior/copy.js | 4 +-
{js/id => modules}/behavior/drag.js | 6 +-
{js/id => modules}/behavior/draw.js | 22 +-
{js/id => modules}/behavior/draw_way.js | 8 +-
{js/id => modules}/behavior/edit.js | 4 +-
{js/id => modules}/behavior/hash.js | 4 +-
{js/id => modules}/behavior/hover.js | 4 +-
modules/behavior/index.js | 13 +
{js/id => modules}/behavior/lasso.js | 4 +-
{js/id => modules}/behavior/paste.js | 4 +-
{js/id => modules}/behavior/select.js | 4 +-
{js/id => modules}/behavior/tail.js | 4 +-
test/index.html | 16 +-
19 files changed, 1458 insertions(+), 81 deletions(-)
delete mode 100644 js/id/behavior.js
create mode 100644 js/lib/id/behavior.js
rename {js/id => modules}/behavior/add_way.js (89%)
rename {js/id => modules}/behavior/breathe.js (98%)
rename {js/id => modules}/behavior/copy.js (98%)
rename {js/id => modules}/behavior/drag.js (99%)
rename {js/id => modules}/behavior/draw.js (94%)
rename {js/id => modules}/behavior/draw_way.js (97%)
rename {js/id => modules}/behavior/edit.js (82%)
rename {js/id => modules}/behavior/hash.js (98%)
rename {js/id => modules}/behavior/hover.js (99%)
create mode 100644 modules/behavior/index.js
rename {js/id => modules}/behavior/lasso.js (97%)
rename {js/id => modules}/behavior/paste.js (97%)
rename {js/id => modules}/behavior/select.js (97%)
rename {js/id => modules}/behavior/tail.js (98%)
diff --git a/Makefile b/Makefile
index 46b51f914..1818e10bf 100644
--- a/Makefile
+++ b/Makefile
@@ -44,6 +44,7 @@ $(BUILDJS_TARGETS): $(BUILDJS_SOURCES) build.js
MODULE_TARGETS = \
js/lib/id/actions.js \
+ js/lib/id/behavior.js \
js/lib/id/core.js \
js/lib/id/geo.js \
js/lib/id/modes.js \
@@ -59,6 +60,10 @@ js/lib/id/actions.js: $(shell find modules/actions -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD.actions modules/actions/index.js --no-strict -o $@
+js/lib/id/behavior.js: $(shell find modules/behavior -type f)
+ @rm -f $@
+ node_modules/.bin/rollup -f umd -n iD.behavior modules/behavior/index.js --no-strict -o $@
+
js/lib/id/core.js: $(shell find modules/core -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD modules/core/index.js --no-strict -o $@
@@ -122,20 +127,6 @@ dist/iD.js: \
js/id/start.js \
js/id/id.js \
$(MODULE_TARGETS) \
- js/id/behavior.js \
- js/id/behavior/add_way.js \
- js/id/behavior/breathe.js \
- js/id/behavior/copy.js \
- js/id/behavior/drag.js \
- js/id/behavior/draw.js \
- js/id/behavior/draw_way.js \
- js/id/behavior/edit.js \
- js/id/behavior/hash.js \
- js/id/behavior/hover.js \
- js/id/behavior/lasso.js \
- js/id/behavior/paste.js \
- js/id/behavior/select.js \
- js/id/behavior/tail.js \
js/id/ui.js \
js/id/ui/account.js \
js/id/ui/attribution.js \
diff --git a/index.html b/index.html
index 0a6b1b8e6..fcb62f626 100644
--- a/index.html
+++ b/index.html
@@ -35,6 +35,7 @@
+
@@ -117,21 +118,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/js/id/behavior.js b/js/id/behavior.js
deleted file mode 100644
index c0801afa3..000000000
--- a/js/id/behavior.js
+++ /dev/null
@@ -1 +0,0 @@
-iD.behavior = {};
diff --git a/js/lib/id/behavior.js b/js/lib/id/behavior.js
new file mode 100644
index 000000000..b81bfc77f
--- /dev/null
+++ b/js/lib/id/behavior.js
@@ -0,0 +1,1394 @@
+(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.behavior = global.iD.behavior || {})));
+}(this, function (exports) { 'use strict';
+
+ function Edit(context) {
+ function edit() {
+ context.map()
+ .minzoom(context.minEditableZoom());
+ }
+
+ edit.off = function() {
+ context.map()
+ .minzoom(0);
+ };
+
+ return edit;
+ }
+
+ /*
+ The hover behavior adds the `.hover` class on mouseover to all elements to which
+ the identical datum is bound, and removes it on mouseout.
+
+ The :hover pseudo-class is insufficient for iD's purposes because a datum's visual
+ representation may consist of several elements scattered throughout the DOM hierarchy.
+ Only one of these elements can have the :hover pseudo-class, but all of them will
+ have the .hover class.
+ */
+ function Hover() {
+ var dispatch = d3.dispatch('hover'),
+ selection,
+ altDisables,
+ target;
+
+ function keydown() {
+ if (altDisables && d3.event.keyCode === d3.keybinding.modifierCodes.alt) {
+ dispatch.hover(null);
+ selection.selectAll('.hover')
+ .classed('hover-suppressed', true)
+ .classed('hover', false);
+ }
+ }
+
+ function keyup() {
+ if (altDisables && d3.event.keyCode === d3.keybinding.modifierCodes.alt) {
+ dispatch.hover(target ? target.id : null);
+ selection.selectAll('.hover-suppressed')
+ .classed('hover-suppressed', false)
+ .classed('hover', true);
+ }
+ }
+
+ var hover = function(__) {
+ selection = __;
+
+ function enter(d) {
+ if (d === target) return;
+
+ target = d;
+
+ selection.selectAll('.hover')
+ .classed('hover', false);
+ selection.selectAll('.hover-suppressed')
+ .classed('hover-suppressed', false);
+
+ if (target instanceof iD.Entity) {
+ var selector = '.' + target.id;
+
+ if (target.type === 'relation') {
+ target.members.forEach(function(member) {
+ selector += ', .' + member.id;
+ });
+ }
+
+ var suppressed = altDisables && d3.event && d3.event.altKey;
+
+ selection.selectAll(selector)
+ .classed(suppressed ? 'hover-suppressed' : 'hover', true);
+
+ dispatch.hover(target.id);
+ } else {
+ dispatch.hover(null);
+ }
+ }
+
+ var down;
+
+ function mouseover() {
+ if (down) return;
+ var target = d3.event.target;
+ enter(target ? target.__data__ : null);
+ }
+
+ function mouseout() {
+ if (down) return;
+ var target = d3.event.relatedTarget;
+ enter(target ? target.__data__ : null);
+ }
+
+ function mousedown() {
+ down = true;
+ d3.select(window)
+ .on('mouseup.hover', mouseup);
+ }
+
+ function mouseup() {
+ down = false;
+ }
+
+ selection
+ .on('mouseover.hover', mouseover)
+ .on('mouseout.hover', mouseout)
+ .on('mousedown.hover', mousedown)
+ .on('mouseup.hover', mouseup);
+
+ d3.select(window)
+ .on('keydown.hover', keydown)
+ .on('keyup.hover', keyup);
+ };
+
+ hover.off = function(selection) {
+ selection.selectAll('.hover')
+ .classed('hover', false);
+ selection.selectAll('.hover-suppressed')
+ .classed('hover-suppressed', false);
+
+ selection
+ .on('mouseover.hover', null)
+ .on('mouseout.hover', null)
+ .on('mousedown.hover', null)
+ .on('mouseup.hover', null);
+
+ d3.select(window)
+ .on('keydown.hover', null)
+ .on('keyup.hover', null)
+ .on('mouseup.hover', null);
+ };
+
+ hover.altDisables = function(_) {
+ if (!arguments.length) return altDisables;
+ altDisables = _;
+ return hover;
+ };
+
+ return d3.rebind(hover, dispatch, 'on');
+ }
+
+ function Tail() {
+ var text,
+ container,
+ xmargin = 25,
+ tooltipSize = [0, 0],
+ selectionSize = [0, 0];
+
+ function tail(selection) {
+ if (!text) return;
+
+ d3.select(window)
+ .on('resize.tail', function() { selectionSize = selection.dimensions(); });
+
+ function show() {
+ container.style('display', 'block');
+ tooltipSize = container.dimensions();
+ }
+
+ function mousemove() {
+ if (container.style('display') === 'none') show();
+ var xoffset = ((d3.event.clientX + tooltipSize[0] + xmargin) > selectionSize[0]) ?
+ -tooltipSize[0] - xmargin : xmargin;
+ container.classed('left', xoffset > 0);
+ iD.util.setTransform(container, d3.event.clientX + xoffset, d3.event.clientY);
+ }
+
+ function mouseleave() {
+ if (d3.event.relatedTarget !== container.node()) {
+ container.style('display', 'none');
+ }
+ }
+
+ function mouseenter() {
+ if (d3.event.relatedTarget !== container.node()) {
+ show();
+ }
+ }
+
+ container = d3.select(document.body)
+ .append('div')
+ .style('display', 'none')
+ .attr('class', 'tail tooltip-inner');
+
+ container.append('div')
+ .text(text);
+
+ selection
+ .on('mousemove.tail', mousemove)
+ .on('mouseenter.tail', mouseenter)
+ .on('mouseleave.tail', mouseleave);
+
+ container
+ .on('mousemove.tail', mousemove);
+
+ tooltipSize = container.dimensions();
+ selectionSize = selection.dimensions();
+ }
+
+ tail.off = function(selection) {
+ if (!text) return;
+
+ container
+ .on('mousemove.tail', null)
+ .remove();
+
+ selection
+ .on('mousemove.tail', null)
+ .on('mouseenter.tail', null)
+ .on('mouseleave.tail', null);
+
+ d3.select(window)
+ .on('resize.tail', null);
+ };
+
+ tail.text = function(_) {
+ if (!arguments.length) return text;
+ text = _;
+ return tail;
+ };
+
+ return tail;
+ }
+
+ function Draw(context) {
+ var event = d3.dispatch('move', 'click', 'clickWay',
+ 'clickNode', 'undo', 'cancel', 'finish'),
+ keybinding = d3.keybinding('draw'),
+ hover = Hover(context)
+ .altDisables(true)
+ .on('hover', context.ui().sidebar.hover),
+ tail = Tail(),
+ edit = Edit(context),
+ closeTolerance = 4,
+ tolerance = 12,
+ mouseLeave = false,
+ lastMouse = null,
+ cached = Draw;
+
+ function datum() {
+ if (d3.event.altKey) return {};
+
+ if (d3.event.type === 'keydown') {
+ return (lastMouse && lastMouse.target.__data__) || {};
+ } else {
+ return d3.event.target.__data__ || {};
+ }
+ }
+
+ function mousedown() {
+
+ function point() {
+ var p = context.container().node();
+ return touchId !== null ? d3.touches(p).filter(function(p) {
+ return p.identifier === touchId;
+ })[0] : d3.mouse(p);
+ }
+
+ var element = d3.select(this),
+ touchId = d3.event.touches ? d3.event.changedTouches[0].identifier : null,
+ t1 = +new Date(),
+ p1 = point();
+
+ element.on('mousemove.draw', null);
+
+ d3.select(window).on('mouseup.draw', function() {
+ var t2 = +new Date(),
+ p2 = point(),
+ dist = iD.geo.euclideanDistance(p1, p2);
+
+ element.on('mousemove.draw', mousemove);
+ d3.select(window).on('mouseup.draw', null);
+
+ if (dist < closeTolerance || (dist < tolerance && (t2 - t1) < 500)) {
+ // Prevent a quick second click
+ d3.select(window).on('click.draw-block', function() {
+ d3.event.stopPropagation();
+ }, true);
+
+ context.map().dblclickEnable(false);
+
+ window.setTimeout(function() {
+ context.map().dblclickEnable(true);
+ d3.select(window).on('click.draw-block', null);
+ }, 500);
+
+ click();
+ }
+ });
+ }
+
+ function mousemove() {
+ lastMouse = d3.event;
+ event.move(datum());
+ }
+
+ function mouseenter() {
+ mouseLeave = false;
+ }
+
+ function mouseleave() {
+ mouseLeave = true;
+ }
+
+ function click() {
+ var d = datum();
+ if (d.type === 'way') {
+ var dims = context.map().dimensions(),
+ mouse = context.mouse(),
+ pad = 5,
+ trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad &&
+ mouse[1] > pad && mouse[1] < dims[1] - pad;
+
+ if (trySnap) {
+ var choice = iD.geo.chooseEdge(context.childNodes(d), context.mouse(), context.projection),
+ edge = [d.nodes[choice.index - 1], d.nodes[choice.index]];
+ event.clickWay(choice.loc, edge);
+ } else {
+ event.click(context.map().mouseCoordinates());
+ }
+
+ } else if (d.type === 'node') {
+ event.clickNode(d);
+
+ } else {
+ event.click(context.map().mouseCoordinates());
+ }
+ }
+
+ function space() {
+ var currSpace = context.mouse();
+ if (cached.disableSpace && cached.lastSpace) {
+ var dist = iD.geo.euclideanDistance(cached.lastSpace, currSpace);
+ if (dist > tolerance) {
+ cached.disableSpace = false;
+ }
+ }
+
+ if (cached.disableSpace || mouseLeave || !lastMouse) return;
+
+ // user must move mouse or release space bar to allow another click
+ cached.lastSpace = currSpace;
+ cached.disableSpace = true;
+
+ d3.select(window).on('keyup.space-block', function() {
+ cached.disableSpace = false;
+ d3.select(window).on('keyup.space-block', null);
+ });
+
+ d3.event.preventDefault();
+ click();
+ }
+
+ function backspace() {
+ d3.event.preventDefault();
+ event.undo();
+ }
+
+ function del() {
+ d3.event.preventDefault();
+ event.cancel();
+ }
+
+ function ret() {
+ d3.event.preventDefault();
+ event.finish();
+ }
+
+ function draw(selection) {
+ context.install(hover);
+ context.install(edit);
+
+ if (!context.inIntro() && !cached.usedTails[tail.text()]) {
+ context.install(tail);
+ }
+
+ keybinding
+ .on('⌫', backspace)
+ .on('⌦', del)
+ .on('⎋', ret)
+ .on('↩', ret)
+ .on('space', space)
+ .on('⌥space', space);
+
+ selection
+ .on('mouseenter.draw', mouseenter)
+ .on('mouseleave.draw', mouseleave)
+ .on('mousedown.draw', mousedown)
+ .on('mousemove.draw', mousemove);
+
+ d3.select(document)
+ .call(keybinding);
+
+ return draw;
+ }
+
+ draw.off = function(selection) {
+ context.ui().sidebar.hover.cancel();
+ context.uninstall(hover);
+ context.uninstall(edit);
+
+ if (!context.inIntro() && !cached.usedTails[tail.text()]) {
+ context.uninstall(tail);
+ cached.usedTails[tail.text()] = true;
+ }
+
+ selection
+ .on('mouseenter.draw', null)
+ .on('mouseleave.draw', null)
+ .on('mousedown.draw', null)
+ .on('mousemove.draw', null);
+
+ d3.select(window)
+ .on('mouseup.draw', null);
+ // note: keyup.space-block, click.draw-block should remain
+
+ d3.select(document)
+ .call(keybinding.off);
+ };
+
+ draw.tail = function(_) {
+ tail.text(_);
+ return draw;
+ };
+
+ return d3.rebind(draw, event, 'on');
+ }
+
+ Draw.usedTails = {};
+ Draw.disableSpace = false;
+ Draw.lastSpace = null;
+
+ function AddWay(context) {
+ var event = d3.dispatch('start', 'startFromWay', 'startFromNode'),
+ draw = Draw(context);
+
+ var addWay = function(surface) {
+ draw.on('click', event.start)
+ .on('clickWay', event.startFromWay)
+ .on('clickNode', event.startFromNode)
+ .on('cancel', addWay.cancel)
+ .on('finish', addWay.cancel);
+
+ context.map()
+ .dblclickEnable(false);
+
+ surface.call(draw);
+ };
+
+ addWay.off = function(surface) {
+ surface.call(draw.off);
+ };
+
+ addWay.cancel = function() {
+ window.setTimeout(function() {
+ context.map().dblclickEnable(true);
+ }, 1000);
+
+ context.enter(iD.modes.Browse(context));
+ };
+
+ addWay.tail = function(text) {
+ draw.tail(text);
+ return addWay;
+ };
+
+ return d3.rebind(addWay, event, 'on');
+ }
+
+ function Breathe(){
+ var duration = 800,
+ selector = '.selected.shadow, .selected .shadow',
+ selected = d3.select(null),
+ classed = '',
+ params = {},
+ done;
+
+ function reset(selection) {
+ selection
+ .style('stroke-opacity', null)
+ .style('stroke-width', null)
+ .style('fill-opacity', null)
+ .style('r', null);
+ }
+
+ function setAnimationParams(transition, fromTo) {
+ transition
+ .style('stroke-opacity', function(d) { return params[d.id][fromTo].opacity; })
+ .style('stroke-width', function(d) { return params[d.id][fromTo].width; })
+ .style('fill-opacity', function(d) { return params[d.id][fromTo].opacity; })
+ .style('r', function(d) { return params[d.id][fromTo].width; });
+ }
+
+ function calcAnimationParams(selection) {
+ selection
+ .call(reset)
+ .each(function(d) {
+ var s = d3.select(this),
+ tag = s.node().tagName,
+ p = {'from': {}, 'to': {}},
+ opacity, width;
+
+ // determine base opacity and width
+ if (tag === 'circle') {
+ opacity = parseFloat(s.style('fill-opacity') || 0.5);
+ width = parseFloat(s.style('r') || 15.5);
+ } else {
+ opacity = parseFloat(s.style('stroke-opacity') || 0.7);
+ width = parseFloat(s.style('stroke-width') || 10);
+ }
+
+ // calculate from/to interpolation params..
+ p.tag = tag;
+ p.from.opacity = opacity * 0.6;
+ p.to.opacity = opacity * 1.25;
+ p.from.width = width * 0.9;
+ p.to.width = width * (tag === 'circle' ? 1.5 : 1.25);
+ params[d.id] = p;
+ });
+ }
+
+ function run(surface, fromTo) {
+ var toFrom = (fromTo === 'from' ? 'to': 'from'),
+ currSelected = surface.selectAll(selector),
+ currClassed = surface.attr('class'),
+ n = 0;
+
+ if (done || currSelected.empty()) {
+ selected.call(reset);
+ return;
+ }
+
+ if (!_.isEqual(currSelected, selected) || currClassed !== classed) {
+ selected.call(reset);
+ classed = currClassed;
+ selected = currSelected.call(calcAnimationParams);
+ }
+
+ selected
+ .transition()
+ .call(setAnimationParams, fromTo)
+ .duration(duration)
+ .each(function() { ++n; })
+ .each('end', function() {
+ if (!--n) { // call once
+ surface.call(run, toFrom);
+ }
+ });
+ }
+
+ var breathe = function(surface) {
+ done = false;
+ d3.timer(function() {
+ if (done) return true;
+
+ var currSelected = surface.selectAll(selector);
+ if (currSelected.empty()) return false;
+
+ surface.call(run, 'from');
+ return true;
+ }, 200);
+ };
+
+ breathe.off = function() {
+ done = true;
+ d3.timer.flush();
+ selected
+ .transition()
+ .call(reset)
+ .duration(0);
+ };
+
+ return breathe;
+ }
+
+ function Copy(context) {
+ var keybinding = d3.keybinding('copy');
+
+ function groupEntities(ids, graph) {
+ var entities = ids.map(function (id) { return graph.entity(id); });
+ return _.extend({relation: [], way: [], node: []},
+ _.groupBy(entities, function(entity) { return entity.type; }));
+ }
+
+ function getDescendants(id, graph, descendants) {
+ var entity = graph.entity(id),
+ i, children;
+
+ descendants = descendants || {};
+
+ if (entity.type === 'relation') {
+ children = _.map(entity.members, 'id');
+ } else if (entity.type === 'way') {
+ children = entity.nodes;
+ } else {
+ children = [];
+ }
+
+ for (i = 0; i < children.length; i++) {
+ if (!descendants[children[i]]) {
+ descendants[children[i]] = true;
+ descendants = getDescendants(children[i], graph, descendants);
+ }
+ }
+
+ return descendants;
+ }
+
+ function doCopy() {
+ d3.event.preventDefault();
+ if (context.inIntro()) return;
+
+ var graph = context.graph(),
+ selected = groupEntities(context.selectedIDs(), graph),
+ canCopy = [],
+ skip = {},
+ i, entity;
+
+ for (i = 0; i < selected.relation.length; i++) {
+ entity = selected.relation[i];
+ if (!skip[entity.id] && entity.isComplete(graph)) {
+ canCopy.push(entity.id);
+ skip = getDescendants(entity.id, graph, skip);
+ }
+ }
+ for (i = 0; i < selected.way.length; i++) {
+ entity = selected.way[i];
+ if (!skip[entity.id]) {
+ canCopy.push(entity.id);
+ skip = getDescendants(entity.id, graph, skip);
+ }
+ }
+ for (i = 0; i < selected.node.length; i++) {
+ entity = selected.node[i];
+ if (!skip[entity.id]) {
+ canCopy.push(entity.id);
+ }
+ }
+
+ context.copyIDs(canCopy);
+ }
+
+ function copy() {
+ keybinding.on(iD.ui.cmd('⌘C'), doCopy);
+ d3.select(document).call(keybinding);
+ return copy;
+ }
+
+ copy.off = function() {
+ d3.select(document).call(keybinding.off);
+ };
+
+ return copy;
+ }
+
+ /*
+ `iD.behavior.drag` is like `d3.behavior.drag`, with the following differences:
+
+ * The `origin` function is expected to return an [x, y] tuple rather than an
+ {x, y} object.
+ * The events are `start`, `move`, and `end`.
+ (https://github.com/mbostock/d3/issues/563)
+ * The `start` event is not dispatched until the first cursor movement occurs.
+ (https://github.com/mbostock/d3/pull/368)
+ * The `move` event has a `point` and `delta` [x, y] tuple properties rather
+ than `x`, `y`, `dx`, and `dy` properties.
+ * The `end` event is not dispatched if no movement occurs.
+ * An `off` function is available that unbinds the drag's internal event handlers.
+ * Delegation is supported via the `delegate` function.
+
+ */
+ function drag() {
+ function d3_eventCancel() {
+ d3.event.stopPropagation();
+ d3.event.preventDefault();
+ }
+
+ var event = d3.dispatch('start', 'move', 'end'),
+ origin = null,
+ selector = '',
+ filter = null,
+ event_, target, surface;
+
+ event.of = function(thiz, argumentz) {
+ return function(e1) {
+ var e0 = e1.sourceEvent = d3.event;
+ e1.target = drag;
+ d3.event = e1;
+ try {
+ event[e1.type].apply(thiz, argumentz);
+ } finally {
+ d3.event = e0;
+ }
+ };
+ };
+
+ var d3_event_userSelectProperty = iD.util.prefixCSSProperty('UserSelect'),
+ d3_event_userSelectSuppress = d3_event_userSelectProperty ?
+ function () {
+ var selection = d3.selection(),
+ select = selection.style(d3_event_userSelectProperty);
+ selection.style(d3_event_userSelectProperty, 'none');
+ return function () {
+ selection.style(d3_event_userSelectProperty, select);
+ };
+ } :
+ function (type) {
+ var w = d3.select(window).on('selectstart.' + type, d3_eventCancel);
+ return function () {
+ w.on('selectstart.' + type, null);
+ };
+ };
+
+ function mousedown() {
+ target = this;
+ event_ = event.of(target, arguments);
+ var eventTarget = d3.event.target,
+ touchId = d3.event.touches ? d3.event.changedTouches[0].identifier : null,
+ offset,
+ origin_ = point(),
+ started = false,
+ selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag');
+
+ var w = d3.select(window)
+ .on(touchId !== null ? 'touchmove.drag-' + touchId : 'mousemove.drag', dragmove)
+ .on(touchId !== null ? 'touchend.drag-' + touchId : 'mouseup.drag', dragend, true);
+
+ if (origin) {
+ offset = origin.apply(target, arguments);
+ offset = [offset[0] - origin_[0], offset[1] - origin_[1]];
+ } else {
+ offset = [0, 0];
+ }
+
+ if (touchId === null) d3.event.stopPropagation();
+
+ function point() {
+ var p = target.parentNode || surface;
+ return touchId !== null ? d3.touches(p).filter(function(p) {
+ return p.identifier === touchId;
+ })[0] : d3.mouse(p);
+ }
+
+ function dragmove() {
+
+ var p = point(),
+ dx = p[0] - origin_[0],
+ dy = p[1] - origin_[1];
+
+ if (dx === 0 && dy === 0)
+ return;
+
+ if (!started) {
+ started = true;
+ event_({
+ type: 'start'
+ });
+ }
+
+ origin_ = p;
+ d3_eventCancel();
+
+ event_({
+ type: 'move',
+ point: [p[0] + offset[0], p[1] + offset[1]],
+ delta: [dx, dy]
+ });
+ }
+
+ function dragend() {
+ if (started) {
+ event_({
+ type: 'end'
+ });
+
+ d3_eventCancel();
+ if (d3.event.target === eventTarget) w.on('click.drag', click, true);
+ }
+
+ w.on(touchId !== null ? 'touchmove.drag-' + touchId : 'mousemove.drag', null)
+ .on(touchId !== null ? 'touchend.drag-' + touchId : 'mouseup.drag', null);
+ selectEnable();
+ }
+
+ function click() {
+ d3_eventCancel();
+ w.on('click.drag', null);
+ }
+ }
+
+ function drag(selection) {
+ var matchesSelector = iD.util.prefixDOMProperty('matchesSelector'),
+ delegate = mousedown;
+
+ if (selector) {
+ delegate = function() {
+ var root = this,
+ target = d3.event.target;
+ for (; target && target !== root; target = target.parentNode) {
+ if (target[matchesSelector](selector) &&
+ (!filter || filter(target.__data__))) {
+ return mousedown.call(target, target.__data__);
+ }
+ }
+ };
+ }
+
+ selection.on('mousedown.drag' + selector, delegate)
+ .on('touchstart.drag' + selector, delegate);
+ }
+
+ drag.off = function(selection) {
+ selection.on('mousedown.drag' + selector, null)
+ .on('touchstart.drag' + selector, null);
+ };
+
+ drag.delegate = function(_) {
+ if (!arguments.length) return selector;
+ selector = _;
+ return drag;
+ };
+
+ drag.filter = function(_) {
+ if (!arguments.length) return origin;
+ filter = _;
+ return drag;
+ };
+
+ drag.origin = function (_) {
+ if (!arguments.length) return origin;
+ origin = _;
+ return drag;
+ };
+
+ drag.cancel = function() {
+ d3.select(window)
+ .on('mousemove.drag', null)
+ .on('mouseup.drag', null);
+ return drag;
+ };
+
+ drag.target = function() {
+ if (!arguments.length) return target;
+ target = arguments[0];
+ event_ = event.of(target, Array.prototype.slice.call(arguments, 1));
+ return drag;
+ };
+
+ drag.surface = function() {
+ if (!arguments.length) return surface;
+ surface = arguments[0];
+ return drag;
+ };
+
+ return d3.rebind(drag, event, 'on');
+ }
+
+ function DrawWay(context, wayId, index, mode, baseGraph) {
+ var way = context.entity(wayId),
+ isArea = context.geometry(wayId) === 'area',
+ finished = false,
+ annotation = t((way.isDegenerate() ?
+ 'operations.start.annotation.' :
+ 'operations.continue.annotation.') + context.geometry(wayId)),
+ draw = Draw(context);
+
+ var startIndex = typeof index === 'undefined' ? way.nodes.length - 1 : 0,
+ start = iD.Node({loc: context.graph().entity(way.nodes[startIndex]).loc}),
+ end = iD.Node({loc: context.map().mouseCoordinates()}),
+ segment = iD.Way({
+ nodes: typeof index === 'undefined' ? [start.id, end.id] : [end.id, start.id],
+ tags: _.clone(way.tags)
+ });
+
+ var f = context[way.isDegenerate() ? 'replace' : 'perform'];
+ if (isArea) {
+ f(iD.actions.AddEntity(end),
+ iD.actions.AddVertex(wayId, end.id, index));
+ } else {
+ f(iD.actions.AddEntity(start),
+ iD.actions.AddEntity(end),
+ iD.actions.AddEntity(segment));
+ }
+
+ function move(datum) {
+ var loc;
+
+ if (datum.type === 'node' && datum.id !== end.id) {
+ loc = datum.loc;
+
+ } else if (datum.type === 'way' && datum.id !== segment.id) {
+ var dims = context.map().dimensions(),
+ mouse = context.mouse(),
+ pad = 5,
+ trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad &&
+ mouse[1] > pad && mouse[1] < dims[1] - pad;
+
+ if (trySnap) {
+ loc = iD.geo.chooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc;
+ }
+ }
+
+ if (!loc) {
+ loc = context.map().mouseCoordinates();
+ }
+
+ context.replace(iD.actions.MoveNode(end.id, loc));
+ }
+
+ function undone() {
+ finished = true;
+ context.enter(iD.modes.Browse(context));
+ }
+
+ function setActiveElements() {
+ var active = isArea ? [wayId, end.id] : [segment.id, start.id, end.id];
+ context.surface().selectAll(iD.util.entitySelector(active))
+ .classed('active', true);
+ }
+
+ var drawWay = function(surface) {
+ draw.on('move', move)
+ .on('click', drawWay.add)
+ .on('clickWay', drawWay.addWay)
+ .on('clickNode', drawWay.addNode)
+ .on('undo', context.undo)
+ .on('cancel', drawWay.cancel)
+ .on('finish', drawWay.finish);
+
+ context.map()
+ .dblclickEnable(false)
+ .on('drawn.draw', setActiveElements);
+
+ setActiveElements();
+
+ surface.call(draw);
+
+ context.history()
+ .on('undone.draw', undone);
+ };
+
+ drawWay.off = function(surface) {
+ if (!finished)
+ context.pop();
+
+ context.map()
+ .on('drawn.draw', null);
+
+ surface.call(draw.off)
+ .selectAll('.active')
+ .classed('active', false);
+
+ context.history()
+ .on('undone.draw', null);
+ };
+
+ function ReplaceTemporaryNode(newNode) {
+ return function(graph) {
+ if (isArea) {
+ return graph
+ .replace(way.addNode(newNode.id, index))
+ .remove(end);
+
+ } else {
+ return graph
+ .replace(graph.entity(wayId).addNode(newNode.id, index))
+ .remove(end)
+ .remove(segment)
+ .remove(start);
+ }
+ };
+ }
+
+ // Accept the current position of the temporary node and continue drawing.
+ drawWay.add = function(loc) {
+
+ // prevent duplicate nodes
+ var last = context.hasEntity(way.nodes[way.nodes.length - (isArea ? 2 : 1)]);
+ if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return;
+
+ var newNode = iD.Node({loc: loc});
+
+ context.replace(
+ iD.actions.AddEntity(newNode),
+ ReplaceTemporaryNode(newNode),
+ annotation);
+
+ finished = true;
+ context.enter(mode);
+ };
+
+ // Connect the way to an existing way.
+ drawWay.addWay = function(loc, edge) {
+ var previousEdge = startIndex ?
+ [way.nodes[startIndex], way.nodes[startIndex - 1]] :
+ [way.nodes[0], way.nodes[1]];
+
+ // Avoid creating duplicate segments
+ if (!isArea && iD.geo.edgeEqual(edge, previousEdge))
+ return;
+
+ var newNode = iD.Node({ loc: loc });
+
+ context.perform(
+ iD.actions.AddMidpoint({ loc: loc, edge: edge}, newNode),
+ ReplaceTemporaryNode(newNode),
+ annotation);
+
+ finished = true;
+ context.enter(mode);
+ };
+
+ // Connect the way to an existing node and continue drawing.
+ drawWay.addNode = function(node) {
+
+ // Avoid creating duplicate segments
+ if (way.areAdjacent(node.id, way.nodes[way.nodes.length - 1])) return;
+
+ context.perform(
+ ReplaceTemporaryNode(node),
+ annotation);
+
+ finished = true;
+ context.enter(mode);
+ };
+
+ // Finish the draw operation, removing the temporary node. If the way has enough
+ // nodes to be valid, it's selected. Otherwise, return to browse mode.
+ drawWay.finish = function() {
+ context.pop();
+ finished = true;
+
+ window.setTimeout(function() {
+ context.map().dblclickEnable(true);
+ }, 1000);
+
+ if (context.hasEntity(wayId)) {
+ context.enter(
+ iD.modes.Select(context, [wayId])
+ .suppressMenu(true)
+ .newFeature(true));
+ } else {
+ context.enter(iD.modes.Browse(context));
+ }
+ };
+
+ // Cancel the draw operation and return to browse, deleting everything drawn.
+ drawWay.cancel = function() {
+ context.perform(
+ d3.functor(baseGraph),
+ t('operations.cancel_draw.annotation'));
+
+ window.setTimeout(function() {
+ context.map().dblclickEnable(true);
+ }, 1000);
+
+ finished = true;
+ context.enter(iD.modes.Browse(context));
+ };
+
+ drawWay.tail = function(text) {
+ draw.tail(text);
+ return drawWay;
+ };
+
+ return drawWay;
+ }
+
+ function Hash(context) {
+ var s0 = null, // cached location.hash
+ lat = 90 - 1e-8; // allowable latitude range
+
+ var parser = function(map, s) {
+ var q = iD.util.stringQs(s);
+ var args = (q.map || '').split('/').map(Number);
+ if (args.length < 3 || args.some(isNaN)) {
+ return true; // replace bogus hash
+ } else if (s !== formatter(map).slice(1)) {
+ map.centerZoom([args[1],
+ Math.min(lat, Math.max(-lat, args[2]))], args[0]);
+ }
+ };
+
+ var formatter = function(map) {
+ var mode = context.mode(),
+ center = map.center(),
+ zoom = map.zoom(),
+ precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)),
+ q = _.omit(iD.util.stringQs(location.hash.substring(1)), 'comment'),
+ newParams = {};
+
+ if (mode && mode.id === 'browse') {
+ delete q.id;
+ } else {
+ var selected = context.selectedIDs().filter(function(id) {
+ return !context.entity(id).isNew();
+ });
+ if (selected.length) {
+ newParams.id = selected.join(',');
+ }
+ }
+
+ newParams.map = zoom.toFixed(2) +
+ '/' + center[0].toFixed(precision) +
+ '/' + center[1].toFixed(precision);
+
+ return '#' + iD.util.qsString(_.assign(q, newParams), true);
+ };
+
+ function update() {
+ if (context.inIntro()) return;
+ var s1 = formatter(context.map());
+ if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map!
+ }
+
+ var throttledUpdate = _.throttle(update, 500);
+
+ function hashchange() {
+ if (location.hash === s0) return; // ignore spurious hashchange events
+ if (parser(context.map(), (s0 = location.hash).substring(1))) {
+ update(); // replace bogus hash
+ }
+ }
+
+ function hash() {
+ context.map()
+ .on('move.hash', throttledUpdate);
+
+ context
+ .on('enter.hash', throttledUpdate);
+
+ d3.select(window)
+ .on('hashchange.hash', hashchange);
+
+ if (location.hash) {
+ var q = iD.util.stringQs(location.hash.substring(1));
+ if (q.id) context.zoomToEntity(q.id.split(',')[0], !q.map);
+ if (q.comment) context.storage('comment', q.comment);
+ hashchange();
+ if (q.map) hash.hadHash = true;
+ }
+ }
+
+ hash.off = function() {
+ throttledUpdate.cancel();
+
+ context.map()
+ .on('move.hash', null);
+
+ context
+ .on('enter.hash', null);
+
+ d3.select(window)
+ .on('hashchange.hash', null);
+
+ location.hash = '';
+ };
+
+ return hash;
+ }
+
+ function Lasso(context) {
+
+ var behavior = function(selection) {
+ var lasso;
+
+ function mousedown() {
+ var button = 0; // left
+ if (d3.event.button === button && d3.event.shiftKey === true) {
+ lasso = null;
+
+ selection
+ .on('mousemove.lasso', mousemove)
+ .on('mouseup.lasso', mouseup);
+
+ d3.event.stopPropagation();
+ }
+ }
+
+ function mousemove() {
+ if (!lasso) {
+ lasso = iD.ui.Lasso(context);
+ context.surface().call(lasso);
+ }
+
+ lasso.p(context.mouse());
+ }
+
+ function normalize(a, b) {
+ return [
+ [Math.min(a[0], b[0]), Math.min(a[1], b[1])],
+ [Math.max(a[0], b[0]), Math.max(a[1], b[1])]];
+ }
+
+ function lassoed() {
+ if (!lasso) return [];
+
+ var graph = context.graph(),
+ bounds = lasso.extent().map(context.projection.invert),
+ extent = iD.geo.Extent(normalize(bounds[0], bounds[1]));
+
+ return _.map(context.intersects(extent).filter(function(entity) {
+ return entity.type === 'node' &&
+ iD.geo.pointInPolygon(context.projection(entity.loc), lasso.coordinates) &&
+ !context.features().isHidden(entity, graph, entity.geometry(graph));
+ }), 'id');
+ }
+
+ function mouseup() {
+ selection
+ .on('mousemove.lasso', null)
+ .on('mouseup.lasso', null);
+
+ if (!lasso) return;
+
+ var ids = lassoed();
+ lasso.close();
+
+ if (ids.length) {
+ context.enter(iD.modes.Select(context, ids));
+ }
+ }
+
+ selection
+ .on('mousedown.lasso', mousedown);
+ };
+
+ behavior.off = function(selection) {
+ selection.on('mousedown.lasso', null);
+ };
+
+ return behavior;
+ }
+
+ function Paste(context) {
+ var keybinding = d3.keybinding('paste');
+
+ function omitTag(v, k) {
+ return (
+ k === 'phone' ||
+ k === 'fax' ||
+ k === 'email' ||
+ k === 'website' ||
+ k === 'url' ||
+ k === 'note' ||
+ k === 'description' ||
+ k.indexOf('name') !== -1 ||
+ k.indexOf('wiki') === 0 ||
+ k.indexOf('addr:') === 0 ||
+ k.indexOf('contact:') === 0
+ );
+ }
+
+ function doPaste() {
+ d3.event.preventDefault();
+ if (context.inIntro()) return;
+
+ var baseGraph = context.graph(),
+ mouse = context.mouse(),
+ projection = context.projection,
+ viewport = iD.geo.Extent(projection.clipExtent()).polygon();
+
+ if (!iD.geo.pointInPolygon(mouse, viewport)) return;
+
+ var extent = iD.geo.Extent(),
+ oldIDs = context.copyIDs(),
+ oldGraph = context.copyGraph(),
+ newIDs = [];
+
+ if (!oldIDs.length) return;
+
+ var action = iD.actions.CopyEntities(oldIDs, oldGraph);
+ context.perform(action);
+
+ var copies = action.copies();
+ for (var id in copies) {
+ var oldEntity = oldGraph.entity(id),
+ newEntity = copies[id];
+
+ extent._extend(oldEntity.extent(oldGraph));
+ newIDs.push(newEntity.id);
+ context.perform(iD.actions.ChangeTags(newEntity.id, _.omit(newEntity.tags, omitTag)));
+ }
+
+ // Put pasted objects where mouse pointer is..
+ var center = projection(extent.center()),
+ delta = [ mouse[0] - center[0], mouse[1] - center[1] ];
+
+ context.perform(iD.actions.Move(newIDs, delta, projection));
+ context.enter(iD.modes.Move(context, newIDs, baseGraph));
+ }
+
+ function paste() {
+ keybinding.on(iD.ui.cmd('⌘V'), doPaste);
+ d3.select(document).call(keybinding);
+ return paste;
+ }
+
+ paste.off = function() {
+ d3.select(document).call(keybinding.off);
+ };
+
+ return paste;
+ }
+
+ function Select(context) {
+ function keydown() {
+ if (d3.event && d3.event.shiftKey) {
+ context.surface()
+ .classed('behavior-multiselect', true);
+ }
+ }
+
+ function keyup() {
+ if (!d3.event || !d3.event.shiftKey) {
+ context.surface()
+ .classed('behavior-multiselect', false);
+ }
+ }
+
+ function click() {
+ var datum = d3.event.target.__data__,
+ lasso = d3.select('#surface .lasso').node(),
+ mode = context.mode();
+
+ if (!(datum instanceof iD.Entity)) {
+ if (!d3.event.shiftKey && !lasso && mode.id !== 'browse')
+ context.enter(iD.modes.Browse(context));
+
+ } else if (!d3.event.shiftKey && !lasso) {
+ // Avoid re-entering Select mode with same entity.
+ if (context.selectedIDs().length !== 1 || context.selectedIDs()[0] !== datum.id) {
+ context.enter(iD.modes.Select(context, [datum.id]));
+ } else {
+ mode.suppressMenu(false).reselect();
+ }
+ } else if (context.selectedIDs().indexOf(datum.id) >= 0) {
+ var selectedIDs = _.without(context.selectedIDs(), datum.id);
+ context.enter(selectedIDs.length ?
+ iD.modes.Select(context, selectedIDs) :
+ iD.modes.Browse(context));
+
+ } else {
+ context.enter(iD.modes.Select(context, context.selectedIDs().concat([datum.id])));
+ }
+ }
+
+ var behavior = function(selection) {
+ d3.select(window)
+ .on('keydown.select', keydown)
+ .on('keyup.select', keyup);
+
+ selection.on('click.select', click);
+
+ keydown();
+ };
+
+ behavior.off = function(selection) {
+ d3.select(window)
+ .on('keydown.select', null)
+ .on('keyup.select', null);
+
+ selection.on('click.select', null);
+
+ keyup();
+ };
+
+ return behavior;
+ }
+
+ exports.AddWay = AddWay;
+ exports.Breathe = Breathe;
+ exports.Copy = Copy;
+ exports.drag = drag;
+ exports.DrawWay = DrawWay;
+ exports.Draw = Draw;
+ exports.Edit = Edit;
+ exports.Hash = Hash;
+ exports.Hover = Hover;
+ exports.Lasso = Lasso;
+ exports.Paste = Paste;
+ exports.Select = Select;
+ exports.Tail = Tail;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+}));
\ No newline at end of file
diff --git a/js/id/behavior/add_way.js b/modules/behavior/add_way.js
similarity index 89%
rename from js/id/behavior/add_way.js
rename to modules/behavior/add_way.js
index 4a52143ee..e85475746 100644
--- a/js/id/behavior/add_way.js
+++ b/modules/behavior/add_way.js
@@ -1,6 +1,8 @@
-iD.behavior.AddWay = function(context) {
+import { Draw } from './draw';
+
+export function AddWay(context) {
var event = d3.dispatch('start', 'startFromWay', 'startFromNode'),
- draw = iD.behavior.Draw(context);
+ draw = Draw(context);
var addWay = function(surface) {
draw.on('click', event.start)
@@ -33,4 +35,4 @@ iD.behavior.AddWay = function(context) {
};
return d3.rebind(addWay, event, 'on');
-};
+}
diff --git a/js/id/behavior/breathe.js b/modules/behavior/breathe.js
similarity index 98%
rename from js/id/behavior/breathe.js
rename to modules/behavior/breathe.js
index 95744c9d3..2f4b5c15b 100644
--- a/js/id/behavior/breathe.js
+++ b/modules/behavior/breathe.js
@@ -1,4 +1,4 @@
-iD.behavior.Breathe = function() {
+export function Breathe(){
var duration = 800,
selector = '.selected.shadow, .selected .shadow',
selected = d3.select(null),
@@ -102,4 +102,4 @@ iD.behavior.Breathe = function() {
};
return breathe;
-};
+}
diff --git a/js/id/behavior/copy.js b/modules/behavior/copy.js
similarity index 98%
rename from js/id/behavior/copy.js
rename to modules/behavior/copy.js
index 7029289ab..e19c4cbd0 100644
--- a/js/id/behavior/copy.js
+++ b/modules/behavior/copy.js
@@ -1,4 +1,4 @@
-iD.behavior.Copy = function(context) {
+export function Copy(context) {
var keybinding = d3.keybinding('copy');
function groupEntities(ids, graph) {
@@ -76,4 +76,4 @@ iD.behavior.Copy = function(context) {
};
return copy;
-};
+}
diff --git a/js/id/behavior/drag.js b/modules/behavior/drag.js
similarity index 99%
rename from js/id/behavior/drag.js
rename to modules/behavior/drag.js
index 839890bd9..04aa2ddbf 100644
--- a/js/id/behavior/drag.js
+++ b/modules/behavior/drag.js
@@ -14,7 +14,7 @@
* Delegation is supported via the `delegate` function.
*/
-iD.behavior.drag = function() {
+export function drag() {
function d3_eventCancel() {
d3.event.stopPropagation();
d3.event.preventDefault();
@@ -91,7 +91,7 @@ iD.behavior.drag = function() {
var p = point(),
dx = p[0] - origin_[0],
dy = p[1] - origin_[1];
-
+
if (dx === 0 && dy === 0)
return;
@@ -198,4 +198,4 @@ iD.behavior.drag = function() {
};
return d3.rebind(drag, event, 'on');
-};
+}
diff --git a/js/id/behavior/draw.js b/modules/behavior/draw.js
similarity index 94%
rename from js/id/behavior/draw.js
rename to modules/behavior/draw.js
index 95057996f..453473dde 100644
--- a/js/id/behavior/draw.js
+++ b/modules/behavior/draw.js
@@ -1,17 +1,21 @@
-iD.behavior.Draw = function(context) {
+import { Edit } from './edit';
+import { Hover } from './hover';
+import { Tail } from './tail';
+
+export function Draw(context) {
var event = d3.dispatch('move', 'click', 'clickWay',
'clickNode', 'undo', 'cancel', 'finish'),
keybinding = d3.keybinding('draw'),
- hover = iD.behavior.Hover(context)
+ hover = Hover(context)
.altDisables(true)
.on('hover', context.ui().sidebar.hover),
- tail = iD.behavior.Tail(),
- edit = iD.behavior.Edit(context),
+ tail = Tail(),
+ edit = Edit(context),
closeTolerance = 4,
tolerance = 12,
mouseLeave = false,
lastMouse = null,
- cached = iD.behavior.Draw;
+ cached = Draw;
function datum() {
if (d3.event.altKey) return {};
@@ -200,8 +204,8 @@ iD.behavior.Draw = function(context) {
};
return d3.rebind(draw, event, 'on');
-};
+}
-iD.behavior.Draw.usedTails = {};
-iD.behavior.Draw.disableSpace = false;
-iD.behavior.Draw.lastSpace = null;
+Draw.usedTails = {};
+Draw.disableSpace = false;
+Draw.lastSpace = null;
diff --git a/js/id/behavior/draw_way.js b/modules/behavior/draw_way.js
similarity index 97%
rename from js/id/behavior/draw_way.js
rename to modules/behavior/draw_way.js
index 5771c7147..c5191869d 100644
--- a/js/id/behavior/draw_way.js
+++ b/modules/behavior/draw_way.js
@@ -1,11 +1,13 @@
-iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) {
+import { Draw } from './draw';
+
+export function DrawWay(context, wayId, index, mode, baseGraph) {
var way = context.entity(wayId),
isArea = context.geometry(wayId) === 'area',
finished = false,
annotation = t((way.isDegenerate() ?
'operations.start.annotation.' :
'operations.continue.annotation.') + context.geometry(wayId)),
- draw = iD.behavior.Draw(context);
+ draw = Draw(context);
var startIndex = typeof index === 'undefined' ? way.nodes.length - 1 : 0,
start = iD.Node({loc: context.graph().entity(way.nodes[startIndex]).loc}),
@@ -207,4 +209,4 @@ iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) {
};
return drawWay;
-};
+}
diff --git a/js/id/behavior/edit.js b/modules/behavior/edit.js
similarity index 82%
rename from js/id/behavior/edit.js
rename to modules/behavior/edit.js
index 7e3b6d405..33e61b1aa 100644
--- a/js/id/behavior/edit.js
+++ b/modules/behavior/edit.js
@@ -1,4 +1,4 @@
-iD.behavior.Edit = function(context) {
+export function Edit(context) {
function edit() {
context.map()
.minzoom(context.minEditableZoom());
@@ -10,4 +10,4 @@ iD.behavior.Edit = function(context) {
};
return edit;
-};
+}
diff --git a/js/id/behavior/hash.js b/modules/behavior/hash.js
similarity index 98%
rename from js/id/behavior/hash.js
rename to modules/behavior/hash.js
index c861ca575..16eed59ef 100644
--- a/js/id/behavior/hash.js
+++ b/modules/behavior/hash.js
@@ -1,4 +1,4 @@
-iD.behavior.Hash = function(context) {
+export function Hash(context) {
var s0 = null, // cached location.hash
lat = 90 - 1e-8; // allowable latitude range
@@ -89,4 +89,4 @@ iD.behavior.Hash = function(context) {
};
return hash;
-};
+}
diff --git a/js/id/behavior/hover.js b/modules/behavior/hover.js
similarity index 99%
rename from js/id/behavior/hover.js
rename to modules/behavior/hover.js
index 11df4c8f9..1fa46439d 100644
--- a/js/id/behavior/hover.js
+++ b/modules/behavior/hover.js
@@ -7,7 +7,7 @@
Only one of these elements can have the :hover pseudo-class, but all of them will
have the .hover class.
*/
-iD.behavior.Hover = function() {
+export function Hover() {
var dispatch = d3.dispatch('hover'),
selection,
altDisables,
@@ -124,4 +124,4 @@ iD.behavior.Hover = function() {
};
return d3.rebind(hover, dispatch, 'on');
-};
+}
diff --git a/modules/behavior/index.js b/modules/behavior/index.js
new file mode 100644
index 000000000..6a04e35bc
--- /dev/null
+++ b/modules/behavior/index.js
@@ -0,0 +1,13 @@
+export { AddWay } from './add_way';
+export { Breathe } from './breathe';
+export { Copy } from './copy';
+export { drag } from './drag';
+export { DrawWay } from './draw_way';
+export { Draw } from './draw';
+export { Edit } from './edit';
+export { Hash } from './hash';
+export { Hover } from './hover';
+export { Lasso } from './lasso';
+export { Paste } from './paste';
+export { Select } from './select';
+export { Tail } from './tail';
diff --git a/js/id/behavior/lasso.js b/modules/behavior/lasso.js
similarity index 97%
rename from js/id/behavior/lasso.js
rename to modules/behavior/lasso.js
index 5cf379564..910e64657 100644
--- a/js/id/behavior/lasso.js
+++ b/modules/behavior/lasso.js
@@ -1,4 +1,4 @@
-iD.behavior.Lasso = function(context) {
+export function Lasso(context) {
var behavior = function(selection) {
var lasso;
@@ -69,4 +69,4 @@ iD.behavior.Lasso = function(context) {
};
return behavior;
-};
+}
diff --git a/js/id/behavior/paste.js b/modules/behavior/paste.js
similarity index 97%
rename from js/id/behavior/paste.js
rename to modules/behavior/paste.js
index 82dd028da..47101aab6 100644
--- a/js/id/behavior/paste.js
+++ b/modules/behavior/paste.js
@@ -1,4 +1,4 @@
-iD.behavior.Paste = function(context) {
+export function Paste(context) {
var keybinding = d3.keybinding('paste');
function omitTag(v, k) {
@@ -67,4 +67,4 @@ iD.behavior.Paste = function(context) {
};
return paste;
-};
+}
diff --git a/js/id/behavior/select.js b/modules/behavior/select.js
similarity index 97%
rename from js/id/behavior/select.js
rename to modules/behavior/select.js
index ce4484e24..c971095ef 100644
--- a/js/id/behavior/select.js
+++ b/modules/behavior/select.js
@@ -1,4 +1,4 @@
-iD.behavior.Select = function(context) {
+export function Select(context) {
function keydown() {
if (d3.event && d3.event.shiftKey) {
context.surface()
@@ -61,4 +61,4 @@ iD.behavior.Select = function(context) {
};
return behavior;
-};
+}
diff --git a/js/id/behavior/tail.js b/modules/behavior/tail.js
similarity index 98%
rename from js/id/behavior/tail.js
rename to modules/behavior/tail.js
index 6288948a3..2850a360b 100644
--- a/js/id/behavior/tail.js
+++ b/modules/behavior/tail.js
@@ -1,4 +1,4 @@
-iD.behavior.Tail = function() {
+export function Tail() {
var text,
container,
xmargin = 25,
@@ -79,4 +79,4 @@ iD.behavior.Tail = function() {
};
return tail;
-};
+}
diff --git a/test/index.html b/test/index.html
index 8032f5595..8b2b0f539 100644
--- a/test/index.html
+++ b/test/index.html
@@ -43,6 +43,7 @@
+
@@ -102,21 +103,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-