Recategorize tags_suggests_area validation rule into mismatched_geometry rule

Warn about points tagged as vertices and vertices tagged as points (close #6319)
This commit is contained in:
Quincy Morgan
2019-09-26 10:56:30 +02:00
parent d43cf85fe6
commit 525916da74
8 changed files with 239 additions and 141 deletions
+1 -1
View File
@@ -349,7 +349,7 @@ export function coreValidator(context) {
ran.impossible_oneway = true;
}
runValidation('tag_suggests_area');
runValidation('mismatched_geometry');
// run all rules not yet run
Object.keys(_rules).forEach(runValidation);
+2 -2
View File
@@ -8,9 +8,9 @@ export { validationImpossibleOneway } from './impossible_oneway';
export { validationIncompatibleSource } from './incompatible_source';
export { validationFormatting } from './invalid_format';
export { validationMaprules } from './maprules';
export { validationMismatchedGeometry } from './mismatched_geometry';
export { validationMissingRole } from './missing_role';
export { validationMissingTag } from './missing_tag';
export { validationOutdatedTags } from './outdated_tags';
export { validationPrivateData } from './private_data';
export { validationTagSuggestsArea } from './tag_suggests_area';
export { validationUnsquareWay } from './unsquare_way';
export { validationUnsquareWay } from './unsquare_way';
+208
View File
@@ -0,0 +1,208 @@
import { actionAddVertex } from '../actions/add_vertex';
import { actionChangeTags } from '../actions/change_tags';
import { actionMergeNodes } from '../actions/merge_nodes';
import { osmNodeGeometriesForTags } from '../osm/tags';
import { geoHasSelfIntersections, geoSphericalDistance } from '../geo';
import { t } from '../util/locale';
import { utilDisplayLabel, utilTagText } from '../util';
import { validationIssue, validationIssueFix } from '../core/validation';
export function validationMismatchedGeometry(context) {
var type = 'mismatched_geometry';
function tagSuggestingLineIsArea(entity) {
if (entity.type !== 'way' || entity.isClosed()) return null;
var tagSuggestingArea = entity.tagSuggestingArea();
if (!tagSuggestingArea) {
return null;
}
if (context.presets().matchTags(tagSuggestingArea, 'line') ===
context.presets().matchTags(tagSuggestingArea, 'area')) {
// these tags also allow lines and making this an area wouldn't matter
return null;
}
return tagSuggestingArea;
}
function makeConnectEndpointsFixOnClick(way, graph) {
// must have at least three nodes to close this automatically
if (way.nodes.length < 3) return null;
var nodes = graph.childNodes(way), testNodes;
var firstToLastDistanceMeters = geoSphericalDistance(nodes[0].loc, nodes[nodes.length-1].loc);
// if the distance is very small, attempt to merge the endpoints
if (firstToLastDistanceMeters < 0.75) {
testNodes = nodes.slice(); // shallow copy
testNodes.pop();
testNodes.push(testNodes[0]);
// make sure this will not create a self-intersection
if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {
return function(context) {
var way = context.entity(this.issue.entityIds[0]);
context.perform(
actionMergeNodes([way.nodes[0], way.nodes[way.nodes.length-1]], nodes[0].loc),
t('issues.fix.connect_endpoints.annotation')
);
};
}
}
// if the points were not merged, attempt to close the way
testNodes = nodes.slice(); // shallow copy
testNodes.push(testNodes[0]);
// make sure this will not create a self-intersection
if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {
return function(context) {
var wayId = this.issue.entityIds[0];
var way = context.entity(wayId);
var nodeId = way.nodes[0];
var index = way.nodes.length;
context.perform(
actionAddVertex(wayId, nodeId, index),
t('issues.fix.connect_endpoints.annotation')
);
};
}
}
function lineTaggedAsAreaIssue(entity, graph) {
var tagSuggestingArea = tagSuggestingLineIsArea(entity);
if (!tagSuggestingArea) return null;
var fixes = [];
var connectEndsOnClick = makeConnectEndpointsFixOnClick(entity, graph);
fixes.push(new validationIssueFix({
title: t('issues.fix.connect_endpoints.title'),
onClick: connectEndsOnClick
}));
fixes.push(new validationIssueFix({
icon: 'iD-operation-delete',
title: t('issues.fix.remove_tag.title'),
onClick: function(context) {
var entityId = this.issue.entityIds[0];
var entity = context.entity(entityId);
var tags = Object.assign({}, entity.tags); // shallow copy
for (var key in tagSuggestingArea) {
delete tags[key];
}
context.perform(
actionChangeTags(entityId, tags),
t('issues.fix.remove_tag.annotation')
);
}
}));
return new validationIssue({
type: type,
subtype: 'area_as_line',
severity: 'warning',
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t('issues.tag_suggests_area.message', {
feature: utilDisplayLabel(entity, context),
tag: utilTagText({ tags: tagSuggestingArea })
}) : '';
},
reference: showReference,
entityIds: [entity.id],
hash: JSON.stringify(tagSuggestingArea) +
// avoid stale "connect endpoints" fix
(typeof connectEndsOnClick),
fixes: fixes
});
function showReference(selection) {
selection.selectAll('.issue-reference')
.data([0])
.enter()
.append('div')
.attr('class', 'issue-reference')
.text(t('issues.tag_suggests_area.reference'));
}
}
function vertexTaggedAsPointIssue(entity, graph) {
// we only care about nodes
if (entity.type !== 'node') return null;
// ignore tagless points
if (Object.keys(entity.tags).length === 0) return null;
// address lines are special so just ignore them
if (entity.isOnAddressLine(graph)) return null;
var geometry = entity.geometry(graph);
var allowedGeometries = osmNodeGeometriesForTags(entity.tags);
if (geometry === 'point' && !allowedGeometries.point && allowedGeometries.vertex) {
return new validationIssue({
type: type,
subtype: 'vertex_as_point',
severity: 'warning',
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t('issues.vertex_as_point.message', {
feature: utilDisplayLabel(entity, context)
}) : '';
},
reference: function showReference(selection) {
selection.selectAll('.issue-reference')
.data([0])
.enter()
.append('div')
.attr('class', 'issue-reference')
.text(t('issues.vertex_as_point.reference'));
},
entityIds: [entity.id]
});
} else if (geometry === 'vertex' && !allowedGeometries.vertex && allowedGeometries.point) {
return new validationIssue({
type: type,
subtype: 'point_as_vertex',
severity: 'warning',
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t('issues.point_as_vertex.message', {
feature: utilDisplayLabel(entity, context)
}) : '';
},
reference: function showReference(selection) {
selection.selectAll('.issue-reference')
.data([0])
.enter()
.append('div')
.attr('class', 'issue-reference')
.text(t('issues.point_as_vertex.reference'));
},
entityIds: [entity.id]
});
}
return null;
}
var validation = function checkMismatchedGeometry(entity, graph) {
var issues = [
vertexTaggedAsPointIssue(entity, graph),
lineTaggedAsAreaIssue(entity, graph)
];
return issues.filter(Boolean);
};
validation.type = type;
return validation;
}
-129
View File
@@ -1,129 +0,0 @@
import { actionAddVertex } from '../actions/add_vertex';
import { actionChangeTags } from '../actions/change_tags';
import { actionMergeNodes } from '../actions/merge_nodes';
import { geoHasSelfIntersections, geoSphericalDistance } from '../geo';
import { t } from '../util/locale';
import { utilDisplayLabel, utilTagText } from '../util';
import { validationIssue, validationIssueFix } from '../core/validation';
export function validationTagSuggestsArea(context) {
var type = 'tag_suggests_area';
var validation = function checkTagSuggestsArea(entity, graph) {
if (entity.type !== 'way' || entity.isClosed()) return [];
var tagSuggestingArea = entity.tagSuggestingArea();
if (!tagSuggestingArea) {
return [];
}
if (context.presets().matchTags(tagSuggestingArea, 'line') ===
context.presets().matchTags(tagSuggestingArea, 'area')) {
// these tags also allow lines and making this an area wouldn't matter
return [];
}
var tagText = utilTagText({ tags: tagSuggestingArea });
var fixes = [];
var connectEndpointsOnClick;
// must have at least three nodes to close this automatically
if (entity.nodes.length >= 3) {
var nodes = graph.childNodes(entity), testNodes;
var firstToLastDistanceMeters = geoSphericalDistance(nodes[0].loc, nodes[nodes.length-1].loc);
// if the distance is very small, attempt to merge the endpoints
if (firstToLastDistanceMeters < 0.75) {
testNodes = nodes.slice(); // shallow copy
testNodes.pop();
testNodes.push(testNodes[0]);
// make sure this will not create a self-intersection
if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {
connectEndpointsOnClick = function(context) {
var way = context.entity(this.issue.entityIds[0]);
context.perform(
actionMergeNodes([way.nodes[0], way.nodes[way.nodes.length-1]], nodes[0].loc),
t('issues.fix.connect_endpoints.annotation')
);
};
}
}
if (!connectEndpointsOnClick) {
// if the points were not merged, attempt to close the way
testNodes = nodes.slice(); // shallow copy
testNodes.push(testNodes[0]);
// make sure this will not create a self-intersection
if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {
connectEndpointsOnClick = function(context) {
var wayId = this.issue.entityIds[0];
var way = context.entity(wayId);
var nodeId = way.nodes[0];
var index = way.nodes.length;
context.perform(
actionAddVertex(wayId, nodeId, index),
t('issues.fix.connect_endpoints.annotation')
);
};
}
}
}
fixes.push(new validationIssueFix({
title: t('issues.fix.connect_endpoints.title'),
onClick: connectEndpointsOnClick
}));
fixes.push(new validationIssueFix({
icon: 'iD-operation-delete',
title: t('issues.fix.remove_tag.title'),
onClick: function(context) {
var entityId = this.issue.entityIds[0];
var entity = context.entity(entityId);
var tags = Object.assign({}, entity.tags); // shallow copy
for (var key in tagSuggestingArea) {
delete tags[key];
}
context.perform(
actionChangeTags(entityId, tags),
t('issues.fix.remove_tag.annotation')
);
}
}));
return [new validationIssue({
type: type,
severity: 'warning',
message: function(context) {
var entity = context.hasEntity(this.entityIds[0]);
return entity ? t('issues.tag_suggests_area.message', {
feature: utilDisplayLabel(entity, context),
tag: tagText
}) : '';
},
reference: showReference,
entityIds: [entity.id],
hash: JSON.stringify(tagSuggestingArea) +
// avoid stale "connect endpoints" fix
(typeof connectEndpointsOnClick),
fixes: fixes
})];
function showReference(selection) {
selection.selectAll('.issue-reference')
.data([0])
.enter()
.append('div')
.attr('class', 'issue-reference')
.text(t('issues.tag_suggests_area.reference'));
}
};
validation.type = type;
return validation;
}