mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-15 21:48:20 +02:00
Merge branch 'develop' into tag_as_not
This commit is contained in:
+22
-4
@@ -3,7 +3,7 @@ import { actionDeleteWay } from './delete_way';
|
||||
import { osmIsInterestingTag } from '../osm/tags';
|
||||
import { osmJoinWays } from '../osm/multipolygon';
|
||||
import { geoPathIntersections } from '../geo';
|
||||
import { utilArrayGroupBy, utilArrayIntersection } from '../util';
|
||||
import { utilArrayGroupBy, utilArrayIdentical, utilArrayIntersection } from '../util';
|
||||
|
||||
|
||||
// Join ways at the end node they share.
|
||||
@@ -129,9 +129,27 @@ export function actionJoin(ids) {
|
||||
return 'not_adjacent';
|
||||
}
|
||||
|
||||
var i;
|
||||
|
||||
// All joined ways must belong to the same set of (non-restriction) relations.
|
||||
// Restriction relations have different logic, below, which allows some cases
|
||||
// this prohibits, and prohibits some cases this allows.
|
||||
var sortedParentRelations = function (id) {
|
||||
return graph.parentRelations(graph.entity(id))
|
||||
.filter((rel) => !rel.isRestriction() && !rel.isConnectivity())
|
||||
.sort((a, b) => a.id - b.id);
|
||||
};
|
||||
var relsA = sortedParentRelations(ids[0]);
|
||||
for (i = 1; i < ids.length; i++) {
|
||||
var relsB = sortedParentRelations(ids[i]);
|
||||
if (!utilArrayIdentical(relsA, relsB)) {
|
||||
return 'conflicting_relations';
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through all combinations of path-pairs
|
||||
// to check potential intersections between all pairs
|
||||
for (var i = 0; i < ids.length - 1; i++) {
|
||||
for (i = 0; i < ids.length - 1; i++) {
|
||||
for (var j = i + 1; j < ids.length; j++) {
|
||||
var path1 = graph.childNodes(graph.entity(ids[i]))
|
||||
.map(function(e) { return e.loc; });
|
||||
@@ -159,7 +177,7 @@ export function actionJoin(ids) {
|
||||
joined[0].forEach(function(way) {
|
||||
var parents = graph.parentRelations(way);
|
||||
parents.forEach(function(parent) {
|
||||
if (parent.isRestriction() && parent.members.some(function(m) { return nodeIds.indexOf(m.id) >= 0; })) {
|
||||
if ((parent.isRestriction() || parent.isConnectivity()) && parent.members.some(function(m) { return nodeIds.indexOf(m.id) >= 0; })) {
|
||||
relation = parent;
|
||||
}
|
||||
});
|
||||
@@ -174,7 +192,7 @@ export function actionJoin(ids) {
|
||||
});
|
||||
|
||||
if (relation) {
|
||||
return 'restriction';
|
||||
return relation.isRestriction() ? 'restriction' : 'connectivity';
|
||||
}
|
||||
|
||||
if (conflicting) {
|
||||
|
||||
@@ -26,7 +26,7 @@ export function coreContext() {
|
||||
let context = utilRebind({}, dispatch, 'on');
|
||||
let _deferred = new Set();
|
||||
|
||||
context.version = '2.20.1-dev';
|
||||
context.version = '2.20.2-dev';
|
||||
context.privacyVersion = '20201202';
|
||||
|
||||
// iD will alter the hash so cache the parameters intended to setup the session
|
||||
|
||||
@@ -14,6 +14,7 @@ export function validationIssue(attrs) {
|
||||
this.hash = attrs.hash; // optional - string to further differentiate the issue
|
||||
|
||||
this.id = generateID.apply(this); // generated - see below
|
||||
this.key = generateKey.apply(this); // generated - see below (call after generating this.id)
|
||||
this.autoFix = null; // generated - if autofix exists, will be set below
|
||||
|
||||
// A unique, deterministic string hash.
|
||||
@@ -39,6 +40,13 @@ export function validationIssue(attrs) {
|
||||
return parts.join(':');
|
||||
}
|
||||
|
||||
// An identifier suitable for use as the second argument to d3.selection#data().
|
||||
// (i.e. this should change whenever the data needs to be refreshed)
|
||||
function generateKey() {
|
||||
return this.id + ':' + Date.now().toString(); // include time of creation
|
||||
}
|
||||
|
||||
|
||||
this.extent = function(resolver) {
|
||||
if (this.loc) {
|
||||
return geoExtent(this.loc);
|
||||
|
||||
+256
-225
@@ -4,7 +4,7 @@ import { prefs } from './preferences';
|
||||
import { coreDifference } from './difference';
|
||||
import { geoExtent } from '../geo/extent';
|
||||
import { modeSelect } from '../modes/select';
|
||||
import { utilArrayChunk, utilArrayGroupBy, utilRebind } from '../util';
|
||||
import { utilArrayChunk, utilArrayGroupBy, utilEntityAndDeepMemberIDs, utilRebind } from '../util';
|
||||
import * as Validations from '../validations/index';
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ export function coreValidator(context) {
|
||||
|
||||
let _ignoredIssueIDs = new Set();
|
||||
let _resolvedIssueIDs = new Set();
|
||||
let _baseCache = validationCache(); // issues before any user edits
|
||||
let _headCache = validationCache(); // issues after all user edits
|
||||
let _headGraph = null;
|
||||
let _baseCache = validationCache('base'); // issues before any user edits
|
||||
let _headCache = validationCache('head'); // issues after all user edits
|
||||
let _completeDiff = {}; // complete diff base -> head of what the user changed
|
||||
let _headIsCurrent = false;
|
||||
|
||||
let _deferredRIC = new Set(); // Set( RequestIdleCallback handles )
|
||||
@@ -28,7 +28,10 @@ export function coreValidator(context) {
|
||||
|
||||
const RETRY = 5000; // wait 5sec before revalidating provisional entities
|
||||
|
||||
|
||||
// Allow validation severity to be overridden by url queryparams...
|
||||
// See: https://github.com/openstreetmap/iD/pull/8243
|
||||
//
|
||||
// Each param should contain a urlencoded comma separated list of
|
||||
// `type/subtype` rules. `*` may be used as a wildcard..
|
||||
// Examples:
|
||||
@@ -37,31 +40,38 @@ export function coreValidator(context) {
|
||||
// `validationError=crossing_ways/bridge*`
|
||||
// `validationError=crossing_ways/bridge*,crossing_ways/tunnel*`
|
||||
|
||||
var _errorOverrides = parseHashParam(context.initialHashParams.validationError);
|
||||
var _warningOverrides = parseHashParam(context.initialHashParams.validationWarning);
|
||||
var _disableOverrides = parseHashParam(context.initialHashParams.validationDisable);
|
||||
const _errorOverrides = parseHashParam(context.initialHashParams.validationError);
|
||||
const _warningOverrides = parseHashParam(context.initialHashParams.validationWarning);
|
||||
const _disableOverrides = parseHashParam(context.initialHashParams.validationDisable);
|
||||
|
||||
// `parseHashParam()` (private)
|
||||
// Checks hash parameters for severity overrides
|
||||
// Arguments
|
||||
// `param` - a url hash parameter (`validationError`, `validationWarning`, or `validationDisable`)
|
||||
// Returns
|
||||
// Array of Objects like { type: RegExp, subtype: RegExp }
|
||||
//
|
||||
function parseHashParam(param) {
|
||||
var result = [];
|
||||
var rules = (param || '').split(',');
|
||||
rules.forEach(function (rule) {
|
||||
let result = [];
|
||||
let rules = (param || '').split(',');
|
||||
rules.forEach(rule => {
|
||||
rule = rule.trim();
|
||||
var parts = rule.split('/', 2); // "type/subtype"
|
||||
var type = parts[0];
|
||||
var subtype = parts[1] || '*';
|
||||
const parts = rule.split('/', 2); // "type/subtype"
|
||||
const type = parts[0];
|
||||
const subtype = parts[1] || '*';
|
||||
if (!type || !subtype) return;
|
||||
|
||||
result.push({ type: makeRegExp(type), subtype: makeRegExp(subtype) });
|
||||
});
|
||||
return result;
|
||||
|
||||
function makeRegExp(str) {
|
||||
const escaped = str
|
||||
.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&') // escape all reserved chars except for the '*'
|
||||
.replace(/\*/g, '.*'); // treat a '*' like '.*'
|
||||
return new RegExp('^' + escaped + '$');
|
||||
}
|
||||
}
|
||||
|
||||
function makeRegExp(str) {
|
||||
var escaped = str
|
||||
.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&') // escape all reserved chars except for the '*'
|
||||
.replace(/\*/g, '.*'); // treat a '*' like '.*'
|
||||
return new RegExp('^' + escaped + '$');
|
||||
}
|
||||
|
||||
// `init()`
|
||||
// Initialize the validator, called once on iD startup
|
||||
@@ -103,9 +113,9 @@ export function coreValidator(context) {
|
||||
// clear caches
|
||||
if (resetIgnored) _ignoredIssueIDs.clear();
|
||||
_resolvedIssueIDs.clear();
|
||||
_baseCache = validationCache();
|
||||
_headCache = validationCache();
|
||||
_headGraph = null;
|
||||
_baseCache = validationCache('base');
|
||||
_headCache = validationCache('head');
|
||||
_completeDiff = {};
|
||||
_headIsCurrent = false;
|
||||
}
|
||||
|
||||
@@ -133,24 +143,24 @@ export function coreValidator(context) {
|
||||
// It reruns just the "unsquare_way" validation on all buildings.
|
||||
//
|
||||
validator.revalidateUnsquare = () => {
|
||||
revalidateUnsquare(_headCache, _headGraph);
|
||||
revalidateUnsquare(_baseCache, context.history().base());
|
||||
revalidateUnsquare(_headCache);
|
||||
revalidateUnsquare(_baseCache);
|
||||
dispatch.call('validated');
|
||||
};
|
||||
|
||||
function revalidateUnsquare(cache, graph) {
|
||||
function revalidateUnsquare(cache) {
|
||||
const checkUnsquareWay = _rules.unsquare_way;
|
||||
if (!graph || typeof checkUnsquareWay !== 'function') return;
|
||||
if (!cache.graph || typeof checkUnsquareWay !== 'function') return;
|
||||
|
||||
// uncache existing
|
||||
cache.uncacheIssuesOfType('unsquare_way');
|
||||
|
||||
const buildings = context.history().tree().intersects(geoExtent([-180,-90],[180, 90]), graph) // everywhere
|
||||
const buildings = context.history().tree().intersects(geoExtent([-180,-90],[180, 90]), cache.graph) // everywhere
|
||||
.filter(entity => (entity.type === 'way' && entity.tags.building && entity.tags.building !== 'no'));
|
||||
|
||||
// rerun for all buildings
|
||||
buildings.forEach(entity => {
|
||||
const detected = checkUnsquareWay(entity, graph);
|
||||
const detected = checkUnsquareWay(entity, cache.graph);
|
||||
if (!detected.length) return;
|
||||
cache.cacheIssues(detected);
|
||||
});
|
||||
@@ -176,33 +186,40 @@ export function coreValidator(context) {
|
||||
validator.getIssues = (options) => {
|
||||
const opts = Object.assign({ what: 'all', where: 'all', includeIgnored: false, includeDisabledRules: false }, options);
|
||||
const view = context.map().extent();
|
||||
let issues = [];
|
||||
let seen = new Set();
|
||||
let results = [];
|
||||
|
||||
// collect head issues - caused by user edits
|
||||
let cache = _headCache;
|
||||
if (_headGraph) {
|
||||
Object.values(cache.issuesByIssueID).forEach(issue => {
|
||||
if (!filter(issue, _headGraph, cache)) return;
|
||||
// collect head issues - present in the user edits
|
||||
if (_headCache.graph && _headCache.graph !== _baseCache.graph) {
|
||||
Object.values(_headCache.issuesByIssueID).forEach(issue => {
|
||||
// In the head cache, only count features that the user is responsible for - #8632
|
||||
// For example, a user can undo some work and an issue will still present in the
|
||||
// head graph, but we don't want to credit the user for causing that issue.
|
||||
const userModified = (issue.entityIds || []).some(id => _completeDiff.hasOwnProperty(id));
|
||||
if (opts.what === 'edited' && !userModified) return; // present in head but user didn't touch it
|
||||
|
||||
if (!filter(issue)) return;
|
||||
seen.add(issue.id);
|
||||
issues.push(issue);
|
||||
results.push(issue);
|
||||
});
|
||||
}
|
||||
|
||||
// collect base issues - not caused by user edits
|
||||
// collect base issues - present before user edits
|
||||
if (opts.what === 'all') {
|
||||
cache = _baseCache;
|
||||
Object.values(cache.issuesByIssueID).forEach(issue => {
|
||||
if (!filter(issue, context.history().base(), cache)) return;
|
||||
Object.values(_baseCache.issuesByIssueID).forEach(issue => {
|
||||
if (!filter(issue)) return;
|
||||
seen.add(issue.id);
|
||||
issues.push(issue);
|
||||
results.push(issue);
|
||||
});
|
||||
}
|
||||
|
||||
return issues;
|
||||
return results;
|
||||
|
||||
|
||||
function filter(issue, resolver, cache) {
|
||||
// Filter the issue set to include only what the calling code wants to see.
|
||||
// Note that we use `context.graph()`/`context.hasEntity()` here, not `cache.graph`,
|
||||
// because that is the graph that the calling code will be using.
|
||||
function filter(issue) {
|
||||
if (!issue) return false;
|
||||
if (seen.has(issue.id)) return false;
|
||||
if (_resolvedIssueIDs.has(issue.id)) return false;
|
||||
@@ -212,19 +229,12 @@ export function coreValidator(context) {
|
||||
if (opts.includeIgnored === 'only' && !_ignoredIssueIDs.has(issue.id)) return false;
|
||||
if (!opts.includeIgnored && _ignoredIssueIDs.has(issue.id)) return false;
|
||||
|
||||
// Sanity check: This issue may be for an entity that not longer exists.
|
||||
// If we detect this, uncache and return false so it is not included..
|
||||
const entityIDs = issue.entityIds || [];
|
||||
for (let i = 0; i < entityIDs.length; i++) {
|
||||
const entityID = entityIDs[i];
|
||||
if (!resolver.hasEntity(entityID)) {
|
||||
cache.uncacheEntityID(entityID);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// This issue may involve an entity that doesn't exist in context.graph()
|
||||
// This can happen because validation is async and rendering the issue lists is async.
|
||||
if ((issue.entityIds || []).some(id => !context.hasEntity(id))) return false;
|
||||
|
||||
if (opts.where === 'visible') {
|
||||
const extent = issue.extent(resolver);
|
||||
const extent = issue.extent(context.graph());
|
||||
if (!view.intersects(extent)) return false;
|
||||
}
|
||||
|
||||
@@ -235,22 +245,17 @@ export function coreValidator(context) {
|
||||
|
||||
// `getResolvedIssues()`
|
||||
// Gets the issues that have been fixed by the user.
|
||||
// Resolved issues are tracked in the `_resolvedIssueIDs` Set
|
||||
//
|
||||
// Resolved issues are tracked in the `_resolvedIssueIDs` Set,
|
||||
// and they should all be issues that exist in the _baseCache.
|
||||
//
|
||||
// Returns
|
||||
// An Array containing the issues
|
||||
//
|
||||
validator.getResolvedIssues = () => {
|
||||
let collected = new Set();
|
||||
|
||||
Object.values(_baseCache.issuesByIssueID).forEach(issue => {
|
||||
if (_resolvedIssueIDs.has(issue.id)) collected.add(issue);
|
||||
});
|
||||
Object.values(_headCache.issuesByIssueID).forEach(issue => {
|
||||
if (_resolvedIssueIDs.has(issue.id)) collected.add(issue);
|
||||
});
|
||||
|
||||
return Array.from(collected);
|
||||
return Array.from(_resolvedIssueIDs)
|
||||
.map(issueID => _baseCache.issuesByIssueID[issueID])
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
|
||||
@@ -262,17 +267,49 @@ export function coreValidator(context) {
|
||||
// `issue` - the issue to focus on
|
||||
//
|
||||
validator.focusIssue = (issue) => {
|
||||
const extent = issue.extent(context.graph());
|
||||
if (!extent) return;
|
||||
// Note that we use `context.graph()`/`context.hasEntity()` here, not `cache.graph`,
|
||||
// because that is the graph that the calling code will be using.
|
||||
const graph = context.graph();
|
||||
let selectID;
|
||||
let focusCenter;
|
||||
|
||||
const setZoom = Math.max(context.map().zoom(), 19);
|
||||
context.map().unobscuredCenterZoomEase(extent.center(), setZoom);
|
||||
// Try to focus the map at the center of the issue..
|
||||
const issueExtent = issue.extent(graph);
|
||||
if (issueExtent) {
|
||||
focusCenter = issueExtent.center();
|
||||
}
|
||||
|
||||
// select the first entity
|
||||
// Try to select the first entity in the issue..
|
||||
if (issue.entityIds && issue.entityIds.length) {
|
||||
selectID = issue.entityIds[0];
|
||||
|
||||
// If a relation, focus on one of its members instead.
|
||||
// Otherwise we might be focusing on a part of map where the relation is not visible.
|
||||
if (selectID && selectID.charAt(0) === 'r') { // relation
|
||||
const ids = utilEntityAndDeepMemberIDs([selectID], graph);
|
||||
let nodeID = ids.find(id => id.charAt(0) === 'n' && graph.hasEntity(id));
|
||||
|
||||
if (!nodeID) { // relation has no downloaded nodes to focus on
|
||||
const wayID = ids.find(id => id.charAt(0) === 'w' && graph.hasEntity(id));
|
||||
if (wayID) {
|
||||
nodeID = graph.entity(wayID).first(); // focus on the first node of this way
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeID) {
|
||||
focusCenter = graph.entity(nodeID).loc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (focusCenter) { // Adjust the view
|
||||
const setZoom = Math.max(context.map().zoom(), 19);
|
||||
context.map().unobscuredCenterZoomEase(focusCenter, setZoom);
|
||||
}
|
||||
|
||||
if (selectID) { // Enter select mode
|
||||
window.setTimeout(() => {
|
||||
let ids = issue.entityIds;
|
||||
context.enter(modeSelect(context, [ids[0]]));
|
||||
context.enter(modeSelect(context, [selectID]));
|
||||
dispatch.call('focusedIssue', this, issue);
|
||||
}, 250); // after ease
|
||||
}
|
||||
@@ -312,16 +349,11 @@ export function coreValidator(context) {
|
||||
// An Array containing the issues
|
||||
//
|
||||
validator.getSharedEntityIssues = (entityIDs, options) => {
|
||||
// show some issue types in a particular order
|
||||
const orderedIssueTypes = [
|
||||
// flag missing data first
|
||||
'missing_tag', 'missing_role',
|
||||
// then flag identity issues
|
||||
'outdated_tags', 'mismatched_geometry',
|
||||
// flag geometry issues where fixing them might solve connectivity issues
|
||||
'crossing_ways', 'almost_junction',
|
||||
// then flag connectivity issues
|
||||
'disconnected_way', 'impossible_oneway'
|
||||
const orderedIssueTypes = [ // Show some issue types in a particular order:
|
||||
'missing_tag', 'missing_role', // - missing data first
|
||||
'outdated_tags', 'mismatched_geometry', // - identity issues
|
||||
'crossing_ways', 'almost_junction', // - geometry issues where fixing them might solve connectivity issues
|
||||
'disconnected_way', 'impossible_oneway' // - finally connectivity issues
|
||||
];
|
||||
|
||||
const allIssues = validator.getIssues(options);
|
||||
@@ -438,10 +470,16 @@ export function coreValidator(context) {
|
||||
// This may take time but happen in the background during browser idle time.
|
||||
//
|
||||
validator.validate = () => {
|
||||
const currGraph = context.graph();
|
||||
const prevGraph = _headGraph || context.history().base();
|
||||
// Make sure the caches have graphs assigned to them.
|
||||
// (we don't do this in `reset` because context is still resetting things and `history.base()` is unstable then)
|
||||
const baseGraph = context.history().base();
|
||||
if (!_headCache.graph) _headCache.graph = baseGraph;
|
||||
if (!_baseCache.graph) _baseCache.graph = baseGraph;
|
||||
|
||||
if (currGraph === prevGraph) { // _headGraph is current - we are caught up
|
||||
const prevGraph = _headCache.graph;
|
||||
const currGraph = context.graph();
|
||||
|
||||
if (currGraph === prevGraph) { // _headCache.graph is current - we are caught up
|
||||
_headIsCurrent = true;
|
||||
dispatch.call('validated');
|
||||
return Promise.resolve();
|
||||
@@ -452,26 +490,19 @@ export function coreValidator(context) {
|
||||
return _headPromise;
|
||||
}
|
||||
|
||||
_headGraph = currGraph; // take snapshot
|
||||
const difference = coreDifference(prevGraph, _headGraph);
|
||||
|
||||
// Gather all entities related to this difference..
|
||||
// For created/modified, use the head graph
|
||||
let entityIDs = difference.extantIDs(true); // created/modified (true = w/relation members)
|
||||
entityIDs = entityIDsToValidate(entityIDs, _headGraph);
|
||||
|
||||
// For modified/deleted, use the previous graph
|
||||
// (e.g. deleting the only highway connected to a road should create a disconnected highway issue)
|
||||
let previousEntityIDs = difference.deleted().concat(difference.modified()).map(entity => entity.id);
|
||||
previousEntityIDs = entityIDsToValidate(previousEntityIDs, prevGraph);
|
||||
previousEntityIDs.forEach(entityIDs.add, entityIDs); // concat the sets
|
||||
// If we get here, its time to start validating stuff.
|
||||
_headCache.graph = currGraph; // take snapshot
|
||||
_completeDiff = context.history().difference().complete();
|
||||
const incrementalDiff = coreDifference(prevGraph, currGraph);
|
||||
let entityIDs = Object.keys(incrementalDiff.complete());
|
||||
entityIDs = _headCache.withAllRelatedEntities(entityIDs); // expand set
|
||||
|
||||
if (!entityIDs.size) {
|
||||
dispatch.call('validated');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
_headPromise = validateEntitiesAsync(entityIDs, _headGraph, _headCache)
|
||||
_headPromise = validateEntitiesAsync(entityIDs, _headCache)
|
||||
.then(() => updateResolvedIssues(entityIDs))
|
||||
.then(() => dispatch.call('validated'))
|
||||
.catch(() => { /* ignore */ })
|
||||
@@ -508,10 +539,16 @@ export function coreValidator(context) {
|
||||
context.history()
|
||||
.on('merge.validator', entities => {
|
||||
if (!entities) return;
|
||||
|
||||
// Make sure the caches have graphs assigned to them.
|
||||
// (we don't do this in `reset` because context is still resetting things and `history.base()` is unstable then)
|
||||
const baseGraph = context.history().base();
|
||||
if (!_headCache.graph) _headCache.graph = baseGraph;
|
||||
if (!_baseCache.graph) _baseCache.graph = baseGraph;
|
||||
|
||||
let entityIDs = entities.map(entity => entity.id);
|
||||
entityIDs = entityIDsToValidate(entityIDs, baseGraph);
|
||||
validateEntitiesAsync(entityIDs, baseGraph, _baseCache);
|
||||
entityIDs = _baseCache.withAllRelatedEntities(entityIDs); // expand set
|
||||
validateEntitiesAsync(entityIDs, _baseCache);
|
||||
});
|
||||
|
||||
|
||||
@@ -538,6 +575,9 @@ export function coreValidator(context) {
|
||||
//
|
||||
function validateEntity(entity, graph) {
|
||||
let result = { issues: [], provisional: false };
|
||||
Object.keys(_rules).forEach(runValidation); // run all rules
|
||||
return result;
|
||||
|
||||
|
||||
// runs validation and appends resulting issues
|
||||
function runValidation(key) {
|
||||
@@ -547,97 +587,41 @@ export function coreValidator(context) {
|
||||
return;
|
||||
}
|
||||
|
||||
const detected = fn(entity, graph).filter(applySeverityOverrides);
|
||||
let detected = fn(entity, graph);
|
||||
if (detected.provisional) { // this validation should be run again later
|
||||
result.provisional = true;
|
||||
}
|
||||
detected = detected.filter(applySeverityOverrides);
|
||||
result.issues = result.issues.concat(detected);
|
||||
}
|
||||
|
||||
// run all rules
|
||||
Object.keys(_rules).forEach(runValidation);
|
||||
|
||||
return result;
|
||||
}
|
||||
// If there are any override rules that match the issue type/subtype,
|
||||
// adjust severity (or disable it) and keep/discard as quickly as possible.
|
||||
function applySeverityOverrides(issue) {
|
||||
const type = issue.type;
|
||||
const subtype = issue.subtype || '';
|
||||
let i;
|
||||
|
||||
// If there are any override rules that match the issue type/subtype,
|
||||
// adjust severity (or disable it) and keep/discard as quickly as possible.
|
||||
function applySeverityOverrides(issue) {
|
||||
var type = issue.type;
|
||||
var subtype = issue.subtype || '';
|
||||
var i;
|
||||
|
||||
for (i = 0; i < _errorOverrides.length; i++) {
|
||||
if (_errorOverrides[i].type.test(type) && _errorOverrides[i].subtype.test(subtype)) {
|
||||
issue.severity = 'error';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < _warningOverrides.length; i++) {
|
||||
if (_warningOverrides[i].type.test(type) && _warningOverrides[i].subtype.test(subtype)) {
|
||||
issue.severity = 'warning';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < _disableOverrides.length; i++) {
|
||||
if (_disableOverrides[i].type.test(type) && _disableOverrides[i].subtype.test(subtype)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// `entityIDsToValidate()` (private)
|
||||
// Collects the complete list of entityIDs related to the input entityIDs.
|
||||
//
|
||||
// Arguments
|
||||
// `entityIDs` - Set or Array containing entityIDs.
|
||||
// `graph` - graph containing the entities
|
||||
//
|
||||
// Returns
|
||||
// Set containing entityIDs
|
||||
//
|
||||
function entityIDsToValidate(entityIDs, graph) {
|
||||
let seen = new Set();
|
||||
let collected = new Set();
|
||||
|
||||
entityIDs.forEach(entityID => {
|
||||
// keep `seen` separate from `collected` because an `entityID`
|
||||
// could have been added to `collected` as a related entity through an earlier pass
|
||||
if (seen.has(entityID)) return;
|
||||
seen.add(entityID);
|
||||
|
||||
const entity = graph.hasEntity(entityID);
|
||||
if (!entity) return;
|
||||
|
||||
collected.add(entityID); // collect self
|
||||
|
||||
let checkParentRels = [entity];
|
||||
|
||||
if (entity.type === 'node') {
|
||||
graph.parentWays(entity).forEach(parentWay => {
|
||||
collected.add(parentWay.id); // collect parent ways
|
||||
checkParentRels.push(parentWay);
|
||||
});
|
||||
|
||||
} else if (entity.type === 'relation') {
|
||||
entity.members.forEach(member => collected.add(member.id)); // collect members
|
||||
|
||||
} else if (entity.type === 'way') {
|
||||
entity.nodes.forEach(nodeID => {
|
||||
collected.add(nodeID); // collect child nodes
|
||||
graph._parentWays[nodeID].forEach(wayID => collected.add(wayID)); // collect connected ways
|
||||
});
|
||||
}
|
||||
|
||||
checkParentRels.forEach(entity => { // collect parent relations
|
||||
if (entity.type !== 'relation') { // but not super-relations
|
||||
graph.parentRelations(entity).forEach(parentRelation => collected.add(parentRelation.id));
|
||||
for (i = 0; i < _errorOverrides.length; i++) {
|
||||
if (_errorOverrides[i].type.test(type) && _errorOverrides[i].subtype.test(subtype)) {
|
||||
issue.severity = 'error';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return collected;
|
||||
for (i = 0; i < _warningOverrides.length; i++) {
|
||||
if (_warningOverrides[i].type.test(type) && _warningOverrides[i].subtype.test(subtype)) {
|
||||
issue.severity = 'warning';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < _disableOverrides.length; i++) {
|
||||
if (_disableOverrides[i].type.test(type) && _disableOverrides[i].subtype.test(subtype)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -645,20 +629,29 @@ export function coreValidator(context) {
|
||||
// Determine if any issues were resolved for the given entities.
|
||||
// This is called by `validate()` after validation of the head graph
|
||||
//
|
||||
// Give the user credit for fixing an issue if:
|
||||
// - the issue is in the base cache
|
||||
// - the issue is not in the head cache
|
||||
// - the user did something to one of the entities involved in the issue
|
||||
//
|
||||
// Arguments
|
||||
// `entityIDs` - Set containing entity IDs.
|
||||
// `entityIDs` - Array or Set containing entity IDs.
|
||||
//
|
||||
function updateResolvedIssues(entityIDs) {
|
||||
entityIDs.forEach(entityID => {
|
||||
const headIssues = _headCache.issuesByEntityID[entityID];
|
||||
const baseIssues = _baseCache.issuesByEntityID[entityID];
|
||||
if (!baseIssues) return;
|
||||
|
||||
baseIssues.forEach(issueID => {
|
||||
if (headIssues && headIssues.has(issueID)) { // issue still not resolved
|
||||
_resolvedIssueIDs.delete(issueID); // (did undo, or possibly fixed and then re-caused the issue)
|
||||
} else {
|
||||
// Check if the user did something to one of the entities involved in this issue.
|
||||
// (This issue could involve multiple entities, e.g. disconnected routable features)
|
||||
const issue = _baseCache.issuesByIssueID[issueID];
|
||||
const userModified = (issue.entityIds || []).some(id => _completeDiff.hasOwnProperty(id));
|
||||
|
||||
if (userModified && !_headCache.issuesByIssueID[issueID]) { // issue seems fixed
|
||||
_resolvedIssueIDs.add(issueID);
|
||||
} else { // issue still not resolved
|
||||
_resolvedIssueIDs.delete(issueID); // (did undo, or possibly fixed and then re-caused the issue)
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -669,7 +662,7 @@ export function coreValidator(context) {
|
||||
// Schedule validation for many entities.
|
||||
//
|
||||
// Arguments
|
||||
// `entityIDs` - Set containing entity IDs.
|
||||
// `entityIDs` - Array or Set containing entityIDs.
|
||||
// `graph` - the graph to validate that contains those entities
|
||||
// `cache` - the cache to store results in (_headCache or _baseCache)
|
||||
//
|
||||
@@ -677,26 +670,31 @@ export function coreValidator(context) {
|
||||
// A Promise fulfilled when the validation has completed.
|
||||
// This may take time but happen in the background during browser idle time.
|
||||
//
|
||||
function validateEntitiesAsync(entityIDs, graph, cache) {
|
||||
function validateEntitiesAsync(entityIDs, cache) {
|
||||
// Enqueue the work
|
||||
const jobs = Array.from(entityIDs).map(entityID => {
|
||||
if (cache.queuedEntityIDs.has(entityID)) return null; // queued already
|
||||
cache.queuedEntityIDs.add(entityID);
|
||||
|
||||
// Clear caches for existing issues related to this entity
|
||||
cache.uncacheEntityID(entityID);
|
||||
|
||||
return () => {
|
||||
// clear caches for existing issues related to this entity
|
||||
cache.uncacheEntityID(entityID);
|
||||
cache.queuedEntityIDs.delete(entityID);
|
||||
|
||||
const graph = cache.graph;
|
||||
if (!graph) return; // was reset?
|
||||
|
||||
const entity = graph.hasEntity(entityID); // Sanity check: don't validate deleted entities
|
||||
if (!entity) return;
|
||||
|
||||
// detect new issues and update caches
|
||||
const entity = graph.hasEntity(entityID); // Sanity check: don't validate deleted entities
|
||||
if (entity) {
|
||||
const result = validateEntity(entity, graph);
|
||||
if (result.provisional) { // provisional result
|
||||
cache.provisionalEntityIDs.add(entityID); // we'll need to revalidate this entity again later
|
||||
}
|
||||
cache.cacheIssues(result.issues); // update cache
|
||||
const result = validateEntity(entity, graph);
|
||||
if (result.provisional) { // provisional result
|
||||
cache.provisionalEntityIDs.add(entityID); // we'll need to revalidate this entity again later
|
||||
}
|
||||
|
||||
cache.cacheIssues(result.issues); // update cache
|
||||
};
|
||||
|
||||
}).filter(Boolean);
|
||||
@@ -733,9 +731,7 @@ export function coreValidator(context) {
|
||||
const handle = window.setTimeout(() => {
|
||||
_deferredST.delete(handle);
|
||||
if (!cache.provisionalEntityIDs.size) return; // nothing to do
|
||||
|
||||
const graph = (cache === _headCache ? _headGraph : context.history().base());
|
||||
validateEntitiesAsync(cache.provisionalEntityIDs, graph, cache);
|
||||
validateEntitiesAsync(Array.from(cache.provisionalEntityIDs), cache);
|
||||
}, RETRY);
|
||||
|
||||
_deferredST.add(handle);
|
||||
@@ -753,8 +749,7 @@ export function coreValidator(context) {
|
||||
// This may take time but happen in the background during browser idle time.
|
||||
//
|
||||
function processQueue(cache) {
|
||||
// const which = (cache === _headCache) ? 'head' : 'base';
|
||||
// console.log(`${which} queue length ${cache.queue.length}`);
|
||||
// console.log(`${cache.which} queue length ${cache.queue.length}`);
|
||||
|
||||
if (!cache.queue.length) return Promise.resolve(); // we're done
|
||||
const chunk = cache.queue.pop();
|
||||
@@ -787,34 +782,35 @@ export function coreValidator(context) {
|
||||
// `_baseCache` for validation on the base graph (unedited)
|
||||
// `_headCache` for validation on the head graph (user edits applied)
|
||||
//
|
||||
function validationCache() {
|
||||
// Arguments
|
||||
// `which` - just a String 'base' or 'head' to keep track of it
|
||||
//
|
||||
function validationCache(which) {
|
||||
let cache = {
|
||||
which: which,
|
||||
graph: null,
|
||||
queue: [],
|
||||
queuePromise: null,
|
||||
queuedEntityIDs: new Set(),
|
||||
provisionalEntityIDs: new Set(),
|
||||
issuesByIssueID: {}, // issue.id -> issue
|
||||
issuesByEntityID: {} // entity.id -> set(issue.id)
|
||||
issuesByEntityID: {} // entity.id -> Set(issue.id)
|
||||
};
|
||||
|
||||
cache.cacheIssues = (issues) => {
|
||||
issues.forEach(issue => {
|
||||
const entityIDs = issue.entityIds || [];
|
||||
entityIDs.forEach(entityID => {
|
||||
if (!cache.issuesByEntityID[entityID]) {
|
||||
cache.issuesByEntityID[entityID] = new Set();
|
||||
}
|
||||
cache.issuesByEntityID[entityID].add(issue.id);
|
||||
});
|
||||
cache.issuesByIssueID[issue.id] = issue;
|
||||
|
||||
cache.cacheIssue = (issue) => {
|
||||
(issue.entityIds || []).forEach(entityID => {
|
||||
if (!cache.issuesByEntityID[entityID]) {
|
||||
cache.issuesByEntityID[entityID] = new Set();
|
||||
}
|
||||
cache.issuesByEntityID[entityID].add(issue.id);
|
||||
});
|
||||
cache.issuesByIssueID[issue.id] = issue;
|
||||
};
|
||||
|
||||
|
||||
cache.uncacheIssue = (issue) => {
|
||||
// When multiple entities are involved (e.g. crossing_ways),
|
||||
// remove this issue from the other entity caches too..
|
||||
const entityIDs = issue.entityIds || [];
|
||||
entityIDs.forEach(entityID => {
|
||||
(issue.entityIds || []).forEach(entityID => {
|
||||
if (cache.issuesByEntityID[entityID]) {
|
||||
cache.issuesByEntityID[entityID].delete(issue.id);
|
||||
}
|
||||
@@ -822,25 +818,33 @@ function validationCache() {
|
||||
delete cache.issuesByIssueID[issue.id];
|
||||
};
|
||||
|
||||
|
||||
cache.cacheIssues = (issues) => {
|
||||
issues.forEach(cache.cacheIssue);
|
||||
};
|
||||
|
||||
|
||||
cache.uncacheIssues = (issues) => {
|
||||
issues.forEach(cache.uncacheIssue);
|
||||
};
|
||||
|
||||
|
||||
cache.uncacheIssuesOfType = (type) => {
|
||||
const issuesOfType = Object.values(cache.issuesByIssueID)
|
||||
.filter(issue => issue.type === type);
|
||||
cache.uncacheIssues(issuesOfType);
|
||||
};
|
||||
|
||||
|
||||
// Remove a single entity and all its related issues from the caches
|
||||
cache.uncacheEntityID = (entityID) => {
|
||||
const issueIDs = cache.issuesByEntityID[entityID];
|
||||
if (issueIDs) {
|
||||
issueIDs.forEach(issueID => {
|
||||
const entityIssueIDs = cache.issuesByEntityID[entityID];
|
||||
if (entityIssueIDs) {
|
||||
entityIssueIDs.forEach(issueID => {
|
||||
const issue = cache.issuesByIssueID[issueID];
|
||||
if (issue) {
|
||||
cache.uncacheIssue(issue);
|
||||
} else {
|
||||
} else { // shouldnt happen, clean up
|
||||
delete cache.issuesByIssueID[issueID];
|
||||
}
|
||||
});
|
||||
@@ -851,5 +855,32 @@ function validationCache() {
|
||||
};
|
||||
|
||||
|
||||
// Return the expandeded set of entityIDs related to issues for the given entityIDs
|
||||
//
|
||||
// Arguments
|
||||
// `entityIDs` - Array or Set containing entityIDs.
|
||||
//
|
||||
cache.withAllRelatedEntities = (entityIDs) => {
|
||||
let result = new Set();
|
||||
(entityIDs || []).forEach(entityID => {
|
||||
result.add(entityID); // include self
|
||||
|
||||
const entityIssueIDs = cache.issuesByEntityID[entityID];
|
||||
if (entityIssueIDs) {
|
||||
entityIssueIDs.forEach(issueID => {
|
||||
const issue = cache.issuesByIssueID[issueID];
|
||||
if (issue) {
|
||||
(issue.entityIds || []).forEach(relatedID => result.add(relatedID));
|
||||
} else { // shouldnt happen, clean up
|
||||
delete cache.issuesByIssueID[issueID];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
@@ -429,11 +429,14 @@ export function modeSelect(context, selectedIDs) {
|
||||
actionAddMidpoint({ loc: choice.loc, edge: [prev, next] }, osmNode()),
|
||||
t('operations.add.annotation.vertex')
|
||||
);
|
||||
context.validator().validate();
|
||||
|
||||
} else if (entity.type === 'midpoint') {
|
||||
context.perform(
|
||||
actionAddMidpoint({ loc: entity.loc, edge: entity.edge }, osmNode()),
|
||||
t('operations.add.annotation.vertex'));
|
||||
t('operations.add.annotation.vertex')
|
||||
);
|
||||
context.validator().validate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,6 +691,7 @@ export function modeSelect(context, selectedIDs) {
|
||||
// the user added this relation but didn't edit it at all, so just delete it
|
||||
var deleteAction = actionDeleteRelation(entity.id, true /* don't delete untagged members */);
|
||||
context.perform(deleteAction, t('operations.delete.annotation.relation'));
|
||||
context.validator().validate();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -74,9 +74,12 @@ export function operationMerge(context, selectedIDs) {
|
||||
operation.tooltip = function() {
|
||||
var disabled = operation.disabled();
|
||||
if (disabled) {
|
||||
if (disabled === 'restriction') {
|
||||
return t('operations.merge.restriction',
|
||||
{ relation: presetManager.item('type/restriction').name() });
|
||||
if (disabled === 'conflicting_relations') {
|
||||
return t('operations.merge.conflicting_relations');
|
||||
}
|
||||
if (disabled === 'restriction' || disabled === 'connectivity') {
|
||||
return t('operations.merge.damage_relation',
|
||||
{ relation: presetManager.item('type/' + disabled).name() });
|
||||
}
|
||||
return t('operations.merge.' + disabled);
|
||||
}
|
||||
|
||||
@@ -289,6 +289,9 @@ Object.assign(osmRelation.prototype, {
|
||||
return true;
|
||||
},
|
||||
|
||||
isConnectivity: function() {
|
||||
return !!(this.tags.type && this.tags.type.match(/^connectivity:?/));
|
||||
},
|
||||
|
||||
// Returns an array [A0, ... An], each Ai being an array of node arrays [Nds0, ... Ndsm],
|
||||
// where Nds0 is an outer ring and subsequent Ndsi's (if any i > 0) being inner rings.
|
||||
|
||||
@@ -251,6 +251,7 @@ export function rendererMap(context) {
|
||||
|
||||
context.on('enter.map', function() {
|
||||
if (!map.editableDataEnabled(true /* skip zoom check */)) return;
|
||||
if (_isTransformed) return;
|
||||
|
||||
// redraw immediately any objects affected by a change in selectedIDs.
|
||||
var graph = context.graph();
|
||||
@@ -554,11 +555,11 @@ export function rendererMap(context) {
|
||||
x2 = p0[0] - p1[0] * k2;
|
||||
y2 = p0[1] - p1[1] * k2;
|
||||
|
||||
// 2 finger map panning (Mac only, all browsers) - #5492, #5512
|
||||
// 2 finger map panning (Mac only, all browsers except Firefox #8595) - #5492, #5512
|
||||
// Panning via the `wheel` event will always have:
|
||||
// - `ctrlKey = false`
|
||||
// - `deltaX`,`deltaY` are round integer pixels
|
||||
} else if (detected.os === 'mac' && !source.ctrlKey && isInteger(dX) && isInteger(dY)) {
|
||||
} else if (detected.os === 'mac' && detected.browser !== 'Firefox' && !source.ctrlKey && isInteger(dX) && isInteger(dY)) {
|
||||
p1 = projection.translate();
|
||||
x2 = p1[0] - dX;
|
||||
y2 = p1[1] - dY;
|
||||
|
||||
+21
-7
@@ -163,7 +163,7 @@ function loadNsiData() {
|
||||
// and fallbacks like
|
||||
// "amenity/yes"
|
||||
// excluding things like
|
||||
// "highway", "surface", "ref", etc.
|
||||
// "tiger:reviewed", "surface", "ref", etc.
|
||||
//
|
||||
// Arguments
|
||||
// `tags`: `Object` containing the feature's OSM tags
|
||||
@@ -182,13 +182,16 @@ function gatherKVs(tags) {
|
||||
const osmvalue = tags[osmkey];
|
||||
if (!osmvalue) return;
|
||||
|
||||
const vmap = _nsi.kvt.get(osmkey);
|
||||
if (!vmap) return;
|
||||
// Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184
|
||||
if (osmkey === 'route_master') osmkey = 'route';
|
||||
|
||||
if (osmvalue !== 'yes') {
|
||||
primary.add(`${osmkey}/${osmvalue}`);
|
||||
} else {
|
||||
alternate.add(`${osmkey}/${osmvalue}`);
|
||||
const vmap = _nsi.kvt.get(osmkey);
|
||||
if (!vmap) return; // not an interesting key
|
||||
|
||||
if (vmap.get(osmvalue)) { // Matched a category in NSI
|
||||
primary.add(`${osmkey}/${osmvalue}`); // interesting key/value
|
||||
} else if (osmvalue === 'yes') {
|
||||
alternate.add(`${osmkey}/${osmvalue}`); // fallback key/yes
|
||||
}
|
||||
});
|
||||
|
||||
@@ -228,6 +231,9 @@ function identifyTree(tags) {
|
||||
const osmvalue = tags[osmkey];
|
||||
if (!osmvalue) return;
|
||||
|
||||
// Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184
|
||||
if (osmkey === 'route_master') osmkey = 'route';
|
||||
|
||||
const vmap = _nsi.kvt.get(osmkey);
|
||||
if (!vmap) return; // this key is not in nsi
|
||||
|
||||
@@ -431,6 +437,8 @@ function _upgradeTags(tags, loc) {
|
||||
}
|
||||
});
|
||||
|
||||
// Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184
|
||||
const isRouteMaster = (tags.type === 'route_master');
|
||||
|
||||
// Gather key/value tag pairs to try to match
|
||||
const tryKVs = gatherKVs(tags);
|
||||
@@ -530,6 +538,12 @@ function _upgradeTags(tags, loc) {
|
||||
// Do the tag upgrade
|
||||
Object.assign(newTags, item.tags, keepTags);
|
||||
|
||||
// Swap `route` back to `route_master` - name-suggestion-index#5184
|
||||
if (isRouteMaster) {
|
||||
newTags.route_master = newTags.route;
|
||||
delete newTags.route;
|
||||
}
|
||||
|
||||
// Special `branch` splitting rules - IF..
|
||||
// - NSI is suggesting to replace `name`, AND
|
||||
// - `branch` doesn't already contain something, AND
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ import { text as d3_text } from 'd3-fetch';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
import stringify from 'fast-json-stable-stringify';
|
||||
import toGeoJSON from '@mapbox/togeojson';
|
||||
import { gpx, kml } from '@tmcw/togeojson';
|
||||
|
||||
import { geoExtent, geoPolygonIntersectsPolygon } from '../geo';
|
||||
import { services } from '../services';
|
||||
@@ -324,10 +324,10 @@ export function svgData(projection, context, dispatch) {
|
||||
var gj;
|
||||
switch (extension) {
|
||||
case '.gpx':
|
||||
gj = toGeoJSON.gpx(xmlToDom(data));
|
||||
gj = gpx(xmlToDom(data));
|
||||
break;
|
||||
case '.kml':
|
||||
gj = toGeoJSON.kml(xmlToDom(data));
|
||||
gj = kml(xmlToDom(data));
|
||||
break;
|
||||
case '.geojson':
|
||||
case '.json':
|
||||
|
||||
@@ -193,7 +193,10 @@ export function uiCommit(context) {
|
||||
|
||||
// add counts of warnings generated by the user's edits
|
||||
var warnings = context.validator()
|
||||
.getIssuesBySeverity({ what: 'edited', where: 'all', includeIgnored: true, includeDisabledRules: true }).warning;
|
||||
.getIssuesBySeverity({ what: 'edited', where: 'all', includeIgnored: true, includeDisabledRules: true })
|
||||
.warning
|
||||
.filter(function(issue) { return issue.type !== 'help_request'; }); // exclude 'fixme' and similar - #8603
|
||||
|
||||
addIssueCounts(warnings, 'warnings');
|
||||
|
||||
// add counts of issues resolved by the user's edits
|
||||
|
||||
@@ -12,6 +12,11 @@ export function uiCommitWarnings(context) {
|
||||
|
||||
for (var severity in issuesBySeverity) {
|
||||
var issues = issuesBySeverity[severity];
|
||||
|
||||
if (severity !== 'error') { // exclude 'fixme' and similar - #8603
|
||||
issues = issues.filter(function(issue) { return issue.type !== 'help_request'; });
|
||||
}
|
||||
|
||||
var section = severity + '-section';
|
||||
var issueItem = severity + '-item';
|
||||
|
||||
@@ -38,7 +43,7 @@ export function uiCommitWarnings(context) {
|
||||
|
||||
|
||||
var items = container.select('ul').selectAll('li')
|
||||
.data(issues, function(d) { return d.id; });
|
||||
.data(issues, function(d) { return d.key; });
|
||||
|
||||
items.exit()
|
||||
.remove();
|
||||
|
||||
@@ -149,6 +149,24 @@ export function uiFieldText(field, context) {
|
||||
}
|
||||
})
|
||||
.merge(outlinkButton);
|
||||
} else if (field.type === 'url') {
|
||||
input.attr('type', 'text');
|
||||
|
||||
outlinkButton = wrap.selectAll('.foreign-id-permalink')
|
||||
.data([0]);
|
||||
|
||||
outlinkButton.enter()
|
||||
.append('button')
|
||||
.call(svgIcon('#iD-icon-out-link'))
|
||||
.attr('class', 'form-field-button foreign-id-permalink')
|
||||
.attr('title', () => t('icons.visit_website'))
|
||||
.on('click', function(d3_event) {
|
||||
d3_event.preventDefault();
|
||||
|
||||
const value = validIdentifierValueForLink();
|
||||
if (value) window.open(value, '_blank');
|
||||
})
|
||||
.merge(outlinkButton);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,8 +182,10 @@ export function uiFieldText(field, context) {
|
||||
|
||||
|
||||
function validIdentifierValueForLink() {
|
||||
const value = utilGetSetValue(input).trim().split(';')[0];
|
||||
|
||||
if (field.type === 'url' && value) return value;
|
||||
if (field.type === 'identifier' && field.pattern) {
|
||||
var value = utilGetSetValue(input).trim().split(';')[0];
|
||||
return value && value.match(new RegExp(field.pattern));
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -201,6 +201,12 @@ export function uiInit(context) {
|
||||
.attr('class', 'map-control geolocate-control')
|
||||
.call(uiGeolocate(context));
|
||||
|
||||
controls.on('wheel.mapControls', function(d3_event) {
|
||||
if (!d3_event.deltaX) {
|
||||
controls.node().scrollTop += d3_event.deltaY;
|
||||
}
|
||||
});
|
||||
|
||||
// Add panes
|
||||
// This should happen after map is initialized, as some require surface()
|
||||
var panes = overMap
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
import _debounce from 'lodash-es/debounce';
|
||||
|
||||
import { presetManager } from '../presets';
|
||||
import { t, localizer } from '../core/localizer';
|
||||
@@ -122,7 +123,7 @@ export function uiPresetList(context) {
|
||||
.call(utilNoAuto)
|
||||
.on('keydown', initialKeydown)
|
||||
.on('keypress', keypress)
|
||||
.on('input', inputevent);
|
||||
.on('input', _debounce(inputevent));
|
||||
|
||||
if (_autofocus) {
|
||||
search.node().focus();
|
||||
|
||||
@@ -55,7 +55,7 @@ export function uiSectionEntityIssues(context) {
|
||||
_activeIssueID = _issues.length > 0 ? _issues[0].id : null;
|
||||
|
||||
var containers = selection.selectAll('.issue-container')
|
||||
.data(_issues, function(d) { return d.id; });
|
||||
.data(_issues, function(d) { return d.key; });
|
||||
|
||||
// Exit
|
||||
containers.exit()
|
||||
|
||||
@@ -73,7 +73,7 @@ export function uiSectionValidationIssues(id, severity, context) {
|
||||
|
||||
|
||||
var items = list.selectAll('li')
|
||||
.data(issues, function(d) { return d.id; });
|
||||
.data(issues, function(d) { return d.key; });
|
||||
|
||||
// Exit
|
||||
items.exit()
|
||||
|
||||
@@ -22,9 +22,10 @@ export { utilDisplayType } from './util';
|
||||
export { utilDisplayLabel } from './util';
|
||||
export { utilEntityRoot } from './util';
|
||||
export { utilEditDistance } from './util';
|
||||
export { utilEntitySelector } from './util';
|
||||
export { utilEntityAndDeepMemberIDs } from './util';
|
||||
export { utilEntityOrMemberSelector } from './util';
|
||||
export { utilEntityOrDeepMemberSelector } from './util';
|
||||
export { utilEntitySelector } from './util';
|
||||
export { utilFastMouse } from './util';
|
||||
export { utilFetchJson } from './util';
|
||||
export { utilFunctor } from './util';
|
||||
|
||||
@@ -395,6 +395,9 @@ export function validationCrossingWays(context) {
|
||||
crossingTypeID += '_connectable';
|
||||
}
|
||||
|
||||
// Differentiate based on the loc rounded to 4 digits, since two ways can cross multiple times.
|
||||
var uniqueID = '' + crossing.crossPoint[0].toFixed(4) + ',' + crossing.crossPoint[1].toFixed(4);
|
||||
|
||||
return new validationIssue({
|
||||
type: type,
|
||||
subtype: subtype,
|
||||
@@ -417,15 +420,7 @@ export function validationCrossingWays(context) {
|
||||
featureTypes: featureTypes,
|
||||
connectionTags: connectionTags
|
||||
},
|
||||
// differentiate based on the loc since two ways can cross multiple times
|
||||
hash: crossing.crossPoint.toString() +
|
||||
// if the edges change then so does the fix
|
||||
edges.slice().sort(function(edge1, edge2) {
|
||||
// order to assure hash is deterministic
|
||||
return edge1[0] < edge2[0] ? -1 : 1;
|
||||
}).toString() +
|
||||
// ensure the correct connection tags are added in the fix
|
||||
JSON.stringify(connectionTags),
|
||||
hash: uniqueID,
|
||||
loc: crossing.crossPoint,
|
||||
dynamicFixes: function(context) {
|
||||
var mode = context.mode();
|
||||
|
||||
Reference in New Issue
Block a user