diff --git a/modules/core/context.js b/modules/core/context.js index c0041b7c9..22cae0e69 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -441,28 +441,6 @@ export function coreContext() { locale = locale.split('-')[0]; } - /* Presets */ - presets = presetIndex(); - if (utilStringQs(window.location.hash).presets) { - presets.fromExternal(); - } else { - presets.init(); - } - - context.presets = function() { return presets; }; - - if (utilStringQs(window.location.hash).validations) { - var validationsUrl = utilStringQs(window.location.hash).validations; - d3_json(validationsUrl, function (err, mapcss) { - if (err) return; - services.maprules.init(context.presets().areaKeys()); - _each(mapcss, function(mapcssSelector) { - return services.maprules.addRule(mapcssSelector); - }); - context.validationRules = true; - }); - } - history = coreHistory(context); context.graph = history.graph; context.changes = history.changes; @@ -492,6 +470,18 @@ export function coreContext() { background = rendererBackground(context); features = rendererFeatures(context); presets = presetIndex(); + + if (utilStringQs(window.location.hash).validations) { + var validationsUrl = utilStringQs(window.location.hash).validations; + d3_json(validationsUrl, 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; @@ -511,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){ + presets = externalPresets; // default + external presets... + areaKeys = presets.areaKeys(); + }); + } else { + presets.init(); + areaKeys = presets.areaKeys(); + } return utilRebind(context, dispatch, 'on'); } diff --git a/modules/presets/index.js b/modules/presets/index.js index 7a71c5d63..8cf233a62 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -123,7 +123,7 @@ export function presetIndex() { return areaKeys; }; - all.build = function(d, visibility) { + all.build = function(d, visible) { if (d.fields) { _forEach(d.fields, function(d, id) { _fields[id] = presetField(id, d); @@ -139,7 +139,7 @@ export function presetIndex() { if (d.presets) { _forEach(d.presets, function(d, id) { - all.collection.push(presetPreset(id, d, _fields, visibility)); + all.collection.push(presetPreset(id, d, _fields, visible)); }); } @@ -171,6 +171,7 @@ export function presetIndex() { } } } + return all; }; all.init = function() { @@ -180,22 +181,40 @@ export function presetIndex() { _universal = []; _index = { point: {}, vertex: {}, line: {}, area: {}, relation: {} }; - all.build(data.presets); + return all.build(data.presets); + }; + + + 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() { - var external = utilQsString(window.location.hash).presets; - d3_json(external, function(err, presets) { + 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(presets, true); // make the external visible + all.build(externalPresets, true); // make the external visible } + done(all); }); - return all; }; all.field = function(id) { diff --git a/test/spec/presets/index.js b/test/spec/presets/index.js index e09536077..229726bf3 100644 --- a/test/spec/presets/index.js +++ b/test/spec/presets/index.js @@ -1,5 +1,5 @@ -describe('iD.presetIndex', function() { - var savedPresets; +describe('iD.presetIndex', function () { + var savedPresets, server; before(function () { savedPresets = iD.data.presets; @@ -9,7 +9,7 @@ describe('iD.presetIndex', function() { iD.data.presets = savedPresets; }); - describe('#match', function() { + describe('#match', function () { var testPresets = { presets: { point: { @@ -35,7 +35,7 @@ describe('iD.presetIndex', function() { } }; - it('returns a collection containing presets matching a geometry and tags', function() { + it('returns a collection containing presets matching a geometry and tags', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(), way = iD.Way({ tags: { highway: 'residential' } }), @@ -44,7 +44,7 @@ describe('iD.presetIndex', function() { expect(presets.match(way, graph).id).to.eql('residential'); }); - it('returns the appropriate fallback preset when no tags match', function() { + it('returns the appropriate fallback preset when no tags match', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(), point = iD.Node(), @@ -55,7 +55,7 @@ describe('iD.presetIndex', function() { expect(presets.match(line, graph).id).to.eql('line'); }); - it('matches vertices on a line as vertices', function() { + it('matches vertices on a line as vertices', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(), point = iD.Node({ tags: { leisure: 'park' } }), @@ -65,7 +65,7 @@ describe('iD.presetIndex', function() { expect(presets.match(point, graph).id).to.eql('vertex'); }); - it('matches vertices on an addr:interpolation line as points', function() { + it('matches vertices on an addr:interpolation line as points', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(), point = iD.Node({ tags: { leisure: 'park' } }), @@ -77,12 +77,12 @@ describe('iD.presetIndex', function() { }); - describe('#areaKeys', function() { + describe('#areaKeys', function () { var testPresets = { presets: { 'amenity/fuel/shell': { tags: { 'amenity': 'fuel' }, - geometry: ['point','area'], + geometry: ['point', 'area'], suggestion: true }, 'highway/foo': { @@ -113,44 +113,44 @@ describe('iD.presetIndex', function() { }; - it('whitelists keys for presets with area geometry', function() { + it('whitelists keys for presets with area geometry', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(); expect(presets.areaKeys()).to.include.keys('natural'); }); - it('blacklists key-values for presets with a line geometry', function() { + it('blacklists key-values for presets with a line geometry', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(); expect(presets.areaKeys().natural).to.include.keys('tree_row'); expect(presets.areaKeys().natural.tree_row).to.be.true; }); - it('blacklists key-values for presets with both area and line geometry', function() { + it('blacklists key-values for presets with both area and line geometry', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(); expect(presets.areaKeys().leisure).to.include.keys('track'); }); - it('does not blacklist key-values for presets with neither area nor line geometry', function() { + it('does not blacklist key-values for presets with neither area nor line geometry', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(); expect(presets.areaKeys().natural).not.to.include.keys('peak'); }); - it('does not blacklist generic \'*\' key-values', function() { + it('does not blacklist generic \'*\' key-values', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(); expect(presets.areaKeys().natural).not.to.include.keys('natural'); }); - it('ignores keys like \'highway\' that are assumed to be lines', function() { + it('ignores keys like \'highway\' that are assumed to be lines', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(); expect(presets.areaKeys()).not.to.include.keys('highway'); }); - it('ignores suggestion presets', function() { + it('ignores suggestion presets', function () { iD.data.presets = testPresets; var presets = iD.Context().presets(); expect(presets.areaKeys()).not.to.include.keys('amenity'); @@ -158,8 +158,8 @@ describe('iD.presetIndex', function() { }); describe('#build', function () { - it('builds presets from provided', function() { - var surfShop = iD.Node({ tags: { amenity: 'shop', 'shop:type': 'surf' }}), + it('builds presets from provided', function () { + var surfShop = iD.Node({ tags: { amenity: 'shop', 'shop:type': 'surf' } }), graph = iD.Graph([surfShop]), presets = iD.Context().presets(), morePresets = { @@ -173,11 +173,11 @@ describe('iD.presetIndex', function() { expect(presets.match(surfShop, graph)).to.eql(undefined); // no surfshop preset yet... presets.build(morePresets, true); - expect(presets.match(surfShop, graph).addTags).to.eql({amenity: 'shop', 'shop:type': 'surf' }); + expect(presets.match(surfShop, graph).addTags).to.eql({ amenity: 'shop', 'shop:type': 'surf' }); }); - it('configures presets\' initial visibility', function() { - var surfShop = iD.Node({ tags: { amenity: 'shop', 'shop:type': 'surf' }}), - firstStreetJetty = iD.Node({ tags: { man_made: 'jetty' }}), + it('configures presets\' initial visibility', function () { + var surfShop = iD.Node({ tags: { amenity: 'shop', 'shop:type': 'surf' } }), + firstStreetJetty = iD.Node({ tags: { man_made: 'jetty' } }), entities = [surfShop, firstStreetJetty], graph = iD.Graph(entities), presets = iD.Context().presets(), @@ -189,44 +189,259 @@ describe('iD.presetIndex', function() { }, 'man_made/jetty': { tags: { man_made: 'jetty' }, - geometry: [ 'point' ] + geometry: ['point'] } } }; presets.build(morePresets, false); - entities.forEach(function(entity) { + entities.forEach(function (entity) { var preset = presets.match(entity, graph); expect(preset.visible()).to.be.false; }); }); }); - - describe('expected matches', function() { - it('prefers building to multipolygon', function() { + describe('expected matches', function () { + + it('prefers building to multipolygon', function () { iD.data.presets = savedPresets; var presets = iD.Context().presets(), - relation = iD.Relation({ tags: { type: 'multipolygon', building: 'yes' }}), + relation = iD.Relation({ tags: { type: 'multipolygon', building: 'yes' } }), graph = iD.Graph([relation]); expect(presets.match(relation, graph).id).to.eql('building'); }); - it('prefers building to address', function() { + it('prefers building to address', function () { iD.data.presets = savedPresets; var presets = iD.Context().presets(), - way = iD.Way({ tags: { area: 'yes', building: 'yes', 'addr:housenumber': '1234' }}), + way = iD.Way({ tags: { area: 'yes', building: 'yes', 'addr:housenumber': '1234' } }), graph = iD.Graph([way]); expect(presets.match(way, graph).id).to.eql('building'); }); - it('prefers pedestrian to area', function() { + it('prefers pedestrian to area', function () { iD.data.presets = savedPresets; var presets = iD.Context().presets(), - way = iD.Way({ tags: { area: 'yes', highway: 'pedestrian' }}), + way = iD.Way({ tags: { area: 'yes', highway: 'pedestrian' } }), graph = iD.Graph([way]); expect(presets.match(way, graph).id).to.eql('highway/pedestrian_area'); }); }); + describe('#fromExternal', function () { + var morePresets; + before(function () { + morePresets = { + 'categories': { + 'category-area': { + 'icon': 'maki-natural', + 'geometry': 'area', + 'name': 'MapRules area Features', + 'members': [ + '8bc64d6d-1dbb-44a8-a2f9-80d41d067d78', + 'a9b78746-ca8a-4380-b340-157414f1464d' + ] + }, + 'category-point': { + 'icon': 'maki-natural', + 'geometry': 'point', + 'name': 'MapRules point Features', + 'members': [ + '8bc64d6d-1dbb-44a8-a2f9-80d41d067d78', + '8f83ed0b-6514-4772-a644-f04aad9d2308' + ] + } + }, + 'presets': { + '8bc64d6d-1dbb-44a8-a2f9-80d41d067d78': { + 'geometry': ['area', 'point'], + 'tags': { 'amenity': 'shop', 'shop:type': 'surf' }, + 'icon': 'maki-natural', + 'name': 'Surf Shop', + 'fields': ['358f404a-c7d5-4267-94ed-41f789b16228'], + 'matchScore': 0.99 + }, + 'a9b78746-ca8a-4380-b340-157414f1464d': { + 'geometry': ['area'], + 'tags': { 'amenity': 'marketplace' }, + 'icon': 'maki-natural', + 'name': 'Market', + 'fields': [ + 'name', + 'source', + '2161a712-f67f-4759-92fa-f5d9488ba969', + '368ecbdf-bc02-4de2-a82e-d51c250602da', + '1887834c-0cdd-4d40-852b-d29b8df94567' + ], + 'matchScore': 0.99 + }, + '8f83ed0b-6514-4772-a644-f04aad9d2308': { + 'geometry': ['point'], + 'tags': { + 'amenity': 'drinking_water', + 'man_made': 'water_tap' + }, + 'icon': 'maki-natural', + 'name': 'Water Tap', + 'fields': ['name'], + 'matchScore': 0.99 + } + }, + 'fields': { + '358f404a-c7d5-4267-94ed-41f789b16228': { + 'key': 'healthcare', + 'label': 'Healthcare', + 'overrideLabel': 'Healthcare', + 'placeholder': '...', + 'type': 'text' + }, + 'name': { + 'key': 'name', + 'type': 'localized', + 'label': 'Name', + 'universal': true, + 'placeholder': 'Common name (if any)' + }, + 'source': { + 'key': 'source', + 'type': 'semiCombo', + 'icon': 'source', + 'universal': true, + 'label': 'Sources', + 'snake_case': false, + 'caseSensitive': true, + 'options': [ + 'survey', + 'local knowledge', + 'gps', + 'aerial imagery', + 'streetlevel imagery' + ] + }, + '2161a712-f67f-4759-92fa-f5d9488ba969': { + 'key': 'building', + 'label': 'Building', + 'overrideLabel': 'Building', + 'placeholder': '...', + 'type': 'text' + }, + '368ecbdf-bc02-4de2-a82e-d51c250602da': { + 'key': 'opening_hours', + 'label': 'Opening Hours', + 'overrideLabel': 'Opening Hours', + 'placeholder': '24/7, sunrise to sunset...', + 'strings': { + 'options': { + '24/7': '24/7', + 'sunrise to sunset': 'sunrise to sunset' + } + }, + 'type': 'combo' + }, + '1887834c-0cdd-4d40-852b-d29b8df94567': { + 'key': 'height', + 'label': 'Height', + 'overrideLabel': 'Height', + 'placeholder': '...', + 'minValue': 1, 'type': 'number' + }, + 'relation': { + 'key': 'type', + 'type': 'combo', + 'label': 'Type' + }, + 'comment': { + 'key': 'comment', + 'type': 'textarea', + 'label': 'Changeset Comment', + 'placeholder': 'Brief description of your contributions (required)' + }, + 'hashtags': { + 'key': 'hashtags', + 'type': 'semiCombo', + 'label': 'Suggested Hashtags', + 'placeholder': '#example' + } + }, + 'defaults': { + 'point': [ + 'point', + '8bc64d6d-1dbb-44a8-a2f9-80d41d067d78', + '8f83ed0b-6514-4772-a644-f04aad9d2308' + ], + 'line': ['line'], + 'area': [ + 'area', + '8bc64d6d-1dbb-44a8-a2f9-80d41d067d78', + 'a9b78746-ca8a-4380-b340-157414f1464d' + ], + 'vertex': ['vertex'], + 'relation': ['relation'] + } + }; + + + }); + beforeEach(function () { + server = sinon.fakeServer.create(); + }); + afterEach(function () { + server.restore(); + }); + it('builds presets w/external sources set to visible', function () { + var surfShop = iD.Node({ tags: { amenity: 'shop', 'shop:type': 'surf' } }), + graph = iD.Graph([surfShop]), + maprules = 'https://fakemaprules.io', + presetLocation = '/config/dfcfac13-ba7c-4223-8880-c856180e5c5b/presets/iD/', + match = new RegExp(presetLocation), + external = maprules + presetLocation; + + // no exernal presets yet + expect(iD.Context().presets().match(surfShop, graph).id).to.eql('amenity'); + // reset graph... + graph = iD.Graph([surfShop]); + + // add the validations query param... + iD.Context().presets().fromExternal(external, function (externalPresets) { + // includes newer presets... + expect(externalPresets.match(surfShop, graph).id).to.eql('8bc64d6d-1dbb-44a8-a2f9-80d41d067d78'); + }); + + server.respondWith('GET', match, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(morePresets)] + ); + server.respond(); + }); + it('makes only the external presets initially visible', function () { + var maprules = 'https://fakemaprules.io', + presetLocation = '/config/dfcfac13-ba7c-4223-8880-c856180e5c5b/presets/iD/', + match = new RegExp(presetLocation), + external = maprules + presetLocation; + + iD.Context().presets().fromExternal(external, function(externalPresets) { + var external = externalPresets.collection.reduce(function(presets, preset) { + if (!preset.hasOwnProperty('members') && preset.visible()) { + presets.push(preset.id); + } + return presets; + }, []); + + var morePresetKeys = Object.keys(morePresets.presets); + + expect(morePresetKeys.length).to.eql(external.length); + + morePresetKeys.forEach(function(presetId) { + expect(external.indexOf(presetId)).to.be.at.least(0); + }); + }); + + + server.respondWith('GET', match, + [200, { 'Content-Type': 'application/json' }, JSON.stringify(morePresets)] + ); + server.respond(); + }); + }); + });