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 @@ - - - - - - - - - - - - - - -