diff --git a/data/address-formats.json b/data/address-formats.json index 18057351b..e300cbc27 100644 --- a/data/address-formats.json +++ b/data/address-formats.json @@ -1,51 +1,115 @@ { "dataAddressFormats": [ { - "format": [["housenumber", "street"], ["city", "postcode"]] + "format": [ + ["housenumber", "street"], + ["city", "postcode"] + ] }, { "countryCodes": ["gb"], - "format": [["housename"], ["housenumber", "street"], ["city", "postcode"]] + "format": [ + ["housename"], + ["housenumber", "street"], + ["city", "postcode"] + ] }, { "countryCodes": ["ie"], - "format": [["housename"], ["housenumber", "street"], ["city"], ["postcode"]] + "format": [ + ["housename"], + ["housenumber", "street"], + ["city"], + ["postcode"] + ] }, { - "countryCodes": ["ad", "at", "ba", "be", "ch", "cz", "de", "dk", "es", "fi", "gr", "hr", "is", "it", "li", "nl", "no", "pl", "pt", "se", "si", "sk", "sm", "va"], - "format": [["street", "housenumber"], ["postcode", "city"]] + "countryCodes": [ + "ad", "at", "ba", "be", "ch", "cz", + "de", "dk", "es", "fi", "gr", "hr", + "is", "it", "li", "nl", "no", "pl", + "pt", "se", "si", "sk", "sm", "va" + ], + "format": [ + ["street", "housenumber"], + ["postcode", "city"] + ] }, { "countryCodes": ["fr", "lu", "mo"], - "format": [["housenumber", "street"], ["postcode", "city"]] + "format": [ + ["housenumber", "street"], + ["postcode", "city"] + ] }, { "countryCodes": ["br"], - "format": [["street"], ["housenumber", "suburb"], ["city", "postcode"]] + "format": [ + ["street"], + ["housenumber", "suburb"], + ["city", "postcode"] + ] }, { "countryCodes": ["vn"], - "format": [["housenumber", "street"], ["subdistrict"], ["district"], ["city"], ["province", "postcode"]] + "format": [ + ["housenumber", "street"], + ["subdistrict"], + ["district"], + ["city"], + ["province", "postcode"] + ] }, { "countryCodes": ["us"], - "format": [["housenumber", "street"], ["city", "state", "postcode"]] + "format": [ + ["housenumber", "street"], + ["city", "state", "postcode"] + ] }, { "countryCodes": ["ca"], - "format": [["housenumber", "street"], ["city", "province", "postcode"]] + "format": [ + ["housenumber", "street"], + ["city", "province", "postcode"] + ] }, { "countryCodes": ["tw"], - "format": [["postcode", "city", "district"], ["place", "street"], ["housenumber", "floor"]] + "format": [ + ["postcode", "city", "district"], + ["place", "street"], + ["housenumber", "floor"] + ] }, { "countryCodes": ["jp"], - "format": [["postcode", "province", "county"], ["city", "suburb", "quarter"], ["neighbourhood", "block_number", "housenumber"]] + "format": [ + ["postcode", "province", "county"], + ["city", "suburb"], + ["quarter", "neighbourhood"], + ["block_number", "housenumber"] + ], + "dropdowns": [ + "postcode", "province", "county", + "city", "suburb", + "quarter", "neighbourhood", + "block_number" + ], + "widths": { + "postcode": 0.3, "province": 0.35, "county": 0.35, + "city": 0.65, "suburb": 0.35, + "quarter": 0.5, "neighbourhood": 0.5, + "block_number": 0.5, "housenumber": 0.5 + } }, { "countryCodes": ["tr"], - "format": [["neighbourhood"], ["street", "housenumber"], ["postcode", "district", "city"]] + "format": [ + ["neighbourhood"], + ["street", "housenumber"], + ["postcode", "district", "city"] + ] } ] } diff --git a/data/presets.yaml b/data/presets.yaml index 5a96319c7..456ca41a0 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -84,28 +84,39 @@ en: # access=* label: Access address: - # 'addr:block_number=*, addr:city=*, addr:conscriptionnumber=*, addr:county=*, addr:country=*, addr:district=*, addr:floor=*, addr:hamlet=*, addr:housename=*, addr:housenumber=*, addr:neighbourhood=*, addr:place=*, addr:postcode=*, addr:province=*, addr:quarter=*, addr:state=*, addr:street=*, addr:subdistrict=*, addr:suburb=*' + # 'addr:block_number=*, addr:city=*, addr:block_number=*, addr:conscriptionnumber=*, addr:county=*, addr:country=*, addr:county=*, addr:district=*, addr:floor=*, addr:hamlet=*, addr:housename=*, addr:housenumber=*, addr:neighbourhood=*, addr:place=*, addr:postcode=*, addr:province=*, addr:quarter=*, addr:state=*, addr:street=*, addr:subdistrict=*, addr:suburb=*' label: Address placeholders: - block_number: Block number + block_number: Block Number + block_number!jp: Block No. city: City + city!jp: City/Town/Village/Tokyo Special Ward + city!vn: City/Town conscriptionnumber: '123' country: Country county: County + county!jp: District district: District + district!vn: Arrondissement/Town/District floor: Floor hamlet: Hamlet housename: Housename housenumber: '123' + housenumber!jp: Building No./Lot No. neighbourhood: Neighbourhood + neighbourhood!jp: Chōme/Aza/Koaza place: Place postcode: Postcode province: Province + province!jp: Prefecture quarter: Quarter + quarter!jp: Ōaza/Machi state: State street: Street subdistrict: Subdistrict + subdistrict!vn: Ward/Commune/Townlet suburb: Suburb + suburb!jp: Ward admin_level: # admin_level=* label: Admin Level diff --git a/data/presets/fields.json b/data/presets/fields.json index c857d551b..68ec1dbf4 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -81,9 +81,11 @@ "keys": [ "addr:block_number", "addr:city", + "addr:block_number", "addr:conscriptionnumber", "addr:county", "addr:country", + "addr:county", "addr:district", "addr:floor", "addr:hamlet", @@ -107,25 +109,36 @@ "label": "Address", "strings": { "placeholders": { - "block_number": "Block number", + "block_number": "Block Number", + "block_number!jp": "Block No.", "city": "City", + "city!jp": "City/Town/Village/Tokyo Special Ward", + "city!vn": "City/Town", "conscriptionnumber": "123", - "county": "County", "country": "Country", + "county": "County", + "county!jp": "District", "district": "District", + "district!vn": "Arrondissement/Town/District", "floor": "Floor", "hamlet": "Hamlet", "housename": "Housename", "housenumber": "123", + "housenumber!jp": "Building No./Lot No.", "neighbourhood": "Neighbourhood", + "neighbourhood!jp": "Chōme/Aza/Koaza", "place": "Place", "postcode": "Postcode", "province": "Province", + "province!jp": "Prefecture", "quarter": "Quarter", + "quarter!jp": "Ōaza/Machi", "state": "State", "street": "Street", "subdistrict": "Subdistrict", - "suburb": "Suburb" + "subdistrict!vn": "Ward/Commune/Townlet", + "suburb": "Suburb", + "suburb!jp": "Ward" } } }, diff --git a/data/presets/fields/address.json b/data/presets/fields/address.json index ff1e5f505..198967c7d 100644 --- a/data/presets/fields/address.json +++ b/data/presets/fields/address.json @@ -3,9 +3,11 @@ "keys": [ "addr:block_number", "addr:city", + "addr:block_number", "addr:conscriptionnumber", "addr:county", "addr:country", + "addr:county", "addr:district", "addr:floor", "addr:hamlet", @@ -27,25 +29,36 @@ "label": "Address", "strings": { "placeholders": { - "block_number": "Block number", + "block_number": "Block Number", + "block_number!jp": "Block No.", "city": "City", + "city!jp": "City/Town/Village/Tokyo Special Ward", + "city!vn": "City/Town", "conscriptionnumber": "123", - "county": "County", "country": "Country", + "county": "County", + "county!jp": "District", "district": "District", + "district!vn": "Arrondissement/Town/District", "floor": "Floor", "hamlet": "Hamlet", "housename": "Housename", "housenumber": "123", + "housenumber!jp": "Building No./Lot No.", "neighbourhood": "Neighbourhood", + "neighbourhood!jp": "Chōme/Aza/Koaza", "place": "Place", "postcode": "Postcode", "province": "Province", + "province!jp": "Prefecture", "quarter": "Quarter", + "quarter!jp": "Ōaza/Machi", "state": "State", "street": "Street", "subdistrict": "Subdistrict", - "suburb": "Suburb" + "subdistrict!vn": "Ward/Commune/Townlet", + "suburb": "Suburb", + "suburb!jp": "Ward" } } } diff --git a/dist/locales/en.json b/dist/locales/en.json index 0ba0ef32d..9913d9127 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -789,25 +789,36 @@ "address": { "label": "Address", "placeholders": { - "block_number": "Block number", + "block_number": "Block Number", + "block_number!jp": "Block No.", "city": "City", + "city!jp": "City/Town/Village/Tokyo Special Ward", + "city!vn": "City/Town", "conscriptionnumber": "123", - "county": "County", "country": "Country", + "county": "County", + "county!jp": "District", "district": "District", + "district!vn": "Arrondissement/Town/District", "floor": "Floor", "hamlet": "Hamlet", "housename": "Housename", "housenumber": "123", + "housenumber!jp": "Building No./Lot No.", "neighbourhood": "Neighbourhood", + "neighbourhood!jp": "Chōme/Aza/Koaza", "place": "Place", "postcode": "Postcode", "province": "Province", + "province!jp": "Prefecture", "quarter": "Quarter", + "quarter!jp": "Ōaza/Machi", "state": "State", "street": "Street", "subdistrict": "Subdistrict", - "suburb": "Suburb" + "subdistrict!vn": "Ward/Commune/Townlet", + "suburb": "Suburb", + "suburb!jp": "Ward" } }, "admin_level": { diff --git a/modules/actions/disconnect.js b/modules/actions/disconnect.js index 85f9ce454..82274234f 100644 --- a/modules/actions/disconnect.js +++ b/modules/actions/disconnect.js @@ -31,6 +31,9 @@ export function actionDisconnect(nodeId, newNodeId) { if (connection.index === 0 && way.isArea()) { // replace shared node with shared node.. graph = graph.replace(way.replaceNode(way.nodes[0], newNode.id)); + } else if (way.isClosed() && connection.index === way.nodes.length - 1) { + // replace closing node with new new node.. + graph = graph.replace(way.unclose().addNode(newNode.id)); } else { // replace shared node with multiple new nodes.. graph = graph.replace(way.updateNode(newNode.id, connection.index)); @@ -52,11 +55,11 @@ export function actionDisconnect(nodeId, newNodeId) { return; } if (way.isArea() && (way.nodes[0] === nodeId)) { - candidates.push({wayID: way.id, index: 0}); + candidates.push({ wayID: way.id, index: 0 }); } else { way.nodes.forEach(function(waynode, index) { if (waynode === nodeId) { - candidates.push({wayID: way.id, index: index}); + candidates.push({ wayID: way.id, index: index }); } }); } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index db3081177..4e969058a 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -41,20 +41,27 @@ export function behaviorDrawWay(context, wayId, index, mode, baseGraph) { annotation = t((way.isDegenerate() ? 'operations.start.annotation.' : 'operations.continue.annotation.') + context.geometry(wayId)), - draw = behaviorDraw(context); + draw = behaviorDraw(context), + startIndex, start, end, segment; - var startIndex = typeof index === 'undefined' ? way.nodes.length - 1 : 0, - start = osmNode({loc: context.graph().entity(way.nodes[startIndex]).loc}), - end = osmNode({loc: context.map().mouseCoordinates()}), - segment = osmWay({ + + if (!isArea) { + startIndex = typeof index === 'undefined' ? way.nodes.length - 1 : 0; + start = osmNode({ id: 'nStart', loc: context.entity(way.nodes[startIndex]).loc }); + end = osmNode({ id: 'nEnd', loc: context.map().mouseCoordinates() }); + segment = osmWay({ id: 'wTemp', nodes: typeof index === 'undefined' ? [start.id, end.id] : [end.id, start.id], tags: _.clone(way.tags) }); + } else { + end = osmNode({ loc: context.map().mouseCoordinates() }); + } + var fn = context[way.isDegenerate() ? 'replace' : 'perform']; if (isArea) { fn(actionAddEntity(end), - actionAddVertex(wayId, end.id, index) + actionAddVertex(wayId, end.id) ); } else { fn(actionAddEntity(start), @@ -70,7 +77,7 @@ export function behaviorDrawWay(context, wayId, index, mode, baseGraph) { if (datum.type === 'node' && datum.id !== end.id) { loc = datum.loc; - } else if (datum.type === 'way' && datum.id !== segment.id) { + } else if (datum.type === 'way') { // && (segment || datum.id !== segment.id)) { var dims = context.map().dimensions(), mouse = context.mouse(), pad = 5, @@ -145,9 +152,8 @@ export function behaviorDrawWay(context, wayId, index, mode, baseGraph) { return function(graph) { if (isArea) { return graph - .replace(way.addNode(newNode.id, index)) + .replace(way.addNode(newNode.id)) .remove(end); - } else { return graph .replace(graph.entity(wayId).addNode(newNode.id, index)) @@ -165,13 +171,19 @@ export function behaviorDrawWay(context, wayId, index, mode, baseGraph) { var last = context.hasEntity(way.nodes[way.nodes.length - (isArea ? 2 : 1)]); if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return; - var newNode = osmNode({loc: loc}); - - context.replace( - actionAddEntity(newNode), - ReplaceTemporaryNode(newNode), - annotation - ); + if (isArea) { + context.replace( + actionMoveNode(end.id, loc), + annotation + ); + } else { + var newNode = osmNode({loc: loc}); + context.replace( + actionAddEntity(newNode), + ReplaceTemporaryNode(newNode), + annotation + ); + } finished = true; context.enter(mode); @@ -180,21 +192,28 @@ export function behaviorDrawWay(context, wayId, index, mode, baseGraph) { // Connect the way to an existing way. drawWay.addWay = function(loc, edge) { - var previousEdge = startIndex ? - [way.nodes[startIndex], way.nodes[startIndex - 1]] : - [way.nodes[0], way.nodes[1]]; - // Avoid creating duplicate segments - if (!isArea && geoEdgeEqual(edge, previousEdge)) - return; + if (isArea) { + context.perform( + actionAddMidpoint({ loc: loc, edge: edge}, end), + annotation + ); + } else { + var previousEdge = startIndex ? + [way.nodes[startIndex], way.nodes[startIndex - 1]] : + [way.nodes[0], way.nodes[1]]; - var newNode = osmNode({ loc: loc }); + // Avoid creating duplicate segments + if (geoEdgeEqual(edge, previousEdge)) + return; - context.perform( - actionAddMidpoint({ loc: loc, edge: edge}, newNode), - ReplaceTemporaryNode(newNode), - annotation - ); + var newNode = osmNode({ loc: loc }); + context.perform( + actionAddMidpoint({ loc: loc, edge: edge}, newNode), + ReplaceTemporaryNode(newNode), + annotation + ); + } finished = true; context.enter(mode); diff --git a/modules/modes/add_area.js b/modules/modes/add_area.js index cf92eab68..f36ebef16 100644 --- a/modules/modes/add_area.js +++ b/modules/modes/add_area.js @@ -27,6 +27,13 @@ export function modeAddArea(context) { defaultTags = { area: 'yes' }; + function actionClose(wayId) { + return function (graph) { + return graph.replace(graph.entity(wayId).close()); + }; + } + + function start(loc) { var graph = context.graph(), node = osmNode({ loc: loc }), @@ -36,7 +43,7 @@ export function modeAddArea(context) { actionAddEntity(node), actionAddEntity(way), actionAddVertex(way.id, node.id), - actionAddVertex(way.id, node.id) + actionClose(way.id) ); context.enter(modeDrawArea(context, way.id, graph)); @@ -52,7 +59,7 @@ export function modeAddArea(context) { actionAddEntity(node), actionAddEntity(way), actionAddVertex(way.id, node.id), - actionAddVertex(way.id, node.id), + actionClose(way.id), actionAddMidpoint({ loc: loc, edge: edge }, node) ); @@ -67,7 +74,7 @@ export function modeAddArea(context) { context.perform( actionAddEntity(way), actionAddVertex(way.id, node.id), - actionAddVertex(way.id, node.id) + actionClose(way.id) ); context.enter(modeDrawArea(context, way.id, graph)); diff --git a/modules/modes/browse.js b/modules/modes/browse.js index 5552d91d6..16ed974d7 100644 --- a/modules/modes/browse.js +++ b/modules/modes/browse.js @@ -7,8 +7,6 @@ import { behaviorSelect } from '../behavior/index'; -import { modeDragNode } from './index'; - export function modeBrowse(context) { var mode = { @@ -22,8 +20,7 @@ export function modeBrowse(context) { behaviorPaste(context), behaviorHover(context).on('hover', context.ui().sidebar.hover), behaviorSelect(context), - behaviorLasso(context), - modeDragNode(context).behavior + behaviorLasso(context) ]; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 0eea5533b..a36402908 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -79,12 +79,16 @@ export function modeDragNode(context) { function start(entity) { + activeIDs = _.map(context.graph().parentWays(entity), 'id'); + activeIDs.push(entity.id); + wasMidpoint = entity.type === 'midpoint'; + isCancelled = d3.event.sourceEvent.shiftKey || + !(wasMidpoint || _.some(activeIDs, function (activeID) { return selectedIDs.indexOf(activeID) !== -1; })) || context.features().hasHiddenConnections(entity, context.graph()); if (isCancelled) return behavior.cancel(); - wasMidpoint = entity.type === 'midpoint'; if (wasMidpoint) { var midpoint = entity; entity = osmNode(); @@ -93,12 +97,14 @@ export function modeDragNode(context) { var vertex = context.surface().selectAll('.' + entity.id); behavior.target(vertex.node(), entity); + activeIDs = _.map(context.graph().parentWays(entity), 'id'); + activeIDs.push(entity.id); + } else { context.perform(actionNoop()); } - activeIDs = _.map(context.graph().parentWays(entity), 'id'); - activeIDs.push(entity.id); + setActiveElements(); context.enter(mode); } diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 763a45944..643ec9afe 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -11,17 +11,18 @@ export function modeDrawArea(context, wayId, baseGraph) { mode.enter = function() { - var way = context.entity(wayId), - headId = way.nodes[way.nodes.length - 2], - tailId = way.first(); + var way = context.entity(wayId); - behavior = behaviorDrawWay(context, wayId, -1, mode, baseGraph) + behavior = behaviorDrawWay(context, wayId, undefined, mode, baseGraph) .tail(t('modes.draw_area.tail')); var addNode = behavior.addNode; behavior.addNode = function(node) { - if (node.id === headId || node.id === tailId) { + var length = way.nodes.length, + penultimate = length > 2 ? way.nodes[length - 2] : null; + + if (node.id === way.first() || node.id === penultimate) { behavior.finish(); } else { addNode(node); diff --git a/modules/operations/continue.js b/modules/operations/continue.js index 4feaab5ef..5d24f463a 100644 --- a/modules/operations/continue.js +++ b/modules/operations/continue.js @@ -15,6 +15,7 @@ export function operationContinue(selectedIDs, context) { function candidateWays() { return graph.parentWays(vertex).filter(function(parent) { return parent.geometry(graph) === 'line' && + !parent.isClosed() && parent.affix(vertex.id) && (geometries.line.length === 0 || geometries.line[0] === parent); }); diff --git a/modules/osm/way.js b/modules/osm/way.js index 7f328894c..99ccc3716 100644 --- a/modules/osm/way.js +++ b/modules/osm/way.js @@ -122,7 +122,7 @@ _.extend(osmWay.prototype, { isClosed: function() { - return this.nodes.length > 0 && this.first() === this.last(); + return this.nodes.length > 1 && this.first() === this.last(); }, @@ -188,46 +188,169 @@ _.extend(osmWay.prototype, { }, + // If this way is not closed, append the beginning node to the end of the nodelist to close it. + close: function() { + if (this.isClosed() || !this.nodes.length) return this; + + var nodes = this.nodes.slice(); + nodes = nodes.filter(noRepeatNodes); + nodes.push(nodes[0]); + return this.update({ nodes: nodes }); + }, + + + // If this way is closed, remove any connector nodes from the end of the nodelist to unclose it. + unclose: function() { + if (!this.isClosed()) return this; + + var nodes = this.nodes.slice(), + connector = this.first(), + i = nodes.length - 1; + + // remove trailing connectors.. + while (i > 0 && nodes.length > 1 && nodes[i] === connector) { + nodes.splice(i, 1); + i = nodes.length - 1; + } + + nodes = nodes.filter(noRepeatNodes); + return this.update({ nodes: nodes }); + }, + + + // Adds a node (id) in front of the node which is currently at position index. + // If index is undefined, the node will be added to the end of the way for linear ways, + // or just before the final connecting node for circular ways. + // Consecutive duplicates are eliminated including existing ones. + // Circularity is always preserved when adding a node. addNode: function(id, index) { - var nodes = this.nodes.slice(); - nodes.splice(index === undefined ? nodes.length : index, 0, id); - return this.update({nodes: nodes}); + var nodes = this.nodes.slice(), + isClosed = this.isClosed(), + max = isClosed ? nodes.length - 1 : nodes.length; + + if (index === undefined) { + index = max; + } + + if (index < 0 || index > max) { + throw new RangeError('index ' + index + ' out of range 0..' + max); + } + + // If this is a closed way, remove all connector nodes except the first one + // (there may be duplicates) and adjust index if necessary.. + if (isClosed) { + var connector = this.first(); + + // leading connectors.. + var i = 1; + while (i < nodes.length && nodes.length > 2 && nodes[i] === connector) { + nodes.splice(i, 1); + if (index > i) index--; + } + + // trailing connectors.. + i = nodes.length - 1; + while (i > 0 && nodes.length > 1 && nodes[i] === connector) { + nodes.splice(i, 1); + if (index > i) index--; + i = nodes.length - 1; + } + } + + nodes.splice(index, 0, id); + nodes = nodes.filter(noRepeatNodes); + + // If the way was closed before, append a connector node to keep it closed.. + if (isClosed && (nodes.length === 1 || nodes[0] !== nodes[nodes.length - 1])) { + nodes.push(nodes[0]); + } + + return this.update({ nodes: nodes }); }, + // Replaces the node which is currently at position index with the given node (id). + // Consecutive duplicates are eliminated including existing ones. + // Circularity is preserved when updating a node. updateNode: function(id, index) { - var nodes = this.nodes.slice(); + var nodes = this.nodes.slice(), + isClosed = this.isClosed(), + max = nodes.length - 1; + + if (index === undefined || index < 0 || index > max) { + throw new RangeError('index ' + index + ' out of range 0..' + max); + } + + // If this is a closed way, remove all connector nodes except the first one + // (there may be duplicates) and adjust index if necessary.. + if (isClosed) { + var connector = this.first(); + + // leading connectors.. + var i = 1; + while (i < nodes.length && nodes.length > 2 && nodes[i] === connector) { + nodes.splice(i, 1); + if (index > i) index--; + } + + // trailing connectors.. + i = nodes.length - 1; + while (i > 0 && nodes.length > 1 && nodes[i] === connector) { + nodes.splice(i, 1); + if (index === i) index = 0; // update leading connector instead + i = nodes.length - 1; + } + } + nodes.splice(index, 1, id); + nodes = nodes.filter(noRepeatNodes); + + // If the way was closed before, append a connector node to keep it closed.. + if (isClosed && (nodes.length === 1 || nodes[0] !== nodes[nodes.length - 1])) { + nodes.push(nodes[0]); + } + return this.update({nodes: nodes}); }, + // Replaces each occurrence of node id needle with replacement. + // Consecutive duplicates are eliminated including existing ones. + // Circularity is preserved. replaceNode: function(needle, replacement) { - if (this.nodes.indexOf(needle) < 0) - return this; + var nodes = this.nodes.slice(), + isClosed = this.isClosed(); - var nodes = this.nodes.slice(); for (var i = 0; i < nodes.length; i++) { if (nodes[i] === needle) { nodes[i] = replacement; } } + + nodes = nodes.filter(noRepeatNodes); + + // If the way was closed before, append a connector node to keep it closed.. + if (isClosed && (nodes.length === 1 || nodes[0] !== nodes[nodes.length - 1])) { + nodes.push(nodes[0]); + } + return this.update({nodes: nodes}); }, + // Removes each occurrence of node id needle with replacement. + // Consecutive duplicates are eliminated including existing ones. + // Circularity is preserved. removeNode: function(id) { - var nodes = []; + var nodes = this.nodes.slice(), + isClosed = this.isClosed(); - for (var i = 0; i < this.nodes.length; i++) { - var node = this.nodes[i]; - if (node !== id && nodes[nodes.length - 1] !== node) { - nodes.push(node); - } - } + nodes = nodes + .filter(function(node) { return node !== id; }) + .filter(noRepeatNodes); - // Preserve circularity - if (this.nodes.length > 1 && this.first() === id && this.last() === id && nodes[nodes.length - 1] !== nodes[0]) { + // If the way was closed before, append a connector node to keep it closed.. + if (isClosed && (nodes.length === 1 || nodes[0] !== nodes[nodes.length - 1])) { nodes.push(nodes[0]); } @@ -248,7 +371,9 @@ _.extend(osmWay.prototype, { }) } }; - if (changeset_id) r.way['@changeset'] = changeset_id; + if (changeset_id) { + r.way['@changeset'] = changeset_id; + } return r; }, @@ -297,3 +422,9 @@ _.extend(osmWay.prototype, { }); } }); + + +// Filter function to eliminate consecutive duplicates. +function noRepeatNodes(node, i, arr) { + return i === 0 || node !== arr[i - 1]; +} diff --git a/modules/ui/commit.js b/modules/ui/commit.js index b6b7080fa..998b10d7f 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -58,19 +58,18 @@ export function uiCommit(context) { context.connection().userChangesets(function (err, changesets) { if (err) return; - var comments = []; - - for (var i = 0; i < changesets.length; i++) { - if (changesets[i].tags.comment) { - comments.push({ - title: changesets[i].tags.comment, - value: changesets[i].tags.comment - }); - } - } + var comments = changesets.map(function(changeset) { + return { + title: changeset.tags.comment, + value: changeset.tags.comment + }; + }); commentField - .call(d3combobox().caseSensitive(true).data(comments)); + .call(d3combobox() + .caseSensitive(true) + .data(_.uniqBy(comments, 'title')) + ); }); var clippyArea = commentSection.append('div') diff --git a/modules/ui/fields/address.js b/modules/ui/fields/address.js index c3eb7f4f2..4ed7cb1b4 100644 --- a/modules/ui/fields/address.js +++ b/modules/ui/fields/address.js @@ -16,20 +16,11 @@ import { utilGetSetValue } from '../../util/get_set_value'; export function uiFieldAddress(field, context) { var dispatch = d3.dispatch('init', 'change'), - nominatim = services.nominatim, + nominatim = services.geocoder, wrap = d3.select(null), isInitialized = false, entity; - var widths = { - housenumber: 1/3, - street: 2/3, - city: 2/3, - state: 1/4, - postcode: 1/3 - }; - - function getNearStreets() { var extent = entity.extent(context.graph()), l = extent.center(), @@ -98,7 +89,6 @@ export function uiFieldAddress(field, context) { } } - function getNearValues(key) { var extent = entity.extent(context.graph()), l = extent.center(), @@ -130,6 +120,11 @@ export function uiFieldAddress(field, context) { return a && a.countryCodes && _.includes(a.countryCodes, countryCode); }) || _.first(dataAddressFormats); + var widths = addressFormat.widths || { + housenumber: 1/3, street: 2/3, + city: 2/3, state: 1/4, postcode: 1/3 + }; + function row(r) { // Normalize widths. var total = _.reduce(r, function(sum, field) { @@ -154,19 +149,25 @@ export function uiFieldAddress(field, context) { .enter() .append('input') .property('type', 'text') - .attr('placeholder', function (d) { return field.t('placeholders.' + d.id); }) + .attr('placeholder', function (d) { + var localkey = d.id + '!' + countryCode, + tkey = field.strings.placeholders[localkey] ? localkey : d.id; + return field.t('placeholders.' + tkey); + }) .attr('class', function (d) { return 'addr-' + d.id; }) .style('width', function (d) { return d.width * 100 + '%'; }); // Update - var addrTags = [ + + // setup dropdowns for common address tags + var dropdowns = addressFormat.dropdowns || [ 'city', 'county', 'country', 'district', 'hamlet', 'neighbourhood', 'place', 'postcode', 'province', 'quarter', 'state', 'street', 'subdistrict', 'suburb' ]; // If fields exist for any of these tags, create dropdowns to pick nearby values.. - addrTags.forEach(function(tag) { + dropdowns.forEach(function(tag) { var nearValues = (tag === 'street') ? getNearStreets : (tag === 'city') ? getNearCities : getNearValues; @@ -202,7 +203,6 @@ export function uiFieldAddress(field, context) { .attr('class', 'preset-input-wrap') .merge(wrap); - if (nominatim && entity) { var center = entity.extent(context.graph()).center(); nominatim.countryCode(center, initCallback); diff --git a/modules/ui/fields/combo.js b/modules/ui/fields/combo.js index 669d40e4f..06b58fafa 100644 --- a/modules/ui/fields/combo.js +++ b/modules/ui/fields/combo.js @@ -15,7 +15,7 @@ export { export function uiFieldCombo(field, context) { var dispatch = d3.dispatch('change'), - nominatim = services.nominatim, + nominatim = services.geocoder, taginfo = services.taginfo, isMulti = (field.type === 'multiCombo'), isNetwork = (field.type === 'networkCombo'), diff --git a/modules/ui/fields/input.js b/modules/ui/fields/input.js index 1b343de8c..9faf7597f 100644 --- a/modules/ui/fields/input.js +++ b/modules/ui/fields/input.js @@ -15,7 +15,7 @@ export { export function uiFieldText(field, context) { var dispatch = d3.dispatch('change'), - nominatim = services.nominatim, + nominatim = services.geocoder, input, entity; diff --git a/modules/ui/index.js b/modules/ui/index.js index 8a51fdf98..37bfc22cc 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -1,5 +1,4 @@ export { uiInit } from './init'; -export { uiFields } from './fields/index'; export { uiAccount } from './account'; export { uiAttribution } from './attribution'; export { uiBackground } from './background'; @@ -18,7 +17,6 @@ export { uiGeolocate } from './geolocate'; export { uiHelp } from './help'; export { uiInfo } from './info'; export { uiInspector } from './inspector'; -export { uiIntro } from './intro'; export { uiLasso } from './lasso'; export { uiLoading } from './loading'; export { uiMapData } from './map_data'; diff --git a/package.json b/package.json index a5d90d53c..8bb4e2ade 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "js-yaml": "~3.7.0", "jsonschema": "~1.1.0", "json-stable-stringify": "~1.0.1", - "mapillary-js": "2.2.0", + "mapillary-js": "2.3.0", "minimist": "~1.2.0", "mocha": "~3.2.0", "mocha-phantomjs-core": "~2.1.0", @@ -59,7 +59,7 @@ "npm-run-all": "~4.0.0", "phantomjs-prebuilt": "~2.1.11", "request": "~2.79.0", - "rollup": "0.41.1", + "rollup": "0.41.4", "rollup-plugin-commonjs": "7.0.0", "rollup-plugin-json": "2.0.2", "rollup-plugin-node-resolve": "2.0.0", diff --git a/test/spec/osm/way.js b/test/spec/osm/way.js index a49ea4082..f45a98ae1 100644 --- a/test/spec/osm/way.js +++ b/test/spec/osm/way.js @@ -127,16 +127,24 @@ describe('iD.osmWay', function() { }); describe('#isClosed', function() { - it('returns false when the way has no nodes', function() { - expect(iD.Way().isClosed()).to.equal(false); + it('returns false when the way contains no nodes', function() { + expect(iD.Way().isClosed()).to.be.false; + }); + + it('returns false when the way contains a single node', function() { + expect(iD.Way({ nodes: 'a'.split('') }).isClosed()).to.be.false; }); it('returns false when the way ends are not equal', function() { - expect(iD.Way({nodes: ['n1', 'n2']}).isClosed()).to.equal(false); + expect(iD.Way({ nodes: 'abc'.split('') }).isClosed()).to.be.false; }); it('returns true when the way ends are equal', function() { - expect(iD.Way({nodes: ['n1', 'n2', 'n1']}).isClosed()).to.equal(true); + expect(iD.Way({ nodes: 'aba'.split('') }).isClosed()).to.be.true; + }); + + it('returns true when the way contains two of the same node', function() { + expect(iD.Way({ nodes: 'aa'.split('') }).isClosed()).to.be.true; }); }); @@ -409,54 +417,474 @@ describe('iD.osmWay', function() { }); }); + describe('#close', function () { + it('returns self for empty way', function () { + var w = iD.Way(); + expect(w.close()).to.deep.equal(w); + }); + + it('returns self for already closed way', function () { + var w1 = iD.Way({ nodes: 'aba'.split('') }); + expect(w1.close()).to.deep.equal(w1); + var w2 = iD.Way({ nodes: 'aa'.split('') }); + expect(w2.close()).to.deep.equal(w2); + }); + + it('closes a way', function () { + var w1 = iD.Way({ nodes: 'ab'.split('') }); + expect(w1.close().nodes.join('')).to.eql('aba', 'multiple'); + var w2 = iD.Way({ nodes: 'a'.split('') }); + expect(w2.close().nodes.join('')).to.eql('aa', 'single'); + }); + + it('eliminates duplicate consecutive nodes when closing a linear way', function () { + var w1 = iD.Way({ nodes: 'abb'.split('') }); + expect(w1.close().nodes.join('')).to.eql('aba', 'duplicate at end'); + var w2 = iD.Way({ nodes: 'abbc'.split('') }); + expect(w2.close().nodes.join('')).to.eql('abca', 'duplicate in middle'); + var w3 = iD.Way({ nodes: 'aabc'.split('') }); + expect(w3.close().nodes.join('')).to.eql('abca', 'duplicate at beginning'); + var w4 = iD.Way({ nodes: 'abbbcbb'.split('') }); + expect(w4.close().nodes.join('')).to.eql('abcba', 'duplicates multiple places'); + }); + }); + + describe('#unclose', function () { + it('returns self for empty way', function () { + var w = iD.Way(); + expect(w.unclose()).to.deep.equal(w); + }); + + it('returns self for already unclosed way', function () { + var w1 = iD.Way({ nodes: 'a'.split('') }); + expect(w1.unclose()).to.deep.equal(w1); + var w2 = iD.Way({ nodes: 'ab'.split('') }); + expect(w2.unclose()).to.deep.equal(w2); + }); + + it('uncloses a circular way', function () { + var w1 = iD.Way({ nodes: 'aba'.split('') }); + expect(w1.unclose().nodes.join('')).to.eql('ab', 'multiple'); + var w2 = iD.Way({ nodes: 'aa'.split('') }); + expect(w2.unclose().nodes.join('')).to.eql('a', 'single'); + }); + + it('eliminates duplicate consecutive nodes when unclosing a circular way', function () { + var w1 = iD.Way({ nodes: 'abcca'.split('') }); + expect(w1.unclose().nodes.join('')).to.eql('abc', 'duplicate internal node at end'); + var w2 = iD.Way({ nodes: 'abbca'.split('') }); + expect(w2.unclose().nodes.join('')).to.eql('abc', 'duplicate internal node in middle'); + var w3 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w3.unclose().nodes.join('')).to.eql('abc', 'duplicate connector node at beginning'); + var w4 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w4.unclose().nodes.join('')).to.eql('abc', 'duplicate connector node at end'); + var w5 = iD.Way({ nodes: 'abbbcbba'.split('') }); + expect(w5.unclose().nodes.join('')).to.eql('abcb', 'duplicates multiple places'); + var w6 = iD.Way({ nodes: 'aa'.split('') }); + expect(w6.unclose().nodes.join('')).to.eql('a', 'single node circular'); + var w7 = iD.Way({ nodes: 'aaa'.split('') }); + expect(w7.unclose().nodes.join('')).to.eql('a', 'single node circular with duplicates'); + }); + }); + describe('#addNode', function () { - it('adds a node to the end of a way', function () { + it('adds a node to an empty way', function () { var w = iD.Way(); expect(w.addNode('a').nodes).to.eql(['a']); }); - it('adds a node to a way at index 0', function () { - var w = iD.Way({nodes: ['a', 'b']}); - expect(w.addNode('c', 0).nodes).to.eql(['c', 'a', 'b']); + it('adds a node to the end of a linear way when index is undefined', function () { + var w = iD.Way({ nodes: 'ab'.split('') }); + expect(w.addNode('c').nodes.join('')).to.eql('abc'); }); - it('adds a node to a way at a positive index', function () { - var w = iD.Way({nodes: ['a', 'b']}); - expect(w.addNode('c', 1).nodes).to.eql(['a', 'c', 'b']); + it('adds a node before the end connector of a circular way when index is undefined', function () { + var w1 = iD.Way({ nodes: 'aba'.split('') }); + expect(w1.addNode('c').nodes.join('')).to.eql('abca', 'circular'); + var w2 = iD.Way({ nodes: 'aa'.split('') }); + expect(w2.addNode('c').nodes.join('')).to.eql('aca', 'single node circular'); }); - it('adds a node to a way at a negative index', function () { - var w = iD.Way({nodes: ['a', 'b']}); - expect(w.addNode('c', -1).nodes).to.eql(['a', 'c', 'b']); + it('adds an internal node to a linear way at a positive index', function () { + var w = iD.Way({ nodes: 'ab'.split('') }); + expect(w.addNode('c', 1).nodes.join('')).to.eql('acb'); + }); + + it('adds an internal node to a circular way at a positive index', function () { + var w1 = iD.Way({ nodes: 'aba'.split('') }); + expect(w1.addNode('c', 1).nodes.join('')).to.eql('acba', 'circular'); + var w2 = iD.Way({ nodes: 'aa'.split('') }); + expect(w2.addNode('c', 1).nodes.join('')).to.eql('aca', 'single node circular'); + }); + + it('adds a leading node to a linear way at index 0', function () { + var w = iD.Way({ nodes: 'ab'.split('') }); + expect(w.addNode('c', 0).nodes.join('')).to.eql('cab'); + }); + + it('adds a leading node to a circular way at index 0, preserving circularity', function () { + var w1 = iD.Way({ nodes: 'aba'.split('') }); + expect(w1.addNode('c', 0).nodes.join('')).to.eql('cabc', 'circular'); + var w2 = iD.Way({ nodes: 'aa'.split('') }); + expect(w2.addNode('c', 0).nodes.join('')).to.eql('cac', 'single node circular'); + }); + + it('throws RangeError if index outside of array range for linear way', function () { + var w = iD.Way({ nodes: 'ab'.split('') }); + expect(w.addNode.bind(w, 'c', 3)).to.throw(RangeError, /out of range 0\.\.2/, 'over range'); + expect(w.addNode.bind(w, 'c', -1)).to.throw(RangeError, /out of range 0\.\.2/, 'under range'); + }); + + it('throws RangeError if index outside of array range for circular way', function () { + var w = iD.Way({ nodes: 'aba'.split('') }); + expect(w.addNode.bind(w, 'c', 3)).to.throw(RangeError, /out of range 0\.\.2/, 'over range'); + expect(w.addNode.bind(w, 'c', -1)).to.throw(RangeError, /out of range 0\.\.2/, 'under range'); + }); + + it('eliminates duplicate consecutive nodes when adding to the end of a linear way', function () { + var w1 = iD.Way({ nodes: 'abb'.split('') }); + expect(w1.addNode('b').nodes.join('')).to.eql('ab', 'duplicate at end'); + var w2 = iD.Way({ nodes: 'abbc'.split('') }); + expect(w2.addNode('c').nodes.join('')).to.eql('abc', 'duplicate in middle'); + var w3 = iD.Way({ nodes: 'aabc'.split('') }); + expect(w3.addNode('c').nodes.join('')).to.eql('abc', 'duplicate at beginning'); + var w4 = iD.Way({ nodes: 'abbbcbb'.split('') }); + expect(w4.addNode('b').nodes.join('')).to.eql('abcb', 'duplicates multiple places'); + }); + + it('eliminates duplicate consecutive nodes when adding same node before the end connector of a circular way', function () { + var w1 = iD.Way({ nodes: 'abcca'.split('') }); + expect(w1.addNode('c').nodes.join('')).to.eql('abca', 'duplicate internal node at end'); + var w2 = iD.Way({ nodes: 'abbca'.split('') }); + expect(w2.addNode('c').nodes.join('')).to.eql('abca', 'duplicate internal node in middle'); + var w3 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w3.addNode('c').nodes.join('')).to.eql('abca', 'duplicate connector node at beginning'); + var w4 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w4.addNode('a').nodes.join('')).to.eql('abca', 'duplicate connector node at end'); + var w5 = iD.Way({ nodes: 'abbbcbba'.split('') }); + expect(w5.addNode('b').nodes.join('')).to.eql('abcba', 'duplicates multiple places'); + var w6 = iD.Way({ nodes: 'aa'.split('') }); + expect(w6.addNode('a').nodes.join('')).to.eql('aa', 'single node circular'); + var w7 = iD.Way({ nodes: 'aaa'.split('') }); + expect(w7.addNode('a').nodes.join('')).to.eql('aa', 'single node circular with duplicates'); + }); + + it('eliminates duplicate consecutive nodes when adding different node before the end connector of a circular way', function () { + var w1 = iD.Way({ nodes: 'abcca'.split('') }); + expect(w1.addNode('d').nodes.join('')).to.eql('abcda', 'duplicate internal node at end'); + var w2 = iD.Way({ nodes: 'abbca'.split('') }); + expect(w2.addNode('d').nodes.join('')).to.eql('abcda', 'duplicate internal node in middle'); + var w3 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w3.addNode('d').nodes.join('')).to.eql('abcda', 'duplicate connector node at beginning'); + var w4 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w4.addNode('d').nodes.join('')).to.eql('abcda', 'duplicate connector node at end'); + var w5 = iD.Way({ nodes: 'abbbcbba'.split('') }); + expect(w5.addNode('d').nodes.join('')).to.eql('abcbda', 'duplicates multiple places'); + var w6 = iD.Way({ nodes: 'aa'.split('') }); + expect(w6.addNode('d').nodes.join('')).to.eql('ada', 'single node circular'); + var w7 = iD.Way({ nodes: 'aaa'.split('') }); + expect(w7.addNode('d').nodes.join('')).to.eql('ada', 'single node circular with duplicates'); + }); + + it('eliminates duplicate consecutive nodes when adding to the beginning of a linear way', function () { + var w1 = iD.Way({ nodes: 'abb'.split('') }); + expect(w1.addNode('a', 0).nodes.join('')).to.eql('ab', 'duplicate at end'); + var w2 = iD.Way({ nodes: 'abbc'.split('') }); + expect(w2.addNode('a', 0).nodes.join('')).to.eql('abc', 'duplicate in middle'); + var w3 = iD.Way({ nodes: 'aabc'.split('') }); + expect(w3.addNode('a', 0).nodes.join('')).to.eql('abc', 'duplicate at beginning'); + var w4 = iD.Way({ nodes: 'abbbcbb'.split('') }); + expect(w4.addNode('a', 0).nodes.join('')).to.eql('abcb', 'duplicates multiple places'); + }); + + it('eliminates duplicate consecutive nodes when adding same node as beginning connector a circular way', function () { + var w1 = iD.Way({ nodes: 'abcca'.split('') }); + expect(w1.addNode('a', 0).nodes.join('')).to.eql('abca', 'duplicate internal node at end'); + var w2 = iD.Way({ nodes: 'abbca'.split('') }); + expect(w2.addNode('a', 0).nodes.join('')).to.eql('abca', 'duplicate internal node in middle'); + var w3 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w3.addNode('a', 0).nodes.join('')).to.eql('abca', 'duplicate connector node at beginning'); + var w4 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w4.addNode('a', 0).nodes.join('')).to.eql('abca', 'duplicate connector node at end'); + var w5 = iD.Way({ nodes: 'abbbcbba'.split('') }); + expect(w5.addNode('a', 0).nodes.join('')).to.eql('abcba', 'duplicates multiple places'); + var w6 = iD.Way({ nodes: 'aa'.split('') }); + expect(w6.addNode('a', 0).nodes.join('')).to.eql('aa', 'single node circular'); + var w7 = iD.Way({ nodes: 'aaa'.split('') }); + expect(w7.addNode('a', 0).nodes.join('')).to.eql('aa', 'single node circular with duplicates'); + }); + + it('eliminates duplicate consecutive nodes when adding different node as beginning connector of a circular way', function () { + var w1 = iD.Way({ nodes: 'abcca'.split('') }); + expect(w1.addNode('d', 0).nodes.join('')).to.eql('dabcd', 'duplicate internal node at end'); + var w2 = iD.Way({ nodes: 'abbca'.split('') }); + expect(w2.addNode('d', 0).nodes.join('')).to.eql('dabcd', 'duplicate internal node in middle'); + var w3 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w3.addNode('d', 0).nodes.join('')).to.eql('dabcd', 'duplicate connector node at beginning'); + var w4 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w4.addNode('d', 0).nodes.join('')).to.eql('dabcd', 'duplicate connector node at end'); + var w5 = iD.Way({ nodes: 'abbbcbba'.split('') }); + expect(w5.addNode('d', 0).nodes.join('')).to.eql('dabcbd', 'duplicates multiple places'); + var w6 = iD.Way({ nodes: 'aa'.split('') }); + expect(w6.addNode('d', 0).nodes.join('')).to.eql('dad', 'single node circular'); + var w7 = iD.Way({ nodes: 'aaa'.split('') }); + expect(w7.addNode('d', 0).nodes.join('')).to.eql('dad', 'single node circular with duplicates'); }); }); describe('#updateNode', function () { - it('updates the node id at the specified index', function () { - var w = iD.Way({nodes: ['a', 'b', 'c']}); - expect(w.updateNode('d', 1).nodes).to.eql(['a', 'd', 'c']); + it('throws RangeError if empty way', function () { + var w = iD.Way(); + expect(w.updateNode.bind(w, 'd', 0)).to.throw(RangeError, /out of range 0\.\.-1/); + }); + + it('updates an internal node on a linear way at a positive index', function () { + var w = iD.Way({ nodes: 'ab'.split('') }); + expect(w.updateNode('d', 1).nodes.join('')).to.eql('ad'); + }); + + it('updates an internal node on a circular way at a positive index', function () { + var w = iD.Way({ nodes: 'aba'.split('') }); + expect(w.updateNode('d', 1).nodes.join('')).to.eql('ada', 'circular'); + }); + + it('updates a leading node on a linear way at index 0', function () { + var w = iD.Way({ nodes: 'ab'.split('') }); + expect(w.updateNode('d', 0).nodes.join('')).to.eql('db'); + }); + + it('updates a leading node on a circular way at index 0, preserving circularity', function () { + var w1 = iD.Way({ nodes: 'aba'.split('') }); + expect(w1.updateNode('d', 0).nodes.join('')).to.eql('dbd', 'circular'); + var w2 = iD.Way({ nodes: 'aa'.split('') }); + expect(w2.updateNode('d', 0).nodes.join('')).to.eql('dd', 'single node circular'); + }); + + it('throws RangeError if index outside of array range for linear way', function () { + var w = iD.Way({ nodes: 'ab'.split('') }); + expect(w.updateNode.bind(w, 'd', 2)).to.throw(RangeError, /out of range 0\.\.1/, 'over range'); + expect(w.updateNode.bind(w, 'd', -1)).to.throw(RangeError, /out of range 0\.\.1/, 'under range'); + }); + + it('throws RangeError if index outside of array range for circular way', function () { + var w = iD.Way({ nodes: 'aba'.split('') }); + expect(w.updateNode.bind(w, 'd', 3)).to.throw(RangeError, /out of range 0\.\.2/, 'over range'); + expect(w.updateNode.bind(w, 'd', -1)).to.throw(RangeError, /out of range 0\.\.2/, 'under range'); + }); + + it('eliminates duplicate consecutive nodes when updating the end of a linear way', function () { + var w1 = iD.Way({ nodes: 'abcc'.split('') }); + expect(w1.updateNode('c', 3).nodes.join('')).to.eql('abc', 'duplicate at end'); + var w2 = iD.Way({ nodes: 'abbc'.split('') }); + expect(w2.updateNode('c', 3).nodes.join('')).to.eql('abc', 'duplicate in middle'); + var w3 = iD.Way({ nodes: 'aabc'.split('') }); + expect(w3.updateNode('c', 3).nodes.join('')).to.eql('abc', 'duplicate at beginning'); + var w4 = iD.Way({ nodes: 'abbbcbb'.split('') }); + expect(w4.updateNode('b', 6).nodes.join('')).to.eql('abcb', 'duplicates multiple places'); + }); + + it('eliminates duplicate consecutive nodes when updating same node before the end connector of a circular way', function () { + var w1 = iD.Way({ nodes: 'abcca'.split('') }); + expect(w1.updateNode('c', 3).nodes.join('')).to.eql('abca', 'duplicate internal node at end'); + var w2 = iD.Way({ nodes: 'abbca'.split('') }); + expect(w2.updateNode('c', 3).nodes.join('')).to.eql('abca', 'duplicate internal node in middle'); + var w3 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w3.updateNode('c', 3).nodes.join('')).to.eql('abca', 'duplicate connector node at beginning'); + var w4 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w4.updateNode('a', 3).nodes.join('')).to.eql('abca', 'duplicate connector node at end'); + var w5 = iD.Way({ nodes: 'abbbcbba'.split('') }); + expect(w5.updateNode('b', 6).nodes.join('')).to.eql('abcba', 'duplicates multiple places'); + }); + + it('eliminates duplicate consecutive nodes when updating different node before the end connector of a circular way', function () { + var w1 = iD.Way({ nodes: 'abcca'.split('') }); + expect(w1.updateNode('d', 3).nodes.join('')).to.eql('abcda', 'duplicate internal node at end'); + var w2 = iD.Way({ nodes: 'abbca'.split('') }); + expect(w2.updateNode('d', 3).nodes.join('')).to.eql('abda', 'duplicate internal node in middle'); + var w3 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w3.updateNode('d', 3).nodes.join('')).to.eql('abda', 'duplicate connector node at beginning'); + var w4 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w4.updateNode('d', 3).nodes.join('')).to.eql('dbcd', 'duplicate connector node at end'); + var w5 = iD.Way({ nodes: 'abbbcbba'.split('') }); + expect(w5.updateNode('d', 6).nodes.join('')).to.eql('abcbda', 'duplicates multiple places'); + }); + + it('eliminates duplicate consecutive nodes when updating the beginning of a linear way', function () { + var w1 = iD.Way({ nodes: 'abb'.split('') }); + expect(w1.updateNode('b', 0).nodes.join('')).to.eql('b', 'duplicate at end'); + var w2 = iD.Way({ nodes: 'abbc'.split('') }); + expect(w2.updateNode('b', 0).nodes.join('')).to.eql('bc', 'duplicate in middle'); + var w3 = iD.Way({ nodes: 'aabc'.split('') }); + expect(w3.updateNode('a', 0).nodes.join('')).to.eql('abc', 'duplicate at beginning'); + var w4 = iD.Way({ nodes: 'abbbcbb'.split('') }); + expect(w4.updateNode('a', 0).nodes.join('')).to.eql('abcb', 'duplicates multiple places'); + }); + + it('eliminates duplicate consecutive nodes when updating same node as beginning connector a circular way', function () { + var w1 = iD.Way({ nodes: 'abcca'.split('') }); + expect(w1.updateNode('a', 0).nodes.join('')).to.eql('abca', 'duplicate internal node at end'); + var w2 = iD.Way({ nodes: 'abbca'.split('') }); + expect(w2.updateNode('a', 0).nodes.join('')).to.eql('abca', 'duplicate internal node in middle'); + var w3 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w3.updateNode('a', 0).nodes.join('')).to.eql('abca', 'duplicate connector node at beginning'); + var w4 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w4.updateNode('a', 0).nodes.join('')).to.eql('abca', 'duplicate connector node at end'); + var w5 = iD.Way({ nodes: 'abbbcbba'.split('') }); + expect(w5.updateNode('a', 0).nodes.join('')).to.eql('abcba', 'duplicates multiple places'); + var w6 = iD.Way({ nodes: 'aa'.split('') }); + expect(w6.updateNode('a', 0).nodes.join('')).to.eql('aa', 'single node circular'); + var w7 = iD.Way({ nodes: 'aaa'.split('') }); + expect(w7.updateNode('a', 0).nodes.join('')).to.eql('aa', 'single node circular with duplicates'); + }); + + it('eliminates duplicate consecutive nodes when updating different node as beginning connector of a circular way', function () { + var w1 = iD.Way({ nodes: 'abcca'.split('') }); + expect(w1.updateNode('d', 0).nodes.join('')).to.eql('dbcd', 'duplicate internal node at end'); + var w2 = iD.Way({ nodes: 'abbca'.split('') }); + expect(w2.updateNode('d', 0).nodes.join('')).to.eql('dbcd', 'duplicate internal node in middle'); + var w3 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w3.updateNode('d', 0).nodes.join('')).to.eql('dbcd', 'duplicate connector node at beginning'); + var w4 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w4.updateNode('d', 0).nodes.join('')).to.eql('dbcd', 'duplicate connector node at end'); + var w5 = iD.Way({ nodes: 'abbbcbba'.split('') }); + expect(w5.updateNode('d', 0).nodes.join('')).to.eql('dbcbd', 'duplicates multiple places'); + var w6 = iD.Way({ nodes: 'aa'.split('') }); + expect(w6.updateNode('d', 0).nodes.join('')).to.eql('dd', 'single node circular'); + var w7 = iD.Way({ nodes: 'aaa'.split('') }); + expect(w7.updateNode('d', 0).nodes.join('')).to.eql('dd', 'single node circular with duplicates'); + }); + + it('eliminates duplicate consecutive nodes when updating different node as ending connector of a circular way', function () { + var w1 = iD.Way({ nodes: 'abcca'.split('') }); + expect(w1.updateNode('d', 4).nodes.join('')).to.eql('dbcd', 'duplicate internal node at end'); + var w2 = iD.Way({ nodes: 'abbca'.split('') }); + expect(w2.updateNode('d', 4).nodes.join('')).to.eql('dbcd', 'duplicate internal node in middle'); + var w3 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w3.updateNode('d', 4).nodes.join('')).to.eql('dbcd', 'duplicate connector node at beginning'); + var w4 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w4.updateNode('d', 4).nodes.join('')).to.eql('dbcd', 'duplicate connector node at end'); + var w5 = iD.Way({ nodes: 'abbbcbba'.split('') }); + expect(w5.updateNode('d', 7).nodes.join('')).to.eql('dbcbd', 'duplicates multiple places'); + var w6 = iD.Way({ nodes: 'aa'.split('') }); + expect(w6.updateNode('d', 1).nodes.join('')).to.eql('dd', 'single node circular'); + var w7 = iD.Way({ nodes: 'aaa'.split('') }); + expect(w7.updateNode('d', 2).nodes.join('')).to.eql('dd', 'single node circular with duplicates'); + }); + }); + + describe('#replaceNode', function () { + it('replaces a node', function () { + var w1 = iD.Way({ nodes: 'a'.split('') }); + expect(w1.replaceNode('a','b').nodes.join('')).to.eql('b', 'single replace, single node'); + var w2 = iD.Way({ nodes: 'abc'.split('') }); + expect(w2.replaceNode('b','d').nodes.join('')).to.eql('adc', 'single replace, linear'); + var w4 = iD.Way({ nodes: 'abca'.split('') }); + expect(w4.replaceNode('b','d').nodes.join('')).to.eql('adca', 'single replace, circular'); + }); + + it('replaces multiply occurring nodes', function () { + var w1 = iD.Way({ nodes: 'abcb'.split('') }); + expect(w1.replaceNode('b','d').nodes.join('')).to.eql('adcd', 'multiple replace, linear'); + var w2 = iD.Way({ nodes: 'abca'.split('') }); + expect(w2.replaceNode('a','d').nodes.join('')).to.eql('dbcd', 'multiple replace, circular'); + var w3 = iD.Way({ nodes: 'aa'.split('') }); + expect(w3.replaceNode('a','d').nodes.join('')).to.eql('dd', 'multiple replace, single node circular'); + }); + + it('eliminates duplicate consecutive nodes when replacing along a linear way', function () { + var w1 = iD.Way({ nodes: 'abbcd'.split('') }); + expect(w1.replaceNode('c','b').nodes.join('')).to.eql('abd', 'duplicate before'); + var w2 = iD.Way({ nodes: 'abcdd'.split('') }); + expect(w2.replaceNode('c','d').nodes.join('')).to.eql('abd', 'duplicate after'); + var w3 = iD.Way({ nodes: 'abbcbb'.split('')}); + expect(w3.replaceNode('c','b').nodes.join('')).to.eql('ab', 'duplicate before and after'); + }); + + it('eliminates duplicate consecutive nodes when replacing internal nodes along a circular way', function () { + var w1 = iD.Way({ nodes: 'abbcda'.split('') }); + expect(w1.replaceNode('c','b').nodes.join('')).to.eql('abda', 'duplicate before'); + var w2 = iD.Way({ nodes: 'abcdda'.split('') }); + expect(w2.replaceNode('c','d').nodes.join('')).to.eql('abda', 'duplicate after'); + var w3 = iD.Way({ nodes: 'abbcbba'.split('')}); + expect(w3.replaceNode('c','b').nodes.join('')).to.eql('aba', 'duplicate before and after'); + }); + + it('eliminates duplicate consecutive nodes when replacing adjacent to connecting nodes along a circular way', function () { + var w1 = iD.Way({ nodes: 'abcda'.split('') }); + expect(w1.replaceNode('d','a').nodes.join('')).to.eql('abca', 'before single end connector'); + var w2 = iD.Way({ nodes: 'abcda'.split('') }); + expect(w2.replaceNode('b','a').nodes.join('')).to.eql('acda', 'after single beginning connector'); + var w3 = iD.Way({ nodes: 'abcdaa'.split('') }); + expect(w3.replaceNode('d','a').nodes.join('')).to.eql('abca', 'before duplicate end connector'); + var w4 = iD.Way({ nodes: 'aabcda'.split('') }); + expect(w4.replaceNode('b','a').nodes.join('')).to.eql('acda', 'after duplicate beginning connector'); + }); + + it('eliminates duplicate consecutive nodes when replacing connecting nodes along a circular way', function () { + var w1 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w1.replaceNode('a','d').nodes.join('')).to.eql('dbcd', 'duplicate end connector'); + var w2 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w2.replaceNode('a','d').nodes.join('')).to.eql('dbcd', 'duplicate beginning connector'); + var w3 = iD.Way({ nodes: 'aabcaa'.split('') }); + expect(w3.replaceNode('a','d').nodes.join('')).to.eql('dbcd', 'duplicate beginning and end connectors'); + var w4 = iD.Way({ nodes: 'aabaacaa'.split('') }); + expect(w4.replaceNode('a','d').nodes.join('')).to.eql('dbdcd', 'duplicates multiple places'); }); }); describe('#removeNode', function () { - it('removes the node', function () { - var w = iD.Way({nodes: ['a']}); - expect(w.removeNode('a').nodes).to.eql([]); + it('removes a node', function () { + var w1 = iD.Way({ nodes: 'a'.split('') }); + expect(w1.removeNode('a').nodes.join('')).to.eql('', 'single remove, single node'); + var w2 = iD.Way({ nodes: 'abc'.split('') }); + expect(w2.removeNode('b').nodes.join('')).to.eql('ac', 'single remove, linear'); + var w3 = iD.Way({ nodes: 'abca'.split('') }); + expect(w3.removeNode('b').nodes.join('')).to.eql('aca', 'single remove, circular'); + var w4 = iD.Way({ nodes: 'aa'.split('') }); + expect(w4.removeNode('a').nodes.join('')).to.eql('', 'multiple remove, single node circular'); }); - it('prevents duplicate consecutive nodes', function () { - var w = iD.Way({nodes: ['a', 'b', 'c', 'b']}); - expect(w.removeNode('c').nodes).to.eql(['a', 'b']); + it('removes multiply occurring nodes', function () { + var w1 = iD.Way({ nodes: 'abcb'.split('') }); + expect(w1.removeNode('b').nodes.join('')).to.eql('ac', 'multiple remove, linear'); + var w2 = iD.Way({ nodes: 'abcba'.split('') }); + expect(w2.removeNode('b').nodes.join('')).to.eql('aca', 'multiple remove, circular'); }); - it('preserves circularity', function () { - var w = iD.Way({nodes: ['a', 'b', 'c', 'd', 'a']}); - expect(w.removeNode('a').nodes).to.eql(['b', 'c', 'd', 'b']); + it('eliminates duplicate consecutive nodes when removing along a linear way', function () { + var w1 = iD.Way({ nodes: 'abbcd'.split('') }); + expect(w1.removeNode('c').nodes.join('')).to.eql('abd', 'duplicate before'); + var w2 = iD.Way({ nodes: 'abcdd'.split('') }); + expect(w2.removeNode('c').nodes.join('')).to.eql('abd', 'duplicate after'); + var w3 = iD.Way({ nodes: 'abbcbb'.split('')}); + expect(w3.removeNode('c').nodes.join('')).to.eql('ab', 'duplicate before and after'); }); - it('prevents duplicate consecutive nodes when preserving circularity', function () { - var w = iD.Way({nodes: ['a', 'b', 'c', 'd', 'b', 'a']}); - expect(w.removeNode('a').nodes).to.eql(['b', 'c', 'd', 'b']); + it('eliminates duplicate consecutive nodes when removing internal nodes along a circular way', function () { + var w1 = iD.Way({ nodes: 'abbcda'.split('') }); + expect(w1.removeNode('c').nodes.join('')).to.eql('abda', 'duplicate before'); + var w2 = iD.Way({ nodes: 'abcdda'.split('') }); + expect(w2.removeNode('c').nodes.join('')).to.eql('abda', 'duplicate after'); + var w3 = iD.Way({ nodes: 'abbcbba'.split('')}); + expect(w3.removeNode('c').nodes.join('')).to.eql('aba', 'duplicate before and after'); + }); + + it('eliminates duplicate consecutive nodes when removing adjacent to connecting nodes along a circular way', function () { + var w1 = iD.Way({ nodes: 'abcdaa'.split('') }); + expect(w1.removeNode('d').nodes.join('')).to.eql('abca', 'duplicate end connector'); + var w2 = iD.Way({ nodes: 'aabcda'.split('') }); + expect(w2.removeNode('b').nodes.join('')).to.eql('acda', 'duplicate beginning connector'); + }); + + it('eliminates duplicate consecutive nodes when removing connecting nodes along a circular way', function () { + var w1 = iD.Way({ nodes: 'abcaa'.split('') }); + expect(w1.removeNode('a').nodes.join('')).to.eql('bcb', 'duplicate end connector'); + var w2 = iD.Way({ nodes: 'aabca'.split('') }); + expect(w2.removeNode('a').nodes.join('')).to.eql('bcb', 'duplicate beginning connector'); + var w3 = iD.Way({ nodes: 'aabcaa'.split('') }); + expect(w3.removeNode('a').nodes.join('')).to.eql('bcb', 'duplicate beginning and end connectors'); + var w4 = iD.Way({ nodes: 'aabaacaa'.split('') }); + expect(w4.removeNode('a').nodes.join('')).to.eql('bcb', 'duplicates multiple places'); }); }); diff --git a/test/spec/services/nominatim.js b/test/spec/services/nominatim.js index a64d4ddb1..df3a098e9 100644 --- a/test/spec/services/nominatim.js +++ b/test/spec/services/nominatim.js @@ -3,7 +3,7 @@ describe('iD.serviceNominatim', function() { beforeEach(function() { server = sinon.fakeServer.create(); - nominatim = iD.services.nominatim; + nominatim = iD.services.geocoder; nominatim.reset(); });