modules/validations/mismatched_geometry.js (194 lines of code) (raw):
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 { 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) {
var tagSuggestingArea = tagSuggestingLineIsArea(entity);
if (!tagSuggestingArea) return null;
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),
dynamicFixes: function(context) {
var fixes = [];
var entity = context.entity(this.entityIds[0]);
var connectEndsOnClick = makeConnectEndpointsFixOnClick(entity, context.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 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) {
var extractOnClick = null;
if (!context.hasHiddenConnections(entity.id) &&
!actionExtract(entity.id, context.projection).disabled(context.graph())) {
extractOnClick = function(context) {
var entityId = this.issue.entityIds[0];
var action = actionExtract(entityId, context.projection);
context.perform(
action,
t('operations.extract.annotation.single')
);
// re-enter mode to trigger updates
context.enter(modeSelect(context, [action.getExtractedNodeID()]));
};
}
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],
dynamicFixes: function() {
return [
new validationIssueFix({
icon: 'iD-operation-extract',
title: t('issues.fix.extract_point.title'),
onClick: extractOnClick
})
];
},
hash: typeof extractOnClick, // avoid stale extraction fix
});
}
return null;
}
var validation = function checkMismatchedGeometry(entity, graph) {
var issues = [
vertexTaggedAsPointIssue(entity, graph),
lineTaggedAsAreaIssue(entity)
];
return issues.filter(Boolean);
};
validation.type = type;
return validation;
}