diff --git a/modules/core/context.js b/modules/core/context.js index d6fbd9cb8..2d4166b79 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -464,10 +464,9 @@ export function coreContext() { var validationsUrl = utilStringQs(window.location.hash).validations; d3_json(validationsUrl, function (err, mapcss) { if (err) return; - services.maprules.init(); - var areaKeys = context.presets().areaKeys(); + services.maprules.init(context.presets().areaKeys()); _each(mapcss, function(mapcssSelector) { - return services.maprules.addRule(mapcssSelector, areaKeys); + return services.maprules.addRule(mapcssSelector); }); context.validationRules = true; }); diff --git a/modules/services/maprules.js b/modules/services/maprules.js index fd49f6fa4..b55f39017 100644 --- a/modules/services/maprules.js +++ b/modules/services/maprules.js @@ -3,9 +3,6 @@ import _intersection from 'lodash-es/intersection'; import _reduce from 'lodash-es/reduce'; import _every from 'lodash-es/every'; -var ruleChecks, - validationRules; - var buildRuleChecks = function() { return { equals: function (equals) { @@ -81,17 +78,35 @@ var buildRuleChecks = function() { }; }; +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() { - ruleChecks = buildRuleChecks(); - validationRules = []; + 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])); + rules.push(_ruleChecks[key](selector[key])); } return rules; }, []); @@ -123,8 +138,14 @@ export default { } else if (/(greater|less)Than(Equal)?|presence/g.test(key)) { var tagKey = /presence/.test(key) ? selector[key] : Object.keys(selector[key])[0]; - expectedTags[tagKey] = []; + + values = [selector[key][tagKey]]; + if (expectedTags.hasOwnProperty(tagKey)) { + values = values.concat(expectedTags[tagKey]); + } + + expectedTags[tagKey] = values; } return expectedTags; @@ -133,25 +154,15 @@ export default { return tagMap; }, // inspired by osmWay#isArea() - inferGeometry: function(tagMap, areaKeys) { - var lineKeys = { - highway: { - rest_area: true, - services: true - }, - railway: { - roundhouse: true, - station: true, - traverser: true, - turntable: true, - wash: true - } - }; + 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; + return _intersection(tagMap[key], Object.keys(_areaKeys[key])).length > 0; }; var isLineKeysWhiteList = function(key) { - return _intersection(tagMap[key], Object.keys(lineKeys[key])).length > 0; + return _intersection(tagMap[key], Object.keys(_lineKeys[key])).length > 0; }; if (tagMap.hasOwnProperty('area')) { @@ -164,10 +175,10 @@ export default { } for (var key in tagMap) { - if (key in areaKeys && !isAreaKeyBlackList(key)) { + if (key in _areaKeys && !isAreaKeyBlackList(key)) { return 'area'; } - if (key in lineKeys && isLineKeysWhiteList(key)) { + if (key in _lineKeys && isLineKeysWhiteList(key)) { return 'area'; } } @@ -175,7 +186,8 @@ export default { return 'line'; }, // adds from mapcss-parse selector check... - addRule: function(selector, areaKeys) { + addRule: function(selector) { + var _areaKeys = this._areaKeys; var rule = { // checks relevant to mapcss-selector checks: this.filterRuleChecks(selector), @@ -186,7 +198,7 @@ export default { }); }, // borrowed from Way#isArea() - inferredGeometry: this.inferGeometry(this.buildTagMap(selector), areaKeys), + inferredGeometry: this.inferGeometry(this.buildTagMap(selector), this._areaKeys), geometryMatches: function(entity, graph) { if (entity.type === 'node' || entity.type === 'relation') { return selector.geometry === entity.type; @@ -206,8 +218,11 @@ export default { } } }; - validationRules.push(rule); + this._validationRules.push(rule); }, + clearRules: function() { this._validationRules = []; }, // returns validationRules... - validationRules: function() { return validationRules; } + validationRules: function() { return this._validationRules; }, + // returns ruleChecks + ruleChecks: function() { return this._ruleChecks; } }; diff --git a/test/index.html b/test/index.html index d8d5ef4d6..caf87bc2e 100644 --- a/test/index.html +++ b/test/index.html @@ -112,7 +112,9 @@ - + --> + + diff --git a/test/spec/services/maprules.js b/test/spec/services/maprules.js new file mode 100644 index 000000000..34b741b90 --- /dev/null +++ b/test/spec/services/maprules.js @@ -0,0 +1,574 @@ +describe('maprules', function() { + var _ruleChecks, validationRules; + before(function() { + var areaKeys = iD.Context().presets().areaKeys(); + iD.serviceMapRules.init(areaKeys); + _ruleChecks = iD.serviceMapRules.ruleChecks(); + }); + + describe('#filterRuleChecks', function() { + it('returns shortlist of mapcss checks relevant to provided selector', function() { + var selector = { + geometry: 'closedway', + equals: {amenity: 'marketplace'}, + absence: 'name', + error: '\'Marketplace\' preset must be coupled with name' + }; + var filteredChecks = iD.serviceMapRules.filterRuleChecks(selector); + var equalsCheck = filteredChecks[0]; + var absenceCheck = filteredChecks[1]; + var entityTags = {amenity: 'marketplace'}; + + expect(filteredChecks.length).eql(2); + expect(equalsCheck(entityTags)).to.be.true; + expect(absenceCheck(entityTags)).to.be.true; + }); + }); + + describe('#buildTagMap', function() { + it('builds a map of tag keys/values found in mapcss selector', function() { + [ + { + t: { + equals: { + man_made: 'tower', + 'tower:type': 'communication' + } + }, + r: { + man_made: ['tower'], + 'tower:type': ['communication'] + } + }, + { + t: { + equals: { + building: 'yes', + amenity: 'school' + }, + positiveRegex: { + opening_hours: [ + '24/7', + 'sunrise_sundown' + ] + }, + negativeRegex: { + source: [ + 'missing_maps', + 'american_red_cross' + ] + }, + greaterThanEqual: { floors: 2 }, + lessThanEqual: { floors: 4 } + + }, + r: { + building: ['yes'], + amenity: ['school'], + opening_hours: ['24/7', 'sunrise_sundown'], + source: ['missing_maps', 'american_red_cross'], + floors: [4, 2] + } + }, + { + t: { + equals: { highway: 'yes' }, + greaterThan: { lanes: 1 }, + lessThan: { lanes: 4 } + }, + r: { + highway: ['yes'], + lanes: [4, 1] + } + } + ].forEach(function(test) { + expect(iD.serviceMapRules.buildTagMap(test.t)).to.eql(test.r); + }); + }); + }); + + describe('#inferGeometry', function() { + it('infers geometry using selector keys', function() { + + var amenityDerivedArea = { + geometry: 'closedway', + presence: 'amenity', + positiveRegex: { amenity: ['^school$', '^healthcare$'] }, + error: 'amenity cannot be healthcare or school!' + }; + + var areaDerivedArea = { + geometry: 'closedway', + equals: { area: 'yes' }, + }; + + var badAreaDerivedLine = { + geometry: 'closedway', + equals: { 'area': 'no' } + }; + + var roundHouseRailwayDerivedArea = { + geometry: 'closedway', + equals: { 'railway': 'roundhouse' } + }; + + var justClosedWayDerivedLine = { + geometry: 'closedway' + }; + + var tagMap, geom; + tagMap = iD.serviceMapRules.buildTagMap(amenityDerivedArea); + geom = iD.serviceMapRules.inferGeometry(tagMap); + expect(geom).to.be.eql('area'); + + tagMap = iD.serviceMapRules.buildTagMap(areaDerivedArea); + geom = iD.serviceMapRules.inferGeometry(tagMap); + expect(geom).to.be.eql('area'); + + tagMap = iD.serviceMapRules.buildTagMap(badAreaDerivedLine); + geom = iD.serviceMapRules.inferGeometry(tagMap); + expect(geom).to.be.eql('line'); + + tagMap = iD.serviceMapRules.buildTagMap(roundHouseRailwayDerivedArea); + geom = iD.serviceMapRules.inferGeometry(tagMap); + expect(geom).to.be.eql('area'); + + tagMap = iD.serviceMapRules.buildTagMap(justClosedWayDerivedLine); + geom = iD.serviceMapRules.inferGeometry(tagMap); + expect(geom).to.be.eql('line'); + }); + }); + + describe('#addRule', function() { + it ('builds a rule from provided selector and adds it to _validationRules', function () { + var selector = { + geometry:'node', + equals: {amenity:'marketplace'}, + absence:'name', + warning:'\'Marketplace\' preset must be coupled with name' + }; + expect(iD.serviceMapRules.validationRules()).to.be.empty; + iD.serviceMapRules.addRule(selector); + expect(iD.serviceMapRules.validationRules().length).to.eql(1); + }); + }); + describe('#clearRules', function() { + it ('clears _validationRules array', function() { + expect(iD.serviceMapRules.validationRules().length).to.eql(1); + iD.serviceMapRules.clearRules(); + expect(iD.serviceMapRules.validationRules()).to.be.empty; + }); + }); + + describe('#validationRules', function() { + it('returns _validationRules array', function() { + var selector = { + geometry: 'closedway', + equals: {amenity: 'marketplace'}, + absence: 'name', + error: '\'Marketplace\' preset must be coupled with name' + }; + iD.serviceMapRules.addRule(selector); + var rules = iD.serviceMapRules.validationRules(); + expect(rules).instanceof(Array); + expect(rules.length).to.eql(1); + }); + }); + + describe('_ruleChecks', function () { + describe('#equals', function() { + it('is true when two tag maps intersect', function() { + var a = { amenity: 'school'}; + var b = { amenity: 'school' }; + expect(_ruleChecks.equals(a)(b)).to.be.true; + }); + it('is false when two tag maps intersect', function() { + var a = { man_made: 'water_tap'}; + var b = { amenity: 'school'}; + expect(_ruleChecks.equals(a)(b)).to.be.false; + }); + }); + describe('#notEquals', function() { + it('is true when two tag maps do not intersect', function() { + var a = { man_made: 'water_tap'}; + var b = { amenity: 'school' }; + expect(_ruleChecks.notEquals(a)(b)).to.be.true; + }); + it('is not true when two tag maps intersect', function() { + var a = { amenity: 'school' }; + var b = { amenity: 'school', opening_hours: '9-5' }; + expect(_ruleChecks.notEquals(a)(b)).to.be.false; + }); + }); + describe('absence', function() { + it('is true when tag map keys does not include key in question', function() { + var key = 'amenity'; + var map = { building: 'yes' }; + expect(_ruleChecks.absence(key)(map)).to.be.true; + }); + it('is false when tag map keys does include key in question', function() { + var key = 'amenity'; + var map = { amenity: 'school' }; + expect(_ruleChecks.absence(key)(map)).to.be.false; + }); + }); + describe('presence', function() { + it('is true when tag map keys includes key in question', function() { + var key = 'amenity'; + var map = { amenity: 'school'}; + expect(_ruleChecks.presence(key)(map)).to.be.true; + }); + it('is false when tag map keys do not include key in question', function() { + var key = 'amenity'; + var map = { building: 'yes'}; + expect(_ruleChecks.presence(key)(map)).to.be.false; + }); + }); + describe('greaterThan', function() { + it ('is true when a tag value is greater than the selector value', function() { + var selectorTags = { lanes: 5 }; + var tags = { lanes : 6 }; + expect(_ruleChecks.greaterThan(selectorTags)(tags)).to.be.true; + }); + it ('is false when a tag value is less than or equal to the selector value', function() { + var selectorTags = { lanes: 5 }; + [4, 5].forEach(function(val) { + expect(_ruleChecks.greaterThan(selectorTags)({ lanes: val })).to.be.false; + }); + }); + }); + describe('greaterThanEqual', function() { + it ('is true when a tag value is greater than or equal to the selector value', function() { + var selectorTags = { lanes: 5 }; + [5, 6].forEach(function(val) { + expect(_ruleChecks.greaterThanEqual(selectorTags)({ lanes: val })).to.be.true; + }); + }); + it ('is false when a tag value is less than the selector value', function () { + var selectorTags = { lanes: 5 }; + var tags = { lanes: 4 }; + expect(_ruleChecks.greaterThanEqual(selectorTags)(tags)).to.be.false; + }); + }); + describe('lessThan', function() { + it ('is true when a tag value is less than the selector value', function() { + var selectorTags = { lanes: 5 }; + var tags = { lanes: 4 }; + expect(_ruleChecks.lessThan(selectorTags)(tags)).to.be.true; + }); + it ('is false when a tag value is greater than or equal to the selector value', function() { + var selectorTags = { lanes: 5 }; + [6, 7].forEach(function(val) { + expect(_ruleChecks.lessThan(selectorTags)({ lanes: val })).to.be.false; + }); + }); + }); + describe('lessThanEqual', function() { + it ('is true when a tag value is less than or equal to the selector value', function() { + var selectorTags = { lanes: 5 }; + [4, 5].forEach(function(val) { + expect(_ruleChecks.lessThanEqual(selectorTags)({ lanes: val })).to.be.true; + }); + }); + it ('is false when a tag value is greater than the selector value', function() { + var selectorTags = { lanes: 5 }; + var tags = { lanes: 6 }; + expect(_ruleChecks.lessThanEqual(selectorTags)(tags)).to.be.false; + }); + }); + describe('positiveRegex', function() { + var positiveRegex = { amenity: ['^hospital$','^clinic$']}; + it ('is true when tag value matches positiveRegex', function() { + var tags = { amenity: 'hospital' }; + expect(_ruleChecks.positiveRegex(positiveRegex)(tags)).to.be.true; + }); + it ('is false when tag value does not match negative regex', function() { + var tags = { amenity: 'school' }; + expect(_ruleChecks.positiveRegex(positiveRegex)(tags)).to.be.false; + }); + }); + describe('negativeRegex', function() { + var negativeRegex = { bicycle: [ 'use_path', 'designated' ] }; + it ('is true when tag value does not match negativeRegex', function() { + var tags = { bicycle: 'yes' }; + expect(_ruleChecks.negativeRegex(negativeRegex)(tags)).to.be.true; + }); + it ('is false when tag value matches negativeRegex', function() { + var tags = { bicycle: 'designated' }; + expect(_ruleChecks.negativeRegex(negativeRegex)(tags)).to.be.false; + }); + }); + }); + describe('rule', function() { + var selectors, entities; + before(function() { + selectors = [ + { + geometry:'node', + equals: {amenity:'marketplace'}, + absence:'name', + error:'\'Marketplace\' preset must be coupled with name' + }, + { + geometry: 'closedway', + notEquals: { building: 'yes', amenity: 'clinic' }, + error: '\'Clinic\' preset must be coupled with building=yes' + }, + { + geometry:'node', + equals: {man_made: 'tower', 'tower:type': 'communication'}, + presence: 'height', + error:'\'Communication Tower\' preset must not be coupled with height' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + lessThanEqual: { height: 6 }, + error: '\'Tower\' preset height must be greater than 6' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + greaterThanEqual: { height: 9 }, + error: '\'Tower\' preset height must be less than 9' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + lessThan: { height: 6 }, + error: '\'Tower\' preset height must be greater than or equal to 6' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + greaterThan: { height: 9 }, + error: '\'Tower\' preset height must be greater less than or equal to 9' + }, + { + geometry: 'closedway', + equals: { amenity: 'clinic' }, + negativeRegex: { emergency: ['yes', 'no'] }, + error: '\'Clinic\' preset\'s emergency tag must be equal to \'yes\' or \'no\'' + }, + { + geometry: 'way', + equals: { highway: 'residential' }, + positiveRegex: { structure: ['bridge', 'tunnel'] }, + error: '\'suburban road\' structure tag cannot be \'bridge\' or \'tunnel\'' + } + ]; + entities = [ + iD.Entity({ type: 'node', tags: { amenity: 'marketplace' }}), + iD.Way({ tags: { building: 'house', amenity: 'clinic' }, nodes: [ 'a', 'b', 'c', 'a' ]}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', 'tower:type': 'communication', height: 5 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 6 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 9 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 5 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 10 }}), + iD.Way({ tags: { amenity: 'clinic', emergency: 'definitely' }, nodes: [ 'd', 'e', 'f', 'd' ]}), + iD.Way({ tags: { highway: 'residential', structure: 'bridge' }}), + ]; + + iD.serviceMapRules.clearRules(); + selectors.forEach(function(selector) { iD.serviceMapRules.addRule(selector); }); + validationRules = iD.serviceMapRules.validationRules(); + }); + describe('#matches', function() { + var selectors, entities; + before(function() { + selectors = [ + { + geometry:'node', + equals: {amenity:'marketplace'}, + absence:'name', + error:'\'Marketplace\' preset must be coupled with name' + }, + { + geometry: 'closedway', + notEquals: { building: 'yes', amenity: 'clinic' }, + error: '\'Clinic\' preset must be coupled with building=yes' + }, + { + geometry:'node', + equals: {man_made: 'tower', 'tower:type': 'communication'}, + presence: 'height', + error:'\'Communication Tower\' preset must not be coupled with height' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + lessThanEqual: { height: 6 }, + error: '\'Tower\' preset height must be greater than 6' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + greaterThanEqual: { height: 9 }, + error: '\'Tower\' preset height must be less than 9' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + lessThan: { height: 6 }, + error: '\'Tower\' preset height must be greater than or equal to 6' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + greaterThan: { height: 9 }, + error: '\'Tower\' preset height must be greater less than or equal to 9' + }, + { + geometry: 'closedway', + equals: { amenity: 'clinic' }, + negativeRegex: { emergency: ['yes', 'no'] }, + error: '\'Clinic\' preset\'s emergency tag must be equal to \'yes\' or \'no\'' + }, + { + geometry: 'way', + equals: { highway: 'residential' }, + positiveRegex: { structure: ['bridge', 'tunnel'] }, + error: '\'suburban road\' structure tag cannot be \'bridge\' or \'tunnel\'' + } + ]; + entities = [ + iD.Entity({ type: 'node', tags: { amenity: 'marketplace' }}), + iD.Way({ tags: { building: 'house', amenity: 'clinic' }, nodes: [ 'a', 'b', 'c', 'a' ]}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', 'tower:type': 'communication', height: 5 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 6 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 9 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 5 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 10 }}), + iD.Way({ tags: { amenity: 'clinic', emergency: 'definitely' }, nodes: [ 'd', 'e', 'f', 'd' ]}), + iD.Way({ tags: { highway: 'residential', structure: 'bridge' }}), + ]; + + iD.serviceMapRules.clearRules(); + selectors.forEach(function(selector) { iD.serviceMapRules.addRule(selector); }); + validationRules = iD.serviceMapRules.validationRules(); + }); + it('is true when each rule check is \'true\'', function() { + validationRules.forEach(function(rule, i) { + expect(rule.matches(entities[i])).to.be.true; + }); + }); + it ('is true when at least one rule check is \'false\'', function() { + var selector = { + geometry: 'way', + equals: { highway: 'residential' }, + positiveRegex: { structure: ['embarkment', 'bridge'] }, + error: '\'suburban road\' structure tag cannot be \'bridge\' or \'tunnel\'' + }; + var entity = iD.Way({ tags: { highway: 'residential', structure: 'tunnel' }}); + iD.serviceMapRules.clearRules(); + iD.serviceMapRules.addRule(selector); + var rule = iD.serviceMapRules.validationRules()[0]; + + expect(rule.matches(entity)).to.be.false; + }); + }); + describe('#findWarnings', function() { + var selectors, entities, _graph; + + before(function() { + selectors = [ + { + geometry:'node', + equals: {amenity:'marketplace'}, + absence:'name', + error:'\'Marketplace\' preset must be coupled with name' + }, + { + geometry: 'closedway', + notEquals: { building: 'yes', amenity: 'clinic' }, + error: '\'Clinic\' preset must be coupled with building=yes' + }, + { + geometry:'node', + equals: {man_made: 'tower', 'tower:type': 'communication'}, + presence: 'height', + error:'\'Communication Tower\' preset must not be coupled with height' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + lessThanEqual: { height: 6 }, + error: '\'Tower\' preset height must be greater than 6' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + greaterThanEqual: { height: 9 }, + error: '\'Tower\' preset height must be less than 9' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + lessThan: { height: 6 }, + error: '\'Tower\' preset height must be greater than or equal to 6' + }, + { + geometry: 'node', + equals: { man_made: 'tower' }, + greaterThan: { height: 9 }, + error: '\'Tower\' preset height must be greater less than or equal to 9' + }, + { + geometry: 'closedway', + equals: { amenity: 'clinic' }, + negativeRegex: { emergency: ['yes', 'no'] }, + error: '\'Clinic\' preset\'s emergency tag must be equal to \'yes\' or \'no\'' + }, + { + geometry: 'way', + equals: { highway: 'residential' }, + positiveRegex: { structure: ['bridge', 'tunnel'] }, + error: '\'suburban road\' structure tag cannot be \'bridge\' or \'tunnel\'' + } + ]; + entities = [ + iD.Entity({ type: 'node', tags: { amenity: 'marketplace' }}), + iD.Way({ tags: { building: 'house', amenity: 'clinic' }, nodes: [ 'a', 'b', 'c', 'a' ]}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', 'tower:type': 'communication', height: 5 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 6 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 9 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 5 }}), + iD.Entity({ type: 'node', tags: { man_made: 'tower', height: 10 }}), + iD.Way({ tags: { amenity: 'clinic', emergency: 'definitely' }, nodes: [ 'd', 'e', 'f', 'd' ]}), + iD.Way({ tags: { highway: 'residential', structure: 'bridge' }}), + ]; + + var wayNodes = [ + iD.osmNode({ id: 'a' }), + iD.osmNode({ id: 'b' }), + iD.osmNode({ id: 'c' }), + iD.osmNode({ id: 'd' }), + iD.osmNode({ id: 'e' }), + iD.osmNode({ id: 'f' }), + ]; + _graph = iD.Graph(entities.concat(wayNodes)); + iD.serviceMapRules.clearRules(); + selectors.forEach(function(selector) { iD.serviceMapRules.addRule(selector); }); + validationRules = iD.serviceMapRules.validationRules(); + }); + it('finds warnings', function() { + validationRules.forEach(function(rule, i) { + var warnings = []; + var entity = entities[i]; + var selector = selectors[i]; + + rule.findWarnings(entity, _graph, warnings); + + var warning = warnings[0]; + var type = Object.keys(selector).indexOf('error') ? 'error' : 'warning'; + + expect(warnings.length).to.eql(1); + expect(warning.entity).to.eql(entity); + expect(warning.message).to.eql(selector[type]); + expect('mapcss_' + type).to.eql(warning.id); + }); + }); + }); + }); +}); + diff --git a/test/spec/util/mapcss_rule.js b/test/spec/util/mapcss_rule.js deleted file mode 100644 index d6168b750..000000000 --- a/test/spec/util/mapcss_rule.js +++ /dev/null @@ -1,356 +0,0 @@ -describe('iD.utilMapCSSRule', function() { - var entities = [ - iD.Entity({ type: 'node', tags: { amenity: 'marketplace' }}), - iD.Entity({ type: 'node', tags: { man_made: 'water_tap' }}), - iD.Entity({ type: 'node', tags: { amenity: 'marketplace', height: 0 }}), - iD.Entity({ type: 'node', tags: { amenity: 'school', height: 5, width: 3 }}), - iD.Entity({ type: 'node', tags: { amenity: 'healthcare' }}), - iD.Entity({ type: 'node', tags: { amenity: 'place_of_worship' }}), - ]; - var selectors = [ - { - 'geometry':'node', - 'equals':{'amenity':'marketplace'}, - 'absence':'name', - 'warning':'throwWarning: "[amenity=marketplace]: MapRules preset \'Market\': must be coupled with name";' - }, - { - 'geometry':'node', - 'equals':{'man_made':'water_tap'}, - 'absence':'name', - 'warning':'throwWarning: "[amenity=drinking_water][man_made=water_tap]: MapRules preset \'Water Tap\': must be coupled with name";' - }, - { - 'geometry':'node', - 'equals':{'amenity':'marketplace'}, - 'presence':'height', - 'lessThanEqual': { 'height': 0 }, - 'warning':'throwWarning: "[amenity=marketplace]: height must be greater than 0";' - }, - { - 'geometry': 'node', - 'equals': {'amenity': 'school'}, - 'greaterThan': { 'height': 0 }, - 'greaterThanEqual': { 'width': 1 }, - 'lessThanEqual': { 'width': 10 }, - 'lessThan': { 'height': 10 }, - 'warning': 'this is the warning!' - }, - { - 'geometry': 'node', - 'presence': 'amenity', - 'positiveRegex': { amenity: ['^school$', '^healthcare$'] }, - 'error': 'amenity cannot be healthcare or school!' - } - ]; - var areaKeys = iD.Context().presets().areaKeys(); - var rules = selectors.map(function(s) { return iD.utilMapCSSRule(s, areaKeys); }); - it ('turns selector object in mapcssRule', function () { - var ruleKeys = [ - 'ruleChecks', 'type','buildChecks', 'selector', 'buildTagMap', 'matches', - 'areaKeys', 'inferGeometry', 'geometryMatches','findWarnings' - ]; - rules.forEach(function(rule) { - expect(Object.keys(rule)).to.eql(ruleKeys); - }); - }); - describe('#type', function() { - it('is either error or warning', function() { - selectors.forEach(function(s) { - expect(['error', 'warning'].indexOf(iD.utilMapCSSRule(s, areaKeys).type)).to.be.greaterThan(-1); - }); - }); - }); - describe('#geometryMatches', function() { - it('determines if entity and rule geometries match', function() { - var node = iD.Entity({ type: 'node'}); - var way = iD.Entity({ type: 'way'}); - var graph = iD.Graph([node, way]); - rules.forEach(function(rule) { - expect(rule.geometryMatches(node, graph)).to.be.true; - expect(rule.geometryMatches(way, graph)).to.be.false; - }); - }); - }); - describe('#buildsChecks', function() { - it('builds array of MapCSS rule check functions to run entities against', function() { - rules.forEach(function(rule) { - expect(rule.buildChecks().every(function(fn) { return fn instanceof Function; })).to.be.true; - }); - }); - }); - describe('#buildTagMap', function() { - it('builds tag map from selector config', function () { - var selector = { - 'geometry':'node', - 'equals':{'amenity':'marketplace'}, - 'positiveRegex': { 'marketplace:type': ['open', 'indoor', 'mall']}, - 'greaterThan': { 'width': 10, 'area': 300 }, - 'presence': 'opening_hours', - 'absence':'name', - 'warning':'throwWarning: "[amenity=marketplace]: MapRules preset \'Market\': must be coupled with name";' - }; - var tagMap = { - 'amenity':['marketplace'], - 'marketplace:type':['open','indoor','mall'], - 'width':[], - 'opening_hours':[] - }; - - var rule = iD.utilMapCSSRule(selector, areaKeys); - expect(rule.buildTagMap()).to.be.eql(tagMap); - }); - }); - describe('#matches', function() { - it('determines if an entity matches the MapCSS rule checks', function() { - var node = iD.Entity({ type: 'node', tags: { power: 'tower' }}); - rules.forEach(function(rule, i) { - expect(rule.matches(entities[i])).to.be.true; - expect(rule.matches(node)).to.be.false; - }); - }); - }); - describe('#selector', function() { - it('returns selector used to construct rule', function() { - var rule = iD.utilMapCSSRule(selectors[1], areaKeys); - expect(rule.selector()).to.eql(selectors[1]); - }); - }); - describe('#areaKeys', function() { - it('returns areaKeys used to construct rule', function() { - var rule = iD.utilMapCSSRule(selectors[0], areaKeys); - expect(rule.areaKeys()).to.eql(areaKeys); - }); - }); - describe('#ruleChecks', function() { - describe('equals', function() { - it('is true when entity.tags intersects selector.equals', function() { - var pseudoSelector = { equals: {'amenity': 'school'} }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var school = iD.Entity({ type: 'node', tags: { amenity: 'school' }}); - expect(pseudoRule.ruleChecks.equals(school.tags)).to.be.true; - }); - it('is false when entity.tags intersects selector.equals', function() { - var pseudoSelector = { equals: { 'man_made': 'water_tap'} }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var school = iD.Entity({ type: 'node', tags: { amenity: 'school' } } ); - expect(pseudoRule.ruleChecks.equals(school.tags)).to.be.false; - }); - }); - describe('notEquals', function() { - it('is true when entity.tags does not intersect selector.notEquals', function() { - var pseudoSelector = { notEquals: { 'man_made': 'water_tap'} }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var school = iD.Entity({ type: 'node', tags: { amenity: 'school' } } ); - expect(pseudoRule.ruleChecks.notEquals(school.tags)).to.be.true; - }); - it('is false when entity.tags does not intersect selector.notEquals', function() { - var pseudoSelector = { notEquals: { 'amenity': 'school'} }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var school = iD.Entity({ type: 'node', tags: { amenity: 'school' } } ); - expect(pseudoRule.ruleChecks.notEquals(school.tags)).to.be.false; - }); - }); - describe('presence', function() { - it('is true when entity.tags\' key s include selector.presence', function() { - var pseudoSelector = { presence: 'name' }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var kHouse = iD.Entity({ type: 'node', tags: { amenity: 'marketplace', name: 'Kensington Square' }}); - expect(pseudoRule.ruleChecks.presence(kHouse.tags)).to.be.true; - }); - it('is false when entity tags\' keys do not include selector.presence', function() { - var pseudoSelector = { presence: 'name' }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var notKHouse = iD.Entity({ type: 'node', tags: { amenity: 'marketplace' }}); - expect(pseudoRule.ruleChecks.presence(notKHouse.tags)).to.be.false; - }); - }); - describe('absence', function() { - it('is true when entity.tags\' keys do not include selector.absence', function() { - var pseudoSelector = { absence: 'name' }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var notKHouse = iD.Entity({ type: 'node', tags: { amenity: 'marketplace' }}); - expect(pseudoRule.ruleChecks.absence(notKHouse.tags)).to.be.true; - }); - it('is false when entity.tags\' keys include selector.absence', function() { - var pseudoSelector = { absence: 'name' }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var kHouse = iD.Entity({ type: 'node', tags: { amenity: 'marketplace', name: 'Kensington Square' }}); - expect(pseudoRule.ruleChecks.presence(kHouse.tags)).to.be.false; - }); - }); - describe('greaterThan', function() { - it('is true when entity.tags\' equivalent value is greater than selector.greaterThan', function() { - var pseudoSelector = { greaterThan: { height: 10 }}; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var tallSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 9000 }}); - expect(pseudoRule.ruleChecks.greaterThan(tallSchool.tags)).to.be.true; - }); - it('is false when entity.tags\' equivalent value is less than or equal to selector.greaterThan', function() { - var pseudoSelector = { greaterThan: { height: 10 }}; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var smallSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 9 }}); - expect(pseudoRule.ruleChecks.greaterThan(smallSchool.tags)).to.be.false; - }); - }); - describe('greaterThanEqual', function() { - it('is true when entity.tags\' equivalent value is greater than or equal to selector.greaterThanEqual', function() { - var pseudoSelector = { greaterThanEqual: { height: 10 } }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var okHeightSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 10 }}); - expect(pseudoRule.ruleChecks.greaterThanEqual(okHeightSchool.tags)).to.be.true; - }); - it('is false when entity.tags\' equivalent value is less than to selector.greaterThanEqual', function() { - var pseudoSelector = { greaterThanEqual: { height: 10 }}; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var smallSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 9 }}); - expect(pseudoRule.ruleChecks.greaterThanEqual(smallSchool.tags)).to.be.false; - }); - }); - describe('lessThan', function() { - it('is true when entity.tags\' equivalent value is less than to selector.lessThan', function() { - var pseudoSelector = { lessThan: { height: 10 } }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var smallSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 3 }}); - expect(pseudoRule.ruleChecks.lessThan(smallSchool.tags)).to.be.tru; - }); - it('is false when entity.tags\' equivalent value is greater than or equal to selector.lessThan', function() { - var pseudoSelector = { lessThan: { height: 10 } }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var notOkHeightSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 10 }}); - expect(pseudoRule.ruleChecks.lessThan(notOkHeightSchool.tags)).to.be.false; - }); - }); - describe('lessThanEqual', function() { - it('is true when entity.tags\' equivalent value is less than or equal to to selector.lessThan', function() { - var pseudoSelector = { lessThanEqual: { height: 10 } }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var okHeightSchool = iD.Entity({ type: 'node', tags: { 'amenity': 'school', 'height': 10 }}); - expect(pseudoRule.ruleChecks.lessThanEqual(okHeightSchool.tags)).to.be.true; - }); - it('is false when entity.tags\' equivalent value is greater than to selector.lessThan', function() { - var pseudoSelector = { lessThanEqual: { height: 10 } }; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var notOkHeightSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 11 }}); - expect(pseudoRule.ruleChecks.lessThanEqual(notOkHeightSchool.tags)).to.be.false; - }); - }); - describe('positiveRegex', function() { - it('is true when entity.tags\' equivalent value matches regular expression built from selector.positiveRegex', function() { - var pseudoSelector = { positiveRegex: { amenity: ['^school$', '^healthcare$'] }}; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var okAmenities = [ - iD.Entity({ type: 'node', tags: { amenity: 'school' }}), - iD.Entity({ type: 'node', tags: { amenity: 'healthcare' }}) - ]; - okAmenities.forEach(function(amenity) { - expect(pseudoRule.ruleChecks.positiveRegex(amenity.tags)).to.be.true; - }); - }); - it('is false when entity.tags\' equivalent value does not match regular expression built from selector.positiveRegex', function() { - var pseudoSelector = { positiveRegex: { amenity: ['^school$', '^healthcare$'] }}; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var notOkAmenities = [ - iD.Entity({ type: 'node', tags: { amenity: 'parking' }}), - iD.Entity({ type: 'node', tags: { amenity: 'place_of_worship' }}) - ]; - notOkAmenities.forEach(function(amenity) { - expect(pseudoRule.ruleChecks.positiveRegex(amenity.tags)).to.be.false; - }); - }); - }); - describe('negativeRegex', function() { - it('is true when entity.tags\' equivalent value does not match regular exprsesion built from selector.negativeRegex', function() { - var pseudoSelector = { negativeRegex: { amenity: ['^school$', '^healthcare$'] }}; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var notOkAmenities = [ - iD.Entity({ type: 'node', tags: { amenity: 'parking' }}), - iD.Entity({ type: 'node', tags: { amenity: 'place_of_worship' }}) - ]; - notOkAmenities.forEach(function(amenity) { - expect(pseudoRule.ruleChecks.negativeRegex(amenity.tags)).to.be.true; - }); - }); - it('is false when entity.tags\' equivalent value matches regular expression built from selector.negativeRegex', function() { - var pseudoSelector = { negativeRegex: { amenity: ['^school$', '^healthcare$'] }}; - var pseudoRule = iD.utilMapCSSRule(pseudoSelector, areaKeys); - var okAmenities = [ - iD.Entity({ type: 'node', tags: { amenity: 'school' }}), - iD.Entity({ type: 'node', tags: { amenity: 'healthcare' }}) - ]; - okAmenities.forEach(function(amenity) { - expect(pseudoRule.ruleChecks.negativeRegex(amenity.tags)).to.be.false; - }); - }); - }); - }); - describe('#inferGeometry', function() { - it ('infers selector geometry from its tags', function() { - var amenityDerivedArea = { - 'geometry': 'closedway', - 'presence': 'amenity', - 'positiveRegex': { amenity: ['^school$', '^healthcare$'] }, - 'error': 'amenity cannot be healthcare or school!' - }; - - var areaDerivedArea = { - 'geometry': 'closedway', - 'equals': { area: 'yes' }, - }; - - var badAreaDerivedLine = { - 'geometry': 'closedway', - 'equals': { 'area': 'no' } - }; - - var roundHouseRailwayDerivedArea = { - 'geometry': 'closedway', - 'equals': { 'railway': 'roundhouse' } - }; - - var justClosedWayDerivedLine = { - 'geometry': 'closedway' - }; - - var rule, geom; - rule = iD.utilMapCSSRule(amenityDerivedArea, areaKeys); - geom = rule.inferGeometry(); - expect(geom).to.be.eql('area'); - - rule = iD.utilMapCSSRule(areaDerivedArea, areaKeys); - geom = rule.inferGeometry(); - expect(geom).to.be.eql('area'); - - rule = iD.utilMapCSSRule(badAreaDerivedLine, areaKeys); - geom = rule.inferGeometry(); - expect(geom).to.be.eql('line'); - - rule = iD.utilMapCSSRule(roundHouseRailwayDerivedArea, areaKeys); - geom = rule.inferGeometry(); - expect(geom).to.be.eql('area'); - - rule = iD.utilMapCSSRule(justClosedWayDerivedLine, areaKeys); - geom = rule.inferGeometry(); - expect(geom).to.be.eql('line'); - }); - }); - describe('#findWarnings', function() { - it('adds found warnings to warnings array', function() { - var graph = iD.Graph([entities]); - var warnings = []; - - rules.forEach(function(rule) { - entities.forEach(function(entity) { - rule.findWarnings(entity, graph, warnings); - }); - }); - - warnings.forEach(function(warning) { - expect(warning.message).to.not.be.null; - expect(['mapcss_warning', 'mapcss_error'].indexOf(warning.id)).to.be.greaterThan(-1); - expect(warning.entity).to.be.instanceOf(iD.Entity); - }); - }); - }); -}); -