Merge branch 'develop' into unbundled-presets

# Conflicts:
#	dist/locales/en.json
This commit is contained in:
Quincy Morgan
2020-12-07 10:57:14 -05:00
20 changed files with 403 additions and 127 deletions
-1
View File
@@ -15,7 +15,6 @@ type = YAML
[id-editor.presets]
file_filter = .tx/tmp/presets/<lang>.yaml
source_file = data/presets.yaml
source_lang = en
type = YAML
+10 -1
View File
@@ -457,8 +457,17 @@ An issue with the active [MapRules](https://github.com/radiant-maxar/maprules) v
A feature's tags indicate it should have a different geometry than it currently does.
* `area_as_line`: an unclosed way has tags implying it should be a closed area (e.g. `area=yes` or `building=yes`)
* `vertex_as_point`: a detached node has tags implying it should be part of a way (e.g. `highway=stop`)
* `area_as_point`
* `area_as_vertex`
* `line_as_area`
* `line_as_point`
* `line_as_vertex`: a detached node has tags implying it should be a line (e.g. `highway=motorway`)
* `point_as_area`
* `point_as_line`
* `point_as_vertex`: a vertex node has tags implying it should be detached from ways (e.g. `amenity=cafe`)
* `vertex_as_area`
* `vertex_as_line`
* `vertex_as_point`: a detached node has tags implying it should be part of a way (e.g. `highway=stop`)
* `unclosed_multipolygon_part`: a relation is tagged as a multipolygon but not all of its member ways form closed rings
##### `missing_role`
+14 -3
View File
@@ -1094,6 +1094,14 @@ a.hide-toggle {
fill: transparent;
}
.preset-icon-category-border path {
stroke: #999;
stroke-width: 1px;
fill: transparent;
backface-visibility: hidden;
vector-effect: non-scaling-stroke;
}
.preset-icon-line {
margin: auto;
position: absolute;
@@ -1169,7 +1177,7 @@ a.hide-toggle {
.preset-icon.framed .icon {
transform: scale(0.4);
}
.preset-icon.framed.line-geom .icon,
.preset-icon.framed.line-geom:not(.category) .icon,
.preset-icon.framed.route-geom .icon {
top: 20%;
transform: translateY(-30%) scale(0.4);
@@ -1180,7 +1188,7 @@ a.hide-toggle {
.preset-icon-iD.framed .icon {
transform: scale(0.74);
}
.preset-icon-iD.framed.line-geom .icon,
.preset-icon-iD.framed.line-geom:not(.category) .icon,
.preset-icon-iD.framed.route-geom .icon {
transform: translateY(-30%) scale(0.74);
}
@@ -4382,9 +4390,12 @@ img.tile-debug {
justify-content: space-between;
align-items: flex-end;
z-index: 0;
pointer-events: none;
}
.attribution-wrap * { pointer-events: all; }
.attribution-wrap > * {
pointer-events: auto;
}
.attribution-wrap .base-layer-attribution,
.attribution-wrap .overlay-layer-attribution {
+15
View File
@@ -580,6 +580,7 @@ en:
upload_explanation: "The changes you upload will be visible on all maps that use OpenStreetMap data."
upload_explanation_with_user: "The changes you upload as {user} will be visible on all maps that use OpenStreetMap data."
request_review: "I would like someone to review my edits."
request_review_info: "Unsure about something? Invite an experienced mapper to check your work once it's live."
save: Upload
cancel: Cancel
changes: Changes
@@ -1649,6 +1650,8 @@ en:
message: "{feature} ends very close to itself but does not reconnect"
highway-highway:
reference: Intersecting highways should share a junction vertex.
area_as_point:
message: '{feature} should be an area, not a point'
close_nodes:
title: "Very Close Points"
tip: "Find redundant and crowded points"
@@ -1728,9 +1731,14 @@ en:
message: '{feature} has an invalid email address'
message_multi: '{feature} has multiple invalid email addresses'
reference: 'Email addresses must look like "user@example.com".'
line_as_area:
message: '{feature} should be a line, not an area'
line_as_point:
message: '{feature} should be a line, not a point'
mismatched_geometry:
title: Mismatched Geometry
tip: "Find features with conflicting tags and geometry"
reference: Most features are limited to certain geometry types.
missing_role:
title: Missing Roles
message: "{member} has no role within {relation}"
@@ -1762,6 +1770,10 @@ en:
message: "{feature} looks like a brand with nonstandard tags"
message_incomplete: "{feature} looks like a brand with incomplete tags"
reference: "All features of the same brand should be tagged the same way."
point_as_area:
message: '{feature} should be a point, not an area'
point_as_line:
message: '{feature} should be a point, not a line'
point_as_vertex:
message: '{feature} should be a standalone point based on its tags'
reference: "Some features shouldn't be part of lines or areas."
@@ -1835,6 +1847,9 @@ en:
title: Continue drawing from start
continue_from_end:
title: Continue drawing from end
convert_to_line:
title: Convert this to a line
annotation: Converted an area to a line.
delete_feature:
title: Delete this feature
extract_point:
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -89,7 +89,8 @@ export function behaviorDraw(context) {
var p2 = downPointer.pointerLocGetter(d3_event);
var dist = geoVecLength(downPointer.downLoc, p2);
if (dist < _closeTolerance || (dist < _tolerance && (t2 - downPointer.downTime) < 500)) {
if (dist < _closeTolerance ||
(dist < _tolerance && (t2 - downPointer.downTime) < 500)) {
// Prevent a quick second click
d3_select(window).on('click.draw-block', function() {
d3_event.stopPropagation();
+9 -2
View File
@@ -238,8 +238,15 @@ export function behaviorDrawWay(context, wayID, mode, startGraph) {
_drawNode = undefined;
_didResolveTempEdit = false;
_origWay = context.entity(wayID);
_headNodeID = typeof _nodeIndex === 'number' ? _origWay.nodes[_nodeIndex] :
(_origWay.isClosed() ? _origWay.nodes[_origWay.nodes.length - 2] : _origWay.nodes[_origWay.nodes.length - 1]);
if (typeof _nodeIndex === 'number') {
_headNodeID = _origWay.nodes[_nodeIndex];
} else if (_origWay.isClosed()) {
_headNodeID = _origWay.nodes[_origWay.nodes.length - 2];
} else {
_headNodeID = _origWay.nodes[_origWay.nodes.length - 1];
}
_wayGeometry = _origWay.geometry(context.graph());
_annotation = t((_origWay.nodes.length === (_origWay.isClosed() ? 2 : 1) ?
'operations.start.annotation.' :
+35 -7
View File
@@ -7,10 +7,12 @@ import { t } from '../core/localizer';
import { actionMove } from '../actions/move';
import { actionNoop } from '../actions/noop';
import { behaviorEdit } from '../behavior/edit';
import { geoViewportEdge, geoVecSubtract } from '../geo';
import { geoVecLength, geoVecSubtract } from '../geo/vector';
import { geoViewportEdge } from '../geo/geom';
import { modeBrowse } from './browse';
import { modeSelect } from './select';
import { utilKeybinding } from '../util';
import { utilFastMouse } from '../util/util';
import { operationCircularize } from '../operations/circularize';
@@ -21,6 +23,9 @@ import { operationRotate } from '../operations/rotate';
export function modeMove(context, entityIDs, baseGraph) {
var _tolerancePx = 4; // see also behaviorDrag, behaviorSelect, modeRotate
var mode = {
id: 'move',
button: 'browse'
@@ -45,6 +50,9 @@ export function modeMove(context, entityIDs, baseGraph) {
var _origin;
var _nudgeInterval;
// use pointer events on supported platforms; fallback to mouse events
var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';
function doMove(nudge) {
nudge = nudge || [0, 0];
@@ -129,12 +137,29 @@ export function modeMove(context, entityIDs, baseGraph) {
behaviors.forEach(context.install);
var downEvent;
context.surface()
.on('mousemove.move', move)
.on('click.move', finish);
.on(_pointerPrefix + 'down.modeMove', function(d3_event) {
downEvent = d3_event;
});
d3_select(window)
.on(_pointerPrefix + 'move.modeMove', move, true)
.on(_pointerPrefix + 'up.modeMove', function(d3_event) {
if (!downEvent) return;
var mapNode = context.container().select('.main-map').node();
var pointGetter = utilFastMouse(mapNode);
var p1 = pointGetter(downEvent);
var p2 = pointGetter(d3_event);
var dist = geoVecLength(p1, p2);
if (dist <= _tolerancePx) finish(d3_event);
downEvent = null;
}, true);
context.history()
.on('undone.move', undone);
.on('undone.modeMove', undone);
keybinding
.on('⎋', cancel)
@@ -153,11 +178,14 @@ export function modeMove(context, entityIDs, baseGraph) {
});
context.surface()
.on('mousemove.move', null)
.on('click.move', null);
.on(_pointerPrefix + 'down.modeMove', null);
d3_select(window)
.on(_pointerPrefix + 'move.modeMove', null, true)
.on(_pointerPrefix + 'up.modeMove', null, true);
context.history()
.on('undone.move', null);
.on('undone.modeMove', null);
d3_select(document)
.call(keybinding.unbind);
+37 -10
View File
@@ -11,7 +11,7 @@ import { t } from '../core/localizer';
import { actionRotate } from '../actions/rotate';
import { actionNoop } from '../actions/noop';
import { behaviorEdit } from '../behavior/edit';
import { geoVecInterp } from '../geo';
import { geoVecInterp, geoVecLength } from '../geo/vector';
import { modeBrowse } from './browse';
import { modeSelect } from './select';
@@ -21,10 +21,14 @@ import { operationMove } from '../operations/move';
import { operationOrthogonalize } from '../operations/orthogonalize';
import { operationReflectLong, operationReflectShort } from '../operations/reflect';
import { utilGetAllNodes, utilKeybinding } from '../util';
import { utilKeybinding } from '../util/keybinding';
import { utilFastMouse, utilGetAllNodes } from '../util/util';
export function modeRotate(context, entityIDs) {
var _tolerancePx = 4; // see also behaviorDrag, behaviorSelect, modeMove
var mode = {
id: 'rotate',
button: 'browse'
@@ -49,8 +53,11 @@ export function modeRotate(context, entityIDs) {
var _prevTransform;
var _pivot;
// use pointer events on supported platforms; fallback to mouse events
var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';
function doRotate() {
function doRotate(d3_event) {
var fn;
if (context.graph() !== _prevGraph) {
fn = context.perform;
@@ -73,7 +80,7 @@ export function modeRotate(context, entityIDs) {
}
var currMouse = context.map().mouse();
var currMouse = context.map().mouse(d3_event);
var currAngle = Math.atan2(currMouse[1] - _pivot[1], currMouse[0] - _pivot[0]);
if (typeof _prevAngle === 'undefined') _prevAngle = currAngle;
@@ -127,12 +134,29 @@ export function modeRotate(context, entityIDs) {
behaviors.forEach(context.install);
var downEvent;
context.surface()
.on('mousemove.rotate', doRotate)
.on('click.rotate', finish);
.on(_pointerPrefix + 'down.modeRotate', function(d3_event) {
downEvent = d3_event;
});
d3_select(window)
.on(_pointerPrefix + 'move.modeRotate', doRotate, true)
.on(_pointerPrefix + 'up.modeRotate', function(d3_event) {
if (!downEvent) return;
var mapNode = context.container().select('.main-map').node();
var pointGetter = utilFastMouse(mapNode);
var p1 = pointGetter(downEvent);
var p2 = pointGetter(d3_event);
var dist = geoVecLength(p1, p2);
if (dist <= _tolerancePx) finish(d3_event);
downEvent = null;
}, true);
context.history()
.on('undone.rotate', undone);
.on('undone.modeRotate', undone);
keybinding
.on('⎋', cancel)
@@ -147,11 +171,14 @@ export function modeRotate(context, entityIDs) {
behaviors.forEach(context.uninstall);
context.surface()
.on('mousemove.rotate', null)
.on('click.rotate', null);
.on(_pointerPrefix + 'down.modeRotate', null);
d3_select(window)
.on(_pointerPrefix + 'move.modeRotate', null, true)
.on(_pointerPrefix + 'up.modeRotate', null, true);
context.history()
.on('undone.rotate', null);
.on('undone.modeRotate', null);
d3_select(document)
.call(keybinding.unbind);
+1 -2
View File
@@ -17,8 +17,7 @@ export function operationMove(context, selectedIDs) {
operation.available = function() {
return selectedIDs.length > 1 ||
context.entity(selectedIDs[0]).type !== 'node';
return selectedIDs.length > 0;
};
+1 -1
View File
@@ -710,7 +710,7 @@ export function rendererMap(context) {
map.mouse = function(d3_event) {
var event = _lastPointerEvent || d3_event;
var event = d3_event || _lastPointerEvent;
if (event) {
var s;
while ((s = event.sourceEvent)) { event = s; }
+3 -1
View File
@@ -109,7 +109,9 @@ function preventCoincident(loc, bumpUp) {
let coincident = false;
do {
// first time, move marker up. after that, move marker right.
let delta = coincident ? [0.00001, 0] : (bumpUp ? [0, 0.00001] : [0, 0]);
let delta = coincident ? [0.00001, 0] :
bumpUp ? [0, 0.00001] :
[0, 0];
loc = geoVecAdd(loc, delta);
let bbox = geoExtent(loc).bbox();
coincident = _cache.rtree.search(bbox).length;
+6 -2
View File
@@ -153,7 +153,9 @@ export function svgNotes(projection, context, dispatch) {
.attr('x', '-3px')
.attr('y', '-19px')
.attr('xlink:href', function(d) {
return '#iD-icon-' + (d.id < 0 ? 'plus' : (d.status === 'open' ? 'close' : 'apply'));
if (d.id < 0) return '#iD-icon-plus';
if (d.status === 'open') return '#iD-icon-close';
return '#iD-icon-apply';
});
// update
@@ -196,7 +198,9 @@ export function svgNotes(projection, context, dispatch) {
function sortY(a, b) {
return (a.id === selectedID) ? 1 : (b.id === selectedID) ? -1 : b.loc[1] - a.loc[1];
if (a.id === selectedID) return 1;
if (b.id === selectedID) return -1;
return b.loc[1] - a.loc[1];
}
}
+5
View File
@@ -323,6 +323,11 @@ export function uiCommit(context) {
.append('label')
.attr('for', requestReviewDomId);
if (!labelEnter.empty()) {
labelEnter
.call(uiTooltip().title(t.html('commit.request_review_info')).placement('top'));
}
labelEnter
.append('input')
.attr('type', 'checkbox')
+18 -11
View File
@@ -125,7 +125,7 @@ export function uiFieldLocalized(field, context) {
var existingLangs = new Set(existingLangsOrdered.filter(Boolean));
for (var k in tags) {
var m = k.match(/^(.*):(.+)$/);
var m = k.match(/^(.*):(.*)$/);
if (m && m[1] === field.key && m[2]) {
var item = { lang: m[2], value: tags[k] };
if (existingLangs.has(item.lang)) {
@@ -138,8 +138,12 @@ export function uiFieldLocalized(field, context) {
}
}
_multilingual = _multilingual.filter(function(item) {
return !item.lang || !existingLangs.has(item.lang);
// Don't remove items based on deleted tags, since this makes the UI
// disappear unexpectedly when clearing values - #8164
_multilingual.forEach(function(item) {
if (item.lang && existingLangs.has(item.lang)) {
item.value = '';
}
});
}
@@ -512,16 +516,20 @@ export function uiFieldLocalized(field, context) {
if (field.locked()) return;
d3_event.preventDefault();
if (!d.lang || !d.value) {
_multilingual.splice(index, 1);
renderMultilingual(selection);
} else {
// remove the UI item manually
_multilingual.splice(_multilingual.indexOf(d), 1);
var langKey = d.lang && key(d.lang);
if (langKey && langKey in _tags) {
delete _tags[langKey];
// remove from entity tags
var t = {};
t[key(d.lang)] = undefined;
t[langKey] = undefined;
dispatch.call('change', this, t);
return;
}
renderMultilingual(selection);
})
.call(svgIcon('#iD-operation-delete'));
@@ -562,9 +570,8 @@ export function uiFieldLocalized(field, context) {
entries.order();
entries.classed('present', function(d) {
return d.lang && d.value;
});
// allow removing the entry UIs even if there isn't a tag to remove
entries.classed('present', true);
utilGetSetValue(entries.select('.localized-lang'), function(d) {
var langItem = _languagesArray.find(function(item) {
+8 -1
View File
@@ -31,7 +31,14 @@ export function uiNoteHeader() {
.call(svgIcon('#iD-icon-note', 'note-fill'));
iconEnter.each(function(d) {
var statusIcon = '#iD-icon-' + (d.id < 0 ? 'plus' : (d.status === 'open' ? 'close' : 'apply'));
var statusIcon;
if (d.id < 0) {
statusIcon = '#iD-icon-plus';
} else if (d.status === 'open') {
statusIcon = '#iD-icon-close';
} else {
statusIcon = '#iD-icon-apply';
}
iconEnter
.append('div')
.attr('class', 'note-icon-annotation')
+83 -45
View File
@@ -59,6 +59,30 @@ export function uiPresetIcon() {
}
function renderCategoryBorder(container, drawBorder) {
let categoryBorder = container.selectAll('.preset-icon-category-border')
.data(drawBorder ? [0] : []);
categoryBorder.exit()
.remove();
let categoryBorderEnter = categoryBorder.enter();
const d = 60;
categoryBorderEnter
.append('svg')
.attr('class', 'preset-icon-fill preset-icon-category-border')
.attr('width', d)
.attr('height', d)
.attr('viewBox', `0 0 ${d} ${d}`)
.append('path')
.attr('d', 'M9.5,7.5 L25.5,7.5 L28.5,12.5 L49.5,12.5 C51.709139,12.5 53.5,14.290861 53.5,16.5 L53.5,43.5 C53.5,45.709139 51.709139,47.5 49.5,47.5 L10.5,47.5 C8.290861,47.5 6.5,45.709139 6.5,43.5 L6.5,12.5 L9.5,7.5 Z');
categoryBorder = categoryBorderEnter.merge(categoryBorder);
}
function renderCircleFill(container, drawVertex) {
let vertexFill = container.selectAll('.preset-icon-fill-vertex')
.data(drawVertex ? [0] : []);
@@ -271,6 +295,60 @@ export function uiPresetIcon() {
}
}
function renderSvgIcon(container, picon, geom, isFramed, category, tagClasses) {
const isMaki = picon && /^maki-/.test(picon);
const isTemaki = picon && /^temaki-/.test(picon);
const isFa = picon && /^fa[srb]-/.test(picon);
const isiDIcon = picon && !(isMaki || isTemaki || isFa);
let icon = container.selectAll('.preset-icon')
.data(picon ? [0] : []);
icon.exit()
.remove();
icon = icon.enter()
.append('div')
.attr('class', 'preset-icon')
.call(svgIcon(''))
.merge(icon);
icon
.attr('class', 'preset-icon ' + (geom ? geom + '-geom' : ''))
.classed('category', category)
.classed('framed', isFramed)
.classed('preset-icon-iD', isiDIcon);
icon.selectAll('svg')
.attr('class', 'icon ' + picon + ' ' + (!isiDIcon && geom !== 'line' ? '' : tagClasses));
var suffix = '';
if (isMaki) {
suffix = isSmall() && geom === 'point' ? '-11' : '-15';
}
icon.selectAll('use')
.attr('href', '#' + picon + suffix);
}
function renderImageIcon(container, imageURL) {
let imageIcon = container.selectAll('img.image-icon')
.data(imageURL ? [0] : []);
imageIcon.exit()
.remove();
imageIcon = imageIcon.enter()
.append('img')
.attr('class', 'image-icon')
.on('load', () => container.classed('showing-img', true) )
.on('error', () => container.classed('showing-img', false) )
.merge(imageIcon);
imageIcon
.attr('src', imageURL);
}
// Route icons are drawn with a zigzag annotation underneath:
// o o
@@ -310,17 +388,13 @@ export function uiPresetIcon() {
const isFallback = isSmall() && p.isFallback && p.isFallback();
const imageURL = (showThirdPartyIcons === 'true') && p.imageURL;
const picon = getIcon(p, geom);
const isMaki = picon && /^maki-/.test(picon);
const isTemaki = picon && /^temaki-/.test(picon);
const isFa = picon && /^fa[srb]-/.test(picon);
const isiDIcon = picon && !(isMaki || isTemaki || isFa);
const isCategory = !p.setTags;
const drawPoint = picon && geom === 'point' && isSmall() && !isFallback;
const drawVertex = picon !== null && geom === 'vertex' && (!isSmall() || !isFallback);
const drawLine = picon && geom === 'line' && !isFallback && !isCategory;
const drawArea = picon && geom === 'area' && !isFallback;
const drawArea = picon && geom === 'area' && !isFallback && !isCategory;
const drawRoute = picon && geom === 'route';
const isFramed = (drawVertex || drawArea || drawLine || drawRoute);
const isFramed = drawVertex || drawArea || drawLine || drawRoute || isCategory;
let tags = !isCategory ? p.setTags({}, geom) : {};
for (let k in tags) {
@@ -344,50 +418,14 @@ export function uiPresetIcon() {
.classed('showing-img', !!imageURL)
.classed('fallback', isFallback);
renderCategoryBorder(container, isCategory);
renderPointBorder(container, drawPoint);
renderCircleFill(container, drawVertex);
renderSquareFill(container, drawArea, tagClasses);
renderLine(container, drawLine, tagClasses);
renderRoute(container, drawRoute, p);
let icon = container.selectAll('.preset-icon')
.data(picon ? [0] : []);
icon.exit()
.remove();
icon = icon.enter()
.append('div')
.attr('class', 'preset-icon')
.call(svgIcon(''))
.merge(icon);
icon
.attr('class', 'preset-icon ' + (geom ? geom + '-geom' : ''))
.classed('framed', isFramed)
.classed('preset-icon-iD', isiDIcon);
icon.selectAll('svg')
.attr('class', 'icon ' + picon + ' ' + (!isiDIcon && geom !== 'line' ? '' : tagClasses));
icon.selectAll('use')
.attr('href', '#' + picon + (isMaki ? (isSmall() && geom === 'point' ? '-11' : '-15') : ''));
let imageIcon = container.selectAll('img.image-icon')
.data(imageURL ? [0] : []);
imageIcon.exit()
.remove();
imageIcon = imageIcon.enter()
.append('img')
.attr('class', 'image-icon')
.on('load', () => container.classed('showing-img', true) )
.on('error', () => container.classed('showing-img', false) )
.merge(imageIcon);
imageIcon
.attr('src', imageURL);
renderSvgIcon(container, picon, geom, isFramed, isCategory, tagClasses);
renderImageIcon(container, imageURL);
}
+2 -1
View File
@@ -447,7 +447,8 @@ export function utilFastMouse(container) {
return function(e) {
return [
e.clientX - rectLeft - clientLeft,
e.clientY - rectTop - clientTop];
e.clientY - rectTop - clientTop
];
};
}
+152 -36
View File
@@ -1,10 +1,11 @@
import deepEqual from 'fast-deep-equal';
import { actionAddVertex } from '../actions/add_vertex';
import { actionChangeTags } from '../actions/change_tags';
import { actionMergeNodes } from '../actions/merge_nodes';
import { actionExtract } from '../actions/extract';
import { modeSelect } from '../modes/select';
import { osmJoinWays } from '../osm/multipolygon';
import { osmNodeGeometriesForTags } from '../osm/tags';
import { osmNodeGeometriesForTags, osmTagSuggestingArea } from '../osm/tags';
import { presetManager } from '../presets';
import { geoHasSelfIntersections, geoSphericalDistance } from '../geo';
import { t } from '../core/localizer';
@@ -139,7 +140,7 @@ export function validationMismatchedGeometry() {
}
}
function vertexTaggedAsPointIssue(entity, graph) {
function vertexPointIssue(entity, graph) {
// we only care about nodes
if (entity.type !== 'node') return null;
@@ -196,46 +197,157 @@ export function validationMismatchedGeometry() {
.html(t.html('issues.point_as_vertex.reference'));
},
entityIds: [entity.id],
dynamicFixes: function(context) {
var entityId = this.entityIds[0];
var extractOnClick = null;
if (!context.hasHiddenConnections(entityId)) {
extractOnClick = function(context) {
var entityId = this.issue.entityIds[0];
var action = actionExtract(entityId);
context.perform(
action,
t('operations.extract.annotation', { n: 1 })
);
// re-enter mode to trigger updates
context.enter(modeSelect(context, [action.getExtractedNodeID()]));
};
}
return [
new validationIssueFix({
icon: 'iD-operation-extract',
title: t.html('issues.fix.extract_point.title'),
onClick: extractOnClick
})
];
}
dynamicFixes: extractPointDynamicFixes
});
}
return null;
}
function otherMismatchIssue(entity, graph) {
// ignore boring features
if (!entity.hasInterestingTags()) return null;
if (entity.type !== 'node' && entity.type !== 'way') return null;
// address lines are special so just ignore them
if (entity.type === 'node' && entity.isOnAddressLine(graph)) return null;
var sourceGeom = entity.geometry(graph);
var targetGeoms = entity.type === 'way' ? ['point', 'vertex'] : ['line', 'area'];
if (sourceGeom === 'area') targetGeoms.unshift('line');
var targetGeom = targetGeoms.find(nodeGeom => {
var asSource = presetManager.matchTags(entity.tags, sourceGeom);
var asTarget = presetManager.matchTags(entity.tags, nodeGeom);
if (!asSource || !asTarget ||
asSource === asTarget ||
// sometimes there are two presets with the same tags for different geometries
deepEqual(asSource.tags, asTarget.tags)) return false;
if (asTarget.isFallback()) return false;
var primaryKey = Object.keys(asTarget.tags)[0];
// special case: buildings-as-points are discouraged by iD, but common in OSM, so ignore them
if (primaryKey === 'building') return false;
if (asTarget.tags[primaryKey] === '*') return false;
return asSource.isFallback() || asSource.tags[primaryKey] === '*';
});
if (!targetGeom) return null;
var subtype = targetGeom + '_as_' + sourceGeom;
if (targetGeom === 'vertex') targetGeom = 'point';
if (sourceGeom === 'vertex') sourceGeom = 'point';
var referenceId = targetGeom + '_as_' + sourceGeom;
var dynamicFixes;
if (targetGeom === 'point') {
dynamicFixes = extractPointDynamicFixes;
} else if (sourceGeom === 'area' && targetGeom === 'line') {
dynamicFixes = lineToAreaDynamicFixes;
}
return new validationIssue({
type: type,
subtype: subtype,
severity: 'warning',
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t.html('issues.' + referenceId + '.message', {
feature: utilDisplayLabel(entity, targetGeom)
}) : '';
},
reference: function showReference(selection) {
selection.selectAll('.issue-reference')
.data([0])
.enter()
.append('div')
.attr('class', 'issue-reference')
.html(t.html('issues.mismatched_geometry.reference'));
},
entityIds: [entity.id],
dynamicFixes: dynamicFixes
});
}
function lineToAreaDynamicFixes(context) {
var convertOnClick;
var entityId = this.entityIds[0];
var entity = context.entity(entityId);
var tags = Object.assign({}, entity.tags); // shallow copy
delete tags.area;
if (!osmTagSuggestingArea(tags)) {
// if removing the area tag would make this a line, offer that as a quick fix
convertOnClick = function(context) {
var entityId = this.issue.entityIds[0];
var entity = context.entity(entityId);
var tags = Object.assign({}, entity.tags); // shallow copy
if (tags.area) {
delete tags.area;
}
context.perform(
actionChangeTags(entityId, tags),
t('issues.fix.convert_to_line.annotation')
);
};
}
return [
new validationIssueFix({
icon: 'iD-icon-line',
title: t.html('issues.fix.convert_to_line.title'),
onClick: convertOnClick
})
];
}
function extractPointDynamicFixes(context) {
var entityId = this.entityIds[0];
var extractOnClick = null;
if (!context.hasHiddenConnections(entityId)) {
extractOnClick = function(context) {
var entityId = this.issue.entityIds[0];
var action = actionExtract(entityId);
context.perform(
action,
t('operations.extract.annotation', { n: 1 })
);
// re-enter mode to trigger updates
context.enter(modeSelect(context, [action.getExtractedNodeID()]));
};
}
return [
new validationIssueFix({
icon: 'iD-operation-extract',
title: t.html('issues.fix.extract_point.title'),
onClick: extractOnClick
})
];
}
function unclosedMultipolygonPartIssues(entity, graph) {
if (entity.type !== 'relation' ||
!entity.isMultipolygon() ||
entity.isDegenerate() ||
// cannot determine issues for incompletely-downloaded relations
!entity.isComplete(graph)) return null;
!entity.isComplete(graph)) return [];
var sequences = osmJoinWays(entity.members, graph);
@@ -285,12 +397,16 @@ export function validationMismatchedGeometry() {
}
var validation = function checkMismatchedGeometry(entity, graph) {
var issues = [
vertexTaggedAsPointIssue(entity, graph),
lineTaggedAsAreaIssue(entity)
];
issues = issues.concat(unclosedMultipolygonPartIssues(entity, graph));
return issues.filter(Boolean);
var vertexPoint = vertexPointIssue(entity, graph);
if (vertexPoint) return [vertexPoint];
var lineAsArea = lineTaggedAsAreaIssue(entity);
if (lineAsArea) return [lineAsArea];
var mismatch = otherMismatchIssue(entity, graph);
if (mismatch) return [mismatch];
return unclosedMultipolygonPartIssues(entity, graph);
};
validation.type = type;
+1 -1
View File
@@ -86,7 +86,7 @@
"cldr-localenames-full": "37.0.0",
"colors": "^1.1.2",
"concat-files": "^0.1.1",
"d3": "~6.2.0",
"d3": "~6.3.0",
"editor-layer-index": "github:osmlab/editor-layer-index#gh-pages",
"eslint": "^7.1.0",
"gaze": "^1.1.3",