Merge branch 'master' into validation

This commit is contained in:
Quincy Morgan
2018-12-18 15:56:15 -05:00
26 changed files with 1343 additions and 127 deletions
+24 -4
View File
@@ -21,7 +21,8 @@ import { rendererBackground, rendererFeatures, rendererMap } from '../renderer';
import { services } from '../services';
import { uiInit } from '../ui/init';
import { utilDetect } from '../util/detect';
import { utilCallWhenIdle, utilKeybinding, utilRebind } from '../util';
import { utilCallWhenIdle, utilKeybinding, utilRebind, utilStringQs } from '../util';
export var areaKeys = {};
@@ -470,6 +471,18 @@ export function coreContext() {
features = rendererFeatures(context);
presets = presetIndex();
if (services.maprules && utilStringQs(window.location.hash).validations) {
var validations = utilStringQs(window.location.hash).validations;
d3_json(validations, function (err, mapcss) {
if (err) return;
services.maprules.init(context.presets().areaKeys());
_each(mapcss, function(mapcssSelector) {
return services.maprules.addRule(mapcssSelector);
});
context.validationRules = true;
});
}
map = rendererMap(context);
context.mouse = map.mouse;
context.extent = map.extent;
@@ -488,9 +501,16 @@ export function coreContext() {
background.init();
features.init();
presets.init();
areaKeys = presets.areaKeys();
if (utilStringQs(window.location.hash).presets) {
var external = utilStringQs(window.location.hash).presets;
presets.fromExternal(external, function(externalPresets) {
context.presets = function() { return externalPresets; }; // default + external presets...
areaKeys = presets.areaKeys();
});
} else {
presets.init();
areaKeys = presets.areaKeys();
}
return utilRebind(context, dispatch, 'on');
}
+3 -3
View File
@@ -281,9 +281,9 @@ export function coreHistory(context) {
validate: function(changes) {
return _flatten(
_map(Validations, function(fn) { return fn()(changes, _stack[_index].graph); })
);
return _flatten(_map(Validations, function(fn) {
return fn()(changes, _stack[_index].graph);
}));
},
+6
View File
@@ -1,5 +1,6 @@
import _filter from 'lodash-es/filter';
import _find from 'lodash-es/find';
import _findIndex from 'lodash-es/findIndex';
import _some from 'lodash-es/some';
import _uniq from 'lodash-es/uniq';
import _values from 'lodash-es/values';
@@ -23,6 +24,11 @@ export function presetCollection(collection) {
});
},
index: function(id) {
return _findIndex(this.collection, function(d) {
return d.id === id;
});
},
matchGeometry: function(geometry) {
return presetCollection(this.collection.filter(function(d) {
+1 -1
View File
@@ -21,7 +21,7 @@ export function presetField(id, field) {
field.label = function() {
return field.t('label', {'default': id});
return field.overrideLabel || field.t('label', {'default': id});
};
+57 -13
View File
@@ -3,6 +3,8 @@ import _forEach from 'lodash-es/forEach';
import _reject from 'lodash-es/reject';
import _uniq from 'lodash-es/uniq';
import { json as d3_json } from 'd3-request';
import { data } from '../../data/index';
import { presetCategory } from './category';
import { presetCollection } from './collection';
@@ -70,7 +72,6 @@ export function presetIndex() {
if (address && (!match || match.isFallback())) {
match = address;
}
return match || all.item(geometry);
});
};
@@ -120,16 +121,7 @@ export function presetIndex() {
return areaKeys;
};
all.init = function() {
var d = data.presets;
all.collection = [];
_recent.collection = [];
_fields = {};
_universal = [];
_index = { point: {}, vertex: {}, line: {}, area: {}, relation: {} };
all.build = function(d, visible) {
if (d.fields) {
_forEach(d.fields, function(d, id) {
_fields[id] = presetField(id, d);
@@ -145,13 +137,23 @@ export function presetIndex() {
if (d.presets) {
_forEach(d.presets, function(d, id) {
all.collection.push(presetPreset(id, d, _fields));
var existing = all.index(id);
if (existing !== -1) {
all.collection[existing] = presetPreset(id, d, _fields, visible);
} else {
all.collection.push(presetPreset(id, d, _fields, visible));
}
});
}
if (d.categories) {
_forEach(d.categories, function(d, id) {
all.collection.push(presetCategory(id, d, all));
var existing = all.index(id);
if (existing !== -1) {
all.collection[existing] = presetCategory(id, d, all);
} else {
all.collection.push(presetCategory(id, d, all));
}
});
}
@@ -177,10 +179,52 @@ export function presetIndex() {
}
}
}
return all;
};
all.init = function() {
all.collection = [];
_recent.collection = [];
_fields = {};
_universal = [];
_index = { point: {}, vertex: {}, line: {}, area: {}, relation: {} };
return all.build(data.presets, true);
};
all.reset = function() {
all.collection = [];
_defaults = { area: all, line: all, point: all, vertex: all, relation: all };
_fields = {};
_universal = [];
_recent = presetCollection([]);
// Index of presets by (geometry, tag key).
_index = {
point: {},
vertex: {},
line: {},
area: {},
relation: {}
};
return all;
};
all.fromExternal = function(external, done) {
all.reset();
d3_json(external, function(err, externalPresets) {
if (err) {
all.init();
} else {
all.build(data.presets, false); // make default presets hidden to begin
all.build(externalPresets, true); // make the external visible
}
done(all);
});
};
all.field = function(id) {
return _fields[id];
};
+8 -1
View File
@@ -5,7 +5,7 @@ import { t } from '../util/locale';
import { areaKeys } from '../core/context';
export function presetPreset(id, preset, fields) {
export function presetPreset(id, preset, fields, visible) {
preset = _clone(preset);
preset.id = id;
@@ -13,6 +13,7 @@ export function presetPreset(id, preset, fields) {
preset.moreFields = (preset.moreFields || []).map(getFields);
preset.geometry = (preset.geometry || []);
visible = visible || false;
function getFields(f) {
return fields[f];
@@ -71,6 +72,12 @@ export function presetPreset(id, preset, fields) {
return tagCount === 0 || (tagCount === 1 && preset.tags.hasOwnProperty('area'));
};
preset.visible = function(_) {
if (!arguments.length) return visible;
visible = _;
return visible;
};
var reference = preset.reference || {};
preset.reference = function(geometry) {
+4
View File
@@ -1,4 +1,5 @@
import serviceMapillary from './mapillary';
import serviceMapRules from './maprules';
import serviceNominatim from './nominatim';
import serviceOpenstreetcam from './openstreetcam';
import serviceOsm from './osm';
@@ -8,11 +9,13 @@ import serviceVectorTile from './vector_tile';
import serviceWikidata from './wikidata';
import serviceWikipedia from './wikipedia';
export var services = {
geocoder: serviceNominatim,
mapillary: serviceMapillary,
openstreetcam: serviceOpenstreetcam,
osm: serviceOsm,
maprules: serviceMapRules,
streetside: serviceStreetside,
taginfo: serviceTaginfo,
vectorTile: serviceVectorTile,
@@ -22,6 +25,7 @@ export var services = {
export {
serviceMapillary,
serviceMapRules,
serviceNominatim,
serviceOpenstreetcam,
serviceOsm,
+227
View File
@@ -0,0 +1,227 @@
import _isMatch from 'lodash-es/isMatch';
import _intersection from 'lodash-es/intersection';
import _reduce from 'lodash-es/reduce';
import _every from 'lodash-es/every';
var buildRuleChecks = function() {
return {
equals: function (equals) {
return function(tags) {
return _isMatch(tags, equals);
};
},
notEquals: function (notEquals) {
return function(tags) {
return !_isMatch(tags, notEquals);
};
},
absence: function(absence) {
return function(tags) {
return Object.keys(tags).indexOf(absence) === -1;
};
},
presence: function(presence) {
return function(tags) {
return Object.keys(tags).indexOf(presence) > -1;
};
},
greaterThan: function(greaterThan) {
var key = Object.keys(greaterThan)[0];
var value = greaterThan[key];
return function(tags) {
return tags[key] > value;
};
},
greaterThanEqual: function(greaterThanEqual) {
var key = Object.keys(greaterThanEqual)[0];
var value = greaterThanEqual[key];
return function(tags) {
return tags[key] >= value;
};
},
lessThan: function(lessThan) {
var key = Object.keys(lessThan)[0];
var value = lessThan[key];
return function(tags) {
return tags[key] < value;
};
},
lessThanEqual: function(lessThanEqual) {
var key = Object.keys(lessThanEqual)[0];
var value = lessThanEqual[key];
return function(tags) {
return tags[key] <= value;
};
},
positiveRegex: function(positiveRegex) {
var tagKey = Object.keys(positiveRegex)[0];
var expression = positiveRegex[tagKey].join('|');
var regex = new RegExp(expression);
return function(tags) {
return regex.test(tags[tagKey]);
};
},
negativeRegex: function(negativeRegex) {
var tagKey = Object.keys(negativeRegex)[0];
var expression = negativeRegex[tagKey].join('|');
var regex = new RegExp(expression);
return function(tags) {
return !regex.test(tags[tagKey]);
};
}
};
};
var buildLineKeys = function() {
return {
highway: {
rest_area: true,
services: true
},
railway: {
roundhouse: true,
station: true,
traverser: true,
turntable: true,
wash: true
}
};
};
export default {
init: function(areaKeys) {
this._ruleChecks = buildRuleChecks();
this._validationRules = [];
this._areaKeys = areaKeys;
this._lineKeys = buildLineKeys();
},
// list of rules only relevant to tag checks...
filterRuleChecks: function(selector) {
var _ruleChecks = this._ruleChecks;
return _reduce(Object.keys(selector), function(rules, key) {
if (['geometry', 'error', 'warning'].indexOf(key) === -1) {
rules.push(_ruleChecks[key](selector[key]));
}
return rules;
}, []);
},
// builds tagMap from mapcss-parse selector object...
buildTagMap: function(selector) {
var getRegexValues = function(regexes) {
return regexes.map(function(regex) {
return regex.replace(/\$|\^/g, '');
});
};
var selectorKeys = Object.keys(selector);
var tagMap = _reduce(selectorKeys, function (expectedTags, key) {
var values;
var isRegex = /regex/gi.test(key);
var isEqual = /equals/gi.test(key);
if (isRegex || isEqual) {
Object.keys(selector[key]).forEach(function(selectorKey) {
values = isEqual ? [selector[key][selectorKey]] : getRegexValues(selector[key][selectorKey]);
if (expectedTags.hasOwnProperty(selectorKey)) {
values = values.concat(expectedTags[selectorKey]);
}
expectedTags[selectorKey] = values;
});
} else if (/(greater|less)Than(Equal)?|presence/g.test(key)) {
var tagKey = /presence/.test(key) ? selector[key] : Object.keys(selector[key])[0];
values = [selector[key][tagKey]];
if (expectedTags.hasOwnProperty(tagKey)) {
values = values.concat(expectedTags[tagKey]);
}
expectedTags[tagKey] = values;
}
return expectedTags;
}, {});
return tagMap;
},
// inspired by osmWay#isArea()
inferGeometry: function(tagMap) {
var _lineKeys = this._lineKeys;
var _areaKeys = this._areaKeys;
var isAreaKeyBlackList = function(key) {
return _intersection(tagMap[key], Object.keys(_areaKeys[key])).length > 0;
};
var isLineKeysWhiteList = function(key) {
return _intersection(tagMap[key], Object.keys(_lineKeys[key])).length > 0;
};
if (tagMap.hasOwnProperty('area')) {
if (tagMap.area.indexOf('yes') > -1) {
return 'area';
}
if (tagMap.area.indexOf('no') > -1) {
return 'line';
}
}
for (var key in tagMap) {
if (key in _areaKeys && !isAreaKeyBlackList(key)) {
return 'area';
}
if (key in _lineKeys && isLineKeysWhiteList(key)) {
return 'area';
}
}
return 'line';
},
// adds from mapcss-parse selector check...
addRule: function(selector) {
var rule = {
// checks relevant to mapcss-selector
checks: this.filterRuleChecks(selector),
// true if all conditions for a tag error are true..
matches: function(entity) {
return _every(this.checks, function(check) {
return check(entity.tags);
});
},
// borrowed from Way#isArea()
inferredGeometry: this.inferGeometry(this.buildTagMap(selector), this._areaKeys),
geometryMatches: function(entity, graph) {
if (entity.type === 'node' || entity.type === 'relation') {
return selector.geometry === entity.type;
} else if (entity.type === 'way') {
return this.inferredGeometry === entity.geometry(graph);
}
},
// when geometries match and tag matches are present, return a warning...
findWarnings: function (entity, graph, warnings) {
if (this.geometryMatches(entity, graph) && this.matches(entity)) {
var type = Object.keys(selector).indexOf('error') > -1 ? 'error' : 'warning';
warnings.push({
severity: type,
message: selector[type],
entity: entity
});
}
}
};
this._validationRules.push(rule);
},
clearRules: function() { this._validationRules = []; },
// returns validationRules...
validationRules: function() { return this._validationRules; },
// returns ruleChecks
ruleChecks: function() { return this._ruleChecks; }
};
+88 -71
View File
@@ -3,91 +3,108 @@ import { modeSelect } from '../modes';
import { svgIcon } from '../svg';
import { tooltip } from '../util/tooltip';
import { utilEntityOrMemberSelector } from '../util';
import _reduce from 'lodash-es/reduce';
import _forEach from 'lodash-es/forEach';
import _uniqBy from 'lodash-es/uniqBy';
export function uiCommitWarnings(context) {
function commitWarnings(selection) {
var changes = context.history().changes();
var warnings = context.history().validate(changes);
var validations = context.history().validate(changes);
var container = selection.selectAll('.warning-section')
.data(warnings.length ? [0] : []);
container.exit()
.remove();
var containerEnter = container.enter()
.append('div')
.attr('class', 'modal-section warning-section fillL2');
containerEnter
.append('h3')
.text(t('commit.warnings'));
containerEnter
.append('ul')
.attr('class', 'changeset-list');
container = containerEnter
.merge(container);
var items = container.select('ul').selectAll('li')
.data(warnings);
items.exit()
.remove();
var itemsEnter = items.enter()
.append('li')
.attr('class', 'warning-item');
itemsEnter
.call(svgIcon('#iD-icon-alert', 'pre-text'));
itemsEnter
.append('strong')
.text(function(d) { return d.message; });
itemsEnter.filter(function(d) { return d.tooltip; })
.call(tooltip()
.title(function(d) { return d.tooltip; })
.placement('top')
);
items = itemsEnter
.merge(items);
items
.on('mouseover', mouseover)
.on('mouseout', mouseout)
.on('click', warningClick);
function mouseover(d) {
if (d.entity) {
context.surface().selectAll(
utilEntityOrMemberSelector([d.entity.id], context.graph())
).classed('hover', true);
validations = _reduce(validations, function(validations, val) {
var severity = val.severity;
if (validations.hasOwnProperty(severity)) {
validations[severity].push(val);
} else {
validations[severity] = [val];
}
}
return validations;
}, {});
_forEach(validations, function(instances, type) {
instances = _uniqBy(instances, function(val) { return val.id + '_' + val.message.replace(/\s+/g,''); });
var section = type + '-section';
var instanceItem = type + '-item';
var container = selection.selectAll('.' + section)
.data(instances.length ? [0] : []);
container.exit()
.remove();
var containerEnter = container.enter()
.append('div')
.attr('class', 'modal-section ' + section + ' fillL2');
containerEnter
.append('h3')
.text(type === 'warning' ? t('commit.warnings') : t('commit.errors'));
containerEnter
.append('ul')
.attr('class', 'changeset-list');
container = containerEnter
.merge(container);
function mouseout() {
context.surface().selectAll('.hover')
.classed('hover', false);
}
var items = container.select('ul').selectAll('li')
.data(instances);
items.exit()
.remove();
var itemsEnter = items.enter()
.append('li')
.attr('class', instanceItem);
itemsEnter
.call(svgIcon('#iD-icon-alert', 'pre-text'));
itemsEnter
.append('strong')
.text(function(d) { return d.message; });
itemsEnter.filter(function(d) { return d.tooltip; })
.call(tooltip()
.title(function(d) { return d.tooltip; })
.placement('top')
);
items = itemsEnter
.merge(items);
items
.on('mouseover', mouseover)
.on('mouseout', mouseout)
.on('click', warningClick);
function warningClick(d) {
if (d.entity) {
context.map().zoomTo(d.entity);
context.enter(modeSelect(context, [d.entity.id]));
function mouseover(d) {
if (d.entity) {
context.surface().selectAll(
utilEntityOrMemberSelector([d.entity.id], context.graph())
).classed('hover', true);
}
}
}
function mouseout() {
context.surface().selectAll('.hover')
.classed('hover', false);
}
function warningClick(d) {
if (d.entity) {
context.map().zoomTo(d.entity);
context.enter(modeSelect(context, [d.entity.id]));
}
}
});
}
-1
View File
@@ -38,7 +38,6 @@ export function uiEntityEditor(context) {
var rawMemberEditor = uiRawMemberEditor(context);
var rawMembershipEditor = uiRawMembershipEditor(context);
function entityEditor(selection) {
var entity = context.entity(_entityID);
var tags = _clone(entity.tags);
+3 -3
View File
@@ -467,7 +467,7 @@ export function uiMapData(context) {
function renderDataLayers(selection) {
var container = selection.selectAll('data-layer-container')
var container = selection.selectAll('.data-layer-container')
.data([0]);
_dataLayerContainer = container.enter()
@@ -478,7 +478,7 @@ export function uiMapData(context) {
function renderFillList(selection) {
var container = selection.selectAll('layer-fill-list')
var container = selection.selectAll('.layer-fill-list')
.data([0]);
_fillList = container.enter()
@@ -489,7 +489,7 @@ export function uiMapData(context) {
function renderFeatureList(selection) {
var container = selection.selectAll('layer-feature-list')
var container = selection.selectAll('.layer-feature-list')
.data([0]);
_featureList = container.enter()
-3
View File
@@ -14,7 +14,6 @@ import { svgIcon } from '../svg';
import { tooltip } from '../util/tooltip';
import { uiTooltipHtml } from './tooltipHtml';
export function uiModes(context) {
var modes = [
modeAddPoint(context),
@@ -23,7 +22,6 @@ export function uiModes(context) {
modeAddNote(context)
];
function editable() {
var mode = context.mode();
return context.editable() && mode && mode.id !== 'save';
@@ -39,7 +37,6 @@ export function uiModes(context) {
return context.map().notesEditable() && mode && mode.id !== 'save';
}
return function(selection) {
context
.on('enter.editor', function(entered) {
+8 -3
View File
@@ -151,9 +151,14 @@ export function uiPresetList(context) {
function drawList(list, presets) {
var collection = presets.collection.map(function(preset) {
return preset.members ? CategoryItem(preset) : PresetItem(preset);
});
var collection = presets.collection.reduce(function(collection, preset) {
if (preset.members) {
collection.push(CategoryItem(preset));
} else if (preset.visible()) {
collection.push(PresetItem(preset));
}
return collection;
}, []);
var items = list.selectAll('.preset-list-item')
.data(collection, function(d) { return d.preset.id; });
+3
View File
@@ -8,6 +8,8 @@ export { utilEditDistance } from './util';
export { utilEntitySelector } from './util';
export { utilEntityOrMemberSelector } from './util';
export { utilEntityOrDeepMemberSelector } from './util';
export { utilExternalPresets } from './util';
export { utilExternalValidationRules } from './util';
export { utilFastMouse } from './util';
export { utilFunctor } from './util';
export { utilGetAllNodes } from './util';
@@ -25,6 +27,7 @@ export { utilRebind } from './rebind';
export { utilSetTransform } from './util';
export { utilSessionMutex } from './session_mutex';
export { utilStringQs } from './util';
// export { utilSuggestNames } from './suggest_names';
export { utilTagText } from './util';
export { utilTiler } from './tiler';
export { utilTriggerEvent } from './trigger_event';
+7
View File
@@ -293,6 +293,13 @@ export function utilNoAuto(selection) {
.attr('spellcheck', isText ? 'true' : 'false');
}
export function utilExternalPresets() {
return utilStringQs(window.location.hash).hasOwnProperty('presets');
}
export function utilExternalValidationRules() {
return utilStringQs(window.location.hash).hasOwnProperty('validations');
}
// https://stackoverflow.com/questions/194846/is-there-any-kind-of-hash-code-function-in-javascript
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
+1
View File
@@ -1,6 +1,7 @@
export { validationDeprecatedTag } from './deprecated_tag';
export { validationDisconnectedHighway } from './disconnected_highway';
export { validationManyDeletions } from './many_deletions';
export { validationMapCSSChecks } from './mapcss_checks';
export { validationMissingTag } from './missing_tag';
export { validationOldMultipolygon } from './old_multipolygon';
export { validationTagSuggestsArea } from './tag_suggests_area';
+25
View File
@@ -0,0 +1,25 @@
import { services } from '../services';
export function validationMapCSSChecks() {
var validation = function(changes, graph) {
if (!services.maprules) return [];
var rules = services.maprules.validationRules();
var warnings = [];
var createdModified = ['created', 'modified'];
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
for (var j = 0; j < createdModified.length; j++) {
var type = createdModified[j];
var entities = changes[type];
for (var k = 0; k < entities.length; k++) {
rule.findWarnings(entities[k], graph, warnings);
}
}
}
return warnings;
};
return validation;
}