diff --git a/data/presets/presets.json b/data/presets/presets.json index 4df1926b5..cc165ab62 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -4509,8 +4509,11 @@ "tags": { "type": "multipolygon" }, + "removeTags": {}, "name": "Multipolygon", - "icon": "multipolygon" + "icon": "multipolygon", + "searchable": false, + "matchScore": 0.1 }, "type/restriction": { "geometry": [ diff --git a/data/presets/presets/type/multipolygon.json b/data/presets/presets/type/multipolygon.json index bc6c4296b..01f0f3c21 100644 --- a/data/presets/presets/type/multipolygon.json +++ b/data/presets/presets/type/multipolygon.json @@ -6,6 +6,9 @@ "tags": { "type": "multipolygon" }, + "removeTags": {}, "name": "Multipolygon", - "icon": "multipolygon" + "icon": "multipolygon", + "searchable": false, + "matchScore": 0.1 } \ No newline at end of file diff --git a/data/presets/schema/preset.json b/data/presets/schema/preset.json index 11d3721ed..7e8144db6 100644 --- a/data/presets/schema/preset.json +++ b/data/presets/schema/preset.json @@ -27,6 +27,20 @@ }, "required": true }, + "addTags": { + "description": "Tags that are added when changing to the preset (default is the same value as 'tags')", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "removeTags": { + "description": "Tags that are removed when changing to another preset (default is the same value as 'tags')", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "fields": { "description": "Form fields that are displayed for the preset", "type": "array", @@ -49,6 +63,11 @@ "description": "Whether or not the preset will be suggested via search", "type": "boolean", "default": true + }, + "matchScore": { + "description": "The quality score this preset will receive when being compared with other matches (higher is better)", + "type": "number", + "default": 1.0 } }, "additionalProperties": false diff --git a/js/id/presets/preset.js b/js/id/presets/preset.js index 17ada454f..860d23973 100644 --- a/js/id/presets/preset.js +++ b/js/id/presets/preset.js @@ -12,24 +12,21 @@ iD.presets.Preset = function(id, preset, fields) { return preset.geometry.indexOf(geometry) >= 0; }; + var matchScore = preset.matchScore || 1; preset.matchScore = function(entity) { var tags = preset.tags, score = 0; + for (var t in tags) { if (entity.tags[t] === tags[t]) { - if (t === 'area') { - // score area tag lower to prevent other/area preset - // from being chosen over something more specific - score += 0.5; - } else { - score += 1; - } + score += matchScore; } else if (tags[t] === '*' && t in entity.tags) { - score += 0.5; + score += matchScore / 2; } else { return -1; } } + return score; }; @@ -59,30 +56,32 @@ iD.presets.Preset = function(id, preset, fields) { return reference; }; + var removeTags = preset.removeTags || preset.tags; preset.removeTags = function(tags, geometry) { - tags = _.omit(tags, _.keys(preset.tags)); + tags = _.omit(tags, _.keys(removeTags)); - for (var i in preset.fields) { - var field = preset.fields[i]; + for (var f in preset.fields) { + var field = preset.fields[f]; if (field.matchGeometry(geometry) && field['default'] === tags[field.key]) { delete tags[field.key]; } } - return tags; + return tags; }; + var applyTags = preset.applyTags || preset.tags; preset.applyTags = function(tags, geometry) { tags = _.clone(tags); - for (var k in preset.tags) { - if (preset.tags[k] !== '*') tags[k] = preset.tags[k]; + for (var k in applyTags) { + if (applyTags[k] !== '*') tags[k] = applyTags[k]; } for (var f in preset.fields) { - f = preset.fields[f]; - if (f.matchGeometry(geometry) && f.key && !tags[f.key] && f['default']) { - tags[f.key] = f['default']; + var field = preset.fields[f]; + if (field.matchGeometry(geometry) && field.key && !tags[field.key] && field['default']) { + tags[field.key] = field['default']; } } diff --git a/test/spec/presets/preset.js b/test/spec/presets/preset.js index 6e9b46bc4..8d13fa94c 100644 --- a/test/spec/presets/preset.js +++ b/test/spec/presets/preset.js @@ -1,108 +1,108 @@ describe('iD.presets.Preset', function() { - - var fields, p; - - beforeEach(function() { - if (!p) { - fields = {}; - var i = 0; - for (i in iD.data.presets.fields) { - fields[i] = iD.presets.Field(i, iD.data.presets.fields[i]); - } - p = {}; - for (i in iD.data.presets.presets) { - p[i] = iD.presets.Preset(i, iD.data.presets.presets[i], fields); - } - } - }); - - var w1 = iD.Way({ tags: { - highway: 'motorway' - }}), - w2 = iD.Way({ tags: { - leisure: 'pitch', - sport: 'tennis' - }}), - w3 = iD.Way({ tags: { - highway: 'residential' - }}), - w4 = iD.Way({ tags: { - building: 'yep' - }}), - w5 = iD.Way(), - g = iD.Graph().replace(w1).replace(w2); - - it("has optional fields", function() { - expect(p.point.fields).to.eql([]); + var preset = iD.presets.Preset('test', {}); + expect(preset.fields).to.eql([]); }); describe('#matchGeometry', function() { - var n = iD.Node(); - var g = iD.Graph().replace(n); - it("returns false if it doesn't match", function() { - expect(p['highway/residential'].matchGeometry('point')).to.equal(false); + var preset = iD.presets.Preset('test', {geometry: ['line']}); + expect(preset.matchGeometry('point')).to.equal(false); }); it("returns true if it does match", function() { - expect(p.point.matchGeometry('point')).to.equal(true); + var preset = iD.presets.Preset('test', {geometry: ['point', 'line']}); + expect(preset.matchGeometry('point')).to.equal(true); }); }); describe('#matchScore', function() { it("returns -1 if preset does not match tags", function() { - expect(p['highway/residential'].matchScore(w1)).to.equal(-1); + var preset = iD.presets.Preset('test', {tags: {foo: 'bar'}}), + entity = iD.Way({tags: {highway: 'motorway'}}); + expect(preset.matchScore(entity)).to.equal(-1); }); - it("returns 0 for fallback presets", function() { - expect(p.point.matchScore(w1)).to.equal(0); + it("returns the value of the matchScore property when matched", function() { + var preset = iD.presets.Preset('test', {tags: {highway: 'motorway'}, matchScore: 0.2}), + entity = iD.Way({tags: {highway: 'motorway'}}); + expect(preset.matchScore(entity)).to.equal(0.2); }); - it("returns the number of matched tags", function() { - expect(p['highway/residential'].matchScore(w3)).to.equal(1); - expect(p['leisure/pitch/tennis'].matchScore(w2)).to.equal(2); + it("defaults to the number of matched tags", function() { + var preset = iD.presets.Preset('test', {tags: {highway: 'residential'}}), + entity = iD.Way({tags: {highway: 'residential'}}); + expect(preset.matchScore(entity)).to.equal(1); + + var preset = iD.presets.Preset('test', {tags: {highway: 'service', service: 'alley'}}), + entity = iD.Way({tags: {highway: 'service', service: 'alley'}}); + expect(preset.matchScore(entity)).to.equal(2); }); - it("counts * as a match for any value", function() { - expect(p.building.matchScore(w4)).to.equal(0.5); - expect(p.building.matchScore(w5)).to.equal(-1); + it("counts * as a match for any value with score 0.5", function() { + var preset = iD.presets.Preset('test', {tags: {building: '*'}}), + entity = iD.Way({tags: {building: 'yep'}}); + expect(preset.matchScore(entity)).to.equal(0.5); }); }); describe("isFallback", function() { it("returns true if preset has no tags", function() { - expect(iD.presets.Preset("area", {name: "Area", tags: {}}).isFallback()).to.equal(true); + var preset = iD.presets.Preset("area", {tags: {}}); + expect(preset.isFallback()).to.equal(true); }); - it("returns false if preset has no tags", function() { - expect(p.building.isFallback()).to.equal(false); + it("returns false if preset has tags", function() { + var preset = iD.presets.Preset("area", {tags: {building: 'yes'}}); + expect(preset.isFallback()).to.equal(false); }); }); describe('#applyTags', function() { it("adds match tags", function() { - expect(p['highway/residential'].applyTags({}, 'area')).to.eql({ highway: 'residential' }); + var preset = iD.presets.Preset('test', {tags: {highway: 'residential'}}); + expect(preset.applyTags({}, 'area')).to.eql({highway: 'residential'}); }); it("does not add wildcard tags", function() { - expect(p.amenity.applyTags({}, 'area')).to.eql({}); + var preset = iD.presets.Preset('test', {tags: {building: '*'}}); + expect(preset.applyTags({}, 'area')).to.eql({}); }); - it("adds default tags", function() { - expect(p['amenity/cafe'].applyTags({}, 'area')).to.eql({ amenity: 'cafe', building: 'yes'}); - expect(p['amenity/cafe'].applyTags({}, 'point')).to.eql({ amenity: 'cafe' }); + it("adds default tags of fields with matching geometry", function() { + var field = iD.presets.Field('field', {key: 'building', geometry: 'area', default: 'yes'}), + preset = iD.presets.Preset('test', {fields: ['field']}, {field: field}); + expect(preset.applyTags({}, 'area')).to.eql({building: 'yes'}); + }); + + it("adds no default tags of fields with non-matching geometry", function() { + var field = iD.presets.Field('field', {key: 'building', geometry: 'area', default: 'yes'}), + preset = iD.presets.Preset('test', {fields: ['field']}, {field: field}); + expect(preset.applyTags({}, 'point')).to.eql({}); }); }); describe('#removeTags', function() { - it('removes match tags', function() { - expect(p['highway/residential'].removeTags({ highway: 'residential' }, 'area')).to.eql({}); + it('removes tags that match preset tags', function() { + var preset = iD.presets.Preset('test', {tags: {highway: 'residential'}}); + expect(preset.removeTags({highway: 'residential'}, 'area')).to.eql({}); }); - it('removes default tags', function() { - expect(p['amenity/cafe'].removeTags({ amenity: 'cafe', building: 'yes'}, 'area')).to.eql({}); - expect(p['amenity/cafe'].removeTags({ amenity: 'cafe', building: 'yep'}, 'area')).to.eql({ building: 'yep'}); + it('removes tags that match field default tags', function() { + var field = iD.presets.Field('field', {key: 'building', geometry: 'area', default: 'yes'}), + preset = iD.presets.Preset('test', {fields: ['field']}, {field: field}); + expect(preset.removeTags({building: 'yes'}, 'area')).to.eql({}); + }); + + it('preserves tags that do not match field default tags', function() { + var field = iD.presets.Field('field', {key: 'building', geometry: 'area', default: 'yes'}), + preset = iD.presets.Preset('test', {fields: ['field']}, {field: field}); + expect(preset.removeTags({building: 'yep'}, 'area')).to.eql({ building: 'yep'}); + }); + + it('preserves tags that are not listed in removeTags', function() { + var preset = iD.presets.Preset('test', {tags: {a: 'b'}, removeTags: {}}); + expect(preset.removeTags({a: 'b'}, 'area')).to.eql({a: 'b'}); }); }); });