From a4dc7518c4d3ec94769d6e569960838cc4f83706 Mon Sep 17 00:00:00 2001 From: Benjamin Clark Date: Tue, 7 May 2019 13:10:55 -0400 Subject: [PATCH 1/3] Add a hotkey to highlight edited ways. --- css/20_map.css | 16 ++++++++++++++++ data/core.yaml | 5 +++++ data/shortcuts.json | 4 ++++ modules/svg/lines.js | 12 ++++++++++-- modules/svg/vertices.js | 29 ++++++++++++++++++++++++++--- modules/ui/map_data.js | 10 +++++++++- 6 files changed, 70 insertions(+), 6 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 1c89fb588..53df3798f 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -336,3 +336,19 @@ g.vertex.highlighted .shadow { .surface.tr g.vertex.related.only .shadow { stroke: #68f; } + +.highlight-edited g.points > circle.vertex.edited { + color: rgb(87, 201, 44); + stroke: #fff; + stroke-width: 1; + stroke-opacity: 1; + fill-opacity: 1; +} +.highlight-edited g.lines > path.line.edited { + color: rgb(87, 201, 44); + stroke-width: 9; + stroke-opacity: 1; + } +.low-zoom.highlight-edited g.lines > path.line.edited { + stroke-width: 7; +} diff --git a/data/core.yaml b/data/core.yaml index 8ddc5e233..07aaedf44 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -602,6 +602,10 @@ en: title: Custom Map Data zoom: Zoom to data fill_area: Fill Areas + highlight_way_edits: + description: Highlight edited nodes and segments attached to those nodes in ways + tooltip: Highlight way edits in the current session. + key: G map_features: Map Features autohidden: "These features have been automatically hidden because too many would be shown on the screen. You can zoom in to edit them." osmhidden: "These features have been automatically hidden because the OpenStreetMap layer is hidden." @@ -1861,6 +1865,7 @@ en: wireframe: "Toggle wireframe mode" osm_data: "Toggle OpenStreetMap data" minimap: "Toggle minimap" + highlight_way_edits: "Highlight way edits in current session" selecting: title: "Selecting features" select_one: "Select a single feature" diff --git a/data/shortcuts.json b/data/shortcuts.json index ce189797c..5cba3f4c3 100644 --- a/data/shortcuts.json +++ b/data/shortcuts.json @@ -82,6 +82,10 @@ { "shortcuts": ["background.minimap.key"], "text": "shortcuts.browsing.display_options.minimap" + }, + { + "shortcuts": ["map_data.highlight_way_edits.key"], + "text": "shortcuts.browsing.display_options.highlight_way_edits" } ] }, diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 1e1d36052..46da6e056 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -34,6 +34,7 @@ export function svgLines(projection, context) { var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; var getPath = svgPath(projection).geojson; var activeID = context.activeID(); + var base = context.history().base(); // The targets and nopes will be MultiLineString sub-segments of the ways var data = { targets: [], nopes: [] }; @@ -55,13 +56,20 @@ export function svgLines(projection, context) { targets.exit() .remove(); + var editClass = function(d) { + return d.properties.nodes.some(function(n) { + return graph.entities[n.id] !== base.entities[n.id]; + }) ? ' edited ': ''; + }; + // enter/update targets.enter() .append('path') .merge(targets) .attr('d', getPath) - .attr('class', function(d) { return 'way line target target-allowed ' + targetClass + d.id; }); - + .attr('class', function(d) { + return 'way line target target-allowed ' + targetClass + d.id + editClass(d); + }); // NOPE var nopeData = data.nopes.filter(getPath); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 1c48a64ae..d830f3b01 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -194,6 +194,9 @@ export function svgVertices(projection, context) { var getTransform = svgPointTransform(projection).geojson; var activeID = context.activeID(); var data = { targets: [], nopes: [] }; + var base = context.history().base(); + var radius = 3; + var interestingNodeRadius = 4.5; entities.forEach(function(node) { if (activeID === node.id) return; // draw no target on the activeID @@ -223,6 +226,10 @@ export function svgVertices(projection, context) { } }); + // Class for styling currently edited vertices + var editClass = function(d) { + return (graph.entities[d.id] !== base.entities[d.id]) ? ' edited ' : ''; + }; // Targets allow hover and vertex snapping var targets = selection.selectAll('.vertex.target-allowed') @@ -236,9 +243,18 @@ export function svgVertices(projection, context) { // enter/update targets.enter() .append('circle') - .attr('r', function(d) { return (_radii[d.id] || radiuses.shadow[3]); }) + .attr('r', function(d) { + return ((graph.entities[d.id].isEndpoint(graph) + && !graph.entities[d.id].isConnected(graph) + && isEditedEnt(d, base, graph) && interestingNodeRadius) + || (isEditedEnt(d, base, graph) && radius) + || _radii[d.id] || radiuses.shadow[3]); + }) .merge(targets) - .attr('class', function(d) { return 'node vertex target target-allowed ' + targetClass + d.id; }) + .attr('class', function(d) { + return 'node vertex target target-allowed ' + + targetClass + d.id + editClass(d); + }) .attr('transform', getTransform); @@ -272,6 +288,11 @@ export function svgVertices(projection, context) { } + function isEditedEnt(entity, base, head) { + return head.entities[entity.id] !== base.entities[entity.id]; + } + + function getSiblingAndChildVertices(ids, graph, wireframe, zoom) { var results = {}; @@ -324,6 +345,7 @@ export function svgVertices(projection, context) { var zoom = geoScaleToZoom(projection.scale()); var mode = context.mode(); var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); + var base = context.history().base(); var drawLayer = selection.selectAll('.layer-osm.points .points-group.vertices'); var touchLayer = selection.selectAll('.layer-touch.points'); @@ -347,7 +369,8 @@ export function svgVertices(projection, context) { // a vertex of some importance.. } else if (geometry === 'vertex' && - (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph))) { + (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph) + || isEditedEnt(entity, base, graph))) { _currPersistent[entity.id] = entity; keep = true; } diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index 6abfc4d8d..156f3ad8d 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -86,6 +86,13 @@ export function uiMapData(context) { } + function toggleHighlightEdited() { + d3_event.preventDefault(); + var surface = context.surface(); + surface.classed('highlight-edited', !surface.classed('highlight-edited')); + } + + function showsLayer(which) { var layer = layers.layer(which); if (layer) { @@ -881,7 +888,8 @@ export function uiMapData(context) { d3_event.preventDefault(); d3_event.stopPropagation(); toggleLayer('osm'); - }); + }) + .on(t('map_data.highlight_way_edits.key'), toggleHighlightEdited); }; return uiMapData; From a0f85967b04fc219393c740f21a4ec695f9eff7f Mon Sep 17 00:00:00 2001 From: Benjamin Clark Date: Tue, 10 Sep 2019 10:36:06 -0400 Subject: [PATCH 2/3] highlight edits now differentiates between tag edits and geometry edits for vertices/lines. --- css/20_map.css | 61 ++++++++++++++++++++++++++++++++++------- modules/svg/lines.js | 45 +++++++++++++++++++++++++----- modules/svg/vertices.js | 25 +++++++++++------ 3 files changed, 106 insertions(+), 25 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 53df3798f..a7cf836f6 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -337,18 +337,59 @@ g.vertex.highlighted .shadow { stroke: #68f; } -.highlight-edited g.points > circle.vertex.edited { +.highlight-edited g.points > circle.vertex.graphedited { color: rgb(87, 201, 44); - stroke: #fff; + stroke: #333; stroke-width: 1; - stroke-opacity: 1; + stroke-opacity: .5; fill-opacity: 1; } -.highlight-edited g.lines > path.line.edited { - color: rgb(87, 201, 44); - stroke-width: 9; - stroke-opacity: 1; - } -.low-zoom.highlight-edited g.lines > path.line.edited { - stroke-width: 7; +.highlight-edited g.points > circle.vertex.tagedited { + color: rgb(160, 231, 134); + stroke: #fff; + stroke-width: 2; + stroke-opacity: .5; + fill-opacity: 1; +} + +/*Make the edited stroke line thin so that the original road color can show around it.*/ +.highlight-edited g.lines > path.line.graphedited { + color: rgb(87, 201, 44); + stroke-width: 3 !important; + stroke-opacity: 1; +} + +/*Make the stroke in whole-road graph edits thin and less apparent. */ ++.highlight-edited g.linegroup.line-stroke > path.way.line.stroke.graphedited { + stroke: rgb(87, 201, 44); + stroke-width: 3 !important; + stroke-opacity: 1; +} + +/*Make the casing around tag edits wide enough to be a bit more visible. */ +.highlight-edited g.linegroup.line-casing > path.way.line.casing.tagedited { + stroke: rgb(87, 201, 44) !important; + stroke-width: 12 !important; + stroke-opacity: 1; +} + +.low-zoom.highlight-edited g.linegroup.line-casing > path.way.line.casing.tagedited { + stroke-width: 10 !important; +} + +.highlight-edited.fill-wireframe g.linegroup.line-stroke > path.way.line.stroke.tagedited { + stroke: rgb(160, 231, 134); + stroke-width: 10 !important; + stroke-dasharray: 25 !important; + stroke-opacity: 1 !important; +} + +/*for low zoom levels, make the wireframe view 'tag edit' dashed line finer.*/ +.low-zoom.highlight-edited.fill-wireframe g.linegroup.line-stroke > path.way.line.stroke.tagedited { + stroke-dasharray: 7 !important; +} + +/*In wireframe mode, restrain the stroke width to something barely wider than normal.*/ +.fill-wireframe.highlight-edited g.lines > path.line.graphedited, .fill-wireframe.highlight-edited g.linegroup.line-stroke > path.way.line.stroke.tagedited { + stroke-width: 2 !important; } diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 46da6e056..4ac285ade 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -8,7 +8,10 @@ import { svgTagClasses } from './tag_classes'; import { osmEntity, osmOldMultipolygonOuterMember } from '../osm'; import { utilArrayFlatten, utilArrayGroupBy } from '../util'; import { utilDetect } from '../util/detect'; - +import _isEqual from 'lodash-es/isEqual'; +import _transform from 'lodash-es/transform'; +import _omit from 'lodash-es/omit'; +import _isObject from 'lodash-es/isObject'; export function svgLines(projection, context) { var detected = utilDetect(); @@ -56,10 +59,14 @@ export function svgLines(projection, context) { targets.exit() .remove(); - var editClass = function(d) { + var graphEditClass = function(d) { return d.properties.nodes.some(function(n) { - return graph.entities[n.id] !== base.entities[n.id]; - }) ? ' edited ': ''; + if (!base.entities[n.id]) { + return true; + } + var result = !_isEqual(_omit(graph.entities[n.id], ['tags', 'v']), _omit(base.entities[n.id], ['tags', 'v'])); + return result; + }) ? ' graphedited ': ''; }; // enter/update @@ -68,7 +75,7 @@ export function svgLines(projection, context) { .merge(targets) .attr('d', getPath) .attr('class', function(d) { - return 'way line target target-allowed ' + targetClass + d.id + editClass(d); + return 'way line target target-allowed ' + targetClass + d.id + graphEditClass(d); }); // NOPE @@ -86,11 +93,14 @@ export function svgLines(projection, context) { .append('path') .merge(nopes) .attr('d', getPath) - .attr('class', function(d) { return 'way line target target-nope ' + nopeClass + d.id; }); + .attr('class', function(d) { + return 'way line target target-nope ' + nopeClass + d.id + graphEditClass(d); + }); } function drawLines(selection, graph, entities, filter) { + var base = context.history().base(); function waystack(a, b) { var selected = context.selectedIDs(); @@ -103,6 +113,27 @@ export function svgLines(projection, context) { } + // Class for styling currently tag-edited lines, not changes to geometry + var tagEditClass = function(d) { + var result = graph.entities[d.id] && base.entities[d.id] && !_isEqual(graph.entities[d.id].tags, base.entities[d.id].tags); + + return result ? + ' tagedited ' : ''; + }; + + + // Class for styling currently geometry-edited lines + var graphEditClass = function(d) { + if (!base.entities[d.id]) { + return ' graphedited '; + } + + var result = graph.entities[d.id] && base.entities[d.id] && !_isEqual(_omit(graph.entities[d.id], ['tags', 'v']), _omit(base.entities[d.id], ['tags', 'v'])); + + return result ? ' graphedited ' : ''; + }; + + function drawLineGroup(selection, klass, isSelected) { // Note: Don't add `.selected` class in draw modes var mode = context.mode(); @@ -130,7 +161,7 @@ export function svgLines(projection, context) { } var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : ''; - return prefix + ' ' + klass + ' ' + selectedClass + oldMPClass + d.id; + return prefix + ' ' + klass + ' ' + selectedClass + oldMPClass + graphEditClass(d) + tagEditClass(d) + d.id; }) .call(svgTagClasses()) .merge(lines) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index d830f3b01..3410b99e9 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -3,7 +3,8 @@ import { select as d3_select } from 'd3-selection'; import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; import { svgPassiveVertex, svgPointTransform } from './helpers'; - +import _isEqual from 'lodash-es/isEqual'; +import _omit from 'lodash-es/omit'; export function svgVertices(projection, context) { var radiuses = { @@ -228,7 +229,13 @@ export function svgVertices(projection, context) { // Class for styling currently edited vertices var editClass = function(d) { - return (graph.entities[d.id] !== base.entities[d.id]) ? ' edited ' : ''; + //If it doesn't exist in the base graph, it's new geometry. + if (!base.entities[d.id] || !_isEqual(_omit(graph.entities[d.id], ['tags', 'v']), _omit(base.entities[d.id], ['tags', 'v']))) { + return ' graphedited '; + } else if (!_isEqual(graph.entities[d.id].tags, base.entities[d.id].tags)) { + return ' tagedited '; + } + return ''; }; // Targets allow hover and vertex snapping @@ -240,15 +247,16 @@ export function svgVertices(projection, context) { targets.exit() .remove(); + var threeFourths = function (num) { + return (Math.round(3 * num) / 4).toFixed(2); + }; // enter/update targets.enter() .append('circle') .attr('r', function(d) { - return ((graph.entities[d.id].isEndpoint(graph) - && !graph.entities[d.id].isConnected(graph) - && isEditedEnt(d, base, graph) && interestingNodeRadius) - || (isEditedEnt(d, base, graph) && radius) - || _radii[d.id] || radiuses.shadow[3]); + return isEditedEnt(d, base, graph) && threeFourths(_radii[d.id]) + || _radii[d.id] + || radiuses.shadow[3]; }) .merge(targets) .attr('class', function(d) { @@ -289,7 +297,8 @@ export function svgVertices(projection, context) { function isEditedEnt(entity, base, head) { - return head.entities[entity.id] !== base.entities[entity.id]; + return head.entities[entity.id] !== base.entities[entity.id] || + !_isEqual(head.entities[entity.id].tags, base.entities[entity.id].tags); } From ad5025de3ed931dac89e06d18a6a8ec4bbb46bd4 Mon Sep 17 00:00:00 2001 From: Benjamin Clark Date: Tue, 10 Sep 2019 14:27:20 -0400 Subject: [PATCH 3/3] Add support for area diffs. --- css/20_map.css | 65 +++++++++++++++++++++++++++++++++++++++++--- data/core.yaml | 4 +-- modules/svg/areas.js | 26 ++++++++++++++++-- 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index a7cf836f6..390fb6d2e 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -337,6 +337,12 @@ g.vertex.highlighted .shadow { stroke: #68f; } +/* highlight-edited means 'visual diff activated' + 'graphedited' class means that a geometric (dark green) change has occurred. + 'tagedited' means that a tagging change has occurred. + if both are true, the 'graphedited' class supersedes 'tagedited'. + + /*Vertex-related visual diffs*/ .highlight-edited g.points > circle.vertex.graphedited { color: rgb(87, 201, 44); stroke: #333; @@ -351,16 +357,18 @@ g.vertex.highlighted .shadow { stroke-opacity: .5; fill-opacity: 1; } - + +/*Line-related visual diffs*/ /*Make the edited stroke line thin so that the original road color can show around it.*/ -.highlight-edited g.lines > path.line.graphedited { +.highlight-edited g.lines > path.line.graphedited, +.highlight-edited g.areas > path.line.graphedited { color: rgb(87, 201, 44); stroke-width: 3 !important; stroke-opacity: 1; } /*Make the stroke in whole-road graph edits thin and less apparent. */ -+.highlight-edited g.linegroup.line-stroke > path.way.line.stroke.graphedited { +.highlight-edited g.linegroup.line-stroke > path.way.line.stroke.graphedited { stroke: rgb(87, 201, 44); stroke-width: 3 !important; stroke-opacity: 1; @@ -377,6 +385,49 @@ g.vertex.highlighted .shadow { stroke-width: 10 !important; } +/* Area-related visual diffs */ +.highlight-edited g.areagroup.area-fill > path.way.area.fill.graphedited { + stroke-width: 45px; + stroke-opacity: .5; +} + +.highlight-edited g.areagroup.area-stroke > path.way.area.stroke.graphedited { + stroke: rgb(87, 201, 44) !important; + stroke-width: 5 !important; +} + +.highlight-edited g.areagroup.area-stroke > path.way.area.stroke.tagedited { + stroke: rgb(160, 231, 134) !important; + stroke-width: 3 !important; +} + +.highlight-edited g.areagroup.area-shadow > path.way.area.shadow.graphedited { + fill: rgb(87, 201, 44); + opacity: .2; +} + +.highlight-edited g.areagroup.area-shadow > path.way.area.shadow.tagedited { + fill: rgb(87, 201, 44); + opacity: .1; +} + +.highlight-edited g.areagroup.area-fill > path.way.area.fill.tagedited { + stroke-width: 30; +} + +.low-zoom.highlight-edited.fill-wireframe g.areagroup.area-stroke > path.way.area.stroke.tagedited { + stroke-dasharray: 7; + stroke: rgb(87, 201, 44) !important; + stroke-width: 4px; +} + +.low-zoom.highlight-edited g.areagroup.area-stroke > path.way.area.stroke.tagedited { + stroke: rgb(160, 231, 134); + stroke-width: 40; + stroke-dasharray: 25 !important; + stroke-opacity: 1 !important; +} + .highlight-edited.fill-wireframe g.linegroup.line-stroke > path.way.line.stroke.tagedited { stroke: rgb(160, 231, 134); stroke-width: 10 !important; @@ -384,12 +435,18 @@ g.vertex.highlighted .shadow { stroke-opacity: 1 !important; } +.highlight-edited.fill-wireframe g.areagroup.area-shadow > path.way.area.shadow.graphedited, +.highlight-edited.fill-wireframe g.areagroup.area-shadow > path.way.area.shadow.tagedited { + fill-opacity: .05; +} + /*for low zoom levels, make the wireframe view 'tag edit' dashed line finer.*/ .low-zoom.highlight-edited.fill-wireframe g.linegroup.line-stroke > path.way.line.stroke.tagedited { stroke-dasharray: 7 !important; } /*In wireframe mode, restrain the stroke width to something barely wider than normal.*/ -.fill-wireframe.highlight-edited g.lines > path.line.graphedited, .fill-wireframe.highlight-edited g.linegroup.line-stroke > path.way.line.stroke.tagedited { +.fill-wireframe.highlight-edited g.lines > path.line.graphedited, .fill-wireframe.highlight-edited g.linegroup.line-stroke > path.way.line.stroke.tagedited, +.fill-wireframe.highlight-edited g.areas > path.area.graphedited, .fill-wireframe.highlight-edited g.linegroup.line-stroke > path.way.area.stroke.tagedited { stroke-width: 2 !important; } diff --git a/data/core.yaml b/data/core.yaml index 07aaedf44..16c9f145b 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -604,7 +604,7 @@ en: fill_area: Fill Areas highlight_way_edits: description: Highlight edited nodes and segments attached to those nodes in ways - tooltip: Highlight way edits in the current session. + tooltip: Highlight unsaved edits. key: G map_features: Map Features autohidden: "These features have been automatically hidden because too many would be shown on the screen. You can zoom in to edit them." @@ -1865,7 +1865,7 @@ en: wireframe: "Toggle wireframe mode" osm_data: "Toggle OpenStreetMap data" minimap: "Toggle minimap" - highlight_way_edits: "Highlight way edits in current session" + highlight_way_edits: "Highlight unsaved edits" selecting: title: "Selecting features" select_one: "Select a single feature" diff --git a/modules/svg/areas.js b/modules/svg/areas.js index 5ba0d9cd5..4df9ac0ad 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -3,7 +3,8 @@ import { bisector as d3_bisector } from 'd3-array'; import { osmEntity, osmIsOldMultipolygonOuterMember } from '../osm'; import { svgPath, svgSegmentWay } from './helpers'; import { svgTagClasses } from './tag_classes'; - +import _isEqual from 'lodash-es/isEqual'; +import _omit from 'lodash-es/omit'; export function svgAreas(projection, context) { // Patterns only work in Firefox when set directly on element. @@ -192,6 +193,7 @@ export function svgAreas(projection, context) { var path = svgPath(projection, graph, true); var areas = {}; var multipolygon; + var base = context.history().base(); for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -275,12 +277,32 @@ export function svgAreas(projection, context) { } } + + var graphEditClass = function(d) { + if (d.type !== 'way'){ + return ''; + } + var graphEdited = d.nodes.some(function(n) { + if (!base.entities[n]) { + return true; + } + var result = !_isEqual(_omit(graph.entities[n], ['tags', 'v']), _omit(base.entities[n], ['tags', 'v'])); + return result; + }); + + if (graphEdited){ + return 'graphedited'; + } + + return (!_isEqual(graph.entities[d.id].tags, base.entities[d.id].tags)) ? 'tagedited' : ''; + }; + paths = paths.enter() .insert('path', sortedByArea) .merge(paths) .each(function(entity) { var layer = this.parentNode.__data__; - this.setAttribute('class', entity.type + ' area ' + layer + ' ' + entity.id); + this.setAttribute('class', entity.type + ' area ' + layer + ' ' + graphEditClass(entity) + ' ' + entity.id); if (layer === 'fill') { this.setAttribute('clip-path', 'url(#' + entity.id + '-clippath)');