From 34904f0525be4f21a119058d38f3d4049558e524 Mon Sep 17 00:00:00 2001 From: David Culverhouse Date: Wed, 15 Jun 2016 00:11:32 -0400 Subject: [PATCH 1/2] Migrate "modes" into ES6 modules for #3118 --- Makefile | 18 +- js/lib/id/modes.js | 1340 ++++++++++++++++++++++++ {js/id => modules}/modes/add_area.js | 4 +- {js/id => modules}/modes/add_line.js | 4 +- {js/id => modules}/modes/add_point.js | 4 +- {js/id => modules}/modes/browse.js | 4 +- {js/id => modules}/modes/drag_node.js | 4 +- {js/id => modules}/modes/draw_area.js | 4 +- {js/id => modules}/modes/draw_line.js | 4 +- modules/modes/index.js | 11 + {js/id => modules}/modes/move.js | 4 +- {js/id => modules}/modes/rotate_way.js | 4 +- {js/id => modules}/modes/save.js | 4 +- {js/id => modules}/modes/select.js | 4 +- test/index.html | 13 +- 15 files changed, 1379 insertions(+), 47 deletions(-) create mode 100644 js/lib/id/modes.js rename {js/id => modules}/modes/add_area.js (97%) rename {js/id => modules}/modes/add_line.js (97%) rename {js/id => modules}/modes/add_point.js (96%) rename {js/id => modules}/modes/browse.js (96%) rename {js/id => modules}/modes/drag_node.js (99%) rename {js/id => modules}/modes/draw_area.js (93%) rename {js/id => modules}/modes/draw_line.js (92%) create mode 100644 modules/modes/index.js rename {js/id => modules}/modes/move.js (98%) rename {js/id => modules}/modes/rotate_way.js (97%) rename {js/id => modules}/modes/save.js (99%) rename {js/id => modules}/modes/select.js (99%) diff --git a/Makefile b/Makefile index c04c4128a..a159f8739 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,8 @@ $(BUILDJS_TARGETS): $(BUILDJS_SOURCES) build.js MODULE_TARGETS = \ js/lib/id/actions.js \ - js/lib/id/validations.js + js/lib/id/validations.js \ + js/lib/id/modes.js js/lib/id/actions.js: modules/ node_modules/.bin/rollup -f umd -n iD.actions modules/actions/index.js --no-strict > $@ @@ -52,6 +53,9 @@ js/lib/id/actions.js: modules/ js/lib/id/validations.js: modules/ node_modules/.bin/rollup -f umd -n iD.validations modules/validations/index.js --no-strict > $@ +js/lib/id/modes.js: modules/ + node_modules/.bin/rollup -f umd -n iD.modes modules/modes/index.js --no-strict > $@ + dist/iD.js: \ js/lib/bootstrap-tooltip.js \ js/lib/d3.v3.js \ @@ -103,18 +107,6 @@ dist/iD.js: \ js/id/behavior/paste.js \ js/id/behavior/select.js \ js/id/behavior/tail.js \ - js/id/modes.js \ - js/id/modes/add_area.js \ - js/id/modes/add_line.js \ - js/id/modes/add_point.js \ - js/id/modes/browse.js \ - js/id/modes/drag_node.js \ - js/id/modes/draw_area.js \ - js/id/modes/draw_line.js \ - js/id/modes/move.js \ - js/id/modes/rotate_way.js \ - js/id/modes/save.js \ - js/id/modes/select.js \ js/id/operations.js \ js/id/operations/circularize.js \ js/id/operations/continue.js \ diff --git a/js/lib/id/modes.js b/js/lib/id/modes.js new file mode 100644 index 000000000..cb2d1f28c --- /dev/null +++ b/js/lib/id/modes.js @@ -0,0 +1,1340 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.iD = global.iD || {}, global.iD.modes = global.iD.modes || {}))); +}(this, function (exports) { 'use strict'; + + function AddArea(context) { + var mode = { + id: 'add-area', + button: 'area', + title: t('modes.add_area.title'), + description: t('modes.add_area.description'), + key: '3' + }; + + var behavior = iD.behavior.AddWay(context) + .tail(t('modes.add_area.tail')) + .on('start', start) + .on('startFromWay', startFromWay) + .on('startFromNode', startFromNode), + defaultTags = {area: 'yes'}; + + function start(loc) { + var graph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawArea(context, way.id, graph)); + } + + function startFromWay(loc, edge) { + var graph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddMidpoint({ loc: loc, edge: edge }, node)); + + context.enter(iD.modes.DrawArea(context, way.id, graph)); + } + + function startFromNode(node) { + var graph = context.graph(), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawArea(context, way.id, graph)); + } + + mode.enter = function() { + context.install(behavior); + }; + + mode.exit = function() { + context.uninstall(behavior); + }; + + return mode; + } + + function AddLine(context) { + var mode = { + id: 'add-line', + button: 'line', + title: t('modes.add_line.title'), + description: t('modes.add_line.description'), + key: '2' + }; + + var behavior = iD.behavior.AddWay(context) + .tail(t('modes.add_line.tail')) + .on('start', start) + .on('startFromWay', startFromWay) + .on('startFromNode', startFromNode); + + function start(loc) { + var baseGraph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way(); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawLine(context, way.id, baseGraph)); + } + + function startFromWay(loc, edge) { + var baseGraph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way(); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddMidpoint({ loc: loc, edge: edge }, node)); + + context.enter(iD.modes.DrawLine(context, way.id, baseGraph)); + } + + function startFromNode(node) { + var baseGraph = context.graph(), + way = iD.Way(); + + context.perform( + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawLine(context, way.id, baseGraph)); + } + + mode.enter = function() { + context.install(behavior); + }; + + mode.exit = function() { + context.uninstall(behavior); + }; + + return mode; + } + + function AddPoint(context) { + var mode = { + id: 'add-point', + button: 'point', + title: t('modes.add_point.title'), + description: t('modes.add_point.description'), + key: '1' + }; + + var behavior = iD.behavior.Draw(context) + .tail(t('modes.add_point.tail')) + .on('click', add) + .on('clickWay', addWay) + .on('clickNode', addNode) + .on('cancel', cancel) + .on('finish', cancel); + + function add(loc) { + var node = iD.Node({loc: loc}); + + context.perform( + iD.actions.AddEntity(node), + t('operations.add.annotation.point')); + + context.enter( + iD.modes.Select(context, [node.id]) + .suppressMenu(true) + .newFeature(true)); + } + + function addWay(loc) { + add(loc); + } + + function addNode(node) { + add(node.loc); + } + + function cancel() { + context.enter(iD.modes.Browse(context)); + } + + mode.enter = function() { + context.install(behavior); + }; + + mode.exit = function() { + context.uninstall(behavior); + }; + + return mode; + } + + function Browse(context) { + var mode = { + button: 'browse', + id: 'browse', + title: t('modes.browse.title'), + description: t('modes.browse.description') + }, sidebar; + + var behaviors = [ + iD.behavior.Paste(context), + iD.behavior.Hover(context) + .on('hover', context.ui().sidebar.hover), + iD.behavior.Select(context), + iD.behavior.Lasso(context), + iD.modes.DragNode(context).behavior]; + + mode.enter = function() { + behaviors.forEach(function(behavior) { + context.install(behavior); + }); + + // Get focus on the body. + if (document.activeElement && document.activeElement.blur) { + document.activeElement.blur(); + } + + if (sidebar) { + context.ui().sidebar.show(sidebar); + } else { + context.ui().sidebar.select(null); + } + }; + + mode.exit = function() { + context.ui().sidebar.hover.cancel(); + behaviors.forEach(function(behavior) { + context.uninstall(behavior); + }); + + if (sidebar) { + context.ui().sidebar.hide(); + } + }; + + mode.sidebar = function(_) { + if (!arguments.length) return sidebar; + sidebar = _; + return mode; + }; + + return mode; + } + + function DragNode(context) { + var mode = { + id: 'drag-node', + button: 'browse' + }; + + var nudgeInterval, + activeIDs, + wasMidpoint, + cancelled, + selectedIDs = [], + hover = iD.behavior.Hover(context) + .altDisables(true) + .on('hover', context.ui().sidebar.hover), + edit = iD.behavior.Edit(context); + + function edge(point, size) { + var pad = [30, 100, 30, 100]; + if (point[0] > size[0] - pad[0]) return [-10, 0]; + else if (point[0] < pad[2]) return [10, 0]; + else if (point[1] > size[1] - pad[1]) return [0, -10]; + else if (point[1] < pad[3]) return [0, 10]; + return null; + } + + function startNudge(nudge) { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = window.setInterval(function() { + context.pan(nudge); + }, 50); + } + + function stopNudge() { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = null; + } + + function moveAnnotation(entity) { + return t('operations.move.annotation.' + entity.geometry(context.graph())); + } + + function connectAnnotation(entity) { + return t('operations.connect.annotation.' + entity.geometry(context.graph())); + } + + function origin(entity) { + return context.projection(entity.loc); + } + + function start(entity) { + cancelled = d3.event.sourceEvent.shiftKey || + context.features().hasHiddenConnections(entity, context.graph()); + + if (cancelled) return behavior.cancel(); + + wasMidpoint = entity.type === 'midpoint'; + if (wasMidpoint) { + var midpoint = entity; + entity = iD.Node(); + context.perform(iD.actions.AddMidpoint(midpoint, entity)); + + var vertex = context.surface() + .selectAll('.' + entity.id); + behavior.target(vertex.node(), entity); + + } else { + context.perform( + iD.actions.Noop()); + } + + activeIDs = _.map(context.graph().parentWays(entity), 'id'); + activeIDs.push(entity.id); + + context.enter(mode); + } + + function datum() { + if (d3.event.sourceEvent.altKey) { + return {}; + } + + return d3.event.sourceEvent.target.__data__ || {}; + } + + // via https://gist.github.com/shawnbot/4166283 + function childOf(p, c) { + if (p === c) return false; + while (c && c !== p) c = c.parentNode; + return c === p; + } + + function move(entity) { + if (cancelled) return; + d3.event.sourceEvent.stopPropagation(); + + var nudge = childOf(context.container().node(), + d3.event.sourceEvent.toElement) && + edge(d3.event.point, context.map().dimensions()); + + if (nudge) startNudge(nudge); + else stopNudge(); + + var loc = context.projection.invert(d3.event.point); + + var d = datum(); + if (d.type === 'node' && d.id !== entity.id) { + loc = d.loc; + } else if (d.type === 'way' && !d3.select(d3.event.sourceEvent.target).classed('fill')) { + loc = iD.geo.chooseEdge(context.childNodes(d), context.mouse(), context.projection).loc; + } + + context.replace( + iD.actions.MoveNode(entity.id, loc), + moveAnnotation(entity)); + } + + function end(entity) { + if (cancelled) return; + + var d = datum(); + + if (d.type === 'way') { + var choice = iD.geo.chooseEdge(context.childNodes(d), context.mouse(), context.projection); + context.replace( + iD.actions.AddMidpoint({ loc: choice.loc, edge: [d.nodes[choice.index - 1], d.nodes[choice.index]] }, entity), + connectAnnotation(d)); + + } else if (d.type === 'node' && d.id !== entity.id) { + context.replace( + iD.actions.Connect([d.id, entity.id]), + connectAnnotation(d)); + + } else if (wasMidpoint) { + context.replace( + iD.actions.Noop(), + t('operations.add.annotation.vertex')); + + } else { + context.replace( + iD.actions.Noop(), + moveAnnotation(entity)); + } + + var reselection = selectedIDs.filter(function(id) { + return context.graph().hasEntity(id); + }); + + if (reselection.length) { + context.enter( + iD.modes.Select(context, reselection) + .suppressMenu(true)); + } else { + context.enter(iD.modes.Browse(context)); + } + } + + function cancel() { + behavior.cancel(); + context.enter(iD.modes.Browse(context)); + } + + function setActiveElements() { + context.surface().selectAll(iD.util.entitySelector(activeIDs)) + .classed('active', true); + } + + var behavior = iD.behavior.drag() + .delegate('g.node, g.point, g.midpoint') + .surface(context.surface().node()) + .origin(origin) + .on('start', start) + .on('move', move) + .on('end', end); + + mode.enter = function() { + context.install(hover); + context.install(edit); + + context.history() + .on('undone.drag-node', cancel); + + context.map() + .on('drawn.drag-node', setActiveElements); + + setActiveElements(); + }; + + mode.exit = function() { + context.ui().sidebar.hover.cancel(); + context.uninstall(hover); + context.uninstall(edit); + + context.history() + .on('undone.drag-node', null); + + context.map() + .on('drawn.drag-node', null); + + context.surface() + .selectAll('.active') + .classed('active', false); + + stopNudge(); + }; + + mode.selectedIDs = function(_) { + if (!arguments.length) return selectedIDs; + selectedIDs = _; + return mode; + }; + + mode.behavior = behavior; + + return mode; + } + + function DrawArea(context, wayId, baseGraph) { + var mode = { + button: 'area', + id: 'draw-area' + }; + + var behavior; + + mode.enter = function() { + var way = context.entity(wayId), + headId = way.nodes[way.nodes.length - 2], + tailId = way.first(); + + behavior = iD.behavior.DrawWay(context, wayId, -1, mode, baseGraph) + .tail(t('modes.draw_area.tail')); + + var addNode = behavior.addNode; + + behavior.addNode = function(node) { + if (node.id === headId || node.id === tailId) { + behavior.finish(); + } else { + addNode(node); + } + }; + + context.install(behavior); + }; + + mode.exit = function() { + context.uninstall(behavior); + }; + + mode.selectedIDs = function() { + return [wayId]; + }; + + return mode; + } + + function DrawLine(context, wayId, baseGraph, affix) { + var mode = { + button: 'line', + id: 'draw-line' + }; + + var behavior; + + mode.enter = function() { + var way = context.entity(wayId), + index = (affix === 'prefix') ? 0 : undefined, + headId = (affix === 'prefix') ? way.first() : way.last(); + + behavior = iD.behavior.DrawWay(context, wayId, index, mode, baseGraph) + .tail(t('modes.draw_line.tail')); + + var addNode = behavior.addNode; + + behavior.addNode = function(node) { + if (node.id === headId) { + behavior.finish(); + } else { + addNode(node); + } + }; + + context.install(behavior); + }; + + mode.exit = function() { + context.uninstall(behavior); + }; + + mode.selectedIDs = function() { + return [wayId]; + }; + + return mode; + } + + function Move(context, entityIDs, baseGraph) { + var mode = { + id: 'move', + button: 'browse' + }; + + var keybinding = d3.keybinding('move'), + edit = iD.behavior.Edit(context), + annotation = entityIDs.length === 1 ? + t('operations.move.annotation.' + context.geometry(entityIDs[0])) : + t('operations.move.annotation.multiple'), + cache, + origin, + nudgeInterval; + + function vecSub(a, b) { return [a[0] - b[0], a[1] - b[1]]; } + + function edge(point, size) { + var pad = [30, 100, 30, 100]; + if (point[0] > size[0] - pad[0]) return [-10, 0]; + else if (point[0] < pad[2]) return [10, 0]; + else if (point[1] > size[1] - pad[1]) return [0, -10]; + else if (point[1] < pad[3]) return [0, 10]; + return null; + } + + function startNudge(nudge) { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = window.setInterval(function() { + context.pan(nudge); + + var currMouse = context.mouse(), + origMouse = context.projection(origin), + delta = vecSub(vecSub(currMouse, origMouse), nudge), + action = iD.actions.Move(entityIDs, delta, context.projection, cache); + + context.overwrite(action, annotation); + + }, 50); + } + + function stopNudge() { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = null; + } + + function move() { + var currMouse = context.mouse(), + origMouse = context.projection(origin), + delta = vecSub(currMouse, origMouse), + action = iD.actions.Move(entityIDs, delta, context.projection, cache); + + context.overwrite(action, annotation); + + var nudge = edge(currMouse, context.map().dimensions()); + if (nudge) startNudge(nudge); + else stopNudge(); + } + + function finish() { + d3.event.stopPropagation(); + context.enter(iD.modes.Select(context, entityIDs).suppressMenu(true)); + stopNudge(); + } + + function cancel() { + if (baseGraph) { + while (context.graph() !== baseGraph) context.pop(); + context.enter(iD.modes.Browse(context)); + } else { + context.pop(); + context.enter(iD.modes.Select(context, entityIDs).suppressMenu(true)); + } + stopNudge(); + } + + function undone() { + context.enter(iD.modes.Browse(context)); + } + + mode.enter = function() { + origin = context.map().mouseCoordinates(); + cache = {}; + + context.install(edit); + + context.perform( + iD.actions.Noop(), + annotation); + + context.surface() + .on('mousemove.move', move) + .on('click.move', finish); + + context.history() + .on('undone.move', undone); + + keybinding + .on('⎋', cancel) + .on('↩', finish); + + d3.select(document) + .call(keybinding); + }; + + mode.exit = function() { + stopNudge(); + + context.uninstall(edit); + + context.surface() + .on('mousemove.move', null) + .on('click.move', null); + + context.history() + .on('undone.move', null); + + keybinding.off(); + }; + + return mode; + } + + function RotateWay(context, wayId) { + var mode = { + id: 'rotate-way', + button: 'browse' + }; + + var keybinding = d3.keybinding('rotate-way'), + edit = iD.behavior.Edit(context); + + mode.enter = function() { + context.install(edit); + + var annotation = t('operations.rotate.annotation.' + context.geometry(wayId)), + way = context.graph().entity(wayId), + nodes = _.uniq(context.graph().childNodes(way)), + points = nodes.map(function(n) { return context.projection(n.loc); }), + pivot = d3.geom.polygon(points).centroid(), + angle; + + context.perform( + iD.actions.Noop(), + annotation); + + function rotate() { + + var mousePoint = context.mouse(), + newAngle = Math.atan2(mousePoint[1] - pivot[1], mousePoint[0] - pivot[0]); + + if (typeof angle === 'undefined') angle = newAngle; + + context.replace( + iD.actions.RotateWay(wayId, pivot, newAngle - angle, context.projection), + annotation); + + angle = newAngle; + } + + function finish() { + d3.event.stopPropagation(); + context.enter(iD.modes.Select(context, [wayId]) + .suppressMenu(true)); + } + + function cancel() { + context.pop(); + context.enter(iD.modes.Select(context, [wayId]) + .suppressMenu(true)); + } + + function undone() { + context.enter(iD.modes.Browse(context)); + } + + context.surface() + .on('mousemove.rotate-way', rotate) + .on('click.rotate-way', finish); + + context.history() + .on('undone.rotate-way', undone); + + keybinding + .on('⎋', cancel) + .on('↩', finish); + + d3.select(document) + .call(keybinding); + }; + + mode.exit = function() { + context.uninstall(edit); + + context.surface() + .on('mousemove.rotate-way', null) + .on('click.rotate-way', null); + + context.history() + .on('undone.rotate-way', null); + + keybinding.off(); + }; + + return mode; + } + + function Save(context) { + var ui = iD.ui.Commit(context) + .on('cancel', cancel) + .on('save', save); + + function cancel() { + context.enter(iD.modes.Browse(context)); + } + + function save(e, tryAgain) { + function withChildNodes(ids, graph) { + return _.uniq(_.reduce(ids, function(result, id) { + var e = graph.entity(id); + if (e.type === 'way') { + try { + var cn = graph.childNodes(e); + result.push.apply(result, _.map(_.filter(cn, 'version'), 'id')); + } catch (err) { + /* eslint-disable no-console */ + if (typeof console !== 'undefined') console.error(err); + /* eslint-enable no-console */ + } + } + return result; + }, _.clone(ids))); + } + + var loading = iD.ui.Loading(context).message(t('save.uploading')).blocking(true), + history = context.history(), + origChanges = history.changes(iD.actions.DiscardTags(history.difference())), + localGraph = context.graph(), + remoteGraph = iD.Graph(history.base(), true), + modified = _.filter(history.difference().summary(), {changeType: 'modified'}), + toCheck = _.map(_.map(modified, 'entity'), 'id'), + toLoad = withChildNodes(toCheck, localGraph), + conflicts = [], + errors = []; + + if (!tryAgain) history.perform(iD.actions.Noop()); // checkpoint + context.container().call(loading); + + if (toCheck.length) { + context.connection().loadMultiple(toLoad, loaded); + } else { + finalize(); + } + + + // Reload modified entities into an alternate graph and check for conflicts.. + function loaded(err, result) { + if (errors.length) return; + + if (err) { + errors.push({ + msg: err.responseText, + details: [ t('save.status_code', { code: err.status }) ] + }); + showErrors(); + + } else { + var loadMore = []; + _.each(result.data, function(entity) { + remoteGraph.replace(entity); + toLoad = _.without(toLoad, entity.id); + + // Because loadMultiple doesn't download /full like loadEntity, + // need to also load children that aren't already being checked.. + if (!entity.visible) return; + if (entity.type === 'way') { + loadMore.push.apply(loadMore, + _.difference(entity.nodes, toCheck, toLoad, loadMore)); + } else if (entity.type === 'relation' && entity.isMultipolygon()) { + loadMore.push.apply(loadMore, + _.difference(_.map(entity.members, 'id'), toCheck, toLoad, loadMore)); + } + }); + + if (loadMore.length) { + toLoad.push.apply(toLoad, loadMore); + context.connection().loadMultiple(loadMore, loaded); + } + + if (!toLoad.length) { + checkConflicts(); + } + } + } + + + function checkConflicts() { + function choice(id, text, action) { + return { id: id, text: text, action: function() { history.replace(action); } }; + } + function formatUser(d) { + return '' + d + ''; + } + function entityName(entity) { + return iD.util.displayName(entity) || (iD.util.displayType(entity.id) + ' ' + entity.id); + } + + function compareVersions(local, remote) { + if (local.version !== remote.version) return false; + + if (local.type === 'way') { + var children = _.union(local.nodes, remote.nodes); + + for (var i = 0; i < children.length; i++) { + var a = localGraph.hasEntity(children[i]), + b = remoteGraph.hasEntity(children[i]); + + if (a && b && a.version !== b.version) return false; + } + } + + return true; + } + + _.each(toCheck, function(id) { + var local = localGraph.entity(id), + remote = remoteGraph.entity(id); + + if (compareVersions(local, remote)) return; + + var action = iD.actions.MergeRemoteChanges, + merge = action(id, localGraph, remoteGraph, formatUser); + + history.replace(merge); + + var mergeConflicts = merge.conflicts(); + if (!mergeConflicts.length) return; // merged safely + + var forceLocal = action(id, localGraph, remoteGraph).withOption('force_local'), + forceRemote = action(id, localGraph, remoteGraph).withOption('force_remote'), + keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore')), + keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete')); + + conflicts.push({ + id: id, + name: entityName(local), + details: mergeConflicts, + chosen: 1, + choices: [ + choice(id, keepMine, forceLocal), + choice(id, keepTheirs, forceRemote) + ] + }); + }); + + finalize(); + } + + + function finalize() { + if (conflicts.length) { + conflicts.sort(function(a,b) { return b.id.localeCompare(a.id); }); + showConflicts(); + } else if (errors.length) { + showErrors(); + } else { + var changes = history.changes(iD.actions.DiscardTags(history.difference())); + if (changes.modified.length || changes.created.length || changes.deleted.length) { + context.connection().putChangeset( + changes, + e.comment, + history.imageryUsed(), + function(err, changeset_id) { + if (err) { + errors.push({ + msg: err.responseText, + details: [ t('save.status_code', { code: err.status }) ] + }); + showErrors(); + } else { + history.clearSaved(); + success(e, changeset_id); + // Add delay to allow for postgres replication #1646 #2678 + window.setTimeout(function() { + loading.close(); + context.flush(); + }, 2500); + } + }); + } else { // changes were insignificant or reverted by user + loading.close(); + context.flush(); + cancel(); + } + } + } + + + function showConflicts() { + var selection = context.container() + .select('#sidebar') + .append('div') + .attr('class','sidebar-component'); + + loading.close(); + + selection.call(iD.ui.Conflicts(context) + .list(conflicts) + .on('download', function() { + var data = JXON.stringify(context.connection().osmChangeJXON('CHANGEME', origChanges)), + win = window.open('data:text/xml,' + encodeURIComponent(data), '_blank'); + win.focus(); + }) + .on('cancel', function() { + history.pop(); + selection.remove(); + }) + .on('save', function() { + for (var i = 0; i < conflicts.length; i++) { + if (conflicts[i].chosen === 1) { // user chose "keep theirs" + var entity = context.hasEntity(conflicts[i].id); + if (entity && entity.type === 'way') { + var children = _.uniq(entity.nodes); + for (var j = 0; j < children.length; j++) { + history.replace(iD.actions.Revert(children[j])); + } + } + history.replace(iD.actions.Revert(conflicts[i].id)); + } + } + + selection.remove(); + save(e, true); + }) + ); + } + + + function showErrors() { + var selection = iD.ui.confirm(context.container()); + + history.pop(); + loading.close(); + + selection + .select('.modal-section.header') + .append('h3') + .text(t('save.error')); + + addErrors(selection, errors); + selection.okButton(); + } + + + function addErrors(selection, data) { + var message = selection + .select('.modal-section.message-text'); + + var items = message + .selectAll('.error-container') + .data(data); + + var enter = items.enter() + .append('div') + .attr('class', 'error-container'); + + enter + .append('a') + .attr('class', 'error-description') + .attr('href', '#') + .classed('hide-toggle', true) + .text(function(d) { return d.msg || t('save.unknown_error_details'); }) + .on('click', function() { + var error = d3.select(this), + detail = d3.select(this.nextElementSibling), + exp = error.classed('expanded'); + + detail.style('display', exp ? 'none' : 'block'); + error.classed('expanded', !exp); + + d3.event.preventDefault(); + }); + + var details = enter + .append('div') + .attr('class', 'error-detail-container') + .style('display', 'none'); + + details + .append('ul') + .attr('class', 'error-detail-list') + .selectAll('li') + .data(function(d) { return d.details || []; }) + .enter() + .append('li') + .attr('class', 'error-detail-item') + .text(function(d) { return d; }); + + items.exit() + .remove(); + } + + } + + + function success(e, changeset_id) { + context.enter(iD.modes.Browse(context) + .sidebar(iD.ui.Success(context) + .changeset({ + id: changeset_id, + comment: e.comment + }) + .on('cancel', function() { + context.ui().sidebar.hide(); + }))); + } + + var mode = { + id: 'save' + }; + + mode.enter = function() { + context.connection().authenticate(function(err) { + if (err) { + cancel(); + } else { + context.ui().sidebar.show(ui); + } + }); + }; + + mode.exit = function() { + context.ui().sidebar.hide(); + }; + + return mode; + } + + function Select(context, selectedIDs) { + var mode = { + id: 'select', + button: 'browse' + }; + + var keybinding = d3.keybinding('select'), + timeout = null, + behaviors = [ + iD.behavior.Copy(context), + iD.behavior.Paste(context), + iD.behavior.Breathe(context), + iD.behavior.Hover(context), + iD.behavior.Select(context), + iD.behavior.Lasso(context), + iD.modes.DragNode(context) + .selectedIDs(selectedIDs) + .behavior], + inspector, + radialMenu, + newFeature = false, + suppressMenu = false; + + var wrap = context.container() + .select('.inspector-wrap'); + + + function singular() { + if (selectedIDs.length === 1) { + return context.hasEntity(selectedIDs[0]); + } + } + + function closeMenu() { + if (radialMenu) { + context.surface().call(radialMenu.close); + } + } + + function positionMenu() { + if (suppressMenu || !radialMenu) { return; } + + var entity = singular(); + if (entity && context.geometry(entity.id) === 'relation') { + suppressMenu = true; + } else if (entity && entity.type === 'node') { + radialMenu.center(context.projection(entity.loc)); + } else { + var point = context.mouse(), + viewport = iD.geo.Extent(context.projection.clipExtent()).polygon(); + if (iD.geo.pointInPolygon(point, viewport)) { + radialMenu.center(point); + } else { + suppressMenu = true; + } + } + } + + function showMenu() { + closeMenu(); + if (!suppressMenu && radialMenu) { + context.surface().call(radialMenu); + } + } + + function toggleMenu() { + if (d3.select('.radial-menu').empty()) { + showMenu(); + } else { + closeMenu(); + } + } + + mode.selectedIDs = function() { + return selectedIDs; + }; + + mode.reselect = function() { + var surfaceNode = context.surface().node(); + if (surfaceNode.focus) { // FF doesn't support it + surfaceNode.focus(); + } + + positionMenu(); + showMenu(); + }; + + mode.newFeature = function(_) { + if (!arguments.length) return newFeature; + newFeature = _; + return mode; + }; + + mode.suppressMenu = function(_) { + if (!arguments.length) return suppressMenu; + suppressMenu = _; + return mode; + }; + + mode.enter = function() { + function update() { + closeMenu(); + if (_.some(selectedIDs, function(id) { return !context.hasEntity(id); })) { + // Exit mode if selected entity gets undone + context.enter(iD.modes.Browse(context)); + } + } + + function dblclick() { + var target = d3.select(d3.event.target), + datum = target.datum(); + + if (datum instanceof iD.Way && !target.classed('fill')) { + var choice = iD.geo.chooseEdge(context.childNodes(datum), context.mouse(), context.projection), + node = iD.Node(); + + var prev = datum.nodes[choice.index - 1], + next = datum.nodes[choice.index]; + + context.perform( + iD.actions.AddMidpoint({loc: choice.loc, edge: [prev, next]}, node), + t('operations.add.annotation.vertex')); + + d3.event.preventDefault(); + d3.event.stopPropagation(); + } + } + + function selectElements(drawn) { + var entity = singular(); + if (entity && context.geometry(entity.id) === 'relation') { + suppressMenu = true; + return; + } + + var selection = context.surface() + .selectAll(iD.util.entityOrMemberSelector(selectedIDs, context.graph())); + + if (selection.empty()) { + if (drawn) { // Exit mode if selected DOM elements have disappeared.. + context.enter(iD.modes.Browse(context)); + } + } else { + selection + .classed('selected', true); + } + } + + function esc() { + if (!context.inIntro()) { + context.enter(iD.modes.Browse(context)); + } + } + + + behaviors.forEach(function(behavior) { + context.install(behavior); + }); + + var operations = _.without(d3.values(iD.operations), iD.operations.Delete) + .map(function(o) { return o(selectedIDs, context); }) + .filter(function(o) { return o.available(); }); + + operations.unshift(iD.operations.Delete(selectedIDs, context)); + + keybinding + .on('⎋', esc, true) + .on('space', toggleMenu); + + operations.forEach(function(operation) { + operation.keys.forEach(function(key) { + keybinding.on(key, function() { + if (!(context.inIntro() || operation.disabled())) { + operation(); + } + }); + }); + }); + + d3.select(document) + .call(keybinding); + + radialMenu = iD.ui.RadialMenu(context, operations); + + context.ui().sidebar + .select(singular() ? singular().id : null, newFeature); + + context.history() + .on('undone.select', update) + .on('redone.select', update); + + context.map() + .on('move.select', closeMenu) + .on('drawn.select', selectElements); + + selectElements(); + + var show = d3.event && !suppressMenu; + + if (show) { + positionMenu(); + } + + timeout = window.setTimeout(function() { + if (show) { + showMenu(); + } + + context.surface() + .on('dblclick.select', dblclick); + }, 200); + + if (selectedIDs.length > 1) { + var entities = iD.ui.SelectionList(context, selectedIDs); + context.ui().sidebar.show(entities); + } + }; + + mode.exit = function() { + if (timeout) window.clearTimeout(timeout); + + if (inspector) wrap.call(inspector.close); + + behaviors.forEach(function(behavior) { + context.uninstall(behavior); + }); + + keybinding.off(); + closeMenu(); + radialMenu = undefined; + + context.history() + .on('undone.select', null) + .on('redone.select', null); + + context.surface() + .on('dblclick.select', null) + .selectAll('.selected') + .classed('selected', false); + + context.map().on('drawn.select', null); + context.ui().sidebar.hide(); + }; + + return mode; + } + + exports.AddArea = AddArea; + exports.AddLine = AddLine; + exports.AddPoint = AddPoint; + exports.Browse = Browse; + exports.DragNode = DragNode; + exports.DrawArea = DrawArea; + exports.DrawLine = DrawLine; + exports.Move = Move; + exports.RotateWay = RotateWay; + exports.Save = Save; + exports.Select = Select; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); \ No newline at end of file diff --git a/js/id/modes/add_area.js b/modules/modes/add_area.js similarity index 97% rename from js/id/modes/add_area.js rename to modules/modes/add_area.js index 2f529b9bf..77fdf1151 100644 --- a/js/id/modes/add_area.js +++ b/modules/modes/add_area.js @@ -1,4 +1,4 @@ -iD.modes.AddArea = function(context) { +export function AddArea(context) { var mode = { id: 'add-area', button: 'area', @@ -64,4 +64,4 @@ iD.modes.AddArea = function(context) { }; return mode; -}; +} diff --git a/js/id/modes/add_line.js b/modules/modes/add_line.js similarity index 97% rename from js/id/modes/add_line.js rename to modules/modes/add_line.js index 3dc014c72..ba8f37ef6 100644 --- a/js/id/modes/add_line.js +++ b/modules/modes/add_line.js @@ -1,4 +1,4 @@ -iD.modes.AddLine = function(context) { +export function AddLine(context) { var mode = { id: 'add-line', button: 'line', @@ -60,4 +60,4 @@ iD.modes.AddLine = function(context) { }; return mode; -}; +} diff --git a/js/id/modes/add_point.js b/modules/modes/add_point.js similarity index 96% rename from js/id/modes/add_point.js rename to modules/modes/add_point.js index e7519e3b8..2e33a1ee1 100644 --- a/js/id/modes/add_point.js +++ b/modules/modes/add_point.js @@ -1,4 +1,4 @@ -iD.modes.AddPoint = function(context) { +export function AddPoint(context) { var mode = { id: 'add-point', button: 'point', @@ -49,4 +49,4 @@ iD.modes.AddPoint = function(context) { }; return mode; -}; +} diff --git a/js/id/modes/browse.js b/modules/modes/browse.js similarity index 96% rename from js/id/modes/browse.js rename to modules/modes/browse.js index 1300142f8..345604629 100644 --- a/js/id/modes/browse.js +++ b/modules/modes/browse.js @@ -1,4 +1,4 @@ -iD.modes.Browse = function(context) { +export function Browse(context) { var mode = { button: 'browse', id: 'browse', @@ -49,4 +49,4 @@ iD.modes.Browse = function(context) { }; return mode; -}; +} diff --git a/js/id/modes/drag_node.js b/modules/modes/drag_node.js similarity index 99% rename from js/id/modes/drag_node.js rename to modules/modes/drag_node.js index b012642e9..255f64e2a 100644 --- a/js/id/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -1,4 +1,4 @@ -iD.modes.DragNode = function(context) { +export function DragNode(context) { var mode = { id: 'drag-node', button: 'browse' @@ -212,4 +212,4 @@ iD.modes.DragNode = function(context) { mode.behavior = behavior; return mode; -}; +} diff --git a/js/id/modes/draw_area.js b/modules/modes/draw_area.js similarity index 93% rename from js/id/modes/draw_area.js rename to modules/modes/draw_area.js index 84382996f..2c923d43a 100644 --- a/js/id/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -1,4 +1,4 @@ -iD.modes.DrawArea = function(context, wayId, baseGraph) { +export function DrawArea(context, wayId, baseGraph) { var mode = { button: 'area', id: 'draw-area' @@ -36,4 +36,4 @@ iD.modes.DrawArea = function(context, wayId, baseGraph) { }; return mode; -}; +} diff --git a/js/id/modes/draw_line.js b/modules/modes/draw_line.js similarity index 92% rename from js/id/modes/draw_line.js rename to modules/modes/draw_line.js index d3e52280f..dc71e59d2 100644 --- a/js/id/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -1,4 +1,4 @@ -iD.modes.DrawLine = function(context, wayId, baseGraph, affix) { +export function DrawLine(context, wayId, baseGraph, affix) { var mode = { button: 'line', id: 'draw-line' @@ -36,4 +36,4 @@ iD.modes.DrawLine = function(context, wayId, baseGraph, affix) { }; return mode; -}; +} diff --git a/modules/modes/index.js b/modules/modes/index.js new file mode 100644 index 000000000..046d6bcae --- /dev/null +++ b/modules/modes/index.js @@ -0,0 +1,11 @@ +export { AddArea } from './add_area'; +export { AddLine } from './add_line'; +export { AddPoint } from './add_point'; +export { Browse } from './browse'; +export { DragNode } from './drag_node'; +export { DrawArea } from './draw_area'; +export { DrawLine } from './draw_line'; +export { Move } from './move'; +export { RotateWay } from './rotate_way'; +export { Save } from './save'; +export { Select } from './select'; diff --git a/js/id/modes/move.js b/modules/modes/move.js similarity index 98% rename from js/id/modes/move.js rename to modules/modes/move.js index ccae9cd50..8075d2ec9 100644 --- a/js/id/modes/move.js +++ b/modules/modes/move.js @@ -1,4 +1,4 @@ -iD.modes.Move = function(context, entityIDs, baseGraph) { +export function Move(context, entityIDs, baseGraph) { var mode = { id: 'move', button: 'browse' @@ -119,4 +119,4 @@ iD.modes.Move = function(context, entityIDs, baseGraph) { }; return mode; -}; +} diff --git a/js/id/modes/rotate_way.js b/modules/modes/rotate_way.js similarity index 97% rename from js/id/modes/rotate_way.js rename to modules/modes/rotate_way.js index 75388792b..54072394c 100644 --- a/js/id/modes/rotate_way.js +++ b/modules/modes/rotate_way.js @@ -1,4 +1,4 @@ -iD.modes.RotateWay = function(context, wayId) { +export function RotateWay(context, wayId) { var mode = { id: 'rotate-way', button: 'browse' @@ -80,4 +80,4 @@ iD.modes.RotateWay = function(context, wayId) { }; return mode; -}; +} diff --git a/js/id/modes/save.js b/modules/modes/save.js similarity index 99% rename from js/id/modes/save.js rename to modules/modes/save.js index 8cbd3e317..bc6acdeb4 100644 --- a/js/id/modes/save.js +++ b/modules/modes/save.js @@ -1,4 +1,4 @@ -iD.modes.Save = function(context) { +export function Save(context) { var ui = iD.ui.Commit(context) .on('cancel', cancel) .on('save', save); @@ -327,4 +327,4 @@ iD.modes.Save = function(context) { }; return mode; -}; +} diff --git a/js/id/modes/select.js b/modules/modes/select.js similarity index 99% rename from js/id/modes/select.js rename to modules/modes/select.js index 1d32b8b71..79a4ccdc7 100644 --- a/js/id/modes/select.js +++ b/modules/modes/select.js @@ -1,4 +1,4 @@ -iD.modes.Select = function(context, selectedIDs) { +export function Select(context, selectedIDs) { var mode = { id: 'select', button: 'browse' @@ -243,4 +243,4 @@ iD.modes.Select = function(context, selectedIDs) { }; return mode; -}; +} diff --git a/test/index.html b/test/index.html index f293d4b18..103c28ecd 100644 --- a/test/index.html +++ b/test/index.html @@ -43,6 +43,7 @@ + @@ -149,18 +150,6 @@ - - - - - - - - - - - - From 8313ce284d9bea5ba508ac98d4901fe73fdb2483 Mon Sep 17 00:00:00 2001 From: David Culverhouse Date: Wed, 15 Jun 2016 00:22:31 -0400 Subject: [PATCH 2/2] Merge remote-tracking branch 'openstreetmap/master' --- Makefile | 13 +- index.html | 7 +- js/id/id.js | 2 +- js/lib/id/presets.js | 481 +++++++++++++++++++++++ {js/id => modules}/presets/category.js | 4 +- {js/id => modules}/presets/collection.js | 5 +- {js/id => modules}/presets/field.js | 4 +- modules/presets/index.js | 5 + {js/id => modules}/presets/preset.js | 4 +- {js/id => modules/presets}/presets.js | 5 +- test/index.html | 7 +- test/rendering.html | 6 +- test/spec/presets.js | 8 +- 13 files changed, 511 insertions(+), 40 deletions(-) create mode 100644 js/lib/id/presets.js rename {js/id => modules}/presets/category.js (90%) rename {js/id => modules}/presets/collection.js (98%) rename {js/id => modules}/presets/field.js (92%) create mode 100644 modules/presets/index.js rename {js/id => modules}/presets/preset.js (98%) rename {js/id => modules/presets}/presets.js (99%) diff --git a/Makefile b/Makefile index a159f8739..810d6060d 100644 --- a/Makefile +++ b/Makefile @@ -44,12 +44,16 @@ $(BUILDJS_TARGETS): $(BUILDJS_SOURCES) build.js MODULE_TARGETS = \ js/lib/id/actions.js \ - js/lib/id/validations.js \ - js/lib/id/modes.js + js/lib/id/modes.js \ + js/lib/id/presets.js \ + js/lib/id/validations.js js/lib/id/actions.js: modules/ node_modules/.bin/rollup -f umd -n iD.actions modules/actions/index.js --no-strict > $@ +js/lib/id/presets.js: modules/ + node_modules/.bin/rollup -f umd -n iD.presets modules/presets/index.js --no-strict > $@ + js/lib/id/validations.js: modules/ node_modules/.bin/rollup -f umd -n iD.validations modules/validations/index.js --no-strict > $@ @@ -217,11 +221,6 @@ dist/iD.js: \ js/id/ui/intro/navigation.js \ js/id/ui/intro/point.js \ js/id/ui/intro/start_editing.js \ - js/id/presets.js \ - js/id/presets/category.js \ - js/id/presets/collection.js \ - js/id/presets/field.js \ - js/id/presets/preset.js \ js/id/end.js \ js/lib/locale.js \ data/introGraph.js diff --git a/index.html b/index.html index aadbb355c..4473a9e5b 100644 --- a/index.html +++ b/index.html @@ -35,6 +35,7 @@ + @@ -200,12 +201,6 @@ - - - - - - diff --git a/js/id/id.js b/js/id/id.js index b81400115..60c02b67f 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -379,7 +379,7 @@ window.iD = function () { context.zoomOutFurther = map.zoomOutFurther; context.redrawEnable = map.redrawEnable; - presets = iD.presets(); + presets = iD.presets.presets(); return d3.rebind(context, dispatch, 'on'); }; diff --git a/js/lib/id/presets.js b/js/lib/id/presets.js new file mode 100644 index 000000000..7d92a9598 --- /dev/null +++ b/js/lib/id/presets.js @@ -0,0 +1,481 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.iD = global.iD || {}, global.iD.presets = global.iD.presets || {}))); +}(this, function (exports) { 'use strict'; + + function Category(id, category, all) { + category = _.clone(category); + + category.id = id; + + category.members = iD.presets.Collection(category.members.map(function(id) { + return all.item(id); + })); + + category.matchGeometry = function(geometry) { + return category.geometry.indexOf(geometry) >= 0; + }; + + category.matchScore = function() { return -1; }; + + category.name = function() { + return t('presets.categories.' + id + '.name', {'default': id}); + }; + + category.terms = function() { + return []; + }; + + return category; + } + + function Collection(collection) { + var maxSearchResults = 50, + maxSuggestionResults = 10; + + var presets = { + + collection: collection, + + item: function(id) { + return _.find(collection, function(d) { + return d.id === id; + }); + }, + + matchGeometry: function(geometry) { + return iD.presets.Collection(collection.filter(function(d) { + return d.matchGeometry(geometry); + })); + }, + + search: function(value, geometry) { + if (!value) return this; + + value = value.toLowerCase(); + + var searchable = _.filter(collection, function(a) { + return a.searchable !== false && a.suggestion !== true; + }), + suggestions = _.filter(collection, function(a) { + return a.suggestion === true; + }); + + function leading(a) { + var index = a.indexOf(value); + return index === 0 || a[index - 1] === ' '; + } + + // matches value to preset.name + var leading_name = _.filter(searchable, function(a) { + return leading(a.name().toLowerCase()); + }).sort(function(a, b) { + var i = a.name().toLowerCase().indexOf(value) - b.name().toLowerCase().indexOf(value); + if (i === 0) return a.name().length - b.name().length; + else return i; + }); + + // matches value to preset.terms values + var leading_terms = _.filter(searchable, function(a) { + return _.some(a.terms() || [], leading); + }); + + // matches value to preset.tags values + var leading_tag_values = _.filter(searchable, function(a) { + return _.some(_.without(_.values(a.tags || {}), '*'), leading); + }); + + + // finds close matches to value in preset.name + var levenstein_name = searchable.map(function(a) { + return { + preset: a, + dist: iD.util.editDistance(value, a.name().toLowerCase()) + }; + }).filter(function(a) { + return a.dist + Math.min(value.length - a.preset.name().length, 0) < 3; + }).sort(function(a, b) { + return a.dist - b.dist; + }).map(function(a) { + return a.preset; + }); + + // finds close matches to value in preset.terms + var leventstein_terms = _.filter(searchable, function(a) { + return _.some(a.terms() || [], function(b) { + return iD.util.editDistance(value, b) + Math.min(value.length - b.length, 0) < 3; + }); + }); + + function suggestionName(name) { + var nameArray = name.split(' - '); + if (nameArray.length > 1) { + name = nameArray.slice(0, nameArray.length-1).join(' - '); + } + return name.toLowerCase(); + } + + var leading_suggestions = _.filter(suggestions, function(a) { + return leading(suggestionName(a.name())); + }).sort(function(a, b) { + a = suggestionName(a.name()); + b = suggestionName(b.name()); + var i = a.indexOf(value) - b.indexOf(value); + if (i === 0) return a.length - b.length; + else return i; + }); + + var leven_suggestions = suggestions.map(function(a) { + return { + preset: a, + dist: iD.util.editDistance(value, suggestionName(a.name())) + }; + }).filter(function(a) { + return a.dist + Math.min(value.length - suggestionName(a.preset.name()).length, 0) < 1; + }).sort(function(a, b) { + return a.dist - b.dist; + }).map(function(a) { + return a.preset; + }); + + var other = presets.item(geometry); + + var results = leading_name.concat( + leading_terms, + leading_tag_values, + leading_suggestions.slice(0, maxSuggestionResults+5), + levenstein_name, + leventstein_terms, + leven_suggestions.slice(0, maxSuggestionResults) + ).slice(0, maxSearchResults-1); + + return iD.presets.Collection(_.uniq( + results.concat(other) + )); + } + }; + + return presets; + } + + function Field(id, field) { + field = _.clone(field); + + field.id = id; + + field.matchGeometry = function(geometry) { + return !field.geometry || field.geometry === geometry; + }; + + field.t = function(scope, options) { + return t('presets.fields.' + id + '.' + scope, options); + }; + + field.label = function() { + return field.t('label', {'default': id}); + }; + + var placeholder = field.placeholder; + field.placeholder = function() { + return field.t('placeholder', {'default': placeholder}); + }; + + return field; + } + + function Preset(id, preset, fields) { + preset = _.clone(preset); + + preset.id = id; + preset.fields = (preset.fields || []).map(getFields); + preset.geometry = (preset.geometry || []); + + function getFields(f) { + return fields[f]; + } + + preset.matchGeometry = function(geometry) { + 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]) { + score += matchScore; + } else if (tags[t] === '*' && t in entity.tags) { + score += matchScore / 2; + } else { + return -1; + } + } + + return score; + }; + + preset.t = function(scope, options) { + return t('presets.presets.' + id + '.' + scope, options); + }; + + var name = preset.name; + preset.name = function() { + if (preset.suggestion) { + id = id.split('/'); + id = id[0] + '/' + id[1]; + return name + ' - ' + t('presets.presets.' + id + '.name'); + } + return preset.t('name', {'default': name}); + }; + + preset.terms = function() { + return preset.t('terms', {'default': ''}).toLowerCase().trim().split(/\s*,+\s*/); + }; + + preset.isFallback = function() { + var tagCount = Object.keys(preset.tags).length; + return tagCount === 0 || (tagCount === 1 && preset.tags.hasOwnProperty('area')); + }; + + preset.reference = function(geometry) { + var key = Object.keys(preset.tags)[0], + value = preset.tags[key]; + + if (geometry === 'relation' && key === 'type') { + return { rtype: value }; + } else if (value === '*') { + return { key: key }; + } else { + return { key: key, value: value }; + } + }; + + var removeTags = preset.removeTags || preset.tags; + preset.removeTags = function(tags, geometry) { + tags = _.omit(tags, _.keys(removeTags)); + + for (var f in preset.fields) { + var field = preset.fields[f]; + if (field.matchGeometry(geometry) && field.default === tags[field.key]) { + delete tags[field.key]; + } + } + + delete tags.area; + return tags; + }; + + var applyTags = preset.addTags || preset.tags; + preset.applyTags = function(tags, geometry) { + var k; + + tags = _.clone(tags); + + for (k in applyTags) { + if (applyTags[k] === '*') { + tags[k] = 'yes'; + } else { + tags[k] = applyTags[k]; + } + } + + // Add area=yes if necessary. + // This is necessary if the geometry is already an area (e.g. user drew an area) AND any of: + // 1. chosen preset could be either an area or a line (`barrier=city_wall`) + // 2. chosen preset doesn't have a key in areaKeys (`railway=station`) + if (geometry === 'area') { + var needsAreaTag = true; + if (preset.geometry.indexOf('line') === -1) { + for (k in applyTags) { + if (k in iD.areaKeys) { + needsAreaTag = false; + break; + } + } + } + if (needsAreaTag) { + tags.area = 'yes'; + } + } + + for (var f in preset.fields) { + var field = preset.fields[f]; + if (field.matchGeometry(geometry) && field.key && !tags[field.key] && field.default) { + tags[field.key] = field.default; + } + } + + return tags; + }; + + return preset; + } + + function presets() { + // an iD.presets.Collection with methods for + // loading new data and returning defaults + + var all = iD.presets.Collection([]), + defaults = { area: all, line: all, point: all, vertex: all, relation: all }, + fields = {}, + universal = [], + recent = iD.presets.Collection([]); + + // Index of presets by (geometry, tag key). + var index = { + point: {}, + vertex: {}, + line: {}, + area: {}, + relation: {} + }; + + all.match = function(entity, resolver) { + var geometry = entity.geometry(resolver), + geometryMatches = index[geometry], + best = -1, + match; + + for (var k in entity.tags) { + var keyMatches = geometryMatches[k]; + if (!keyMatches) continue; + + for (var i = 0; i < keyMatches.length; i++) { + var score = keyMatches[i].matchScore(entity); + if (score > best) { + best = score; + match = keyMatches[i]; + } + } + } + + return match || all.item(geometry); + }; + + // Because of the open nature of tagging, iD will never have a complete + // list of tags used in OSM, so we want it to have logic like "assume + // that a closed way with an amenity tag is an area, unless the amenity + // is one of these specific types". This function computes a structure + // that allows testing of such conditions, based on the presets designated + // as as supporting (or not supporting) the area geometry. + // + // The returned object L is a whitelist/blacklist of tags. A closed way + // with a tag (k, v) is considered to be an area if `k in L && !(v in L[k])` + // (see `iD.Way#isArea()`). In other words, the keys of L form the whitelist, + // and the subkeys form the blacklist. + all.areaKeys = function() { + var areaKeys = {}, + ignore = ['barrier', 'highway', 'footway', 'railway', 'type'], + presets = _.reject(all.collection, 'suggestion'); + + // whitelist + presets.forEach(function(d) { + for (var key in d.tags) break; + if (!key) return; + if (ignore.indexOf(key) !== -1) return; + + if (d.geometry.indexOf('area') !== -1) { + areaKeys[key] = areaKeys[key] || {}; + } + }); + + // blacklist + presets.forEach(function(d) { + for (var key in d.tags) break; + if (!key) return; + if (ignore.indexOf(key) !== -1) return; + + var value = d.tags[key]; + if (d.geometry.indexOf('area') === -1 && + d.geometry.indexOf('line') !== -1 && + key in areaKeys && value !== '*') { + areaKeys[key][value] = true; + } + }); + + return areaKeys; + }; + + all.load = function(d) { + + if (d.fields) { + _.forEach(d.fields, function(d, id) { + fields[id] = iD.presets.Field(id, d); + if (d.universal) universal.push(fields[id]); + }); + } + + if (d.presets) { + _.forEach(d.presets, function(d, id) { + all.collection.push(iD.presets.Preset(id, d, fields)); + }); + } + + if (d.categories) { + _.forEach(d.categories, function(d, id) { + all.collection.push(iD.presets.Category(id, d, all)); + }); + } + + if (d.defaults) { + var getItem = _.bind(all.item, all); + defaults = { + area: iD.presets.Collection(d.defaults.area.map(getItem)), + line: iD.presets.Collection(d.defaults.line.map(getItem)), + point: iD.presets.Collection(d.defaults.point.map(getItem)), + vertex: iD.presets.Collection(d.defaults.vertex.map(getItem)), + relation: iD.presets.Collection(d.defaults.relation.map(getItem)) + }; + } + + for (var i = 0; i < all.collection.length; i++) { + var preset = all.collection[i], + geometry = preset.geometry; + + for (var j = 0; j < geometry.length; j++) { + var g = index[geometry[j]]; + for (var k in preset.tags) { + (g[k] = g[k] || []).push(preset); + } + } + } + + return all; + }; + + all.field = function(id) { + return fields[id]; + }; + + all.universal = function() { + return universal; + }; + + all.defaults = function(geometry, n) { + var rec = recent.matchGeometry(geometry).collection.slice(0, 4), + def = _.uniq(rec.concat(defaults[geometry].collection)).slice(0, n - 1); + return iD.presets.Collection(_.uniq(rec.concat(def).concat(all.item(geometry)))); + }; + + all.choose = function(preset) { + if (!preset.isFallback()) { + recent = iD.presets.Collection(_.uniq([preset].concat(recent.collection))); + } + return all; + }; + + return all; + } + + exports.Category = Category; + exports.Collection = Collection; + exports.Field = Field; + exports.Preset = Preset; + exports.presets = presets; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); \ No newline at end of file diff --git a/js/id/presets/category.js b/modules/presets/category.js similarity index 90% rename from js/id/presets/category.js rename to modules/presets/category.js index 5c36d3d80..7eb978b6a 100644 --- a/js/id/presets/category.js +++ b/modules/presets/category.js @@ -1,4 +1,4 @@ -iD.presets.Category = function(id, category, all) { +export function Category(id, category, all) { category = _.clone(category); category.id = id; @@ -22,4 +22,4 @@ iD.presets.Category = function(id, category, all) { }; return category; -}; +} diff --git a/js/id/presets/collection.js b/modules/presets/collection.js similarity index 98% rename from js/id/presets/collection.js rename to modules/presets/collection.js index d42753d34..6ae7f41c2 100644 --- a/js/id/presets/collection.js +++ b/modules/presets/collection.js @@ -1,5 +1,4 @@ -iD.presets.Collection = function(collection) { - +export function Collection(collection) { var maxSearchResults = 50, maxSuggestionResults = 10; @@ -126,4 +125,4 @@ iD.presets.Collection = function(collection) { }; return presets; -}; +} diff --git a/js/id/presets/field.js b/modules/presets/field.js similarity index 92% rename from js/id/presets/field.js rename to modules/presets/field.js index a82813243..99867b0fe 100644 --- a/js/id/presets/field.js +++ b/modules/presets/field.js @@ -1,4 +1,4 @@ -iD.presets.Field = function(id, field) { +export function Field(id, field) { field = _.clone(field); field.id = id; @@ -21,4 +21,4 @@ iD.presets.Field = function(id, field) { }; return field; -}; +} diff --git a/modules/presets/index.js b/modules/presets/index.js new file mode 100644 index 000000000..9462df5f9 --- /dev/null +++ b/modules/presets/index.js @@ -0,0 +1,5 @@ +export { Category } from './category.js'; +export { Collection } from './collection.js'; +export { Field } from './field.js'; +export { Preset } from './preset.js'; +export { presets } from './presets.js'; diff --git a/js/id/presets/preset.js b/modules/presets/preset.js similarity index 98% rename from js/id/presets/preset.js rename to modules/presets/preset.js index 9fc29ce99..89b4084f2 100644 --- a/js/id/presets/preset.js +++ b/modules/presets/preset.js @@ -1,4 +1,4 @@ -iD.presets.Preset = function(id, preset, fields) { +export function Preset(id, preset, fields) { preset = _.clone(preset); preset.id = id; @@ -126,4 +126,4 @@ iD.presets.Preset = function(id, preset, fields) { }; return preset; -}; +} diff --git a/js/id/presets.js b/modules/presets/presets.js similarity index 99% rename from js/id/presets.js rename to modules/presets/presets.js index ad5f4a054..e1df3c0c3 100644 --- a/js/id/presets.js +++ b/modules/presets/presets.js @@ -1,5 +1,4 @@ -iD.presets = function() { - +export function presets() { // an iD.presets.Collection with methods for // loading new data and returning defaults @@ -153,4 +152,4 @@ iD.presets = function() { }; return all; -}; +} diff --git a/test/index.html b/test/index.html index 103c28ecd..c82251824 100644 --- a/test/index.html +++ b/test/index.html @@ -42,6 +42,7 @@ + @@ -174,12 +175,6 @@ - - - - - - diff --git a/test/rendering.html b/test/rendering.html index cf32f9e15..c442b7d80 100644 --- a/test/rendering.html +++ b/test/rendering.html @@ -34,9 +34,7 @@ - - - +
@@ -61,7 +59,7 @@ }; context.presets = function() { - return iD.presets().load({ + return iD.presets.presets().load({ presets: { "amenity/restaurant": { geometry: ['point'], diff --git a/test/spec/presets.js b/test/spec/presets.js index e6425e98c..c77f6d377 100644 --- a/test/spec/presets.js +++ b/test/spec/presets.js @@ -1,4 +1,4 @@ -describe("iD.presets", function() { +describe("iD.presets.presets", function() { var p = { point: { tags: {}, @@ -22,7 +22,7 @@ describe("iD.presets", function() { } }; - var c = iD.presets().load({presets: p}); + var c = iD.presets.presets().load({presets: p}); describe("#match", function() { it("returns a collection containing presets matching a geometry and tags", function() { @@ -41,7 +41,7 @@ describe("iD.presets", function() { }); describe("#areaKeys", function() { - var presets = iD.presets().load({ + var presets = iD.presets.presets().load({ presets: { 'amenity/fuel/shell': { tags: { 'amenity': 'fuel' }, @@ -110,7 +110,7 @@ describe("iD.presets", function() { var presets; before(function() { - presets = iD.presets().load(iD.data.presets); + presets = iD.presets.presets().load(iD.data.presets); }); it("prefers building to multipolygon", function() {