From fb838645ae0190a6d4fc3bd57832393113ace54a Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Thu, 16 Jun 2016 11:58:23 -0700 Subject: [PATCH] refactor svg into ES6 modules for #3118 --- Makefile | 22 +- index.html | 19 +- js/id/svg.js | 98 - js/lib/id/svg.js | 2224 ++++++++++++++++++++ {js/id => modules}/svg/areas.js | 4 +- {js/id => modules}/svg/debug.js | 4 +- {js/id => modules}/svg/defs.js | 4 +- {js/id => modules}/svg/gpx.js | 4 +- {js/id => modules}/svg/icon.js | 4 +- modules/svg/index.js | 20 + {js/id => modules}/svg/labels.js | 4 +- {js/id => modules}/svg/layers.js | 4 +- {js/id => modules}/svg/lines.js | 4 +- {js/id => modules}/svg/mapillary_images.js | 4 +- {js/id => modules}/svg/mapillary_signs.js | 4 +- {js/id => modules}/svg/midpoints.js | 4 +- modules/svg/one_way_segments.js | 59 + {js/id => modules}/svg/osm.js | 4 +- modules/svg/path.js | 15 + modules/svg/point_transform.js | 7 + {js/id => modules}/svg/points.js | 4 +- modules/svg/relation_member_tags.js | 12 + {js/id => modules}/svg/tag_classes.js | 4 +- {js/id => modules}/svg/turns.js | 4 +- {js/id => modules}/svg/vertices.js | 4 +- test/index.html | 19 +- test/rendering.html | 12 +- 27 files changed, 2377 insertions(+), 194 deletions(-) delete mode 100644 js/id/svg.js create mode 100644 js/lib/id/svg.js rename {js/id => modules}/svg/areas.js (99%) rename {js/id => modules}/svg/debug.js (98%) rename {js/id => modules}/svg/defs.js (98%) rename {js/id => modules}/svg/gpx.js (98%) rename {js/id => modules}/svg/icon.js (85%) create mode 100644 modules/svg/index.js rename {js/id => modules}/svg/labels.js (99%) rename {js/id => modules}/svg/layers.js (97%) rename {js/id => modules}/svg/lines.js (98%) rename {js/id => modules}/svg/mapillary_images.js (98%) rename {js/id => modules}/svg/mapillary_signs.js (98%) rename {js/id => modules}/svg/midpoints.js (98%) create mode 100644 modules/svg/one_way_segments.js rename {js/id => modules}/svg/osm.js (90%) create mode 100644 modules/svg/path.js create mode 100644 modules/svg/point_transform.js rename {js/id => modules}/svg/points.js (97%) create mode 100644 modules/svg/relation_member_tags.js rename {js/id => modules}/svg/tag_classes.js (99%) rename {js/id => modules}/svg/turns.js (98%) rename {js/id => modules}/svg/vertices.js (99%) diff --git a/Makefile b/Makefile index 1d7c58454..7dcaadc42 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ MODULE_TARGETS = \ js/lib/id/operations.js \ js/lib/id/presets.js \ js/lib/id/services.js \ + js/lib/id/svg.js \ js/lib/id/util.js \ js/lib/id/validations.js @@ -81,6 +82,10 @@ 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 $@ +js/lib/id/svg.js: $(shell find modules/svg -type f) + @rm -f $@ + node_modules/.bin/rollup -f umd -n iD.svg modules/svg/index.js --no-strict -o $@ + js/lib/id/util.js: $(shell find modules/util -type f) @rm -f $@ node_modules/.bin/rollup -f umd -n iD.util modules/util/index.js --no-strict -o $@ @@ -132,23 +137,6 @@ dist/iD.js: \ js/id/renderer/features.js \ js/id/renderer/map.js \ js/id/renderer/tile_layer.js \ - js/id/svg.js \ - js/id/svg/areas.js \ - js/id/svg/debug.js \ - js/id/svg/defs.js \ - js/id/svg/gpx.js \ - js/id/svg/icon.js \ - js/id/svg/labels.js \ - js/id/svg/layers.js \ - js/id/svg/lines.js \ - js/id/svg/mapillary_images.js \ - js/id/svg/mapillary_signs.js \ - js/id/svg/midpoints.js \ - js/id/svg/osm.js \ - js/id/svg/points.js \ - js/id/svg/tag_classes.js \ - js/id/svg/turns.js \ - js/id/svg/vertices.js \ js/id/ui.js \ js/id/ui/account.js \ js/id/ui/attribution.js \ diff --git a/index.html b/index.html index ee43e0f93..002aad4e5 100644 --- a/index.html +++ b/index.html @@ -41,6 +41,7 @@ + @@ -52,24 +53,6 @@ - - - - - - - - - - - - - - - - - - diff --git a/js/id/svg.js b/js/id/svg.js deleted file mode 100644 index d1ab55b71..000000000 --- a/js/id/svg.js +++ /dev/null @@ -1,98 +0,0 @@ -iD.svg = { - PointTransform: function(projection) { - return function(entity) { - // http://jsperf.com/short-array-join - var pt = projection(entity.loc); - return 'translate(' + pt[0] + ',' + pt[1] + ')'; - }; - }, - - Path: function(projection, graph, polygon) { - var cache = {}, - clip = d3.geo.clipExtent().extent(projection.clipExtent()).stream, - project = projection.stream, - path = d3.geo.path() - .projection({stream: function(output) { return polygon ? project(output) : project(clip(output)); }}); - - return function(entity) { - if (entity.id in cache) { - return cache[entity.id]; - } else { - return cache[entity.id] = path(entity.asGeoJSON(graph)); - } - }; - }, - - OneWaySegments: function(projection, graph, dt) { - return function(entity) { - var a, - b, - i = 0, - offset = dt, - segments = [], - clip = d3.geo.clipExtent().extent(projection.clipExtent()).stream, - coordinates = graph.childNodes(entity).map(function(n) { - return n.loc; - }); - - if (entity.tags.oneway === '-1') coordinates.reverse(); - - d3.geo.stream({ - type: 'LineString', - coordinates: coordinates - }, projection.stream(clip({ - lineStart: function() {}, - lineEnd: function() { - a = null; - }, - point: function(x, y) { - b = [x, y]; - - if (a) { - var span = iD.geo.euclideanDistance(a, b) - offset; - - if (span >= 0) { - var angle = Math.atan2(b[1] - a[1], b[0] - a[0]), - dx = dt * Math.cos(angle), - dy = dt * Math.sin(angle), - p = [a[0] + offset * Math.cos(angle), - a[1] + offset * Math.sin(angle)]; - - var segment = 'M' + a[0] + ',' + a[1] + - 'L' + p[0] + ',' + p[1]; - - for (span -= dt; span >= 0; span -= dt) { - p[0] += dx; - p[1] += dy; - segment += 'L' + p[0] + ',' + p[1]; - } - - segment += 'L' + b[0] + ',' + b[1]; - segments.push({id: entity.id, index: i, d: segment}); - } - - offset = -span; - i++; - } - - a = b; - } - }))); - - return segments; - }; - }, - - RelationMemberTags: function(graph) { - return function(entity) { - var tags = entity.tags; - graph.parentRelations(entity).forEach(function(relation) { - var type = relation.tags.type; - if (type === 'multipolygon' || type === 'boundary') { - tags = _.extend({}, relation.tags, tags); - } - }); - return tags; - }; - } -}; diff --git a/js/lib/id/svg.js b/js/lib/id/svg.js new file mode 100644 index 000000000..ae75bb7f1 --- /dev/null +++ b/js/lib/id/svg.js @@ -0,0 +1,2224 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.iD = global.iD || {}, global.iD.svg = global.iD.svg || {}))); +}(this, function (exports) { 'use strict'; + + function Areas(projection) { + // Patterns only work in Firefox when set directly on element. + // (This is not a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=750632) + var patterns = { + wetland: 'wetland', + beach: 'beach', + scrub: 'scrub', + construction: 'construction', + military: 'construction', + cemetery: 'cemetery', + grave_yard: 'cemetery', + meadow: 'meadow', + farm: 'farmland', + farmland: 'farmland', + orchard: 'orchard' + }; + + var patternKeys = ['landuse', 'natural', 'amenity']; + + function setPattern(d) { + for (var i = 0; i < patternKeys.length; i++) { + if (patterns.hasOwnProperty(d.tags[patternKeys[i]])) { + this.style.fill = this.style.stroke = 'url("#pattern-' + patterns[d.tags[patternKeys[i]]] + '")'; + return; + } + } + this.style.fill = this.style.stroke = ''; + } + + return function drawAreas(surface, graph, entities, filter) { + var path = iD.svg.Path(projection, graph, true), + areas = {}, + multipolygon; + + for (var i = 0; i < entities.length; i++) { + var entity = entities[i]; + if (entity.geometry(graph) !== 'area') continue; + + multipolygon = iD.geo.isSimpleMultipolygonOuterMember(entity, graph); + if (multipolygon) { + areas[multipolygon.id] = { + entity: multipolygon.mergeTags(entity.tags), + area: Math.abs(entity.area(graph)) + }; + } else if (!areas[entity.id]) { + areas[entity.id] = { + entity: entity, + area: Math.abs(entity.area(graph)) + }; + } + } + + areas = d3.values(areas).filter(function hasPath(a) { return path(a.entity); }); + areas.sort(function areaSort(a, b) { return b.area - a.area; }); + areas = _.map(areas, 'entity'); + + var strokes = areas.filter(function(area) { + return area.type === 'way'; + }); + + var data = { + clip: areas, + shadow: strokes, + stroke: strokes, + fill: areas + }; + + var clipPaths = surface.selectAll('defs').selectAll('.clipPath') + .filter(filter) + .data(data.clip, iD.Entity.key); + + clipPaths.enter() + .append('clipPath') + .attr('class', 'clipPath') + .attr('id', function(entity) { return entity.id + '-clippath'; }) + .append('path'); + + clipPaths.selectAll('path') + .attr('d', path); + + clipPaths.exit() + .remove(); + + var areagroup = surface + .selectAll('.layer-areas') + .selectAll('g.areagroup') + .data(['fill', 'shadow', 'stroke']); + + areagroup.enter() + .append('g') + .attr('class', function(d) { return 'layer areagroup area-' + d; }); + + var paths = areagroup + .selectAll('path') + .filter(filter) + .data(function(layer) { return data[layer]; }, iD.Entity.key); + + // Remove exiting areas first, so they aren't included in the `fills` + // array used for sorting below (https://github.com/openstreetmap/iD/issues/1903). + paths.exit() + .remove(); + + var fills = surface.selectAll('.area-fill path.area')[0]; + + var bisect = d3.bisector(function(node) { + return -node.__data__.area(graph); + }).left; + + function sortedByArea(entity) { + if (this.__data__ === 'fill') { + return fills[bisect(fills, -entity.area(graph))]; + } + } + + paths.enter() + .insert('path', sortedByArea) + .each(function(entity) { + var layer = this.parentNode.__data__; + + this.setAttribute('class', entity.type + ' area ' + layer + ' ' + entity.id); + + if (layer === 'fill') { + this.setAttribute('clip-path', 'url(#' + entity.id + '-clippath)'); + setPattern.apply(this, arguments); + } + }) + .call(iD.svg.TagClasses()); + + paths + .attr('d', path); + }; + } + + function Debug(projection, context) { + + function multipolygons(imagery) { + return imagery.map(function(data) { + return { + type: 'MultiPolygon', + coordinates: [ data.polygon ] + }; + }); + } + + function drawDebug(surface) { + var showsTile = context.getDebug('tile'), + showsCollision = context.getDebug('collision'), + showsImagery = context.getDebug('imagery'), + showsImperial = context.getDebug('imperial'), + showsDriveLeft = context.getDebug('driveLeft'), + path = d3.geo.path().projection(projection); + + + var debugData = []; + if (showsTile) { + debugData.push({ class: 'red', label: 'tile' }); + } + if (showsCollision) { + debugData.push({ class: 'yellow', label: 'collision' }); + } + if (showsImagery) { + debugData.push({ class: 'orange', label: 'imagery' }); + } + if (showsImperial) { + debugData.push({ class: 'cyan', label: 'imperial' }); + } + if (showsDriveLeft) { + debugData.push({ class: 'green', label: 'driveLeft' }); + } + + + var legend = d3.select('#content') + .selectAll('.debug-legend') + .data(debugData.length ? [0] : []); + + legend.enter() + .append('div') + .attr('class', 'fillD debug-legend'); + + legend.exit() + .remove(); + + + var legendItems = legend.selectAll('.debug-legend-item') + .data(debugData, function(d) { return d.label; }); + + legendItems.enter() + .append('span') + .attr('class', function(d) { return 'debug-legend-item ' + d.class; }) + .text(function(d) { return d.label; }); + + legendItems.exit() + .remove(); + + + var layer = surface.selectAll('.layer-debug') + .data(showsImagery || showsImperial || showsDriveLeft ? [0] : []); + + layer.enter() + .append('g') + .attr('class', 'layer-debug'); + + layer.exit() + .remove(); + + + var extent = context.map().extent(), + availableImagery = showsImagery && multipolygons(iD.data.imagery.filter(function(source) { + if (!source.polygon) return false; + return source.polygon.some(function(polygon) { + return iD.geo.polygonIntersectsPolygon(polygon, extent, true); + }); + })); + + var imagery = layer.selectAll('path.debug-imagery') + .data(showsImagery ? availableImagery : []); + + imagery.enter() + .append('path') + .attr('class', 'debug-imagery debug orange'); + + imagery.exit() + .remove(); + + + var imperial = layer + .selectAll('path.debug-imperial') + .data(showsImperial ? [iD.data.imperial] : []); + + imperial.enter() + .append('path') + .attr('class', 'debug-imperial debug cyan'); + + imperial.exit() + .remove(); + + + var driveLeft = layer + .selectAll('path.debug-drive-left') + .data(showsDriveLeft ? [iD.data.driveLeft] : []); + + driveLeft.enter() + .append('path') + .attr('class', 'debug-drive-left debug green'); + + driveLeft.exit() + .remove(); + + + // update + layer.selectAll('path') + .attr('d', path); + } + + // This looks strange because `enabled` methods on other layers are + // chainable getter/setters, and this one is just a getter. + drawDebug.enabled = function() { + if (!arguments.length) { + return context.getDebug('tile') || + context.getDebug('collision') || + context.getDebug('imagery') || + context.getDebug('imperial') || + context.getDebug('driveLeft'); + } else { + return this; + } + }; + + return drawDebug; + } + + /* + A standalone SVG element that contains only a `defs` sub-element. To be + used once globally, since defs IDs must be unique within a document. + */ + function Defs(context) { + + function SVGSpriteDefinition(id, href) { + return function(defs) { + d3.xml(href, 'image/svg+xml', function(err, svg) { + if (err) return; + defs.node().appendChild( + d3.select(svg.documentElement).attr('id', id).node() + ); + }); + }; + } + + return function drawDefs(selection) { + var defs = selection.append('defs'); + + // marker + defs.append('marker') + .attr({ + id: 'oneway-marker', + viewBox: '0 0 10 10', + refY: 2.5, + refX: 5, + markerWidth: 2, + markerHeight: 2, + markerUnits: 'strokeWidth', + orient: 'auto' + }) + .append('path') + .attr('class', 'oneway') + .attr('d', 'M 5 3 L 0 3 L 0 2 L 5 2 L 5 0 L 10 2.5 L 5 5 z') + .attr('stroke', 'none') + .attr('fill', '#000') + .attr('opacity', '0.5'); + + // patterns + var patterns = defs.selectAll('pattern') + .data([ + // pattern name, pattern image name + ['wetland', 'wetland'], + ['construction', 'construction'], + ['cemetery', 'cemetery'], + ['orchard', 'orchard'], + ['farmland', 'farmland'], + ['beach', 'dots'], + ['scrub', 'dots'], + ['meadow', 'dots'] + ]) + .enter() + .append('pattern') + .attr({ + id: function (d) { + return 'pattern-' + d[0]; + }, + width: 32, + height: 32, + patternUnits: 'userSpaceOnUse' + }); + + patterns.append('rect') + .attr({ + x: 0, + y: 0, + width: 32, + height: 32, + 'class': function (d) { + return 'pattern-color-' + d[0]; + } + }); + + patterns.append('image') + .attr({ + x: 0, + y: 0, + width: 32, + height: 32 + }) + .attr('xlink:href', function (d) { + return context.imagePath('pattern/' + d[1] + '.png'); + }); + + // clip paths + defs.selectAll() + .data([12, 18, 20, 32, 45]) + .enter().append('clipPath') + .attr('id', function (d) { + return 'clip-square-' + d; + }) + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', function (d) { + return d; + }) + .attr('height', function (d) { + return d; + }); + + defs.call(SVGSpriteDefinition( + 'iD-sprite', + context.imagePath('iD-sprite.svg'))); + + defs.call(SVGSpriteDefinition( + 'maki-sprite', + context.imagePath('maki-sprite.svg'))); + }; + } + + function Gpx(projection, context, dispatch) { + var showLabels = true, + layer; + + function init() { + if (iD.svg.Gpx.initialized) return; // run once + + iD.svg.Gpx.geojson = {}; + iD.svg.Gpx.enabled = true; + + function over() { + d3.event.stopPropagation(); + d3.event.preventDefault(); + d3.event.dataTransfer.dropEffect = 'copy'; + } + + d3.select('body') + .attr('dropzone', 'copy') + .on('drop.localgpx', function() { + d3.event.stopPropagation(); + d3.event.preventDefault(); + if (!iD.detect().filedrop) return; + drawGpx.files(d3.event.dataTransfer.files); + }) + .on('dragenter.localgpx', over) + .on('dragexit.localgpx', over) + .on('dragover.localgpx', over); + + iD.svg.Gpx.initialized = true; + } + + + function drawGpx(surface) { + var geojson = iD.svg.Gpx.geojson, + enabled = iD.svg.Gpx.enabled; + + layer = surface.selectAll('.layer-gpx') + .data(enabled ? [0] : []); + + layer.enter() + .append('g') + .attr('class', 'layer-gpx'); + + layer.exit() + .remove(); + + + var paths = layer + .selectAll('path') + .data([geojson]); + + paths.enter() + .append('path') + .attr('class', 'gpx'); + + paths.exit() + .remove(); + + var path = d3.geo.path() + .projection(projection); + + paths + .attr('d', path); + + + var labels = layer.selectAll('text') + .data(showLabels && geojson.features ? geojson.features : []); + + labels.enter() + .append('text') + .attr('class', 'gpx'); + + labels.exit() + .remove(); + + labels + .text(function(d) { + return d.properties.desc || d.properties.name; + }) + .attr('x', function(d) { + var centroid = path.centroid(d); + return centroid[0] + 7; + }) + .attr('y', function(d) { + var centroid = path.centroid(d); + return centroid[1]; + }); + + } + + function toDom(x) { + return (new DOMParser()).parseFromString(x, 'text/xml'); + } + + drawGpx.showLabels = function(_) { + if (!arguments.length) return showLabels; + showLabels = _; + return this; + }; + + drawGpx.enabled = function(_) { + if (!arguments.length) return iD.svg.Gpx.enabled; + iD.svg.Gpx.enabled = _; + dispatch.change(); + return this; + }; + + drawGpx.hasGpx = function() { + var geojson = iD.svg.Gpx.geojson; + return (!(_.isEmpty(geojson) || _.isEmpty(geojson.features))); + }; + + drawGpx.geojson = function(gj) { + if (!arguments.length) return iD.svg.Gpx.geojson; + if (_.isEmpty(gj) || _.isEmpty(gj.features)) return this; + iD.svg.Gpx.geojson = gj; + dispatch.change(); + return this; + }; + + drawGpx.url = function(url) { + d3.text(url, function(err, data) { + if (!err) { + drawGpx.geojson(toGeoJSON.gpx(toDom(data))); + } + }); + return this; + }; + + drawGpx.files = function(fileList) { + if (!fileList.length) return this; + var f = fileList[0], + reader = new FileReader(); + + reader.onload = function(e) { + drawGpx.geojson(toGeoJSON.gpx(toDom(e.target.result))).fitZoom(); + }; + + reader.readAsText(f); + return this; + }; + + drawGpx.fitZoom = function() { + if (!this.hasGpx()) return this; + var geojson = iD.svg.Gpx.geojson; + + var map = context.map(), + viewport = map.trimmedExtent().polygon(), + coords = _.reduce(geojson.features, function(coords, feature) { + var c = feature.geometry.coordinates; + return _.union(coords, feature.geometry.type === 'Point' ? [c] : c); + }, []); + + if (!iD.geo.polygonIntersectsPolygon(viewport, coords, true)) { + var extent = iD.geo.Extent(d3.geo.bounds(geojson)); + map.centerZoom(extent.center(), map.trimmedExtentZoom(extent)); + } + + return this; + }; + + init(); + return drawGpx; + } + + function Icon(name, svgklass, useklass) { + return function drawIcon(selection) { + selection.selectAll('svg') + .data([0]) + .enter() + .append('svg') + .attr('class', 'icon ' + (svgklass || '')) + .append('use') + .attr('xlink:href', name) + .attr('class', useklass); + }; + } + + function Labels(projection, context) { + var path = d3.geo.path().projection(projection); + + // Replace with dict and iterate over entities tags instead? + var label_stack = [ + ['line', 'aeroway'], + ['line', 'highway'], + ['line', 'railway'], + ['line', 'waterway'], + ['area', 'aeroway'], + ['area', 'amenity'], + ['area', 'building'], + ['area', 'historic'], + ['area', 'leisure'], + ['area', 'man_made'], + ['area', 'natural'], + ['area', 'shop'], + ['area', 'tourism'], + ['point', 'aeroway'], + ['point', 'amenity'], + ['point', 'building'], + ['point', 'historic'], + ['point', 'leisure'], + ['point', 'man_made'], + ['point', 'natural'], + ['point', 'shop'], + ['point', 'tourism'], + ['line', 'name'], + ['area', 'name'], + ['point', 'name'] + ]; + + var default_size = 12; + + var font_sizes = label_stack.map(function(d) { + var style = iD.util.getStyle('text.' + d[0] + '.tag-' + d[1]), + m = style && style.cssText.match('font-size: ([0-9]{1,2})px;'); + if (m) return parseInt(m[1], 10); + + style = iD.util.getStyle('text.' + d[0]); + m = style && style.cssText.match('font-size: ([0-9]{1,2})px;'); + if (m) return parseInt(m[1], 10); + + return default_size; + }); + + var iconSize = 18; + + var pointOffsets = [ + [15, -11, 'start'], // right + [10, -11, 'start'], // unused right now + [-15, -11, 'end'] + ]; + + var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70, 25, + 75, 20, 80, 15, 95, 10, 90, 5, 95]; + + + var noIcons = ['building', 'landuse', 'natural']; + function blacklisted(preset) { + return _.some(noIcons, function(s) { + return preset.id.indexOf(s) >= 0; + }); + } + + function get(array, prop) { + return function(d, i) { return array[i][prop]; }; + } + + var textWidthCache = {}; + + function textWidth(text, size, elem) { + var c = textWidthCache[size]; + if (!c) c = textWidthCache[size] = {}; + + if (c[text]) { + return c[text]; + + } else if (elem) { + c[text] = elem.getComputedTextLength(); + return c[text]; + + } else { + var str = encodeURIComponent(text).match(/%[CDEFcdef]/g); + if (str === null) { + return size / 3 * 2 * text.length; + } else { + return size / 3 * (2 * text.length + str.length); + } + } + } + + function drawLineLabels(group, entities, filter, classes, labels) { + var texts = group.selectAll('text.' + classes) + .filter(filter) + .data(entities, iD.Entity.key); + + texts.enter() + .append('text') + .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; }) + .append('textPath') + .attr('class', 'textpath'); + + + texts.selectAll('.textpath') + .filter(filter) + .data(entities, iD.Entity.key) + .attr({ + 'startOffset': '50%', + 'xlink:href': function(d) { return '#labelpath-' + d.id; } + }) + .text(iD.util.displayName); + + texts.exit().remove(); + } + + function drawLinePaths(group, entities, filter, classes, labels) { + var halos = group.selectAll('path') + .filter(filter) + .data(entities, iD.Entity.key); + + halos.enter() + .append('path') + .style('stroke-width', get(labels, 'font-size')) + .attr('id', function(d) { return 'labelpath-' + d.id; }) + .attr('class', classes); + + halos.attr('d', get(labels, 'lineString')); + + halos.exit().remove(); + } + + function drawPointLabels(group, entities, filter, classes, labels) { + var texts = group.selectAll('text.' + classes) + .filter(filter) + .data(entities, iD.Entity.key); + + texts.enter() + .append('text') + .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; }); + + texts.attr('x', get(labels, 'x')) + .attr('y', get(labels, 'y')) + .style('text-anchor', get(labels, 'textAnchor')) + .text(iD.util.displayName) + .each(function(d, i) { textWidth(iD.util.displayName(d), labels[i].height, this); }); + + texts.exit().remove(); + return texts; + } + + function drawAreaLabels(group, entities, filter, classes, labels) { + entities = entities.filter(hasText); + labels = labels.filter(hasText); + return drawPointLabels(group, entities, filter, classes, labels); + + function hasText(d, i) { + return labels[i].hasOwnProperty('x') && labels[i].hasOwnProperty('y'); + } + } + + function drawAreaIcons(group, entities, filter, classes, labels) { + var icons = group.selectAll('use') + .filter(filter) + .data(entities, iD.Entity.key); + + icons.enter() + .append('use') + .attr('class', 'icon areaicon') + .attr('width', '18px') + .attr('height', '18px'); + + icons.attr('transform', get(labels, 'transform')) + .attr('xlink:href', function(d) { + var icon = context.presets().match(d, context.graph()).icon; + return '#' + icon + (icon === 'hairdresser' ? '-24': '-18'); // workaround: maki hairdresser-18 broken? + }); + + + icons.exit().remove(); + } + + function reverse(p) { + var angle = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]); + return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2); + } + + function lineString(nodes) { + return 'M' + nodes.join('L'); + } + + function subpath(nodes, from, to) { + function segmentLength(i) { + var dx = nodes[i][0] - nodes[i + 1][0]; + var dy = nodes[i][1] - nodes[i + 1][1]; + return Math.sqrt(dx * dx + dy * dy); + } + + var sofar = 0, + start, end, i0, i1; + for (var i = 0; i < nodes.length - 1; i++) { + var current = segmentLength(i); + var portion; + if (!start && sofar + current >= from) { + portion = (from - sofar) / current; + start = [ + nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]), + nodes[i][1] + portion * (nodes[i + 1][1] - nodes[i][1]) + ]; + i0 = i + 1; + } + if (!end && sofar + current >= to) { + portion = (to - sofar) / current; + end = [ + nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]), + nodes[i][1] + portion * (nodes[i + 1][1] - nodes[i][1]) + ]; + i1 = i + 1; + } + sofar += current; + + } + var ret = nodes.slice(i0, i1); + ret.unshift(start); + ret.push(end); + return ret; + + } + + function hideOnMouseover() { + var layers = d3.select(this) + .selectAll('.layer-label, .layer-halo'); + + layers.selectAll('.proximate') + .classed('proximate', false); + + var mouse = context.mouse(), + pad = 50, + rect = [mouse[0] - pad, mouse[1] - pad, mouse[0] + pad, mouse[1] + pad], + ids = _.map(rtree.search(rect), 'id'); + + if (!ids.length) return; + layers.selectAll('.' + ids.join(', .')) + .classed('proximate', true); + } + + var rtree = rbush(), + rectangles = {}; + + function drawLabels(surface, graph, entities, filter, dimensions, fullRedraw) { + var hidePoints = !surface.selectAll('.node.point').node(); + + var labelable = [], i, k, entity; + for (i = 0; i < label_stack.length; i++) labelable.push([]); + + if (fullRedraw) { + rtree.clear(); + rectangles = {}; + } else { + for (i = 0; i < entities.length; i++) { + rtree.remove(rectangles[entities[i].id]); + } + } + + // Split entities into groups specified by label_stack + for (i = 0; i < entities.length; i++) { + entity = entities[i]; + var geometry = entity.geometry(graph); + + if (geometry === 'vertex') + continue; + if (hidePoints && geometry === 'point') + continue; + + var preset = geometry === 'area' && context.presets().match(entity, graph), + icon = preset && !blacklisted(preset) && preset.icon; + + if (!icon && !iD.util.displayName(entity)) + continue; + + for (k = 0; k < label_stack.length; k++) { + if (geometry === label_stack[k][0] && entity.tags[label_stack[k][1]]) { + labelable[k].push(entity); + break; + } + } + } + + var positions = { + point: [], + line: [], + area: [] + }; + + var labelled = { + point: [], + line: [], + area: [] + }; + + // Try and find a valid label for labellable entities + for (k = 0; k < labelable.length; k++) { + var font_size = font_sizes[k]; + for (i = 0; i < labelable[k].length; i++) { + entity = labelable[k][i]; + var name = iD.util.displayName(entity), + width = name && textWidth(name, font_size), + p; + if (entity.geometry(graph) === 'point') { + p = getPointLabel(entity, width, font_size); + } else if (entity.geometry(graph) === 'line') { + p = getLineLabel(entity, width, font_size); + } else if (entity.geometry(graph) === 'area') { + p = getAreaLabel(entity, width, font_size); + } + if (p) { + p.classes = entity.geometry(graph) + ' tag-' + label_stack[k][1]; + positions[entity.geometry(graph)].push(p); + labelled[entity.geometry(graph)].push(entity); + } + } + } + + function getPointLabel(entity, width, height) { + var coord = projection(entity.loc), + m = 5, // margin + offset = pointOffsets[0], + p = { + height: height, + width: width, + x: coord[0] + offset[0], + y: coord[1] + offset[1], + textAnchor: offset[2] + }; + var rect = [p.x - m, p.y - m, p.x + width + m, p.y + height + m]; + if (tryInsert(rect, entity.id)) return p; + } + + + function getLineLabel(entity, width, height) { + var nodes = _.map(graph.childNodes(entity), 'loc').map(projection), + length = iD.geo.pathLength(nodes); + if (length < width + 20) return; + + for (var i = 0; i < lineOffsets.length; i++) { + var offset = lineOffsets[i], + middle = offset / 100 * length, + start = middle - width/2; + if (start < 0 || start + width > length) continue; + var sub = subpath(nodes, start, start + width), + rev = reverse(sub), + rect = [ + Math.min(sub[0][0], sub[sub.length - 1][0]) - 10, + Math.min(sub[0][1], sub[sub.length - 1][1]) - 10, + Math.max(sub[0][0], sub[sub.length - 1][0]) + 20, + Math.max(sub[0][1], sub[sub.length - 1][1]) + 30 + ]; + if (rev) sub = sub.reverse(); + if (tryInsert(rect, entity.id)) return { + 'font-size': height + 2, + lineString: lineString(sub), + startOffset: offset + '%' + }; + } + } + + function getAreaLabel(entity, width, height) { + var centroid = path.centroid(entity.asGeoJSON(graph, true)), + extent = entity.extent(graph), + entitywidth = projection(extent[1])[0] - projection(extent[0])[0], + rect; + + if (isNaN(centroid[0]) || entitywidth < 20) return; + + var iconX = centroid[0] - (iconSize/2), + iconY = centroid[1] - (iconSize/2), + textOffset = iconSize + 5; + + var p = { + transform: 'translate(' + iconX + ',' + iconY + ')' + }; + + if (width && entitywidth >= width + 20) { + p.x = centroid[0]; + p.y = centroid[1] + textOffset; + p.textAnchor = 'middle'; + p.height = height; + rect = [p.x - width/2, p.y, p.x + width/2, p.y + height + textOffset]; + } else { + rect = [iconX, iconY, iconX + iconSize, iconY + iconSize]; + } + + if (tryInsert(rect, entity.id)) return p; + + } + + function tryInsert(rect, id) { + // Check that label is visible + if (rect[0] < 0 || rect[1] < 0 || rect[2] > dimensions[0] || + rect[3] > dimensions[1]) return false; + var v = rtree.search(rect).length === 0; + if (v) { + rect.id = id; + rtree.insert(rect); + rectangles[id] = rect; + } + return v; + } + + var label = surface.selectAll('.layer-label'), + halo = surface.selectAll('.layer-halo'); + + // points + drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point); + drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo', positions.point); + + // lines + drawLinePaths(halo, labelled.line, filter, '', positions.line); + drawLineLabels(label, labelled.line, filter, 'linelabel', positions.line); + drawLineLabels(halo, labelled.line, filter, 'linelabel-halo', positions.line); + + // areas + drawAreaLabels(label, labelled.area, filter, 'arealabel', positions.area); + drawAreaLabels(halo, labelled.area, filter, 'arealabel-halo', positions.area); + drawAreaIcons(label, labelled.area, filter, 'arealabel-icon', positions.area); + + // debug + var showDebug = context.getDebug('collision'); + var debug = label.selectAll('.layer-label-debug') + .data(showDebug ? [true] : []); + + debug.enter() + .append('g') + .attr('class', 'layer-label-debug'); + + debug.exit() + .remove(); + + if (showDebug) { + var gj = rtree.all().map(function(d) { + return { type: 'Polygon', coordinates: [[ + [d[0], d[1]], + [d[2], d[1]], + [d[2], d[3]], + [d[0], d[3]], + [d[0], d[1]] + ]]}; + }); + + var debugboxes = debug.selectAll('.debug').data(gj); + + debugboxes.enter() + .append('path') + .attr('class', 'debug yellow'); + + debugboxes.exit() + .remove(); + + debugboxes + .attr('d', d3.geo.path().projection(null)); + } + } + + drawLabels.supersurface = function(supersurface) { + supersurface + .on('mousemove.hidelabels', hideOnMouseover) + .on('mousedown.hidelabels', function () { + supersurface.on('mousemove.hidelabels', null); + }) + .on('mouseup.hidelabels', function () { + supersurface.on('mousemove.hidelabels', hideOnMouseover); + }); + }; + + return drawLabels; + } + + function Layers(projection, context) { + var dispatch = d3.dispatch('change'), + svg = d3.select(null), + layers = [ + { id: 'osm', layer: iD.svg.Osm(projection, context, dispatch) }, + { id: 'gpx', layer: iD.svg.Gpx(projection, context, dispatch) }, + { id: 'mapillary-images', layer: iD.svg.MapillaryImages(projection, context, dispatch) }, + { id: 'mapillary-signs', layer: iD.svg.MapillarySigns(projection, context, dispatch) }, + { id: 'debug', layer: iD.svg.Debug(projection, context, dispatch) } + ]; + + + function drawLayers(selection) { + svg = selection.selectAll('.surface') + .data([0]); + + svg.enter() + .append('svg') + .attr('class', 'surface') + .append('defs'); + + var groups = svg.selectAll('.data-layer') + .data(layers); + + groups.enter() + .append('g') + .attr('class', function(d) { return 'data-layer data-layer-' + d.id; }); + + groups + .each(function(d) { d3.select(this).call(d.layer); }); + + groups.exit() + .remove(); + } + + drawLayers.all = function() { + return layers; + }; + + drawLayers.layer = function(id) { + var obj = _.find(layers, function(o) {return o.id === id;}); + return obj && obj.layer; + }; + + drawLayers.only = function(what) { + var arr = [].concat(what); + drawLayers.remove(_.difference(_.map(layers, 'id'), arr)); + return this; + }; + + drawLayers.remove = function(what) { + var arr = [].concat(what); + arr.forEach(function(id) { + layers = _.reject(layers, function(o) {return o.id === id;}); + }); + dispatch.change(); + return this; + }; + + drawLayers.add = function(what) { + var arr = [].concat(what); + arr.forEach(function(obj) { + if ('id' in obj && 'layer' in obj) { + layers.push(obj); + } + }); + dispatch.change(); + return this; + }; + + drawLayers.dimensions = function(_) { + if (!arguments.length) return svg.dimensions(); + svg.dimensions(_); + layers.forEach(function(obj) { + if (obj.layer.dimensions) { + obj.layer.dimensions(_); + } + }); + return this; + }; + + + return d3.rebind(drawLayers, dispatch, 'on'); + } + + function Lines(projection) { + + var highway_stack = { + motorway: 0, + motorway_link: 1, + trunk: 2, + trunk_link: 3, + primary: 4, + primary_link: 5, + secondary: 6, + tertiary: 7, + unclassified: 8, + residential: 9, + service: 10, + footway: 11 + }; + + function waystack(a, b) { + var as = 0, bs = 0; + + if (a.tags.highway) { as -= highway_stack[a.tags.highway]; } + if (b.tags.highway) { bs -= highway_stack[b.tags.highway]; } + return as - bs; + } + + return function drawLines(surface, graph, entities, filter) { + var ways = [], pathdata = {}, onewaydata = {}, + getPath = iD.svg.Path(projection, graph); + + for (var i = 0; i < entities.length; i++) { + var entity = entities[i], + outer = iD.geo.simpleMultipolygonOuterMember(entity, graph); + if (outer) { + ways.push(entity.mergeTags(outer.tags)); + } else if (entity.geometry(graph) === 'line') { + ways.push(entity); + } + } + + ways = ways.filter(getPath); + + pathdata = _.groupBy(ways, function(way) { return way.layer(); }); + + _.forOwn(pathdata, function(v, k) { + onewaydata[k] = _(v) + .filter(function(d) { return d.isOneWay(); }) + .map(iD.svg.OneWaySegments(projection, graph, 35)) + .flatten() + .valueOf(); + }); + + var layergroup = surface + .selectAll('.layer-lines') + .selectAll('g.layergroup') + .data(d3.range(-10, 11)); + + layergroup.enter() + .append('g') + .attr('class', function(d) { return 'layer layergroup layer' + String(d); }); + + + var linegroup = layergroup + .selectAll('g.linegroup') + .data(['shadow', 'casing', 'stroke']); + + linegroup.enter() + .append('g') + .attr('class', function(d) { return 'layer linegroup line-' + d; }); + + + var lines = linegroup + .selectAll('path') + .filter(filter) + .data( + function() { return pathdata[this.parentNode.parentNode.__data__] || []; }, + iD.Entity.key + ); + + // Optimization: call simple TagClasses only on enter selection. This + // works because iD.Entity.key is defined to include the entity v attribute. + lines.enter() + .append('path') + .attr('class', function(d) { return 'way line ' + this.parentNode.__data__ + ' ' + d.id; }) + .call(iD.svg.TagClasses()); + + lines + .sort(waystack) + .attr('d', getPath) + .call(iD.svg.TagClasses().tags(iD.svg.RelationMemberTags(graph))); + + lines.exit() + .remove(); + + + var onewaygroup = layergroup + .selectAll('g.onewaygroup') + .data(['oneway']); + + onewaygroup.enter() + .append('g') + .attr('class', 'layer onewaygroup'); + + + var oneways = onewaygroup + .selectAll('path') + .filter(filter) + .data( + function() { return onewaydata[this.parentNode.parentNode.__data__] || []; }, + function(d) { return [d.id, d.index]; } + ); + + oneways.enter() + .append('path') + .attr('class', 'oneway') + .attr('marker-mid', 'url(#oneway-marker)'); + + oneways + .attr('d', function(d) { return d.d; }); + + if (iD.detect().ie) { + oneways.each(function() { this.parentNode.insertBefore(this, this); }); + } + + oneways.exit() + .remove(); + + }; + } + + function MapillaryImages(projection, context, dispatch) { + var debouncedRedraw = _.debounce(function () { dispatch.change(); }, 1000), + minZoom = 12, + layer = d3.select(null), + _mapillary; + + + function init() { + if (iD.svg.MapillaryImages.initialized) return; // run once + iD.svg.MapillaryImages.enabled = false; + iD.svg.MapillaryImages.initialized = true; + } + + function getMapillary() { + if (iD.services.mapillary && !_mapillary) { + _mapillary = iD.services.mapillary(); + _mapillary.on('loadedImages', debouncedRedraw); + } else if (!iD.services.mapillary && _mapillary) { + _mapillary = null; + } + + return _mapillary; + } + + function showLayer() { + var mapillary = getMapillary(); + if (!mapillary) return; + + mapillary.loadViewer(); + editOn(); + + layer + .style('opacity', 0) + .transition() + .duration(500) + .style('opacity', 1) + .each('end', debouncedRedraw); + } + + function hideLayer() { + var mapillary = getMapillary(); + if (mapillary) { + mapillary.hideViewer(); + } + + debouncedRedraw.cancel(); + + layer + .transition() + .duration(500) + .style('opacity', 0) + .each('end', editOff); + } + + function editOn() { + layer.style('display', 'block'); + } + + function editOff() { + layer.selectAll('.viewfield-group').remove(); + layer.style('display', 'none'); + } + + function click(d) { + var mapillary = getMapillary(); + if (!mapillary) return; + + context.map().centerEase(d.loc); + + mapillary + .setSelectedImage(d.key, true) + .updateViewer(d.key, context) + .showViewer(); + } + + function transform(d) { + var t = iD.svg.PointTransform(projection)(d); + if (d.ca) t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; + return t; + } + + function update() { + var mapillary = getMapillary(), + data = (mapillary ? mapillary.images(projection, layer.dimensions()) : []), + imageKey = mapillary ? mapillary.getSelectedImage() : null; + + var markers = layer.selectAll('.viewfield-group') + .data(data, function(d) { return d.key; }); + + // Enter + var enter = markers.enter() + .append('g') + .attr('class', 'viewfield-group') + .classed('selected', function(d) { return d.key === imageKey; }) + .on('click', click); + + enter.append('path') + .attr('class', 'viewfield') + .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') + .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'); + + enter.append('circle') + .attr('dx', '0') + .attr('dy', '0') + .attr('r', '6'); + + // Exit + markers.exit() + .remove(); + + // Update + markers + .attr('transform', transform); + } + + function drawImages(selection) { + var enabled = iD.svg.MapillaryImages.enabled, + mapillary = getMapillary(); + + layer = selection.selectAll('.layer-mapillary-images') + .data(mapillary ? [0] : []); + + layer.enter() + .append('g') + .attr('class', 'layer-mapillary-images') + .style('display', enabled ? 'block' : 'none'); + + layer.exit() + .remove(); + + if (enabled) { + if (mapillary && ~~context.map().zoom() >= minZoom) { + editOn(); + update(); + mapillary.loadImages(projection, layer.dimensions()); + } else { + editOff(); + } + } + } + + drawImages.enabled = function(_) { + if (!arguments.length) return iD.svg.MapillaryImages.enabled; + iD.svg.MapillaryImages.enabled = _; + if (iD.svg.MapillaryImages.enabled) { + showLayer(); + } else { + hideLayer(); + } + dispatch.change(); + return this; + }; + + drawImages.supported = function() { + return !!getMapillary(); + }; + + drawImages.dimensions = function(_) { + if (!arguments.length) return layer.dimensions(); + layer.dimensions(_); + return this; + }; + + init(); + return drawImages; + } + + function MapillarySigns(projection, context, dispatch) { + var debouncedRedraw = _.debounce(function () { dispatch.change(); }, 1000), + minZoom = 12, + layer = d3.select(null), + _mapillary; + + + function init() { + if (iD.svg.MapillarySigns.initialized) return; // run once + iD.svg.MapillarySigns.enabled = false; + iD.svg.MapillarySigns.initialized = true; + } + + function getMapillary() { + if (iD.services.mapillary && !_mapillary) { + _mapillary = iD.services.mapillary().on('loadedSigns', debouncedRedraw); + } else if (!iD.services.mapillary && _mapillary) { + _mapillary = null; + } + return _mapillary; + } + + function showLayer() { + editOn(); + debouncedRedraw(); + } + + function hideLayer() { + debouncedRedraw.cancel(); + editOff(); + } + + function editOn() { + layer.style('display', 'block'); + } + + function editOff() { + layer.selectAll('.icon-sign').remove(); + layer.style('display', 'none'); + } + + function click(d) { + var mapillary = getMapillary(); + if (!mapillary) return; + + context.map().centerEase(d.loc); + + mapillary + .setSelectedImage(d.key, true) + .updateViewer(d.key, context) + .showViewer(); + } + + function update() { + var mapillary = getMapillary(), + data = (mapillary ? mapillary.signs(projection, layer.dimensions()) : []), + imageKey = mapillary ? mapillary.getSelectedImage() : null; + + var signs = layer.selectAll('.icon-sign') + .data(data, function(d) { return d.key; }); + + // Enter + var enter = signs.enter() + .append('foreignObject') + .attr('class', 'icon-sign') + .attr('width', '32px') // for Firefox + .attr('height', '32px') // for Firefox + .classed('selected', function(d) { return d.key === imageKey; }) + .on('click', click); + + enter + .append('xhtml:body') + .html(mapillary.signHTML); + + // Exit + signs.exit() + .remove(); + + // Update + signs + .attr('transform', iD.svg.PointTransform(projection)); + } + + function drawSigns(selection) { + var enabled = iD.svg.MapillarySigns.enabled, + mapillary = getMapillary(); + + layer = selection.selectAll('.layer-mapillary-signs') + .data(mapillary ? [0] : []); + + layer.enter() + .append('g') + .attr('class', 'layer-mapillary-signs') + .style('display', enabled ? 'block' : 'none') + .attr('transform', 'translate(-16, -16)'); // center signs on loc + + layer.exit() + .remove(); + + if (enabled) { + if (mapillary && ~~context.map().zoom() >= minZoom) { + editOn(); + update(); + mapillary.loadSigns(context, projection, layer.dimensions()); + } else { + editOff(); + } + } + } + + drawSigns.enabled = function(_) { + if (!arguments.length) return iD.svg.MapillarySigns.enabled; + iD.svg.MapillarySigns.enabled = _; + if (iD.svg.MapillarySigns.enabled) { + showLayer(); + } else { + hideLayer(); + } + dispatch.change(); + return this; + }; + + drawSigns.supported = function() { + var mapillary = getMapillary(); + return (mapillary && mapillary.signsSupported()); + }; + + drawSigns.dimensions = function(_) { + if (!arguments.length) return layer.dimensions(); + layer.dimensions(_); + return this; + }; + + init(); + return drawSigns; + } + + function Midpoints(projection, context) { + return function drawMidpoints(surface, graph, entities, filter, extent) { + var poly = extent.polygon(), + midpoints = {}; + + for (var i = 0; i < entities.length; i++) { + var entity = entities[i]; + + if (entity.type !== 'way') + continue; + if (!filter(entity)) + continue; + if (context.selectedIDs().indexOf(entity.id) < 0) + continue; + + var nodes = graph.childNodes(entity); + for (var j = 0; j < nodes.length - 1; j++) { + + var a = nodes[j], + b = nodes[j + 1], + id = [a.id, b.id].sort().join('-'); + + if (midpoints[id]) { + midpoints[id].parents.push(entity); + } else { + if (iD.geo.euclideanDistance(projection(a.loc), projection(b.loc)) > 40) { + var point = iD.geo.interp(a.loc, b.loc, 0.5), + loc = null; + + if (extent.intersects(point)) { + loc = point; + } else { + for (var k = 0; k < 4; k++) { + point = iD.geo.lineIntersection([a.loc, b.loc], [poly[k], poly[k+1]]); + if (point && + iD.geo.euclideanDistance(projection(a.loc), projection(point)) > 20 && + iD.geo.euclideanDistance(projection(b.loc), projection(point)) > 20) + { + loc = point; + break; + } + } + } + + if (loc) { + midpoints[id] = { + type: 'midpoint', + id: id, + loc: loc, + edge: [a.id, b.id], + parents: [entity] + }; + } + } + } + } + } + + function midpointFilter(d) { + if (midpoints[d.id]) + return true; + + for (var i = 0; i < d.parents.length; i++) + if (filter(d.parents[i])) + return true; + + return false; + } + + var groups = surface.selectAll('.layer-hit').selectAll('g.midpoint') + .filter(midpointFilter) + .data(_.values(midpoints), function(d) { return d.id; }); + + var enter = groups.enter() + .insert('g', ':first-child') + .attr('class', 'midpoint'); + + enter.append('polygon') + .attr('points', '-6,8 10,0 -6,-8') + .attr('class', 'shadow'); + + enter.append('polygon') + .attr('points', '-3,4 5,0 -3,-4') + .attr('class', 'fill'); + + groups + .attr('transform', function(d) { + var translate = iD.svg.PointTransform(projection), + a = context.entity(d.edge[0]), + b = context.entity(d.edge[1]), + angle = Math.round(iD.geo.angle(a, b, projection) * (180 / Math.PI)); + return translate(d) + ' rotate(' + angle + ')'; + }) + .call(iD.svg.TagClasses().tags( + function(d) { return d.parents[0].tags; } + )); + + // Propagate data bindings. + groups.select('polygon.shadow'); + groups.select('polygon.fill'); + + groups.exit() + .remove(); + }; + } + + function OneWaySegments(projection, graph, dt) { + return function(entity) { + var a, + b, + i = 0, + offset = dt, + segments = [], + clip = d3.geo.clipExtent().extent(projection.clipExtent()).stream, + coordinates = graph.childNodes(entity).map(function(n) { + return n.loc; + }); + + if (entity.tags.oneway === '-1') coordinates.reverse(); + + d3.geo.stream({ + type: 'LineString', + coordinates: coordinates + }, projection.stream(clip({ + lineStart: function() {}, + lineEnd: function() { + a = null; + }, + point: function(x, y) { + b = [x, y]; + + if (a) { + var span = iD.geo.euclideanDistance(a, b) - offset; + + if (span >= 0) { + var angle = Math.atan2(b[1] - a[1], b[0] - a[0]), + dx = dt * Math.cos(angle), + dy = dt * Math.sin(angle), + p = [a[0] + offset * Math.cos(angle), + a[1] + offset * Math.sin(angle)]; + + var segment = 'M' + a[0] + ',' + a[1] + + 'L' + p[0] + ',' + p[1]; + + for (span -= dt; span >= 0; span -= dt) { + p[0] += dx; + p[1] += dy; + segment += 'L' + p[0] + ',' + p[1]; + } + + segment += 'L' + b[0] + ',' + b[1]; + segments.push({id: entity.id, index: i, d: segment}); + } + + offset = -span; + i++; + } + + a = b; + } + }))); + + return segments; + }; + } + + function Osm() { + return function drawOsm(selection) { + var layers = selection.selectAll('.layer-osm') + .data(['areas', 'lines', 'hit', 'halo', 'label']); + + layers.enter().append('g') + .attr('class', function(d) { return 'layer-osm layer-' + d; }); + }; + } + + function Path(projection, graph, polygon) { + var cache = {}, + clip = d3.geo.clipExtent().extent(projection.clipExtent()).stream, + project = projection.stream, + path = d3.geo.path() + .projection({stream: function(output) { return polygon ? project(output) : project(clip(output)); }}); + + return function(entity) { + if (entity.id in cache) { + return cache[entity.id]; + } else { + return cache[entity.id] = path(entity.asGeoJSON(graph)); + } + }; + } + + function PointTransform(projection) { + return function(entity) { + // http://jsperf.com/short-array-join + var pt = projection(entity.loc); + return 'translate(' + pt[0] + ',' + pt[1] + ')'; + }; + } + + function Points(projection, context) { + function markerPath(selection, klass) { + selection + .attr('class', klass) + .attr('transform', 'translate(-8, -23)') + .attr('d', 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z'); + } + + function sortY(a, b) { + return b.loc[1] - a.loc[1]; + } + + return function drawPoints(surface, graph, entities, filter) { + var wireframe = surface.classed('fill-wireframe'), + points = wireframe ? [] : _.filter(entities, function(e) { + return e.geometry(graph) === 'point'; + }); + + points.sort(sortY); + + var groups = surface.selectAll('.layer-hit').selectAll('g.point') + .filter(filter) + .data(points, iD.Entity.key); + + var group = groups.enter() + .append('g') + .attr('class', function(d) { return 'node point ' + d.id; }) + .order(); + + group.append('path') + .call(markerPath, 'shadow'); + + group.append('path') + .call(markerPath, 'stroke'); + + group.append('use') + .attr('transform', 'translate(-6, -20)') + .attr('class', 'icon') + .attr('width', '12px') + .attr('height', '12px'); + + groups.attr('transform', iD.svg.PointTransform(projection)) + .call(iD.svg.TagClasses()); + + // Selecting the following implicitly + // sets the data (point entity) on the element + groups.select('.shadow'); + groups.select('.stroke'); + groups.select('.icon') + .attr('xlink:href', function(entity) { + var preset = context.presets().match(entity, graph); + return preset.icon ? '#' + preset.icon + '-12' : ''; + }); + + groups.exit() + .remove(); + }; + } + + function RelationMemberTags(graph) { + return function(entity) { + var tags = entity.tags; + graph.parentRelations(entity).forEach(function(relation) { + var type = relation.tags.type; + if (type === 'multipolygon' || type === 'boundary') { + tags = _.extend({}, relation.tags, tags); + } + }); + return tags; + }; + } + + function TagClasses() { + var primaries = [ + 'building', 'highway', 'railway', 'waterway', 'aeroway', + 'motorway', 'boundary', 'power', 'amenity', 'natural', 'landuse', + 'leisure', 'place' + ], + statuses = [ + 'proposed', 'construction', 'disused', 'abandoned', 'dismantled', + 'razed', 'demolished', 'obliterated' + ], + secondaries = [ + 'oneway', 'bridge', 'tunnel', 'embankment', 'cutting', 'barrier', + 'surface', 'tracktype', 'crossing' + ], + tagClassRe = /^tag-/, + tags = function(entity) { return entity.tags; }; + + + var tagClasses = function(selection) { + selection.each(function tagClassesEach(entity) { + var value = this.className, + classes, primary, status; + + if (value.baseVal !== undefined) value = value.baseVal; + + classes = value.trim().split(/\s+/).filter(function(name) { + return name.length && !tagClassRe.test(name); + }).join(' '); + + var t = tags(entity), i, k, v; + + // pick at most one primary classification tag.. + for (i = 0; i < primaries.length; i++) { + k = primaries[i]; + v = t[k]; + if (!v || v === 'no') continue; + + primary = k; + if (statuses.indexOf(v) !== -1) { // e.g. `railway=abandoned` + status = v; + classes += ' tag-' + k; + } else { + classes += ' tag-' + k + ' tag-' + k + '-' + v; + } + + break; + } + + // add at most one status tag, only if relates to primary tag.. + if (!status) { + for (i = 0; i < statuses.length; i++) { + k = statuses[i]; + v = t[k]; + if (!v || v === 'no') continue; + + if (v === 'yes') { // e.g. `railway=rail + abandoned=yes` + status = k; + } + else if (primary && primary === v) { // e.g. `railway=rail + abandoned=railway` + status = k; + } else if (!primary && primaries.indexOf(v) !== -1) { // e.g. `abandoned=railway` + status = k; + primary = v; + classes += ' tag-' + v; + } // else ignore e.g. `highway=path + abandoned=railway` + + if (status) break; + } + } + + if (status) { + classes += ' tag-status tag-status-' + status; + } + + // add any secondary (structure) tags + for (i = 0; i < secondaries.length; i++) { + k = secondaries[i]; + v = t[k]; + if (!v || v === 'no') continue; + classes += ' tag-' + k + ' tag-' + k + '-' + v; + } + + // For highways, look for surface tagging.. + if (primary === 'highway') { + var paved = (t.highway !== 'track'); + for (k in t) { + v = t[k]; + if (k in iD.pavedTags) { + paved = !!iD.pavedTags[k][v]; + break; + } + } + if (!paved) { + classes += ' tag-unpaved'; + } + } + + classes = classes.trim(); + + if (classes !== value) { + d3.select(this).attr('class', classes); + } + }); + }; + + tagClasses.tags = function(_) { + if (!arguments.length) return tags; + tags = _; + return tagClasses; + }; + + return tagClasses; + } + + function Turns(projection) { + return function drawTurns(surface, graph, turns) { + function key(turn) { + return [turn.from.node + turn.via.node + turn.to.node].join('-'); + } + + function icon(turn) { + var u = turn.u ? '-u' : ''; + if (!turn.restriction) + return '#turn-yes' + u; + var restriction = graph.entity(turn.restriction).tags.restriction; + return '#turn-' + + (!turn.indirect_restriction && /^only_/.test(restriction) ? 'only' : 'no') + u; + } + + var groups = surface.selectAll('.layer-hit').selectAll('g.turn') + .data(turns, key); + + // Enter + var enter = groups.enter().append('g') + .attr('class', 'turn'); + + var nEnter = enter.filter(function (turn) { return !turn.u; }); + + nEnter.append('rect') + .attr('transform', 'translate(-22, -12)') + .attr('width', '44') + .attr('height', '24'); + + nEnter.append('use') + .attr('transform', 'translate(-22, -12)') + .attr('width', '44') + .attr('height', '24'); + + + var uEnter = enter.filter(function (turn) { return turn.u; }); + + uEnter.append('circle') + .attr('r', '16'); + + uEnter.append('use') + .attr('transform', 'translate(-16, -16)') + .attr('width', '32') + .attr('height', '32'); + + + // Update + groups + .attr('transform', function (turn) { + var v = graph.entity(turn.via.node), + t = graph.entity(turn.to.node), + a = iD.geo.angle(v, t, projection), + p = projection(v.loc), + r = turn.u ? 0 : 60; + + return 'translate(' + (r * Math.cos(a) + p[0]) + ',' + (r * Math.sin(a) + p[1]) + ') ' + + 'rotate(' + a * 180 / Math.PI + ')'; + }); + + groups.select('use') + .attr('xlink:href', icon); + + groups.select('rect'); + groups.select('circle'); + + + // Exit + groups.exit() + .remove(); + + return this; + }; + } + + function Vertices(projection, context) { + var radiuses = { + // z16-, z17, z18+, tagged + shadow: [6, 7.5, 7.5, 11.5], + stroke: [2.5, 3.5, 3.5, 7], + fill: [1, 1.5, 1.5, 1.5] + }; + + var hover; + + function siblingAndChildVertices(ids, graph, extent) { + var vertices = {}; + + function addChildVertices(entity) { + if (!context.features().isHiddenFeature(entity, graph, entity.geometry(graph))) { + var i; + if (entity.type === 'way') { + for (i = 0; i < entity.nodes.length; i++) { + addChildVertices(graph.entity(entity.nodes[i])); + } + } else if (entity.type === 'relation') { + for (i = 0; i < entity.members.length; i++) { + var member = context.hasEntity(entity.members[i].id); + if (member) { + addChildVertices(member); + } + } + } else if (entity.intersects(extent, graph)) { + vertices[entity.id] = entity; + } + } + } + + ids.forEach(function(id) { + var entity = context.hasEntity(id); + if (entity && entity.type === 'node') { + vertices[entity.id] = entity; + context.graph().parentWays(entity).forEach(function(entity) { + addChildVertices(entity); + }); + } else if (entity) { + addChildVertices(entity); + } + }); + + return vertices; + } + + function draw(selection, vertices, klass, graph, zoom) { + var icons = {}, + z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); + + var groups = selection + .data(vertices, iD.Entity.key); + + function icon(entity) { + if (entity.id in icons) return icons[entity.id]; + icons[entity.id] = + entity.hasInterestingTags() && + context.presets().match(entity, graph).icon; + return icons[entity.id]; + } + + function setClass(klass) { + return function(entity) { + this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id); + }; + } + + function setAttributes(selection) { + ['shadow','stroke','fill'].forEach(function(klass) { + var rads = radiuses[klass]; + selection.selectAll('.' + klass) + .each(function(entity) { + var i = z && icon(entity), + c = i ? 0.5 : 0, + r = rads[i ? 3 : z]; + this.setAttribute('cx', c); + this.setAttribute('cy', -c); + this.setAttribute('r', r); + if (i && klass === 'fill') { + this.setAttribute('visibility', 'hidden'); + } else { + this.removeAttribute('visibility'); + } + }); + }); + + selection.selectAll('use') + .each(function() { + if (z) { + this.removeAttribute('visibility'); + } else { + this.setAttribute('visibility', 'hidden'); + } + }); + } + + var enter = groups.enter() + .append('g') + .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); + + enter.append('circle') + .each(setClass('shadow')); + + enter.append('circle') + .each(setClass('stroke')); + + // Vertices with icons get a `use`. + enter.filter(function(d) { return icon(d); }) + .append('use') + .attr('transform', 'translate(-6, -6)') + .attr('xlink:href', function(d) { return '#' + icon(d) + '-12'; }) + .attr('width', '12px') + .attr('height', '12px') + .each(setClass('icon')); + + // Vertices with tags get a fill. + enter.filter(function(d) { return d.hasInterestingTags(); }) + .append('circle') + .each(setClass('fill')); + + groups + .attr('transform', iD.svg.PointTransform(projection)) + .classed('shared', function(entity) { return graph.isShared(entity); }) + .call(setAttributes); + + groups.exit() + .remove(); + } + + function drawVertices(surface, graph, entities, filter, extent, zoom) { + var selected = siblingAndChildVertices(context.selectedIDs(), graph, extent), + wireframe = surface.classed('fill-wireframe'), + vertices = []; + + for (var i = 0; i < entities.length; i++) { + var entity = entities[i], + geometry = entity.geometry(graph); + + if (wireframe && geometry === 'point') { + vertices.push(entity); + continue; + } + + if (geometry !== 'vertex') + continue; + + if (entity.id in selected || + entity.hasInterestingTags() || + entity.isIntersection(graph)) { + vertices.push(entity); + } + } + + surface.selectAll('.layer-hit').selectAll('g.vertex.vertex-persistent') + .filter(filter) + .call(draw, vertices, 'vertex-persistent', graph, zoom); + + drawHover(surface, graph, extent, zoom); + } + + function drawHover(surface, graph, extent, zoom) { + var hovered = hover ? siblingAndChildVertices([hover.id], graph, extent) : {}; + + surface.selectAll('.layer-hit').selectAll('g.vertex.vertex-hover') + .call(draw, d3.values(hovered), 'vertex-hover', graph, zoom); + } + + drawVertices.drawHover = function(surface, graph, target, extent, zoom) { + if (target === hover) return; + hover = target; + drawHover(surface, graph, extent, zoom); + }; + + return drawVertices; + } + + exports.Areas = Areas; + exports.Debug = Debug; + exports.Defs = Defs; + exports.Gpx = Gpx; + exports.Icon = Icon; + exports.Labels = Labels; + exports.Layers = Layers; + exports.Lines = Lines; + exports.MapillaryImages = MapillaryImages; + exports.MapillarySigns = MapillarySigns; + exports.Midpoints = Midpoints; + exports.OneWaySegments = OneWaySegments; + exports.Osm = Osm; + exports.Path = Path; + exports.PointTransform = PointTransform; + exports.Points = Points; + exports.RelationMemberTags = RelationMemberTags; + exports.TagClasses = TagClasses; + exports.Turns = Turns; + exports.Vertices = Vertices; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); \ No newline at end of file diff --git a/js/id/svg/areas.js b/modules/svg/areas.js similarity index 99% rename from js/id/svg/areas.js rename to modules/svg/areas.js index e6556548c..e0487a786 100644 --- a/js/id/svg/areas.js +++ b/modules/svg/areas.js @@ -1,4 +1,4 @@ -iD.svg.Areas = function(projection) { +export function Areas(projection) { // Patterns only work in Firefox when set directly on element. // (This is not a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=750632) var patterns = { @@ -129,4 +129,4 @@ iD.svg.Areas = function(projection) { paths .attr('d', path); }; -}; +} diff --git a/js/id/svg/debug.js b/modules/svg/debug.js similarity index 98% rename from js/id/svg/debug.js rename to modules/svg/debug.js index 5e6376e35..7db2a1a54 100644 --- a/js/id/svg/debug.js +++ b/modules/svg/debug.js @@ -1,4 +1,4 @@ -iD.svg.Debug = function(projection, context) { +export function Debug(projection, context) { function multipolygons(imagery) { return imagery.map(function(data) { @@ -134,4 +134,4 @@ iD.svg.Debug = function(projection, context) { }; return drawDebug; -}; +} diff --git a/js/id/svg/defs.js b/modules/svg/defs.js similarity index 98% rename from js/id/svg/defs.js rename to modules/svg/defs.js index cc6f8ad48..2ab3755f9 100644 --- a/js/id/svg/defs.js +++ b/modules/svg/defs.js @@ -2,7 +2,7 @@ A standalone SVG element that contains only a `defs` sub-element. To be used once globally, since defs IDs must be unique within a document. */ -iD.svg.Defs = function(context) { +export function Defs(context) { function SVGSpriteDefinition(id, href) { return function(defs) { @@ -108,4 +108,4 @@ iD.svg.Defs = function(context) { 'maki-sprite', context.imagePath('maki-sprite.svg'))); }; -}; +} diff --git a/js/id/svg/gpx.js b/modules/svg/gpx.js similarity index 98% rename from js/id/svg/gpx.js rename to modules/svg/gpx.js index d79a4082e..ba0859cb0 100644 --- a/js/id/svg/gpx.js +++ b/modules/svg/gpx.js @@ -1,4 +1,4 @@ -iD.svg.Gpx = function(projection, context, dispatch) { +export function Gpx(projection, context, dispatch) { var showLabels = true, layer; @@ -161,4 +161,4 @@ iD.svg.Gpx = function(projection, context, dispatch) { init(); return drawGpx; -}; +} diff --git a/js/id/svg/icon.js b/modules/svg/icon.js similarity index 85% rename from js/id/svg/icon.js rename to modules/svg/icon.js index 61a432d5a..97f8a1f34 100644 --- a/js/id/svg/icon.js +++ b/modules/svg/icon.js @@ -1,4 +1,4 @@ -iD.svg.Icon = function(name, svgklass, useklass) { +export function Icon(name, svgklass, useklass) { return function drawIcon(selection) { selection.selectAll('svg') .data([0]) @@ -9,4 +9,4 @@ iD.svg.Icon = function(name, svgklass, useklass) { .attr('xlink:href', name) .attr('class', useklass); }; -}; +} diff --git a/modules/svg/index.js b/modules/svg/index.js new file mode 100644 index 000000000..27d16672f --- /dev/null +++ b/modules/svg/index.js @@ -0,0 +1,20 @@ +export { Areas } from './areas.js'; +export { Debug } from './debug.js'; +export { Defs } from './defs.js'; +export { Gpx } from './gpx.js'; +export { Icon } from './icon.js'; +export { Labels } from './labels.js'; +export { Layers } from './layers.js'; +export { Lines } from './lines.js'; +export { MapillaryImages } from './mapillary_images.js'; +export { MapillarySigns } from './mapillary_signs.js'; +export { Midpoints } from './midpoints.js'; +export { OneWaySegments } from './one_way_segments.js'; +export { Osm } from './osm.js'; +export { Path } from './path.js'; +export { PointTransform } from './point_transform.js'; +export { Points } from './points.js'; +export { RelationMemberTags } from './relation_member_tags.js'; +export { TagClasses } from './tag_classes.js'; +export { Turns } from './turns.js'; +export { Vertices } from './vertices.js'; diff --git a/js/id/svg/labels.js b/modules/svg/labels.js similarity index 99% rename from js/id/svg/labels.js rename to modules/svg/labels.js index 36d7628d2..37640a36d 100644 --- a/js/id/svg/labels.js +++ b/modules/svg/labels.js @@ -1,4 +1,4 @@ -iD.svg.Labels = function(projection, context) { +export function Labels(projection, context) { var path = d3.geo.path().projection(projection); // Replace with dict and iterate over entities tags instead? @@ -473,4 +473,4 @@ iD.svg.Labels = function(projection, context) { }; return drawLabels; -}; +} diff --git a/js/id/svg/layers.js b/modules/svg/layers.js similarity index 97% rename from js/id/svg/layers.js rename to modules/svg/layers.js index fea38d417..01a2c0b52 100644 --- a/js/id/svg/layers.js +++ b/modules/svg/layers.js @@ -1,4 +1,4 @@ -iD.svg.Layers = function(projection, context) { +export function Layers(projection, context) { var dispatch = d3.dispatch('change'), svg = d3.select(null), layers = [ @@ -81,4 +81,4 @@ iD.svg.Layers = function(projection, context) { return d3.rebind(drawLayers, dispatch, 'on'); -}; +} diff --git a/js/id/svg/lines.js b/modules/svg/lines.js similarity index 98% rename from js/id/svg/lines.js rename to modules/svg/lines.js index 216612416..be3ac4041 100644 --- a/js/id/svg/lines.js +++ b/modules/svg/lines.js @@ -1,4 +1,4 @@ -iD.svg.Lines = function(projection) { +export function Lines(projection) { var highway_stack = { motorway: 0, @@ -125,4 +125,4 @@ iD.svg.Lines = function(projection) { .remove(); }; -}; +} diff --git a/js/id/svg/mapillary_images.js b/modules/svg/mapillary_images.js similarity index 98% rename from js/id/svg/mapillary_images.js rename to modules/svg/mapillary_images.js index 0443f2391..d1f724d55 100644 --- a/js/id/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -1,4 +1,4 @@ -iD.svg.MapillaryImages = function(projection, context, dispatch) { +export function MapillaryImages(projection, context, dispatch) { var debouncedRedraw = _.debounce(function () { dispatch.change(); }, 1000), minZoom = 12, layer = d3.select(null), @@ -163,4 +163,4 @@ iD.svg.MapillaryImages = function(projection, context, dispatch) { init(); return drawImages; -}; +} diff --git a/js/id/svg/mapillary_signs.js b/modules/svg/mapillary_signs.js similarity index 98% rename from js/id/svg/mapillary_signs.js rename to modules/svg/mapillary_signs.js index 92b220d53..05c27d1b9 100644 --- a/js/id/svg/mapillary_signs.js +++ b/modules/svg/mapillary_signs.js @@ -1,4 +1,4 @@ -iD.svg.MapillarySigns = function(projection, context, dispatch) { +export function MapillarySigns(projection, context, dispatch) { var debouncedRedraw = _.debounce(function () { dispatch.change(); }, 1000), minZoom = 12, layer = d3.select(null), @@ -133,4 +133,4 @@ iD.svg.MapillarySigns = function(projection, context, dispatch) { init(); return drawSigns; -}; +} diff --git a/js/id/svg/midpoints.js b/modules/svg/midpoints.js similarity index 98% rename from js/id/svg/midpoints.js rename to modules/svg/midpoints.js index 3daf82e0e..c32a60915 100644 --- a/js/id/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -1,4 +1,4 @@ -iD.svg.Midpoints = function(projection, context) { +export function Midpoints(projection, context) { return function drawMidpoints(surface, graph, entities, filter, extent) { var poly = extent.polygon(), midpoints = {}; @@ -102,4 +102,4 @@ iD.svg.Midpoints = function(projection, context) { groups.exit() .remove(); }; -}; +} diff --git a/modules/svg/one_way_segments.js b/modules/svg/one_way_segments.js new file mode 100644 index 000000000..79de6c690 --- /dev/null +++ b/modules/svg/one_way_segments.js @@ -0,0 +1,59 @@ +export function OneWaySegments(projection, graph, dt) { + return function(entity) { + var a, + b, + i = 0, + offset = dt, + segments = [], + clip = d3.geo.clipExtent().extent(projection.clipExtent()).stream, + coordinates = graph.childNodes(entity).map(function(n) { + return n.loc; + }); + + if (entity.tags.oneway === '-1') coordinates.reverse(); + + d3.geo.stream({ + type: 'LineString', + coordinates: coordinates + }, projection.stream(clip({ + lineStart: function() {}, + lineEnd: function() { + a = null; + }, + point: function(x, y) { + b = [x, y]; + + if (a) { + var span = iD.geo.euclideanDistance(a, b) - offset; + + if (span >= 0) { + var angle = Math.atan2(b[1] - a[1], b[0] - a[0]), + dx = dt * Math.cos(angle), + dy = dt * Math.sin(angle), + p = [a[0] + offset * Math.cos(angle), + a[1] + offset * Math.sin(angle)]; + + var segment = 'M' + a[0] + ',' + a[1] + + 'L' + p[0] + ',' + p[1]; + + for (span -= dt; span >= 0; span -= dt) { + p[0] += dx; + p[1] += dy; + segment += 'L' + p[0] + ',' + p[1]; + } + + segment += 'L' + b[0] + ',' + b[1]; + segments.push({id: entity.id, index: i, d: segment}); + } + + offset = -span; + i++; + } + + a = b; + } + }))); + + return segments; + }; +} diff --git a/js/id/svg/osm.js b/modules/svg/osm.js similarity index 90% rename from js/id/svg/osm.js rename to modules/svg/osm.js index ac6387bdb..821ab9660 100644 --- a/js/id/svg/osm.js +++ b/modules/svg/osm.js @@ -1,4 +1,4 @@ -iD.svg.Osm = function() { +export function Osm() { return function drawOsm(selection) { var layers = selection.selectAll('.layer-osm') .data(['areas', 'lines', 'hit', 'halo', 'label']); @@ -6,4 +6,4 @@ iD.svg.Osm = function() { layers.enter().append('g') .attr('class', function(d) { return 'layer-osm layer-' + d; }); }; -}; +} diff --git a/modules/svg/path.js b/modules/svg/path.js new file mode 100644 index 000000000..24365a915 --- /dev/null +++ b/modules/svg/path.js @@ -0,0 +1,15 @@ +export function Path(projection, graph, polygon) { + var cache = {}, + clip = d3.geo.clipExtent().extent(projection.clipExtent()).stream, + project = projection.stream, + path = d3.geo.path() + .projection({stream: function(output) { return polygon ? project(output) : project(clip(output)); }}); + + return function(entity) { + if (entity.id in cache) { + return cache[entity.id]; + } else { + return cache[entity.id] = path(entity.asGeoJSON(graph)); + } + }; +} diff --git a/modules/svg/point_transform.js b/modules/svg/point_transform.js new file mode 100644 index 000000000..313d82009 --- /dev/null +++ b/modules/svg/point_transform.js @@ -0,0 +1,7 @@ +export function PointTransform(projection) { + return function(entity) { + // http://jsperf.com/short-array-join + var pt = projection(entity.loc); + return 'translate(' + pt[0] + ',' + pt[1] + ')'; + }; +} diff --git a/js/id/svg/points.js b/modules/svg/points.js similarity index 97% rename from js/id/svg/points.js rename to modules/svg/points.js index 8bcf86fd9..c2633501b 100644 --- a/js/id/svg/points.js +++ b/modules/svg/points.js @@ -1,4 +1,4 @@ -iD.svg.Points = function(projection, context) { +export function Points(projection, context) { function markerPath(selection, klass) { selection .attr('class', klass) @@ -55,4 +55,4 @@ iD.svg.Points = function(projection, context) { groups.exit() .remove(); }; -}; +} diff --git a/modules/svg/relation_member_tags.js b/modules/svg/relation_member_tags.js new file mode 100644 index 000000000..76a45c12c --- /dev/null +++ b/modules/svg/relation_member_tags.js @@ -0,0 +1,12 @@ +export function RelationMemberTags(graph) { + return function(entity) { + var tags = entity.tags; + graph.parentRelations(entity).forEach(function(relation) { + var type = relation.tags.type; + if (type === 'multipolygon' || type === 'boundary') { + tags = _.extend({}, relation.tags, tags); + } + }); + return tags; + }; +} diff --git a/js/id/svg/tag_classes.js b/modules/svg/tag_classes.js similarity index 99% rename from js/id/svg/tag_classes.js rename to modules/svg/tag_classes.js index 63774ec49..714f5dff1 100644 --- a/js/id/svg/tag_classes.js +++ b/modules/svg/tag_classes.js @@ -1,4 +1,4 @@ -iD.svg.TagClasses = function() { +export function TagClasses() { var primaries = [ 'building', 'highway', 'railway', 'waterway', 'aeroway', 'motorway', 'boundary', 'power', 'amenity', 'natural', 'landuse', @@ -110,4 +110,4 @@ iD.svg.TagClasses = function() { }; return tagClasses; -}; +} diff --git a/js/id/svg/turns.js b/modules/svg/turns.js similarity index 98% rename from js/id/svg/turns.js rename to modules/svg/turns.js index c5007a745..9363250b0 100644 --- a/js/id/svg/turns.js +++ b/modules/svg/turns.js @@ -1,4 +1,4 @@ -iD.svg.Turns = function(projection) { +export function Turns(projection) { return function drawTurns(surface, graph, turns) { function key(turn) { return [turn.from.node + turn.via.node + turn.to.node].join('-'); @@ -70,4 +70,4 @@ iD.svg.Turns = function(projection) { return this; }; -}; +} diff --git a/js/id/svg/vertices.js b/modules/svg/vertices.js similarity index 99% rename from js/id/svg/vertices.js rename to modules/svg/vertices.js index c838c7daa..4ed893c76 100644 --- a/js/id/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,4 +1,4 @@ -iD.svg.Vertices = function(projection, context) { +export function Vertices(projection, context) { var radiuses = { // z16-, z17, z18+, tagged shadow: [6, 7.5, 7.5, 11.5], @@ -174,4 +174,4 @@ iD.svg.Vertices = function(projection, context) { }; return drawVertices; -}; +} diff --git a/test/index.html b/test/index.html index f983ba5fb..e20e512da 100644 --- a/test/index.html +++ b/test/index.html @@ -48,6 +48,7 @@ + @@ -57,24 +58,6 @@ - - - - - - - - - - - - - - - - - - diff --git a/test/rendering.html b/test/rendering.html index 798427917..fc9017166 100644 --- a/test/rendering.html +++ b/test/rendering.html @@ -13,19 +13,9 @@ + - - - - - - - - - - -