drop validator which checks for old style multipolygons

these have long been [fixed](https://blog.jochentopf.com/2017-08-28-polygon-fixing-effort-concluded.html) in OSM

see wiki: https://wiki.openstreetmap.org/wiki/Old_style_multipolygons
This commit is contained in:
Martin Raifer
2024-02-29 13:12:52 +01:00
parent 2ae417fe1a
commit 97442403cf
13 changed files with 23 additions and 449 deletions
+1
View File
@@ -42,6 +42,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
#### :scissors: Operations
#### :camera: Street-Level
#### :white_check_mark: Validation
* Drop validation which checks for [old style multipolygons](https://wiki.openstreetmap.org/wiki/Old_style_multipolygons), as these have long been [fixed](https://blog.jochentopf.com/2017-08-28-polygon-fixing-effort-concluded.html) in OSM
#### :bug: Bugfixes
#### :earth_asia: Localization
#### :hourglass: Performance
+1 -9
View File
@@ -1,6 +1,5 @@
import { actionAddMember } from './add_member';
import { geoSphericalDistance } from '../geo/geo';
import { osmIsOldMultipolygonOuterMember } from '../osm/multipolygon';
import { osmRelation } from '../osm/relation';
import { osmWay } from '../osm/way';
import { utilArrayIntersection, utilWrap, utilArrayUniq } from '../util';
@@ -100,7 +99,6 @@ export function actionSplit(nodeIds, newWayIds) {
var nodesA;
var nodesB;
var isArea = wayA.isArea();
var isOuter = osmIsOldMultipolygonOuterMember(wayA, graph);
if (wayA.isClosed()) {
var nodes = wayA.nodes.slice(0, -1);
@@ -220,12 +218,6 @@ export function actionSplit(nodeIds, newWayIds) {
// 1. Both `wayA` and `wayB` remain in the relation
// 2. But must be inserted as a pair (see `actionAddMember` for details)
} else {
if (relation === isOuter) {
graph = graph.replace(relation.mergeTags(wayA.tags));
graph = graph.replace(wayA.update({ tags: {} }));
graph = graph.replace(wayB.update({ tags: {} }));
}
member = {
id: wayB.id,
type: 'way',
@@ -242,7 +234,7 @@ export function actionSplit(nodeIds, newWayIds) {
}
});
if (!isOuter && isArea) {
if (isArea) {
var multipolygon = osmRelation({
tags: Object.assign({}, wayA.tags, { type: 'multipolygon' }),
members: [
-3
View File
@@ -17,9 +17,6 @@ export {
} from './lanes';
export {
osmOldMultipolygonOuterMemberOfRelation,
osmIsOldMultipolygonOuterMember,
osmOldMultipolygonOuterMember,
osmJoinWays
} from './multipolygon';
-102
View File
@@ -1,109 +1,7 @@
import { actionReverse } from '../actions/reverse';
import { osmIsInterestingTag } from './tags';
import { osmWay } from './way';
// "Old" multipolyons, previously known as "simple" multipolygons, are as follows:
//
// 1. Relation tagged with `type=multipolygon` and no interesting tags.
// 2. One and only one member with the `outer` role. Must be a way with interesting tags.
// 3. No members without a role.
//
// Old multipolygons are no longer recommended but are still rendered as areas by iD.
export function osmOldMultipolygonOuterMemberOfRelation(entity, graph) {
if (entity.type !== 'relation' ||
!entity.isMultipolygon()
|| Object.keys(entity.tags).filter(osmIsInterestingTag).length > 1) {
return false;
}
var outerMember;
for (var memberIndex in entity.members) {
var member = entity.members[memberIndex];
if (!member.role || member.role === 'outer') {
if (outerMember) return false;
if (member.type !== 'way') return false;
if (!graph.hasEntity(member.id)) return false;
outerMember = graph.entity(member.id);
if (Object.keys(outerMember.tags).filter(osmIsInterestingTag).length === 0) {
return false;
}
}
}
return outerMember;
}
// For fixing up rendering of multipolygons with tags on the outer member.
// https://github.com/openstreetmap/iD/issues/613
export function osmIsOldMultipolygonOuterMember(entity, graph) {
if (entity.type !== 'way' ||
Object.keys(entity.tags).filter(osmIsInterestingTag).length === 0) {
return false;
}
var parents = graph.parentRelations(entity);
if (parents.length !== 1) return false;
var parent = parents[0];
if (!parent.isMultipolygon() ||
Object.keys(parent.tags).filter(osmIsInterestingTag).length > 1) {
return false;
}
var members = parent.members, member;
for (var i = 0; i < members.length; i++) {
member = members[i];
if (member.id === entity.id && member.role && member.role !== 'outer') {
// Not outer member
return false;
}
if (member.id !== entity.id && (!member.role || member.role === 'outer')) {
// Not a simple multipolygon
return false;
}
}
return parent;
}
export function osmOldMultipolygonOuterMember(entity, graph) {
if (entity.type !== 'way') return false;
var parents = graph.parentRelations(entity);
if (parents.length !== 1) return false;
var parent = parents[0];
if (!parent.isMultipolygon() ||
Object.keys(parent.tags).filter(osmIsInterestingTag).length > 1) {
return false;
}
var members = parent.members, member, outerMember;
for (var i = 0; i < members.length; i++) {
member = members[i];
if (!member.role || member.role === 'outer') {
if (outerMember) return false; // Not a simple multipolygon
outerMember = member;
}
}
if (!outerMember) return false;
var outerEntity = graph.hasEntity(outerMember.id);
if (!outerEntity ||
!Object.keys(outerEntity.tags).filter(osmIsInterestingTag).length) {
return false;
}
return outerEntity;
}
// Join `toJoin` array into sequences of connecting ways.
// Segments which share identical start/end nodes will, as much as possible,
+2 -10
View File
@@ -1,7 +1,7 @@
import deepEqual from 'fast-deep-equal';
import { bisector as d3_bisector } from 'd3-array';
import { osmEntity, osmIsOldMultipolygonOuterMember } from '../osm';
import { osmEntity } from '../osm';
import { svgPath, svgSegmentWay } from './helpers';
import { svgTagClasses } from './tag_classes';
import { svgTagPattern } from './tag_pattern';
@@ -90,20 +90,12 @@ export function svgAreas(projection, context) {
function drawAreas(selection, graph, entities, filter) {
var path = svgPath(projection, graph, true);
var areas = {};
var multipolygon;
var base = context.history().base();
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
if (entity.geometry(graph) !== 'area') continue;
multipolygon = osmIsOldMultipolygonOuterMember(entity, graph);
if (multipolygon) {
areas[multipolygon.id] = {
entity: multipolygon.mergeTags(entity.tags),
area: Math.abs(entity.area(graph))
};
} else if (!areas[entity.id]) {
if (!areas[entity.id]) {
areas[entity.id] = {
entity: entity,
area: Math.abs(entity.area(graph))
+2 -6
View File
@@ -6,7 +6,7 @@ import {
} from './helpers';
import { svgTagClasses } from './tag_classes';
import { osmEntity, osmOldMultipolygonOuterMember } from '../osm';
import { osmEntity } from '../osm';
import { utilArrayFlatten, utilArrayGroupBy } from '../util';
import { utilDetect } from '../util/detect';
@@ -237,11 +237,7 @@ export function svgLines(projection, context) {
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
var outer = osmOldMultipolygonOuterMember(entity, graph);
if (outer) {
ways.push(entity.mergeTags(outer.tags));
oldMultiPolygonOuters[outer.id] = true;
} else if (entity.geometry(graph) === 'line'
if (entity.geometry(graph) === 'line'
// to render side-markers for coastlines (see
// https://github.com/openstreetmap/iD/issues/9293)
|| entity.geometry(graph) === 'area' && entity.sidednessIdentifier
+1 -5
View File
@@ -1,6 +1,5 @@
import { operationDelete } from '../operations/delete';
import { osmIsInterestingTag } from '../osm/tags';
import { osmOldMultipolygonOuterMemberOfRelation } from '../osm/multipolygon';
import { t } from '../core/localizer';
import { utilDisplayLabel } from '../util';
import { validationIssue, validationIssueFix } from '../core/validation';
@@ -25,10 +24,7 @@ export function validationMissingTag(context) {
entity.tags.type === 'multipolygon') {
// this relation's only interesting tag just says its a multipolygon,
// which is not descriptive enough
// It's okay for a simple multipolygon to have no descriptive tags
// if its outer way has them (old model, see `outdated_tags.js`)
return osmOldMultipolygonOuterMemberOfRelation(entity, graph);
return false;
}
return entityDescriptiveKeys.length > 0;
+16 -75
View File
@@ -6,7 +6,6 @@ import { actionUpgradeTags } from '../actions/upgrade_tags';
import { fileFetcher } from '../core';
import { presetManager } from '../presets';
import { services } from '../services';
import { osmIsOldMultipolygonOuterMember, osmOldMultipolygonOuterMemberOfRelation } from '../osm/multipolygon';
import { utilDisplayLabel, utilHashcode, utilTagDiff } from '../util';
import { validationIssue, validationIssueFix } from '../core/validation';
@@ -40,12 +39,18 @@ export function validationOutdatedTags() {
preset = newPreset;
}
const upgradeReasons = [];
// Upgrade deprecated tags..
if (_dataDeprecated) {
const deprecatedTags = entity.deprecatedTags(_dataDeprecated);
if (deprecatedTags.length) {
deprecatedTags.forEach(tag => {
graph = actionUpgradeTags(entity.id, tag.old, tag.replace)(graph);
upgradeReasons.push({
source: 'id-tagging-schema--deprecated',
data: tag
});
});
entity = graph.entity(entity.id);
}
@@ -58,9 +63,13 @@ export function validationOutdatedTags() {
if (!newTags[k]) {
if (preset.addTags[k] === '*') {
newTags[k] = 'yes';
} else {
} else if (preset.addTags[k]) {
newTags[k] = preset.addTags[k];
}
upgradeReasons.push({
source: 'id-tagging-schema--preset-addTags',
data: preset
});
}
});
}
@@ -77,6 +86,10 @@ export function validationOutdatedTags() {
if (nsiResult) {
newTags = nsiResult.newTags;
subtype = 'noncanonical_brand';
upgradeReasons.push({
source: 'name-suggestion-index',
data: nsiResult
});
}
}
}
@@ -224,79 +237,7 @@ export function validationOutdatedTags() {
}
function oldMultipolygonIssues(entity, graph) {
let multipolygon, outerWay;
if (entity.type === 'relation') {
outerWay = osmOldMultipolygonOuterMemberOfRelation(entity, graph);
multipolygon = entity;
} else if (entity.type === 'way') {
multipolygon = osmIsOldMultipolygonOuterMember(entity, graph);
outerWay = entity;
} else {
return [];
}
if (!multipolygon || !outerWay) return [];
return [new validationIssue({
type: type,
subtype: 'old_multipolygon',
severity: 'warning',
message: showMessage,
reference: showReference,
entityIds: [outerWay.id, multipolygon.id],
dynamicFixes: () => {
return [
new validationIssueFix({
autoArgs: [doUpgrade, t('issues.fix.move_tags.annotation')],
title: t.append('issues.fix.move_tags.title'),
onClick: (context) => {
context.perform(doUpgrade, t('issues.fix.move_tags.annotation'));
}
})
];
}
})];
function doUpgrade(graph) {
let currMultipolygon = graph.hasEntity(multipolygon.id);
let currOuterWay = graph.hasEntity(outerWay.id);
if (!currMultipolygon || !currOuterWay) return graph;
currMultipolygon = currMultipolygon.mergeTags(currOuterWay.tags);
graph = graph.replace(currMultipolygon);
return actionChangeTags(currOuterWay.id, {})(graph);
}
function showMessage(context) {
let currMultipolygon = context.hasEntity(multipolygon.id);
if (!currMultipolygon) return '';
return t.append('issues.old_multipolygon.message',
{ multipolygon: utilDisplayLabel(currMultipolygon, context.graph(), true /* verbose */) }
);
}
function showReference(selection) {
selection.selectAll('.issue-reference')
.data([0])
.enter()
.append('div')
.attr('class', 'issue-reference')
.call(t.append('issues.old_multipolygon.reference'));
}
}
let validation = function checkOutdatedTags(entity, graph) {
let issues = oldMultipolygonIssues(entity, graph);
if (!issues.length) issues = oldTagIssues(entity, graph);
return issues;
};
let validation = oldTagIssues;
validation.type = type;
-17
View File
@@ -1359,23 +1359,6 @@ describe('iD.actionSplit', function () {
expect(graph.entity('=').nodes).to.eql(['a', 'b', 'c', 'a']);
expect(graph.parentRelations(graph.entity('='))).to.have.length(0);
});
it('converts simple multipolygon to a proper multipolygon', function () {
var graph = iD.coreGraph([
iD.osmNode({id: 'a'}),
iD.osmNode({id: 'b'}),
iD.osmNode({id: 'c'}),
iD.osmWay({'id': '-', nodes: ['a', 'b', 'c'], tags: { area: 'yes' }}),
iD.osmRelation({id: 'r', members: [{id: '-', type: 'way', role: 'outer'}], tags: {type: 'multipolygon'}})
]);
graph = iD.actionSplit('b', ['='])(graph);
expect(graph.entity('-').tags).to.eql({});
expect(graph.entity('r').tags).to.eql({type: 'multipolygon', area: 'yes' });
var ids = graph.entity('r').members.map(function(m) { return m.id; });
expect(ids).to.have.ordered.members(['-', '=']);
});
});
-146
View File
@@ -1,149 +1,3 @@
describe('iD.osmIsOldMultipolygonOuterMember', function() {
it('returns the parent relation of a simple multipolygon outer', function() {
var outer = iD.osmWay({tags: {'natural':'wood'}});
var relation = iD.osmRelation(
{tags: {type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]}
);
var graph = iD.coreGraph([outer, relation]);
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.equal(relation);
});
it('returns the parent relation of a simple multipolygon outer, assuming role outer if unspecified', function() {
var outer = iD.osmWay({tags: {'natural':'wood'}});
var relation = iD.osmRelation(
{tags: {type: 'multipolygon'}, members: [{id: outer.id}]}
);
var graph = iD.coreGraph([outer, relation]);
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.equal(relation);
});
it('returns false if entity is not a way', function() {
var outer = iD.osmNode({tags: {'natural':'wood'}});
var relation = iD.osmRelation(
{tags: {type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]}
);
var graph = iD.coreGraph([outer, relation]);
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.be.false;
});
it('returns false if entity does not have interesting tags', function() {
var outer = iD.osmWay({tags: {'tiger:reviewed':'no'}});
var relation = iD.osmRelation(
{tags: {type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]}
);
var graph = iD.coreGraph([outer, relation]);
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.be.false;
});
it('returns false if entity does not have a parent relation', function() {
var outer = iD.osmWay({tags: {'natural':'wood'}});
var graph = iD.coreGraph([outer]);
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.be.false;
});
it('returns false if the parent is not a multipolygon', function() {
var outer = iD.osmWay({tags: {'natural':'wood'}});
var relation = iD.osmRelation(
{tags: {type: 'route'}, members: [{id: outer.id, role: 'outer'}]}
);
var graph = iD.coreGraph([outer, relation]);
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.be.false;
});
it('returns false if the parent has interesting tags', function() {
var outer = iD.osmWay({tags: {'natural':'wood'}});
var relation = iD.osmRelation(
{tags: {natural: 'wood', type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]}
);
var graph = iD.coreGraph([outer, relation]);
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.be.false;
});
it('returns the parent relation of a simple multipolygon outer, ignoring uninteresting parent tags', function() {
var outer = iD.osmWay({tags: {'natural':'wood'}});
var relation = iD.osmRelation(
{tags: {'tiger:reviewed':'no', type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]}
);
var graph = iD.coreGraph([outer, relation]);
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.equal(relation);
});
it('returns false if the parent has multiple outer ways', function() {
var outer1 = iD.osmWay({tags: {'natural':'wood'}});
var outer2 = iD.osmWay({tags: {'natural':'wood'}});
var relation = iD.osmRelation(
{tags: {type: 'multipolygon'}, members: [{id: outer1.id, role: 'outer'}, {id: outer2.id, role: 'outer'}]}
);
var graph = iD.coreGraph([outer1, outer2, relation]);
expect(iD.osmIsOldMultipolygonOuterMember(outer1, graph)).to.be.false;
expect(iD.osmIsOldMultipolygonOuterMember(outer2, graph)).to.be.false;
});
it('returns false if the parent has multiple outer ways, assuming role outer if unspecified', function() {
var outer1 = iD.osmWay({tags: {'natural':'wood'}});
var outer2 = iD.osmWay({tags: {'natural':'wood'}});
var relation = iD.osmRelation(
{tags: {type: 'multipolygon'}, members: [{id: outer1.id}, {id: outer2.id}]}
);
var graph = iD.coreGraph([outer1, outer2, relation]);
expect(iD.osmIsOldMultipolygonOuterMember(outer1, graph)).to.be.false;
expect(iD.osmIsOldMultipolygonOuterMember(outer2, graph)).to.be.false;
});
it('returns false if the entity is not an outer', function() {
var inner = iD.osmWay({tags: {'natural':'wood'}});
var relation = iD.osmRelation(
{tags: {type: 'multipolygon'}, members: [{id: inner.id, role: 'inner'}]}
);
var graph = iD.coreGraph([inner, relation]);
expect(iD.osmIsOldMultipolygonOuterMember(inner, graph)).to.be.false;
});
});
describe('iD.osmOldMultipolygonOuterMember', function() {
it('returns the outer member of a simple multipolygon', function() {
var inner = iD.osmWay();
var outer = iD.osmWay({tags: {'natural':'wood'}});
var relation = iD.osmRelation({tags: {type: 'multipolygon'}, members: [
{id: outer.id, role: 'outer'},
{id: inner.id, role: 'inner'}]
});
var graph = iD.coreGraph([inner, outer, relation]);
expect(iD.osmOldMultipolygonOuterMember(inner, graph)).to.equal(outer);
expect(iD.osmOldMultipolygonOuterMember(outer, graph)).to.equal(outer);
});
it('returns falsy for a complex multipolygon', function() {
var inner = iD.osmWay();
var outer1 = iD.osmWay({tags: {'natural':'wood'}});
var outer2 = iD.osmWay({tags: {'natural':'wood'}});
var relation = iD.osmRelation({tags: {type: 'multipolygon'}, members: [
{id: outer1.id, role: 'outer'},
{id: outer2.id, role: 'outer'},
{id: inner.id, role: 'inner'}]
});
var graph = iD.coreGraph([inner, outer1, outer2, relation]);
expect(iD.osmOldMultipolygonOuterMember(inner, graph)).not.to.be.ok;
expect(iD.osmOldMultipolygonOuterMember(outer1, graph)).not.to.be.ok;
expect(iD.osmOldMultipolygonOuterMember(outer2, graph)).not.to.be.ok;
});
it('handles incomplete relations', function() {
var way = iD.osmWay({id: 'w'});
var relation = iD.osmRelation({id: 'r', tags: {type: 'multipolygon'}, members: [
{id: 'o', role: 'outer'},
{id: 'w', role: 'inner'}]
});
var graph = iD.coreGraph([way, relation]);
expect(iD.osmOldMultipolygonOuterMember(way, graph)).not.to.be.ok;
});
});
describe('iD.osmJoinWays', function() {
function getIDs(objects) {
return objects.map(function(node) { return node.id; });
-28
View File
@@ -144,32 +144,4 @@ describe('iD.svgAreas', function () {
expect(_surface.selectAll('.stroke').size()).to.equal(0);
});
it('renders fill for a multipolygon with tags on the outer way', function() {
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [2, 2]});
var c = iD.osmNode({loc: [3, 3]});
var w = iD.osmWay({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]});
var r = iD.osmRelation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}});
var graph = iD.coreGraph([a, b, c, w, r]);
_surface.call(iD.svgAreas(projection, context), graph, [w, r], none);
expect(_surface.selectAll('.way.fill').size()).to.equal(0);
expect(_surface.selectAll('.relation.fill').size()).to.equal(1);
expect(_surface.select('.relation.fill').classed('tag-natural-wood')).to.be.true;
});
it('renders no strokes for a multipolygon with tags on the outer way', function() {
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [2, 2]});
var c = iD.osmNode({loc: [3, 3]});
var w = iD.osmWay({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]});
var r = iD.osmRelation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}});
var graph = iD.coreGraph([a, b, c, w, r]);
_surface.call(iD.svgAreas(projection, context), graph, [w, r], none);
expect(_surface.selectAll('.stroke').size()).to.equal(0);
});
});
-31
View File
@@ -66,37 +66,6 @@ describe('iD.svgLines', function () {
expect(surface.select('.stroke').classed('tag-natural-wood')).to.be.true;
});
it('renders stroke for outer way of multipolygon with tags on the outer way', function() {
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [2, 2]});
var c = iD.osmNode({loc: [3, 3]});
var w = iD.osmWay({id: 'w-1', tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]});
var r = iD.osmRelation({members: [{id: w.id}], tags: {type: 'multipolygon'}});
var graph = iD.coreGraph([a, b, c, w, r]);
surface.call(iD.svgLines(projection, context), graph, [w], all);
expect(surface.select('.stroke.w-1').classed('tag-natural-wood')).to.equal(true, 'outer tag-natural-wood true');
expect(surface.select('.stroke.w-1').classed('old-multipolygon')).to.equal(true, 'outer old-multipolygon true');
});
it('adds stroke classes for the tags of the outer way of multipolygon with tags on the outer way', function() {
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [2, 2]});
var c = iD.osmNode({loc: [3, 3]});
var o = iD.osmWay({id: 'w-1', nodes: [a.id, b.id, c.id, a.id], tags: {natural: 'wood'}});
var i = iD.osmWay({id: 'w-2', nodes: [a.id, b.id, c.id, a.id]});
var r = iD.osmRelation({members: [{id: o.id, role: 'outer'}, {id: i.id, role: 'inner'}], tags: {type: 'multipolygon'}});
var graph = iD.coreGraph([a, b, c, o, i, r]);
surface.call(iD.svgLines(projection, context), graph, [i, o], all);
expect(surface.select('.stroke.w-1').classed('tag-natural-wood')).to.equal(true, 'outer tag-natural-wood true');
expect(surface.select('.stroke.w-1').classed('old-multipolygon')).to.equal(true, 'outer old-multipolygon true');
expect(surface.select('.stroke.w-2').classed('tag-natural-wood')).to.equal(true, 'inner tag-natural-wood true');
expect(surface.select('.stroke.w-2').classed('old-multipolygon')).to.equal(false, 'inner old-multipolygon false');
});
describe('z-indexing', function() {
var graph = iD.coreGraph([
iD.osmNode({id: 'a', loc: [0, 0]}),
-17
View File
@@ -125,21 +125,4 @@ describe('iD.validations.outdated_tags', function () {
done();
}, 20);
});
it('flags multipolygon tagged on the outer way', function(done) {
createRelation({ building: 'yes' }, { type: 'multipolygon' });
var validator = iD.validationOutdatedTags(context);
window.setTimeout(function() { // async, so data will be available
var issues = validate(validator);
expect(issues).to.not.have.lengthOf(0);
var issue = issues[0];
expect(issue.type).to.eql('outdated_tags');
expect(issue.subtype).to.eql('old_multipolygon');
expect(issue.entityIds).to.have.lengthOf(2);
expect(issue.entityIds[0]).to.eql('w-1');
expect(issue.entityIds[1]).to.eql('r-1');
done();
}, 20);
});
});