mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-19 23:14:47 +02:00
Allow validators to return provisional results, revalidate after delay
Also add a ton of commments to validator.js
This commit is contained in:
+290
-120
@@ -21,14 +21,17 @@ export function coreValidator(context) {
|
||||
let _headCache = validationCache(); // issues after all user edits
|
||||
let _previousGraph = null;
|
||||
|
||||
let _deferred = new Set(); // Set( IdleCallback handles )
|
||||
let _inProcess; // Promise fulfilled when validation complete
|
||||
let _deferredRIC = new Set(); // Set( RequestIdleCallback handles )
|
||||
let _deferredST = new Set(); // Set( SetTimeout handles )
|
||||
let _inProcess; // Promise fulfilled when validation complete
|
||||
|
||||
const RETRY = 5000; // wait 5sec before revalidating provisional entities
|
||||
|
||||
|
||||
// `init()`
|
||||
// Initialize the validator, called once on iD startup
|
||||
//
|
||||
// initialize the validator rulesets
|
||||
//
|
||||
validator.init = function() {
|
||||
validator.init = () => {
|
||||
Object.values(Validations).forEach(validation => {
|
||||
if (typeof validation !== 'function') return;
|
||||
const fn = validation(context);
|
||||
@@ -42,12 +45,19 @@ export function coreValidator(context) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// `reset()` (private)
|
||||
// Cancels deferred work and resets all caches
|
||||
//
|
||||
// Arguments
|
||||
// `resetIgnored` - `true` to clear the list of user-ignored issues
|
||||
//
|
||||
function reset(resetIgnored) {
|
||||
// cancel deferred work
|
||||
Array.from(_deferred).forEach(handle => {
|
||||
window.cancelIdleCallback(handle);
|
||||
_deferred.delete(handle);
|
||||
});
|
||||
_deferredRIC.forEach(window.cancelIdleCallback);
|
||||
_deferredRIC.clear();
|
||||
_deferredST.forEach(window.clearTimeout);
|
||||
_deferredST.clear();
|
||||
|
||||
// empty queues and resolve any pending promise
|
||||
_baseCache.queue = [];
|
||||
@@ -64,21 +74,28 @@ export function coreValidator(context) {
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// clear caches, called whenever iD resets after a save
|
||||
// `reset()`
|
||||
// clear caches, called whenever iD resets after a save or switches sources
|
||||
// (clears out the _ignoredIssueIDs set also)
|
||||
//
|
||||
validator.reset = () => {
|
||||
reset(true);
|
||||
};
|
||||
|
||||
|
||||
// `resetIgnoredIssues()`
|
||||
// clears out the _ignoredIssueIDs Set
|
||||
//
|
||||
validator.resetIgnoredIssues = () => {
|
||||
_ignoredIssueIDs = {};
|
||||
_ignoredIssueIDs.clear();
|
||||
dispatch.call('validated'); // redraw UI
|
||||
};
|
||||
|
||||
|
||||
// must update issues when the user changes the unsquare thereshold
|
||||
// `revalidateUnsquare()`
|
||||
// Called whenever the user changes the unsquare threshold
|
||||
// It reruns just the "unsquare_way" validation on all buildings.
|
||||
//
|
||||
validator.revalidateUnsquare = () => {
|
||||
revalidateUnsquare(_headCache, context.graph());
|
||||
revalidateUnsquare(_baseCache, context.history().base());
|
||||
@@ -99,38 +116,76 @@ export function coreValidator(context) {
|
||||
buildings.forEach(entity => {
|
||||
const detected = checkUnsquareWay(entity, graph);
|
||||
if (!detected.length) return;
|
||||
|
||||
const issue = detected[0];
|
||||
if (!cache.issuesByEntityID[entity.id]) {
|
||||
cache.issuesByEntityID[entity.id] = new Set();
|
||||
}
|
||||
cache.issuesByEntityID[entity.id].add(issue.id);
|
||||
cache.issuesByIssueID[issue.id] = issue;
|
||||
cache.cacheIssues(detected);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// options = {
|
||||
// what: 'all', // 'all' or 'edited'
|
||||
// where: 'all', // 'all' or 'visible'
|
||||
// includeIgnored: false, // true, false, or 'only'
|
||||
// includeDisabledRules: false // true, false, or 'only'
|
||||
// };
|
||||
// `getIssues()`
|
||||
// Gets all issues that match the given options
|
||||
// This is called by many other places
|
||||
//
|
||||
// Arguments
|
||||
// `options` Object like:
|
||||
// {
|
||||
// what: 'all', // 'all' or 'edited'
|
||||
// where: 'all', // 'all' or 'visible'
|
||||
// includeIgnored: false, // true, false, or 'only'
|
||||
// includeDisabledRules: false // true, false, or 'only'
|
||||
// }
|
||||
//
|
||||
// Returns
|
||||
// An Array containing the issues
|
||||
//
|
||||
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();
|
||||
|
||||
function filter(issue, resolver) {
|
||||
// collect head issues - caused by user edits
|
||||
let cache = _headCache;
|
||||
let graph = context.graph();
|
||||
Object.values(cache.issuesByIssueID).forEach(issue => {
|
||||
if (!filter(issue, graph, cache)) return;
|
||||
seen.add(issue.id);
|
||||
issues.push(issue);
|
||||
});
|
||||
|
||||
// collect base issues - not caused by user edits
|
||||
if (opts.what === 'all') {
|
||||
cache = _baseCache;
|
||||
graph = context.history().base();
|
||||
Object.values(cache.issuesByIssueID).forEach(issue => {
|
||||
if (!filter(issue, graph, cache)) return;
|
||||
seen.add(issue.id);
|
||||
issues.push(issue);
|
||||
});
|
||||
}
|
||||
|
||||
return issues;
|
||||
|
||||
|
||||
function filter(issue, resolver, cache) {
|
||||
if (!issue) return false;
|
||||
if (seen.has(issue.id)) return false;
|
||||
if (_resolvedIssueIDs.has(issue.id)) return false;
|
||||
if (opts.includeDisabledRules === 'only' && !_disabledRules[issue.type]) return false;
|
||||
if (!opts.includeDisabledRules && _disabledRules[issue.type]) return false;
|
||||
|
||||
if (opts.includeIgnored === 'only' && !_ignoredIssueIDs[issue.id]) return false;
|
||||
if (!opts.includeIgnored && _ignoredIssueIDs[issue.id]) return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.where === 'visible') {
|
||||
const extent = issue.extent(resolver);
|
||||
@@ -139,42 +194,15 @@ export function coreValidator(context) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// collect head issues - caused by user edits
|
||||
Object.values(_headCache.issuesByIssueID).forEach(issue => {
|
||||
if (!filter(issue, context.graph())) return; // pass head graph
|
||||
|
||||
// Sanity check: This issue may be for an entity that not longer exists.
|
||||
// If we detect this, uncache and return so it is not included..
|
||||
const entityIDs = issue.entityIds || [];
|
||||
for (let i = 0; i < entityIDs.length; i++) {
|
||||
const entityID = entityIDs[i];
|
||||
if (!context.hasEntity(entityID)) {
|
||||
_headCache.uncacheEntityID(entityID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// keep the issue
|
||||
seen.add(issue.id);
|
||||
issues.push(issue);
|
||||
});
|
||||
|
||||
// collect base issues - not caused by user edits
|
||||
if (opts.what === 'all') {
|
||||
Object.values(_baseCache.issuesByIssueID).forEach(issue => {
|
||||
if (!filter(issue, context.history().base())) return; // pass base graph
|
||||
// keep the issue
|
||||
seen.add(issue.id);
|
||||
issues.push(issue);
|
||||
});
|
||||
}
|
||||
|
||||
return issues;
|
||||
};
|
||||
|
||||
|
||||
// `getResolvedIssues()`
|
||||
// Gets the issues that have been fixed by the user.
|
||||
// Resolved issues are tracked in the `_resolvedIssueIDs` Set
|
||||
//
|
||||
// issues fixed by the user
|
||||
// Returns
|
||||
// An Array containing the issues
|
||||
//
|
||||
validator.getResolvedIssues = () => {
|
||||
let collected = new Set();
|
||||
@@ -190,6 +218,13 @@ export function coreValidator(context) {
|
||||
};
|
||||
|
||||
|
||||
// `focusIssue()`
|
||||
// Adjusts the map to focus on the given issue.
|
||||
// (requires the issue to have a reasonable extent defined)
|
||||
//
|
||||
// Arguments
|
||||
// `issue` - the issue to focus on
|
||||
//
|
||||
validator.focusIssue = (issue) => {
|
||||
const extent = issue.extent(context.graph());
|
||||
if (!extent) return;
|
||||
@@ -208,6 +243,19 @@ export function coreValidator(context) {
|
||||
};
|
||||
|
||||
|
||||
// `getIssuesBySeverity()`
|
||||
// Gets the issues then groups them by error/warning
|
||||
// (This just calls getIssues, then puts issues in groups)
|
||||
//
|
||||
// Arguments
|
||||
// `options` - (see `getIssues`)
|
||||
// Returns
|
||||
// Object result like:
|
||||
// {
|
||||
// error: Array of errors,
|
||||
// warning: Array of warnings
|
||||
// }
|
||||
//
|
||||
validator.getIssuesBySeverity = (options) => {
|
||||
let groups = utilArrayGroupBy(validator.getIssues(options), 'severity');
|
||||
groups.error = groups.error || [];
|
||||
@@ -216,20 +264,30 @@ export function coreValidator(context) {
|
||||
};
|
||||
|
||||
|
||||
// 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'
|
||||
];
|
||||
// `getEntityIssues()`
|
||||
// Gets the issues that the given entity IDs have in common, matching the given options
|
||||
// (This just calls getIssues, then filters for the given entity IDs)
|
||||
// The issues are sorted for relevance
|
||||
//
|
||||
// Arguments
|
||||
// `entityIDs` - Array or Set of entityIDs to get issues for
|
||||
// `options` - (see `getIssues`)
|
||||
// Returns
|
||||
// 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'
|
||||
];
|
||||
|
||||
// returns the issues that the given entity IDs have in common, matching the given options
|
||||
validator.getSharedEntityIssues = function(entityIDs, options) {
|
||||
const allIssues = validator.getIssues(options);
|
||||
const forEntityIDs = new Set(entityIDs);
|
||||
|
||||
@@ -252,21 +310,50 @@ export function coreValidator(context) {
|
||||
};
|
||||
|
||||
|
||||
// `getEntityIssues()`
|
||||
// Get an array of detected issues for the given entityID.
|
||||
// (This just calls getSharedEntityIssues for a single entity)
|
||||
//
|
||||
// Arguments
|
||||
// `entityID` - the entity ID to get the issues for
|
||||
// `options` - (see `getIssues`)
|
||||
// Returns
|
||||
// An Array containing the issues
|
||||
//
|
||||
validator.getEntityIssues = (entityID, options) => {
|
||||
return validator.getSharedEntityIssues([entityID], options);
|
||||
};
|
||||
|
||||
|
||||
// `getRuleKeys()`
|
||||
//
|
||||
// Returns
|
||||
// An Array containing the rule keys
|
||||
//
|
||||
validator.getRuleKeys = () => {
|
||||
return Object.keys(_rules);
|
||||
};
|
||||
|
||||
|
||||
// `isRuleEnabled()`
|
||||
//
|
||||
// Arguments
|
||||
// `key` - the rule to check (e.g. 'crossing_ways')
|
||||
// Returns
|
||||
// `true`/`false`
|
||||
//
|
||||
validator.isRuleEnabled = (key) => {
|
||||
return !_disabledRules[key];
|
||||
};
|
||||
|
||||
|
||||
// `toggleRule()`
|
||||
// Toggles a single validation rule,
|
||||
// then reruns the validation so that the user sees something happen in the UI
|
||||
//
|
||||
// Arguments
|
||||
// `key` - the rule to toggle (e.g. 'crossing_ways')
|
||||
//
|
||||
validator.toggleRule = (key) => {
|
||||
if (_disabledRules[key]) {
|
||||
delete _disabledRules[key];
|
||||
@@ -279,6 +366,13 @@ export function coreValidator(context) {
|
||||
};
|
||||
|
||||
|
||||
// `disableRules()`
|
||||
// Disables given validation rules,
|
||||
// then reruns the validation so that the user sees something happen in the UI
|
||||
//
|
||||
// Arguments
|
||||
// `keys` - Array or Set containing rule keys to disable
|
||||
//
|
||||
validator.disableRules = (keys) => {
|
||||
_disabledRules = {};
|
||||
keys.forEach(k => _disabledRules[k] = true);
|
||||
@@ -288,17 +382,24 @@ export function coreValidator(context) {
|
||||
};
|
||||
|
||||
|
||||
validator.ignoreIssue = (id) => {
|
||||
_ignoredIssueIDs[id] = true;
|
||||
// `ignoreIssue()`
|
||||
// Don't show the given issue in lists
|
||||
//
|
||||
// Arguments
|
||||
// `issueID` - the issueID
|
||||
//
|
||||
validator.ignoreIssue = (issueID) => {
|
||||
_ignoredIssueIDs.add(issueID);
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// `validate()`
|
||||
// Validates anything that has changed in the head graph since the last time it was run.
|
||||
// (head graph contains user's edits)
|
||||
//
|
||||
// Returns a Promise fulfilled when the validation has completed and then dispatches a `validated` event.
|
||||
// This may take time but happen in the background during browser idle time.
|
||||
// Returns
|
||||
// A Promise fulfilled when the validation has completed and then dispatches a `validated` event.
|
||||
// This may take time but happen in the background during browser idle time.
|
||||
//
|
||||
validator.validate = () => {
|
||||
const currGraph = context.graph();
|
||||
@@ -336,7 +437,6 @@ export function coreValidator(context) {
|
||||
|
||||
// register event handlers:
|
||||
|
||||
|
||||
// WHEN TO RUN VALIDATION:
|
||||
// When graph changes:
|
||||
context.history()
|
||||
@@ -365,11 +465,28 @@ export function coreValidator(context) {
|
||||
|
||||
|
||||
|
||||
// `validateEntity()` (private)
|
||||
// Runs all validation rules on a single entity.
|
||||
// Some things to note:
|
||||
// - Graph is passed in from whenever the validation was started. Validators shouldn't use
|
||||
// `context.graph()` because this all happens async, and the graph might have changed
|
||||
// (for example, nodes getting deleted before the validation can run)
|
||||
// - Validator functions may still be waiting on something and return a "provisional" result.
|
||||
// In this situation, we will schedule to revalidate the entity sometime later.
|
||||
//
|
||||
// Run validation on a single entity for the given graph
|
||||
// Arguments
|
||||
// `entity` - The entity
|
||||
// `graph` - graph containing the entity
|
||||
//
|
||||
// Returns
|
||||
// Object result like:
|
||||
// {
|
||||
// issues: Array of detected issues
|
||||
// provisional: `true` if provisional result, `false` if final result
|
||||
// }
|
||||
//
|
||||
function validateEntity(entity, graph) {
|
||||
let entityIssues = [];
|
||||
let result = { issues: [], provisional: false };
|
||||
|
||||
// runs validation and appends resulting issues
|
||||
function runValidation(key) {
|
||||
@@ -380,16 +497,29 @@ export function coreValidator(context) {
|
||||
}
|
||||
|
||||
const detected = fn(entity, graph);
|
||||
entityIssues = entityIssues.concat(detected);
|
||||
if (detected.provisional) { // this validation should be run again later
|
||||
result.provisional = true;
|
||||
}
|
||||
result.issues = result.issues.concat(detected);
|
||||
}
|
||||
|
||||
// run all rules
|
||||
Object.keys(_rules).forEach(runValidation);
|
||||
|
||||
return entityIssues;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// `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();
|
||||
@@ -434,9 +564,15 @@ export function coreValidator(context) {
|
||||
}
|
||||
|
||||
|
||||
// Determine what issues were resolved for the given entities
|
||||
function updateResolvedIssues(entityIDsToCheck) {
|
||||
entityIDsToCheck.forEach(entityID => {
|
||||
// `updateResolvedIssues()` (private)
|
||||
// Determine if any issues were resolved for the given entities.
|
||||
// This is called by `validate()` after validation of the head graph
|
||||
//
|
||||
// Arguments
|
||||
// `entityIDs` - Set containing entity IDs.
|
||||
//
|
||||
function updateResolvedIssues(entityIDs) {
|
||||
entityIDs.forEach(entityID => {
|
||||
const headIssues = _headCache.issuesByEntityID[entityID];
|
||||
const baseIssues = _baseCache.issuesByEntityID[entityID];
|
||||
if (!baseIssues) return;
|
||||
@@ -452,36 +588,38 @@ export function coreValidator(context) {
|
||||
}
|
||||
|
||||
|
||||
// `validateEntitiesAsync()` (private)
|
||||
// Schedule validation for many entities.
|
||||
//
|
||||
// Run validation for several entities, supplied `entityIDs`,
|
||||
// against `graph` for the given `cache`
|
||||
// Arguments
|
||||
// `entityIDs` - Set containing entity IDs.
|
||||
// `graph` - the graph to validate that contains those entities
|
||||
// `cache` - the cache to store results in (_headCache or _baseCache)
|
||||
//
|
||||
// Returns a Promise fulfilled when the validation has completed.
|
||||
// This may take time but happen in the background during browser idle time.
|
||||
//
|
||||
// `entityIDs` - Array or Set containing entity IDs.
|
||||
// `graph` - the graph to validate that contains those entities
|
||||
// `cache` - the cache to store results in (_headCache or _baseCache)
|
||||
// Returns
|
||||
// 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) {
|
||||
// Enqueue the work
|
||||
const jobs = Array.from(entityIDs).map(entityID => {
|
||||
if (cache.queuedIDs.has(entityID)) return null; // queued already
|
||||
cache.queuedIDs.add(entityID);
|
||||
if (cache.queuedEntityIDs.has(entityID)) return null; // queued already
|
||||
cache.queuedEntityIDs.add(entityID);
|
||||
|
||||
return () => {
|
||||
// clear caches for existing issues related to this entity
|
||||
cache.uncacheEntityID(entityID);
|
||||
cache.queuedEntityIDs.delete(entityID);
|
||||
|
||||
// detect new issues and update caches
|
||||
const entity = graph.hasEntity(entityID);
|
||||
if (entity) { // don't validate deleted entities
|
||||
// todo next: promisify this part
|
||||
const issues = validateEntity(entity, graph);
|
||||
cache.cacheIssues(issues); // update cache
|
||||
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
|
||||
}
|
||||
|
||||
cache.queuedIDs.delete(entityID);
|
||||
};
|
||||
|
||||
}).filter(Boolean);
|
||||
@@ -496,6 +634,7 @@ export function coreValidator(context) {
|
||||
if (_inProcess) return _inProcess;
|
||||
|
||||
_inProcess = processQueue()
|
||||
.then(() => revalidateProvisionalEntities(cache))
|
||||
.catch(() => { /* ignore */ })
|
||||
.finally(() => _inProcess = null);
|
||||
|
||||
@@ -503,11 +642,35 @@ export function coreValidator(context) {
|
||||
}
|
||||
|
||||
|
||||
// `processQueue()`
|
||||
// Process some deferred validation work
|
||||
// `revalidateProvisionalEntities()` (private)
|
||||
// Sometimes a validator will return a "provisional" result.
|
||||
// In this situation, we'll need to revalidate the entity later.
|
||||
// This function waits a delay, then places them back into the validation queue.
|
||||
//
|
||||
// Returns a Promise fulfilled when the validation has completed.
|
||||
// This may take time but happen in the background during browser idle time.
|
||||
// Arguments
|
||||
// `cache` - The cache (_headCache or _baseCache)
|
||||
//
|
||||
function revalidateProvisionalEntities(cache) {
|
||||
if (!cache.provisionalEntityIDs.size) return; // nothing to do
|
||||
|
||||
const handle = window.setTimeout(() => {
|
||||
_deferredST.delete(handle);
|
||||
if (!cache.provisionalEntityIDs.size) return; // nothing to do
|
||||
|
||||
const graph = (cache === _headCache ? context.graph() : context.history().base());
|
||||
validateEntitiesAsync(cache.provisionalEntityIDs, graph, cache);
|
||||
}, RETRY);
|
||||
|
||||
_deferredST.add(handle);
|
||||
}
|
||||
|
||||
|
||||
// `processQueue()` (private)
|
||||
// Process the next chunk of deferred validation work
|
||||
//
|
||||
// Returns
|
||||
// A Promise fulfilled when the validation has completed.
|
||||
// This may take time but happen in the background during browser idle time.
|
||||
//
|
||||
function processQueue() {
|
||||
// console.log(`head queue length ${_headCache.queue.length}`);
|
||||
@@ -522,15 +685,15 @@ export function coreValidator(context) {
|
||||
if (!chunk) return Promise.resolve(); // we're done
|
||||
|
||||
return new Promise(resolvePromise => {
|
||||
const handle = window.requestIdleCallback(() => {
|
||||
_deferred.delete(handle);
|
||||
const handle = window.requestIdleCallback(() => {
|
||||
_deferredRIC.delete(handle);
|
||||
// const t0 = performance.now();
|
||||
chunk.forEach(job => job());
|
||||
// const t1 = performance.now();
|
||||
// console.log('chunk processed in ' + (t1 - t0) + ' ms');
|
||||
resolvePromise();
|
||||
});
|
||||
_deferred.add(handle);
|
||||
_deferredRIC.add(handle);
|
||||
})
|
||||
.then(() => { // dispatch an event sometimes to redraw various UI things
|
||||
const count = _headCache.queue.length + _baseCache.queue.length;
|
||||
@@ -544,22 +707,29 @@ export function coreValidator(context) {
|
||||
}
|
||||
|
||||
|
||||
// `validationCache()` (private)
|
||||
// Creates a cache to store validation state
|
||||
// We create 2 of these:
|
||||
// `_baseCache` for validation on the base graph (unedited)
|
||||
// `_headCache` for validation on the head graph (user edits applied)
|
||||
//
|
||||
function validationCache() {
|
||||
let cache = {
|
||||
queue: [],
|
||||
queuedIDs: new Set(),
|
||||
queuedEntityIDs: new Set(),
|
||||
provisionalEntityIDs: new Set(),
|
||||
issuesByIssueID: {}, // issue.id -> issue
|
||||
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();
|
||||
const entityIDs = issue.entityIds || [];
|
||||
entityIDs.forEach(entityID => {
|
||||
if (!cache.issuesByEntityID[entityID]) {
|
||||
cache.issuesByEntityID[entityID] = new Set();
|
||||
}
|
||||
cache.issuesByEntityID[entityId].add(issue.id);
|
||||
cache.issuesByEntityID[entityID].add(issue.id);
|
||||
});
|
||||
cache.issuesByIssueID[issue.id] = issue;
|
||||
});
|
||||
@@ -568,8 +738,8 @@ function validationCache() {
|
||||
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 => {
|
||||
const entityIDs = issue.entityIds || [];
|
||||
entityIDs.forEach(entityID => {
|
||||
if (cache.issuesByEntityID[entityID]) {
|
||||
cache.issuesByEntityID[entityID].delete(issue.id);
|
||||
}
|
||||
@@ -590,7 +760,6 @@ function validationCache() {
|
||||
// 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 issue = cache.issuesByIssueID[issueID];
|
||||
@@ -603,6 +772,7 @@ function validationCache() {
|
||||
}
|
||||
|
||||
delete cache.issuesByEntityID[entityID];
|
||||
cache.provisionalEntityIDs.delete(entityID);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ export function uiSectionValidationRules(context) {
|
||||
.property('value', degStr);
|
||||
|
||||
prefs('validate-square-degrees', degStr);
|
||||
context.validator().reloadUnsquareIssues();
|
||||
context.validator().revalidateUnsquare();
|
||||
}
|
||||
|
||||
function isRuleEnabled(d) {
|
||||
|
||||
@@ -7,24 +7,22 @@ import { actionChangeTags } from '../actions/change_tags';
|
||||
import { actionUpgradeTags } from '../actions/upgrade_tags';
|
||||
import { presetManager } from '../presets';
|
||||
import { osmIsOldMultipolygonOuterMember, osmOldMultipolygonOuterMemberOfRelation } from '../osm/multipolygon';
|
||||
import { utilArrayUniq, utilDisplayLabel, utilTagDiff } from '../util';
|
||||
import { utilArrayUniq, utilDisplayLabel, utilHashcode, utilTagDiff } from '../util';
|
||||
import { validationIssue, validationIssueFix } from '../core/validation';
|
||||
|
||||
|
||||
let _dataDeprecated;
|
||||
let _nsi;
|
||||
|
||||
export function validationOutdatedTags() {
|
||||
const type = 'outdated_tags';
|
||||
|
||||
// A concern here in switching to async data means that `_dataDeprecated`
|
||||
// and `_nsi` will not be available at first, so the data on early tiles
|
||||
// may not have tags validated fully.
|
||||
let _waitingForDeprecated = true;
|
||||
let _waitingForNSI = true;
|
||||
let _dataDeprecated;
|
||||
let _nsi;
|
||||
|
||||
// fetch deprecated tags
|
||||
fileFetcher.get('deprecated')
|
||||
.then(d => _dataDeprecated = d)
|
||||
.catch(() => { /* ignore */ });
|
||||
.catch(() => { /* ignore */ })
|
||||
.finally(() => _waitingForDeprecated = false);
|
||||
|
||||
|
||||
function delay(msec) {
|
||||
@@ -92,10 +90,10 @@ export function validationOutdatedTags() {
|
||||
});
|
||||
|
||||
_nsi.keys.add('building'); // fallback can match building=* for some categories
|
||||
|
||||
return _nsi;
|
||||
})
|
||||
.catch(() => { /* ignore */ });
|
||||
.catch(() => { /* ignore */ })
|
||||
.finally(() => _waitingForNSI = false);
|
||||
|
||||
|
||||
// Returns true if this tag key is a "namelike" tag that the NSI matcher would have indexed..
|
||||
@@ -124,6 +122,7 @@ export function validationOutdatedTags() {
|
||||
let preset = presetManager.match(entity, graph);
|
||||
let subtype = 'deprecated_tags';
|
||||
if (!preset) return [];
|
||||
if (!entity.hasInterestingTags()) return [];
|
||||
|
||||
// Upgrade preset, if a replacement is available..
|
||||
if (preset.replacement) {
|
||||
@@ -301,9 +300,12 @@ export function validationOutdatedTags() {
|
||||
|
||||
} // end if _nsi
|
||||
|
||||
let issues = [];
|
||||
issues.provisional = (_waitingForDeprecated || _waitingForNSI);
|
||||
|
||||
// determine diff
|
||||
const tagDiff = utilTagDiff(oldTags, newTags);
|
||||
if (!tagDiff.length) return [];
|
||||
if (!tagDiff.length) return issues;
|
||||
|
||||
const isOnlyAddingTags = tagDiff.every(d => d.type === '+');
|
||||
|
||||
@@ -318,14 +320,14 @@ export function validationOutdatedTags() {
|
||||
// don't allow autofixing brand tags
|
||||
let autoArgs = subtype !== 'noncanonical_brand' ? [doUpgrade, t('issues.fix.upgrade_tags.annotation')] : null;
|
||||
|
||||
return [new validationIssue({
|
||||
issues.push(new validationIssue({
|
||||
type: type,
|
||||
subtype: subtype,
|
||||
severity: 'warning',
|
||||
message: showMessage,
|
||||
reference: showReference,
|
||||
entityIds: [entity.id],
|
||||
hash: JSON.stringify(tagDiff),
|
||||
hash: utilHashcode(JSON.stringify(tagDiff)),
|
||||
dynamicFixes: () => {
|
||||
return [
|
||||
new validationIssueFix({
|
||||
@@ -337,7 +339,8 @@ export function validationOutdatedTags() {
|
||||
})
|
||||
];
|
||||
}
|
||||
})];
|
||||
}));
|
||||
return issues;
|
||||
|
||||
|
||||
function doUpgrade(graph) {
|
||||
|
||||
@@ -5,28 +5,30 @@ import { validationIssue, validationIssueFix } from '../core/validation';
|
||||
import { actionChangeTags } from '../actions/change_tags';
|
||||
|
||||
|
||||
let _discardNameRegexes = [];
|
||||
|
||||
export function validationSuspiciousName() {
|
||||
const type = 'suspicious_name';
|
||||
const keysToTestForGenericValues = [
|
||||
'aerialway', 'aeroway', 'amenity', 'building', 'craft', 'highway',
|
||||
'leisure', 'railway', 'man_made', 'office', 'shop', 'tourism', 'waterway'
|
||||
];
|
||||
|
||||
// A concern here in switching to async data means that `_nsiFilters` will not
|
||||
// be available at first, so the data on early tiles may not have tags validated fully.
|
||||
let _dataGenerics;
|
||||
let _waitingForGenerics = true;
|
||||
|
||||
fileFetcher.get('nsi_generics')
|
||||
.then(data => {
|
||||
if (_dataGenerics) return _dataGenerics;
|
||||
|
||||
// known list of generic names (e.g. "bar")
|
||||
_discardNameRegexes = data.genericWords.map(pattern => new RegExp(pattern, 'i'));
|
||||
_dataGenerics = data.genericWords.map(pattern => new RegExp(pattern, 'i'));
|
||||
return _dataGenerics;
|
||||
})
|
||||
.catch(() => { /* ignore */ });
|
||||
.catch(() => { /* ignore */ })
|
||||
.finally(() => _waitingForGenerics = false);
|
||||
|
||||
|
||||
function isDiscardedSuggestionName(lowercaseName) {
|
||||
return _discardNameRegexes.some(regex => regex.test(lowercaseName));
|
||||
if (!_dataGenerics) return false;
|
||||
return _dataGenerics.some(regex => regex.test(lowercaseName));
|
||||
}
|
||||
|
||||
// test if the name is just the key or tag value (e.g. "park")
|
||||
@@ -68,7 +70,7 @@ export function validationSuspiciousName() {
|
||||
},
|
||||
reference: showReference,
|
||||
entityIds: [entityId],
|
||||
hash: nameKey + '=' + genericName,
|
||||
hash: `${nameKey}=${genericName}`,
|
||||
dynamicFixes: function() {
|
||||
return [
|
||||
new validationIssueFix({
|
||||
@@ -114,7 +116,7 @@ export function validationSuspiciousName() {
|
||||
},
|
||||
reference: showReference,
|
||||
entityIds: [entityId],
|
||||
hash: nameKey + '=' + incorrectName,
|
||||
hash: `${nameKey}=${incorrectName}`,
|
||||
dynamicFixes: function() {
|
||||
return [
|
||||
new validationIssueFix({
|
||||
@@ -168,6 +170,7 @@ export function validationSuspiciousName() {
|
||||
}
|
||||
}
|
||||
if (isGenericName(value, entity.tags)) {
|
||||
issues.provisional = _waitingForGenerics; // retry later if we don't have the generics yet
|
||||
issues.push(makeGenericNameIssue(entity.id, key, value, langCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export function validationUnsquareWay(context) {
|
||||
},
|
||||
reference: showReference,
|
||||
entityIds: [entity.id],
|
||||
hash: JSON.stringify(autoArgs !== undefined) + degreeThreshold,
|
||||
hash: degreeThreshold,
|
||||
dynamicFixes: function() {
|
||||
return [
|
||||
new validationIssueFix({
|
||||
|
||||
Reference in New Issue
Block a user