mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-12 16:52:50 +00:00
Add validation warning for unsquare buildings
This commit is contained in:
@@ -1398,6 +1398,12 @@ en:
|
||||
message: "{feature} has no classification"
|
||||
tip: "Find roads that are missing a proper road classification"
|
||||
reference: "Roads without a specific type may not appear in maps or routing."
|
||||
unsquare_way:
|
||||
title: Unsquare Buildings
|
||||
message: "{feature} isn't quite square"
|
||||
tip: "Find buildings with nearly square corners"
|
||||
buildings:
|
||||
reference: "Buildings that are almost square should be squared."
|
||||
fix:
|
||||
connect_almost_junction:
|
||||
annotation: Connected very close features.
|
||||
@@ -1441,6 +1447,8 @@ en:
|
||||
title: Set as inner
|
||||
set_as_outer:
|
||||
title: Set as outer
|
||||
square_feature:
|
||||
title: Square this feature
|
||||
tag_as_disconnected:
|
||||
title: Tag as disconnected
|
||||
annotation: Tagged very close features as disconnected.
|
||||
|
||||
11
dist/locales/en.json
vendored
11
dist/locales/en.json
vendored
@@ -1729,6 +1729,14 @@
|
||||
"tip": "Find roads that are missing a proper road classification",
|
||||
"reference": "Roads without a specific type may not appear in maps or routing."
|
||||
},
|
||||
"unsquare_way": {
|
||||
"title": "Unsquare Buildings",
|
||||
"message": "{feature} isn't quite square",
|
||||
"tip": "Find buildings with nearly square corners",
|
||||
"buildings": {
|
||||
"reference": "Buildings that are almost square should be squared."
|
||||
}
|
||||
},
|
||||
"fix": {
|
||||
"connect_almost_junction": {
|
||||
"annotation": "Connected very close features."
|
||||
@@ -1791,6 +1799,9 @@
|
||||
"set_as_outer": {
|
||||
"title": "Set as outer"
|
||||
},
|
||||
"square_feature": {
|
||||
"title": "Square this feature"
|
||||
},
|
||||
"tag_as_disconnected": {
|
||||
"title": "Tag as disconnected",
|
||||
"annotation": "Tagged very close features as disconnected."
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { actionDeleteNode } from './delete_node';
|
||||
import {
|
||||
geoVecAdd, geoVecEqual, geoVecInterp, geoVecLength, geoVecNormalize,
|
||||
geoVecNormalizedDot, geoVecProject, geoVecScale, geoVecSubtract
|
||||
geoVecProject, geoVecScale, geoVecSubtract,
|
||||
geoOrthoNormalizedDotProduct, geoOrthoCalcScore, geoOrthoCanOrthogonalize
|
||||
} from '../geo';
|
||||
|
||||
|
||||
@@ -72,7 +73,7 @@ export function actionOrthogonalize(wayID, projection, vertexID) {
|
||||
if (isClosed || (i > 0 && i < points.length - 1)) {
|
||||
var a = points[(i - 1 + points.length) % points.length];
|
||||
var b = points[(i + 1) % points.length];
|
||||
dotp = Math.abs(normalizedDotProduct(a.coord, b.coord, point.coord));
|
||||
dotp = Math.abs(geoOrthoNormalizedDotProduct(a.coord, b.coord, point.coord));
|
||||
}
|
||||
|
||||
if (dotp > upperThreshold) {
|
||||
@@ -93,7 +94,7 @@ export function actionOrthogonalize(wayID, projection, vertexID) {
|
||||
for (j = 0; j < motions.length; j++) {
|
||||
simplified[j].coord = geoVecAdd(simplified[j].coord, motions[j]);
|
||||
}
|
||||
var newScore = calcScore(simplified, isClosed);
|
||||
var newScore = geoOrthoCalcScore(simplified, isClosed, epsilon, threshold);
|
||||
if (newScore < score) {
|
||||
bestPoints = clonePoints(simplified);
|
||||
score = newScore;
|
||||
@@ -183,67 +184,6 @@ export function actionOrthogonalize(wayID, projection, vertexID) {
|
||||
};
|
||||
|
||||
|
||||
function normalizedDotProduct(a, b, origin) {
|
||||
if (geoVecEqual(origin, a) || geoVecEqual(origin, b)) {
|
||||
return 1; // coincident points, treat as straight and try to remove
|
||||
}
|
||||
return geoVecNormalizedDot(a, b, origin);
|
||||
}
|
||||
|
||||
|
||||
function filterDotProduct(dotp) {
|
||||
var val = Math.abs(dotp);
|
||||
if (val < epsilon) {
|
||||
return 0; // already orthogonal
|
||||
} else if (val < lowerThreshold || val > upperThreshold) {
|
||||
return dotp; // can be adjusted
|
||||
} else {
|
||||
return null; // ignore vertex
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function calcScore(points, isClosed) {
|
||||
var score = 0;
|
||||
var first = isClosed ? 0 : 1;
|
||||
var last = isClosed ? points.length : points.length - 1;
|
||||
var coords = points.map(function(p) { return p.coord; });
|
||||
|
||||
for (var i = first; i < last; i++) {
|
||||
var a = coords[(i - 1 + coords.length) % coords.length];
|
||||
var origin = coords[i];
|
||||
var b = coords[(i + 1) % coords.length];
|
||||
|
||||
var dotp = filterDotProduct(normalizedDotProduct(a, b, origin));
|
||||
if (dotp === null) continue; // ignore vertex
|
||||
score = score + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1)));
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
|
||||
// similar to calcScore, but returns quickly if there is something to do
|
||||
function canOrthogonalize(coords, isClosed) {
|
||||
var score = null;
|
||||
var first = isClosed ? 0 : 1;
|
||||
var last = isClosed ? coords.length : coords.length - 1;
|
||||
|
||||
for (var i = first; i < last; i++) {
|
||||
var a = coords[(i - 1 + coords.length) % coords.length];
|
||||
var origin = coords[i];
|
||||
var b = coords[(i + 1) % coords.length];
|
||||
|
||||
var dotp = filterDotProduct(normalizedDotProduct(a, b, origin));
|
||||
if (dotp === null) continue; // ignore vertex
|
||||
if (Math.abs(dotp) > 0) return 1; // something to do
|
||||
score = 0; // already square
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
|
||||
// if we are only orthogonalizing one vertex,
|
||||
// get that vertex and the previous and next
|
||||
function nodeSubset(nodes, vertexID, isClosed) {
|
||||
@@ -273,13 +213,15 @@ export function actionOrthogonalize(wayID, projection, vertexID) {
|
||||
var nodes = graph.childNodes(way).slice(); // shallow copy
|
||||
if (isClosed) nodes.pop();
|
||||
|
||||
var allowStraightAngles = false;
|
||||
if (vertexID !== undefined) {
|
||||
allowStraightAngles = true;
|
||||
nodes = nodeSubset(nodes, vertexID, isClosed);
|
||||
if (nodes.length !== 3) return 'end_vertex';
|
||||
}
|
||||
|
||||
var coords = nodes.map(function(n) { return projection(n.loc); });
|
||||
var score = canOrthogonalize(coords, isClosed);
|
||||
var score = geoOrthoCanOrthogonalize(coords, isClosed, epsilon, threshold, allowStraightAngles);
|
||||
|
||||
if (score === null) {
|
||||
return 'not_squarish';
|
||||
|
||||
@@ -155,6 +155,8 @@ export function coreHistory(context) {
|
||||
})
|
||||
.on('end interrupt', function() {
|
||||
_overwrite(origArguments, 1);
|
||||
// run the completion handler, if any
|
||||
if (action0.onCompletion) action0.onCompletion();
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
@@ -41,3 +41,7 @@ export { geoVecNormalizedDot } from './vector.js';
|
||||
export { geoVecProject } from './vector.js';
|
||||
export { geoVecSubtract } from './vector.js';
|
||||
export { geoVecScale } from './vector.js';
|
||||
|
||||
export { geoOrthoNormalizedDotProduct } from './ortho.js';
|
||||
export { geoOrthoCalcScore } from './ortho.js';
|
||||
export { geoOrthoCanOrthogonalize } from './ortho.js';
|
||||
|
||||
72
modules/geo/ortho.js
Normal file
72
modules/geo/ortho.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
geoVecEqual, geoVecNormalizedDot
|
||||
} from './vector';
|
||||
|
||||
|
||||
export function geoOrthoNormalizedDotProduct(a, b, origin) {
|
||||
if (geoVecEqual(origin, a) || geoVecEqual(origin, b)) {
|
||||
return 1; // coincident points, treat as straight and try to remove
|
||||
}
|
||||
return geoVecNormalizedDot(a, b, origin);
|
||||
}
|
||||
|
||||
|
||||
function geoOrthoFilterDotProduct(dotp, epsilon, lowerThreshold, upperThreshold, allowStraightAngles) {
|
||||
var val = Math.abs(dotp);
|
||||
if (val < epsilon) {
|
||||
return 0; // already orthogonal
|
||||
} else if (allowStraightAngles && Math.abs(val-1) < epsilon) {
|
||||
return 0; // straight angle, which is okay in this case
|
||||
} else if (val < lowerThreshold || val > upperThreshold) {
|
||||
return dotp; // can be adjusted
|
||||
} else {
|
||||
return null; // ignore vertex
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function geoOrthoCalcScore(points, isClosed, epsilon, threshold) {
|
||||
var score = 0;
|
||||
var first = isClosed ? 0 : 1;
|
||||
var last = isClosed ? points.length : points.length - 1;
|
||||
var coords = points.map(function(p) { return p.coord; });
|
||||
|
||||
var lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180);
|
||||
var upperThreshold = Math.cos(threshold * Math.PI / 180);
|
||||
|
||||
for (var i = first; i < last; i++) {
|
||||
var a = coords[(i - 1 + coords.length) % coords.length];
|
||||
var origin = coords[i];
|
||||
var b = coords[(i + 1) % coords.length];
|
||||
|
||||
var dotp = geoOrthoFilterDotProduct(geoOrthoNormalizedDotProduct(a, b, origin), epsilon, lowerThreshold, upperThreshold);
|
||||
if (dotp === null) continue; // ignore vertex
|
||||
score = score + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1)));
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
|
||||
// similar to geoOrthoCalcScore, but returns quickly if there is something to do
|
||||
export function geoOrthoCanOrthogonalize(coords, isClosed, epsilon, threshold, allowStraightAngles) {
|
||||
var score = null;
|
||||
var first = isClosed ? 0 : 1;
|
||||
var last = isClosed ? coords.length : coords.length - 1;
|
||||
|
||||
var lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180);
|
||||
var upperThreshold = Math.cos(threshold * Math.PI / 180);
|
||||
|
||||
for (var i = first; i < last; i++) {
|
||||
var a = coords[(i - 1 + coords.length) % coords.length];
|
||||
var origin = coords[i];
|
||||
var b = coords[(i + 1) % coords.length];
|
||||
|
||||
var dotp = geoOrthoFilterDotProduct(geoOrthoNormalizedDotProduct(a, b, origin), epsilon, lowerThreshold, upperThreshold, allowStraightAngles);
|
||||
if (dotp === null) continue; // ignore vertex
|
||||
if (Math.abs(dotp) > 0) return 1; // something to do
|
||||
score = 0; // already square
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
@@ -10,6 +10,10 @@ export function operationCircularize(selectedIDs, context) {
|
||||
var extent = entity.extent(context.graph());
|
||||
var geometry = context.geometry(entityID);
|
||||
var action = actionCircularize(entityID, context.projection);
|
||||
action.onCompletion = function() {
|
||||
// revalidate in case this is a building that's no longer squarable
|
||||
context.validator().validate();
|
||||
};
|
||||
var nodes = utilGetAllNodes(selectedIDs, context.graph());
|
||||
var coords = nodes.map(function(n) { return n.loc; });
|
||||
var _disabled;
|
||||
|
||||
@@ -10,6 +10,13 @@ export function operationOrthogonalize(selectedIDs, context) {
|
||||
var _geometry;
|
||||
var _disabled;
|
||||
var action = chooseAction();
|
||||
if (action) {
|
||||
action.onCompletion = function() {
|
||||
// revalidate in case a building was squared
|
||||
context.validator().validate();
|
||||
};
|
||||
}
|
||||
|
||||
var nodes = utilGetAllNodes(selectedIDs, context.graph());
|
||||
var coords = nodes.map(function(n) { return n.loc; });
|
||||
|
||||
|
||||
@@ -11,3 +11,4 @@ export { validationOutdatedTags } from './outdated_tags';
|
||||
export { validationPrivateData } from './private_data';
|
||||
export { validationTagSuggestsArea } from './tag_suggests_area';
|
||||
export { validationUnknownRoad } from './unknown_road';
|
||||
export { validationUnsquareWay } from './unsquare_way';
|
||||
|
||||
64
modules/validations/unsquare_way.js
Normal file
64
modules/validations/unsquare_way.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { t } from '../util/locale';
|
||||
import { operationOrthogonalize } from '../operations';
|
||||
import { geoOrthoCanOrthogonalize } from '../geo';
|
||||
import { utilDisplayLabel } from '../util';
|
||||
import { validationIssue, validationIssueFix } from '../core/validator';
|
||||
|
||||
|
||||
export function validationUnsquareWay() {
|
||||
var type = 'unsquare_way';
|
||||
|
||||
var validation = function checkMissingRole(entity, context) {
|
||||
|
||||
if (entity.type !== 'way' || entity.geometry(context.graph()) !== 'area') return [];
|
||||
|
||||
if (!entity.tags.building || entity.tags.building === 'no') return [];
|
||||
|
||||
var isClosed = entity.isClosed();
|
||||
var nodes = context.childNodes(entity).slice(); // shallow copy
|
||||
if (isClosed) nodes.pop();
|
||||
|
||||
var locs = nodes.map(function(node) {
|
||||
return context.projection(node.loc);
|
||||
});
|
||||
|
||||
// use loose constraints compared to actionOrthogonalize
|
||||
if (!geoOrthoCanOrthogonalize(locs, isClosed, 0.015, 7, true)) return [];
|
||||
|
||||
return new validationIssue({
|
||||
type: type,
|
||||
severity: 'warning',
|
||||
message: t('issues.unsquare_way.message', {
|
||||
feature: utilDisplayLabel(entity, context)
|
||||
}),
|
||||
reference: showReference,
|
||||
entities: [entity],
|
||||
fixes: [
|
||||
new validationIssueFix({
|
||||
icon: 'iD-operation-orthogonalize',
|
||||
title: t('issues.fix.square_feature.title'),
|
||||
onClick: function() {
|
||||
var id = this.issue.entities[0].id;
|
||||
var operation = operationOrthogonalize([id], context);
|
||||
if (!operation.disabled()) {
|
||||
operation();
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
function showReference(selection) {
|
||||
selection.selectAll('.issue-reference')
|
||||
.data([0])
|
||||
.enter()
|
||||
.append('div')
|
||||
.attr('class', 'issue-reference')
|
||||
.text(t('issues.unsquare_way.buildings.reference'));
|
||||
}
|
||||
};
|
||||
|
||||
validation.type = type;
|
||||
|
||||
return validation;
|
||||
}
|
||||
Reference in New Issue
Block a user