X-Git-Url: https://git.openstreetmap.org/rails.git/blobdiff_plain/1031af317729ac345aed271d9a9b5c73fe62e10b..542c45db01c64d783bab242c88f8b9714ba65795:/vendor/assets/iD/iD.js diff --git a/vendor/assets/iD/iD.js b/vendor/assets/iD/iD.js index 2ecfaf525..a28af8561 100644 --- a/vendor/assets/iD/iD.js +++ b/vendor/assets/iD/iD.js @@ -7050,7 +7050,7 @@ var JXON = new (function () { /** * @license * Lo-Dash 2.3.0 (Custom Build) - * Build: `lodash --debug --output js/lib/lodash.js include="any,assign,bind,clone,compact,contains,debounce,difference,each,every,extend,filter,find,first,forEach,groupBy,indexOf,intersection,isEmpty,isEqual,isFunction,keys,last,map,omit,pairs,pluck,reject,some,throttle,union,uniq,unique,values,without,flatten,value,chain,cloneDeep,merge,pick" exports="global,node"` + * Build: `lodash --debug --output js/lib/lodash.js include="any,assign,bind,clone,compact,contains,debounce,difference,each,every,extend,filter,find,first,forEach,groupBy,indexOf,intersection,isEmpty,isEqual,isFunction,keys,last,map,omit,pairs,pluck,reject,some,throttle,union,uniq,unique,values,without,flatten,value,chain,cloneDeep,merge,pick,reduce" exports="global,node"` * Copyright 2012-2013 The Dojo Foundation * Based on Underscore.js 1.5.2 * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors @@ -9833,6 +9833,60 @@ var JXON = new (function () { */ var pluck = map; + /** + * Reduces a collection to a value which is the accumulated result of running + * each element in the collection through the callback, where each successive + * callback execution consumes the return value of the previous execution. If + * `accumulator` is not provided the first element of the collection will be + * used as the initial `accumulator` value. The callback is bound to `thisArg` + * and invoked with four arguments; (accumulator, value, index|key, collection). + * + * @static + * @memberOf _ + * @alias foldl, inject + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [accumulator] Initial value of the accumulator. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the accumulated value. + * @example + * + * var sum = _.reduce([1, 2, 3], function(sum, num) { + * return sum + num; + * }); + * // => 6 + * + * var mapped = _.reduce({ 'a': 1, 'b': 2, 'c': 3 }, function(result, num, key) { + * result[key] = num * 3; + * return result; + * }, {}); + * // => { 'a': 3, 'b': 6, 'c': 9 } + */ + function reduce(collection, callback, accumulator, thisArg) { + var noaccum = arguments.length < 3; + callback = lodash.createCallback(callback, thisArg, 4); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + if (noaccum) { + accumulator = collection[++index]; + } + while (++index < length) { + accumulator = callback(accumulator, collection[index], index, collection); + } + } else { + baseEach(collection, function(value, index, collection) { + accumulator = noaccum + ? (noaccum = false, value) + : callback(accumulator, value, index, collection) + }); + } + return accumulator; + } + /** * The opposite of `_.filter` this method returns the elements of a * collection that the callback does **not** return truey for. @@ -10970,6 +11024,7 @@ var JXON = new (function () { lodash.isString = isString; lodash.mixin = mixin; lodash.noop = noop; + lodash.reduce = reduce; lodash.some = some; lodash.sortedIndex = sortedIndex; @@ -10978,7 +11033,9 @@ var JXON = new (function () { lodash.any = some; lodash.detect = find; lodash.findWhere = find; + lodash.foldl = reduce; lodash.include = contains; + lodash.inject = reduce; forOwn(lodash, function(func, methodName) { if (!lodash.prototype[methodName]) { @@ -14658,7 +14715,79 @@ if (typeof define === 'function' && define.amd) { } })(); -toGeoJSON = (function() { +(function(e){if("function"==typeof bootstrap)bootstrap("sexagesimal",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeSexagesimal=e}else"undefined"!=typeof window?window.sexagesimal=e():global.sexagesimal=e()})(function(){var define,ses,bootstrap,module,exports; +return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0 ? 0 : 1], + abs = Math.abs(x), + whole = Math.floor(abs), + fraction = abs - whole, + fractionMinutes = fraction * 60, + minutes = Math.floor(fractionMinutes), + seconds = Math.floor((fractionMinutes - minutes) * 60); + + return whole + '° ' + + (minutes ? minutes + "' " : '') + + (seconds ? seconds + '" ' : '') + dir; +} + +function search(x, dims, r) { + if (!dims) dims = 'NSEW'; + if (typeof x !== 'string') return { val: null, regex: r }; + r = r || /[\s\,]*([\-|\—|\―]?[0-9.]+)°? *(?:([0-9.]+)['’′‘] *)?(?:([0-9.]+)(?:''|"|”|″) *)?([NSEW])?/gi; + var m = r.exec(x); + if (!m) return { val: null, regex: r }; + else if (m[4] && dims.indexOf(m[4]) === -1) return { val: null, regex: r }; + else return { + val: (((m[1]) ? parseFloat(m[1]) : 0) + + ((m[2] ? parseFloat(m[2]) / 60 : 0)) + + ((m[3] ? parseFloat(m[3]) / 3600 : 0))) * + ((m[4] && m[4] === 'S' || m[4] === 'W') ? -1 : 1), + regex: r, + raw: m[0], + dim: m[4] + }; +} + +function pair(x, dims) { + x = x.trim(); + var one = search(x, dims); + if (one.val === null) return null; + var two = search(x, dims, one.regex); + if (two.val === null) return null; + // null if one/two are not contiguous. + if (one.raw + two.raw !== x) return null; + if (one.dim) return swapdim(one.val, two.val, one.dim); + else return [one.val, two.val]; +} + +function swapdim(a, b, dim) { + if (dim == 'N' || dim == 'S') return [a, b]; + if (dim == 'W' || dim == 'E') return [b, a]; +} + +},{}]},{},[1]) +(1) +}); +;toGeoJSON = (function() { 'use strict'; var removeSpace = (/\s*/g), @@ -16278,7 +16407,7 @@ window.iD = function () { return d3.rebind(context, dispatch, 'on'); }; -iD.version = '1.4.0'; +iD.version = '1.5.0'; (function() { var detected = {}; @@ -16312,6 +16441,44 @@ iD.version = '1.4.0'; iD.detect = function() { return detected; }; })(); +iD.countryCode = function() { + var countryCode = {}, + endpoint = 'http://nominatim.openstreetmap.org/reverse?'; + + if (!iD.countryCode.cache) { + iD.countryCode.cache = rbush(); + } + + var cache = iD.countryCode.cache; + + countryCode.search = function(location, callback) { + var countryCodes = cache.search([location[0], location[1], location[0], location[1]]); + + if (countryCodes.length > 0) + return callback(null, countryCodes[0][4]); + + d3.json(endpoint + + iD.util.qsString({ + format: 'json', + addressdetails: 1, + lat: location[1], + lon: location[0] + }), function(err, result) { + if (err) + return callback(err); + else if (result && result.error) + return callback(result.error); + + var extent = iD.geo.Extent(location).padByMeters(1000); + + cache.insert([extent[0][0], extent[0][1], extent[1][0], extent[1][1], result.address.country_code]); + + callback(null, result.address.country_code); + }); + }; + + return countryCode; +}; iD.taginfo = function() { var taginfo = {}, endpoint = 'https://taginfo.openstreetmap.org/api/4/', @@ -16761,11 +16928,38 @@ iD.geo.euclideanDistance = function(a, b) { var x = a[0] - b[0], y = a[1] - b[1]; return Math.sqrt((x * x) + (y * y)); }; + +// using WGS84 polar radius (6356752.314245179 m) +// const = 2 * PI * r / 360 +iD.geo.latToMeters = function(dLat) { + return dLat * 110946.257617; +}; + +// using WGS84 equatorial radius (6378137.0 m) +// const = 2 * PI * r / 360 +iD.geo.lonToMeters = function(dLon, atLat) { + return Math.abs(atLat) >= 90 ? 0 : + dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180))); +}; + +// using WGS84 polar radius (6356752.314245179 m) +// const = 2 * PI * r / 360 +iD.geo.metersToLat = function(m) { + return m / 110946.257617; +}; + +// using WGS84 equatorial radius (6378137.0 m) +// const = 2 * PI * r / 360 +iD.geo.metersToLon = function(m, atLat) { + return Math.abs(atLat) >= 90 ? 0 : + m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180))); +}; + // Equirectangular approximation of spherical distances on Earth iD.geo.sphericalDistance = function(a, b) { - var x = Math.cos(a[1]*Math.PI/180) * (a[0] - b[0]), - y = a[1] - b[1]; - return 6.3710E6 * Math.sqrt((x * x) + (y * y)) * Math.PI/180; + var x = iD.geo.lonToMeters(a[0] - b[0], (a[1] + b[1]) / 2), + y = iD.geo.latToMeters(a[1] - b[1]); + return Math.sqrt((x * x) + (y * y)); }; iD.geo.edgeEqual = function(a, b) { @@ -16827,6 +17021,39 @@ iD.geo.chooseEdge = function(nodes, point, projection) { }; }; +// Return the intersection point of 2 line segments. +// From https://github.com/pgkelley4/line-segments-intersect +// This uses the vector cross product approach described below: +// http://stackoverflow.com/a/565282/786339 +iD.geo.lineIntersection = function(a, b) { + function subtractPoints(point1, point2) { + return [point1[0] - point2[0], point1[1] - point2[1]]; + } + function crossProduct(point1, point2) { + return point1[0] * point2[1] - point1[1] * point2[0]; + } + + var p = [a[0][0], a[0][1]], + p2 = [a[1][0], a[1][1]], + q = [b[0][0], b[0][1]], + q2 = [b[1][0], b[1][1]], + r = subtractPoints(p2, p), + s = subtractPoints(q2, q), + uNumerator = crossProduct(subtractPoints(q, p), r), + denominator = crossProduct(r, s); + + if (uNumerator && denominator) { + var u = uNumerator / denominator, + t = crossProduct(subtractPoints(q, p), s) / denominator; + + if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { + return iD.geo.interp(p, p2, t); + } + } + + return null; +}; + // Return whether point is contained in polygon. // // `point` should be a 2-item array of coordinates. @@ -16935,8 +17162,8 @@ _.extend(iD.geo.Extent.prototype, { }, padByMeters: function(meters) { - var dLat = meters / 111200, - dLon = meters / 111200 / Math.abs(Math.cos(this.center()[1])); + var dLat = iD.geo.metersToLat(meters), + dLon = iD.geo.metersToLon(meters, this.center()[1]); return iD.geo.Extent( [this[0][0] - dLon, this[0][1] - dLat], [this[1][0] + dLon, this[1][1] + dLat]); @@ -17585,7 +17812,15 @@ iD.actions.Connect = function(nodeIds) { }; iD.actions.DeleteMember = function(relationId, memberIndex) { return function(graph) { - return graph.replace(graph.entity(relationId).removeMember(memberIndex)); + var relation = graph.entity(relationId) + .removeMember(memberIndex); + + graph = graph.replace(relation); + + if (relation.isDegenerate()) + graph = iD.actions.DeleteRelation(relation.id)(graph); + + return graph; }; }; iD.actions.DeleteMultiple = function(ids) { @@ -19407,15 +19642,29 @@ iD.behavior.Hash = function(context) { }; var formatter = function(map) { - var center = map.center(), + var mode = context.mode(), + center = map.center(), zoom = map.zoom(), - precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); - var q = iD.util.stringQs(location.hash.substring(1)); - return '#' + iD.util.qsString(_.assign(q, { - map: zoom.toFixed(2) + - '/' + center[0].toFixed(precision) + - '/' + center[1].toFixed(precision) - }), true); + precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)), + q = iD.util.stringQs(location.hash.substring(1)), + newParams = {}; + + if (mode && mode.id === 'browse') { + delete q.id; + } else { + var selected = context.selectedIDs().filter(function(id) { + return !context.entity(id).isNew(); + }); + if (selected.length) { + newParams.id = selected.join(','); + } + } + + newParams.map = zoom.toFixed(2) + + '/' + center[0].toFixed(precision) + + '/' + center[1].toFixed(precision); + + return '#' + iD.util.qsString(_.assign(q, newParams), true); }; function update() { @@ -19423,7 +19672,7 @@ iD.behavior.Hash = function(context) { if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map! } - var move = _.throttle(update, 500); + var throttledUpdate = _.throttle(update, 500); function hashchange() { if (location.hash === s0) return; // ignore spurious hashchange events @@ -19434,14 +19683,17 @@ iD.behavior.Hash = function(context) { function hash() { context.map() - .on('move.hash', move); + .on('move.hash', throttledUpdate); + + context + .on('enter.hash', throttledUpdate); d3.select(window) .on('hashchange.hash', hashchange); if (location.hash) { var q = iD.util.stringQs(location.hash.substring(1)); - if (q.id) context.loadEntity(q.id, !q.map); + if (q.id) context.loadEntity(q.id.split(',')[0], !q.map); hashchange(); if (q.map) hash.hadHash = true; } @@ -19451,6 +19703,9 @@ iD.behavior.Hash = function(context) { context.map() .on('move.hash', null); + context + .on('enter.hash', null); + d3.select(window) .on('hashchange.hash', null); @@ -19986,8 +20241,7 @@ iD.modes.Browse = function(context) { button: 'browse', id: 'browse', title: t('modes.browse.title'), - description: t('modes.browse.description'), - key: '1' + description: t('modes.browse.description') }, sidebar; var behaviors = [ @@ -20698,17 +20952,6 @@ iD.modes.Select = function(context, selectedIDs) { }); }); - var notNew = selectedIDs.filter(function(id) { - return !context.entity(id).isNew(); - }); - - if (notNew.length) { - var q = iD.util.stringQs(location.hash.substring(1)); - location.replace('#' + iD.util.qsString(_.assign(q, { - id: notNew.join(',') - }), true)); - } - context.ui().sidebar .select(singular() ? singular().id : null, newFeature); @@ -20792,9 +21035,6 @@ iD.modes.Select = function(context, selectedIDs) { context.uninstall(behavior); }); - var q = iD.util.stringQs(location.hash.substring(1)); - location.replace('#' + iD.util.qsString(_.omit(q, 'id'), true)); - keybinding.off(); context.history() @@ -22776,8 +23016,7 @@ iD.oneWayTags = { 'roundabout': true }, 'man_made': { - 'piste:halfpipe': true, - 'pipeline': true + 'piste:halfpipe': true }, 'piste:type': { 'downhill': true, @@ -23177,6 +23416,29 @@ _.extend(iD.Way.prototype, { if (this.nodes[this.nodes.length - 1] === node) return 'suffix'; }, + layer: function() { + // explicit layer tag, clamp between -10, 10.. + if (this.tags.layer !== undefined) { + return Math.max(-10, Math.min(+(this.tags.layer), 10)); + } + + // implied layer tag.. + if (this.tags.location === 'overground') return 1; + if (this.tags.location === 'underground') return -1; + if (this.tags.location === 'underwater') return -10; + + if (this.tags.power === 'line') return 10; + if (this.tags.power === 'minor_line') return 10; + if (this.tags.aerialway) return 10; + if (this.tags.bridge) return 1; + if (this.tags.cutting) return -1; + if (this.tags.tunnel) return -1; + if (this.tags.waterway) return -1; + if (this.tags.man_made === 'pipeline') return -10; + if (this.tags.boundary) return -10; + return 0; + }, + isOneWay: function() { // explicit oneway tag.. if (['yes', '1', '-1'].indexOf(this.tags.oneway) !== -1) { return true; } @@ -23779,7 +24041,7 @@ iD.GpxLayer = function(context) { .append('text') .attr('class', 'gpx') .text(function(d) { - return d.properties.name; + return d.properties.desc || d.properties.name; }) .attr('x', function(d) { var centroid = path.centroid(d); @@ -23916,10 +24178,9 @@ iD.Map = function(context) { if (map.editable() && !transformed) { var all = context.intersects(map.extent()), filter = d3.functor(true), - extent = map.extent(), graph = context.graph(); - surface.call(vertices, graph, all, filter, extent, map.zoom()); - surface.call(midpoints, graph, all, filter, extent); + surface.call(vertices, graph, all, filter, map.extent(), map.zoom()); + surface.call(midpoints, graph, all, filter, map.trimmedExtent()); dispatch.drawn({full: false}); } }); @@ -23954,7 +24215,7 @@ iD.Map = function(context) { .call(vertices, graph, all, filter, map.extent(), map.zoom()) .call(lines, graph, all, filter) .call(areas, graph, all, filter) - .call(midpoints, graph, all, filter, map.extent()) + .call(midpoints, graph, all, filter, map.trimmedExtent()) .call(labels, graph, all, filter, dimensions, !difference && !extent); if (points.points(context.intersects(map.extent()), 100).length >= 100) { @@ -23967,8 +24228,12 @@ iD.Map = function(context) { } function editOff() { + var mode = context.mode(); surface.selectAll('.layer *').remove(); dispatch.drawn({full: true}); + if (!(mode && mode.id === 'browse')) { + context.enter(iD.modes.Browse(context)); + } } function zoomPan() { @@ -24206,6 +24471,12 @@ iD.Map = function(context) { } }; + map.trimmedExtent = function() { + var headerY = 60, footerY = 30, pad = 10; + return new iD.geo.Extent(projection.invert([pad, dimensions[1] - footerY - pad]), + projection.invert([dimensions[0] - pad, headerY + pad])); + }; + map.extentZoom = function(_) { var extent = iD.geo.Extent(_), tl = projection([extent[0][0], extent[1][1]]), @@ -24442,6 +24713,7 @@ iD.svg = { i = 0, offset = dt, segments = [], + viewport = iD.geo.Extent(projection.clipExtent()), coordinates = graph.childNodes(entity).map(function(n) { return n.loc; }); @@ -24460,9 +24732,10 @@ iD.svg = { b = [x, y]; if (a) { - var span = iD.geo.euclideanDistance(a, b) - offset; + var extent = iD.geo.Extent(a).extend(b), + span = iD.geo.euclideanDistance(a, b) - offset; - if (span >= 0) { + if (extent.intersects(viewport) && span >= 0) { var angle = Math.atan2(b[1] - a[1], b[0] - a[0]), dx = dt * Math.cos(angle), dy = dt * Math.sin(angle), @@ -24572,8 +24845,17 @@ iD.svg.Areas = function(projection) { fill: areas }; - var paths = surface.selectAll('.layer-shadow, .layer-stroke, .layer-fill') - .selectAll('path.area') + var areagroup = surface + .select('.layer-areas') + .selectAll('g.areagroup') + .data(['fill', 'shadow', 'stroke']); + + areagroup.enter() + .append('g') + .attr('class', function(d) { return 'layer areagroup area-' + d; }); + + var paths = areagroup + .selectAll('path') .filter(filter) .data(function(layer) { return data[layer]; }, iD.Entity.key); @@ -24582,7 +24864,7 @@ iD.svg.Areas = function(projection) { paths.exit() .remove(); - var fills = surface.selectAll('.layer-fill path.area')[0]; + var fills = surface.selectAll('.area-fill path.area')[0]; var bisect = d3.bisector(function(node) { return -node.__data__.area(graph); @@ -25199,80 +25481,98 @@ iD.svg.Lines = function(projection) { }; function waystack(a, b) { - if (!a || !b || !a.tags || !b.tags) return 0; - if (a.tags.layer !== undefined && b.tags.layer !== undefined) { - return a.tags.layer - b.tags.layer; - } - if (a.tags.bridge) return 1; - if (b.tags.bridge) return -1; - if (a.tags.tunnel) return -1; - if (b.tags.tunnel) return 1; var as = 0, bs = 0; - if (a.tags.highway && b.tags.highway) { - as -= highway_stack[a.tags.highway]; - bs -= highway_stack[b.tags.highway]; - } + + if (a.tags.highway) { as -= highway_stack[a.tags.highway]; } + if (b.tags.highway) { bs -= highway_stack[b.tags.highway]; } return as - bs; } return function drawLines(surface, graph, entities, filter) { - var lines = [], - path = iD.svg.Path(projection, graph); + var ways = [], pathdata = {}, onewaydata = {}, + getPath = iD.svg.Path(projection, graph); for (var i = 0; i < entities.length; i++) { var entity = entities[i], outer = iD.geo.simpleMultipolygonOuterMember(entity, graph); if (outer) { - lines.push(entity.mergeTags(outer.tags)); + ways.push(entity.mergeTags(outer.tags)); } else if (entity.geometry(graph) === 'line') { - lines.push(entity); + ways.push(entity); } } - lines = lines.filter(path); - lines.sort(waystack); + ways = ways.filter(getPath); - function drawPaths(klass) { - var paths = surface.select('.layer-' + klass) - .selectAll('path.line') - .filter(filter) - .data(lines, iD.Entity.key); + pathdata = _.groupBy(ways, function(way) { return way.layer(); }); - var enter = paths.enter() - .append('path') - .attr('class', function(d) { return 'way line ' + klass + ' ' + d.id; }); + _.forOwn(pathdata, function(v, k) { + onewaydata[k] = _(v) + .filter(function(d) { return d.isOneWay(); }) + .map(iD.svg.OneWaySegments(projection, graph, 35)) + .flatten() + .valueOf(); + }); - // Optimization: call simple TagClasses only on enter selection. This - // works because iD.Entity.key is defined to include the entity v attribute. - if (klass !== 'stroke') { - enter.call(iD.svg.TagClasses()); - } else { - paths.call(iD.svg.TagClasses() - .tags(iD.svg.MultipolygonMemberTags(graph))); - } + var layergroup = surface + .select('.layer-lines') + .selectAll('g.layergroup') + .data(d3.range(-10, 11)); - paths - .order() - .attr('d', path); + layergroup.enter() + .append('g') + .attr('class', function(d) { return 'layer layergroup layer' + String(d); }); - paths.exit() - .remove(); - } - drawPaths('shadow'); - drawPaths('casing'); - drawPaths('stroke'); + var linegroup = layergroup + .selectAll('g.linegroup') + .data(['shadow', 'casing', 'stroke']); - var segments = _(lines) - .filter(function(d) { return d.isOneWay(); }) - .map(iD.svg.OneWaySegments(projection, graph, 35)) - .flatten() - .valueOf(); + linegroup.enter() + .append('g') + .attr('class', function(d) { return 'layer linegroup line-' + d; }); + + + var lines = linegroup + .selectAll('path') + .filter(filter) + .data( + function() { return pathdata[this.parentNode.parentNode.__data__] || []; }, + iD.Entity.key + ); + + // Optimization: call simple TagClasses only on enter selection. This + // works because iD.Entity.key is defined to include the entity v attribute. + lines.enter() + .append('path') + .attr('class', function(d) { return 'way line ' + this.parentNode.__data__ + ' ' + d.id; }) + .call(iD.svg.TagClasses()); + + lines + .sort(waystack) + .attr('d', getPath) + .call(iD.svg.TagClasses().tags(iD.svg.MultipolygonMemberTags(graph))); + + lines.exit() + .remove(); - var oneways = surface.select('.layer-oneway') - .selectAll('path.oneway') + + var onewaygroup = layergroup + .selectAll('g.onewaygroup') + .data(['oneway']); + + onewaygroup.enter() + .append('g') + .attr('class', 'layer onewaygroup'); + + + var oneways = onewaygroup + .selectAll('path') .filter(filter) - .data(segments, function(d) { return [d.id, d.index]; }); + .data( + function() { return onewaydata[this.parentNode.parentNode.__data__] || []; }, + function(d) { return [d.id, d.index]; } + ); oneways.enter() .append('path') @@ -25280,16 +25580,17 @@ iD.svg.Lines = function(projection) { .attr('marker-mid', 'url(#oneway-marker)'); oneways - .order() .attr('d', function(d) { return d.d; }); oneways.exit() .remove(); + }; }; iD.svg.Midpoints = function(projection, context) { return function drawMidpoints(surface, graph, entities, filter, extent) { - var midpoints = {}; + var poly = extent.polygon(), + midpoints = {}; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -25311,15 +25612,34 @@ iD.svg.Midpoints = function(projection, context) { if (midpoints[id]) { midpoints[id].parents.push(entity); } else { - var loc = iD.geo.interp(a.loc, b.loc, 0.5); - if (extent.intersects(loc) && iD.geo.euclideanDistance(projection(a.loc), projection(b.loc)) > 40) { - midpoints[id] = { - type: 'midpoint', - id: id, - loc: loc, - edge: [a.id, b.id], - parents: [entity] - }; + if (iD.geo.euclideanDistance(projection(a.loc), projection(b.loc)) > 40) { + var point = iD.geo.interp(a.loc, b.loc, 0.5), + loc = null; + + if (extent.intersects(point)) { + loc = point; + } else { + for (var k = 0; k < 4; k++) { + point = iD.geo.lineIntersection([a.loc, b.loc], [poly[k], poly[k+1]]); + if (point && + iD.geo.euclideanDistance(projection(a.loc), projection(point)) > 20 && + iD.geo.euclideanDistance(projection(b.loc), projection(point)) > 20) + { + loc = point; + break; + } + } + } + + if (loc) { + midpoints[id] = { + type: 'midpoint', + id: id, + loc: loc, + edge: [a.id, b.id], + parents: [entity] + }; + } } } } @@ -25434,7 +25754,7 @@ iD.svg.Points = function(projection, context) { iD.svg.Surface = function() { return function (selection) { var layers = selection.selectAll('.layer') - .data(['fill', 'shadow', 'casing', 'stroke', 'oneway', 'hit', 'halo', 'label']); + .data(['areas', 'lines', 'hit', 'halo', 'label']); layers.enter().append('g') .attr('class', function(d) { return 'layer layer-' + d; }); @@ -25612,49 +25932,74 @@ iD.svg.Vertices = function(projection, context) { return vertices; } - function draw(groups, vertices, klass, graph, zoom) { - groups = groups.data(vertices, function(entity) { - return iD.Entity.key(entity) + ',' + zoom; - }); + function draw(selection, vertices, klass, graph, zoom) { + var icons = {}, + z; if (zoom < 17) { - zoom = 0; + z = 0; } else if (zoom < 18) { - zoom = 1; + z = 1; } else { - zoom = 2; + z = 2; } - var icons = {}; + var groups = selection.data(vertices, function(entity) { + return iD.Entity.key(entity); + }); + function icon(entity) { if (entity.id in icons) return icons[entity.id]; - icons[entity.id] = zoom !== 0 && + icons[entity.id] = entity.hasInterestingTags() && context.presets().match(entity, graph).icon; return icons[entity.id]; } - function circle(klass) { - var rads = radiuses[klass]; + function classCircle(klass) { return function(entity) { - var i = icon(entity), - c = i ? 0.5 : 0, - r = rads[i ? 3 : zoom]; this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id); - this.setAttribute('cx', c); - this.setAttribute('cy', -c); - this.setAttribute('r', r); }; } - var enter = groups.enter().append('g') + function setAttributes(selection) { + ['shadow','stroke','fill'].forEach(function(klass) { + var rads = radiuses[klass]; + selection.selectAll('.' + klass) + .each(function(entity) { + var i = z && icon(entity), + c = i ? 0.5 : 0, + r = rads[i ? 3 : z]; + this.setAttribute('cx', c); + this.setAttribute('cy', -c); + this.setAttribute('r', r); + if (i && klass === 'fill') { + this.setAttribute('visibility', 'hidden'); + } else { + this.removeAttribute('visibility'); + } + }); + }); + + selection.selectAll('use') + .each(function() { + if (z) { + this.removeAttribute('visibility'); + } else { + this.setAttribute('visibility', 'hidden'); + } + }); + } + + var enter = groups.enter() + .append('g') .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); enter.append('circle') - .each(circle('shadow')); + .each(classCircle('shadow')); enter.append('circle') - .each(circle('stroke')); + .each(classCircle('stroke')); // Vertices with icons get a `use`. enter.filter(function(d) { return icon(d); }) @@ -25663,14 +26008,15 @@ iD.svg.Vertices = function(projection, context) { .attr('clip-path', 'url(#clip-square-12)') .attr('xlink:href', function(d) { return '#maki-' + icon(d) + '-12'; }); - // Vertices with tags get a `circle`. - enter.filter(function(d) { return !icon(d) && d.hasInterestingTags(); }) + // Vertices with tags get a fill. + enter.filter(function(d) { return d.hasInterestingTags(); }) .append('circle') - .each(circle('fill')); + .each(classCircle('fill')); groups .attr('transform', iD.svg.PointTransform(projection)) - .classed('shared', function(entity) { return graph.isShared(entity); }); + .classed('shared', function(entity) { return graph.isShared(entity); }) + .call(setAttributes); groups.exit() .remove(); @@ -25798,23 +26144,24 @@ iD.ui = function(context) { .attr('class', 'map-control help-control') .call(iD.ui.Help(context)); - var about = content.append('div') - .attr('class','col12 about-block fillD'); + var footer = content.append('div') + .attr('id', 'footer') + .attr('class', 'fillD'); + + footer.append('div') + .attr('id', 'scale-block') + .call(iD.ui.Scale(context)); - about.append('div') - .attr('class', 'api-status') - .call(iD.ui.Status(context)); + var linkList = footer.append('div') + .attr('id', 'info-block') + .append('ul') + .attr('id', 'about-list') + .attr('class', 'link-list'); if (!context.embed()) { - about.append('div') - .attr('class', 'account') - .call(iD.ui.Account(context)); + linkList.call(iD.ui.Account(context)); } - var linkList = about.append('ul') - .attr('id', 'about') - .attr('class', 'link-list'); - linkList.append('li') .append('a') .attr('target', '_blank') @@ -25841,6 +26188,10 @@ iD.ui = function(context) { .attr('tabindex', -1) .call(iD.ui.Contributors(context)); + footer.append('div') + .attr('class', 'api-status') + .call(iD.ui.Status(context)); + window.onbeforeunload = function() { return context.save(); }; @@ -25911,20 +26262,25 @@ iD.ui.Account = function(context) { function update(selection) { if (!connection.authenticated()) { - selection.html('') + selection.selectAll('#userLink, #logoutLink') .style('display', 'none'); return; } - selection.style('display', 'block'); - connection.userDetails(function(err, details) { - selection.html(''); + var userLink = selection.select('#userLink'), + logoutLink = selection.select('#logoutLink'); + + userLink.html(''); + logoutLink.html(''); if (err) return; + selection.selectAll('#userLink, #logoutLink') + .style('display', 'list-item'); + // Link - var userLink = selection.append('a') + userLink.append('a') .attr('href', connection.userURL(details.display_name)) .attr('target', '_blank'); @@ -25943,7 +26299,7 @@ iD.ui.Account = function(context) { .attr('class', 'label') .text(details.display_name); - selection.append('a') + logoutLink.append('a') .attr('class', 'logout') .attr('href', '#') .text(t('logout')) @@ -25955,7 +26311,15 @@ iD.ui.Account = function(context) { } return function(selection) { - connection.on('auth', function() { update(selection); }); + selection.append('li') + .attr('id', 'logoutLink') + .style('display', 'none'); + + selection.append('li') + .attr('id', 'userLink') + .style('display', 'none'); + + connection.on('auth.account', function() { update(selection); }); update(selection); }; }; @@ -27040,15 +27404,16 @@ iD.ui.FeatureList = function(context) { }); } - var locationMatch = q.match(/^(-?\d+\.?\d*)\s+(-?\d+\.?\d*)$/); + var locationMatch = sexagesimal.pair(q.toUpperCase()) || q.match(/^(-?\d+\.?\d*)\s+(-?\d+\.?\d*)$/); if (locationMatch) { + var loc = [parseFloat(locationMatch[0]), parseFloat(locationMatch[1])]; result.push({ id: -1, geometry: 'point', type: t('inspector.location'), - name: locationMatch[0], - location: [parseFloat(locationMatch[1]), parseFloat(locationMatch[2])] + name: loc[0].toFixed(6) + ', ' + loc[1].toFixed(6), + location: loc }); } @@ -28091,7 +28456,7 @@ iD.ui.preset = function(context) { function show(field) { field.show = true; - context.presets()(selection); + presets(selection); field.input.focus(); } @@ -28589,6 +28954,10 @@ iD.ui.RawMemberEditor = function(context) { context.perform( iD.actions.DeleteMember(d.relation.id, d.index), t('operations.delete_member.annotation')); + + if (!context.hasEntity(d.relation.id)) { + context.enter(iD.modes.Browse(context)); + } } function rawMemberEditor(selection) { @@ -29236,6 +29605,88 @@ iD.ui.Save = function(context) { }); }; }; +iD.ui.Scale = function(context) { + var projection = context.projection, + imperial = (iD.detect().locale === 'en-us'), + maxLength = 180, + tickHeight = 8; + + function scaleDefs(loc1, loc2) { + var lat = (loc2[1] + loc1[1]) / 2, + conversion = (imperial ? 3.28084 : 1), + dist = iD.geo.lonToMeters(loc2[0] - loc1[0], lat) * conversion, + scale = { dist: 0, px: 0, text: '' }, + buckets, i, val, dLon; + + if (imperial) { + buckets = [5280000, 528000, 52800, 5280, 500, 50, 5, 1]; + } else { + buckets = [5000000, 500000, 50000, 5000, 500, 50, 5, 1]; + } + + // determine a user-friendly endpoint for the scale + for (i = 0; i < buckets.length; i++) { + val = buckets[i]; + if (dist >= val) { + scale.dist = Math.floor(dist / val) * val; + break; + } + } + + dLon = iD.geo.metersToLon(scale.dist / conversion, lat); + scale.px = Math.round(projection([loc1[0] + dLon, loc1[1]])[0]); + + if (imperial) { + if (scale.dist >= 5280) { + scale.dist /= 5280; + scale.text = String(scale.dist) + ' mi'; + } else { + scale.text = String(scale.dist) + ' ft'; + } + } else { + if (scale.dist >= 1000) { + scale.dist /= 1000; + scale.text = String(scale.dist) + ' km'; + } else { + scale.text = String(scale.dist) + ' m'; + } + } + + return scale; + } + + function update(selection) { + // choose loc1, loc2 along bottom of viewport (near where the scale will be drawn) + var dims = context.map().dimensions(), + loc1 = projection.invert([0, dims[1]]), + loc2 = projection.invert([maxLength, dims[1]]), + scale = scaleDefs(loc1, loc2); + + selection.select('#scalepath') + .attr('d', 'M0.5,0.5v' + tickHeight + 'h' + scale.px + 'v-' + tickHeight); + + selection.select('#scaletext') + .attr('x', scale.px + 8) + .attr('y', tickHeight) + .text(scale.text); + } + + return function(selection) { + var g = selection.append('svg') + .attr('id', 'scale') + .append('g') + .attr('transform', 'translate(10,11)'); + + g.append('path').attr('id', 'scalepath'); + g.append('text').attr('id', 'scaletext'); + + update(selection); + + context.map().on('move.scale', function() { + update(selection); + }); + }; +}; iD.ui.SelectionList = function(context, selectedIDs) { function selectionList(selection) { @@ -30078,12 +30529,17 @@ iD.ui.preset.access = function(field) { return d3.rebind(access, event, 'on'); }; iD.ui.preset.address = function(field, context) { - var event = d3.dispatch('change'), - housenumber, - street, - city, - postcode, - entity; + var event = d3.dispatch('init', 'change'), + wrap, + entity, + isInitialized; + + var widths = { + housenumber: 1/3, + street: 2/3, + city: 2/3, + postcode: 1/3 + }; function getStreets() { var extent = entity.extent(context.graph()), @@ -30168,71 +30624,95 @@ iD.ui.preset.address = function(field, context) { } function address(selection) { - var wrap = selection.selectAll('.preset-input-wrap') - .data([0]); + selection.selectAll('.preset-input-wrap') + .remove(); + + var center = entity.extent(context.graph()).center(), + addressFormat; // Enter - var enter = wrap.enter().append('div') + wrap = selection.append('div') .attr('class', 'preset-input-wrap'); - enter.append('input') - .property('type', 'text') - .attr('placeholder', field.t('placeholders.number')) - .attr('class', 'addr-number'); + iD.countryCode().search(center, function (err, countryCode) { + addressFormat = _.find(iD.data.addressFormats, function (a) { + return a && a.countryCodes && _.contains(a.countryCodes, countryCode); + }) || _.first(iD.data.addressFormats); - enter.append('input') - .property('type', 'text') - .attr('placeholder', field.t('placeholders.street')) - .attr('class', 'addr-street'); + function row(r) { + // Normalize widths. + var total = _.reduce(r, function(sum, field) { + return sum + (widths[field] || 0.5); + }, 0); - enter.append('input') - .property('type', 'text') - .attr('placeholder', field.t('placeholders.city')) - .attr('class', 'addr-city'); + return r.map(function (field) { + return { + id: field, + width: (widths[field] || 0.5) / total + }; + }); + } - enter.append('input') - .property('type', 'text') - .attr('placeholder', field.t('placeholders.postcode')) - .attr('class', 'addr-postcode'); + wrap.selectAll('div') + .data(addressFormat.format) + .enter() + .append('div') + .attr('class', 'addr-row') + .selectAll('input') + .data(row) + .enter() + .append('input') + .property('type', 'text') + .attr('placeholder', function (d) { return field.t('placeholders.' + d.id); }) + .attr('class', function (d) { return 'addr-' + d.id; }) + .style('width', function (d) { return d.width * 100 + '%'; }); - // Update + // Update - housenumber = wrap.select('.addr-number'); - street = wrap.select('.addr-street'); - city = wrap.select('.addr-city'); - postcode = wrap.select('.addr-postcode'); + wrap.selectAll('.addr-street') + .call(d3.combobox() + .fetcher(function(value, callback) { + callback(getStreets()); + })); - street - .call(d3.combobox() - .fetcher(function(value, callback) { - callback(getStreets()); - })); + wrap.selectAll('.addr-city') + .call(d3.combobox() + .fetcher(function(value, callback) { + callback(getCities()); + })); - city - .call(d3.combobox() - .fetcher(function(value, callback) { - callback(getCities()); - })); + wrap.selectAll('.addr-postcode') + .call(d3.combobox() + .fetcher(function(value, callback) { + callback(getPostCodes()); + })); - postcode - .call(d3.combobox() - .fetcher(function(value, callback) { - callback(getPostCodes()); - })); + wrap.selectAll('input') + .on('blur', change) + .on('change', change); - wrap.selectAll('input') - .on('blur', change) - .on('change', change); + event.init(); + isInitialized = true; + }); } function change() { - event.change({ - 'addr:housenumber': housenumber.value() || undefined, - 'addr:street': street.value() || undefined, - 'addr:city': city.value() || undefined, - 'addr:postcode': postcode.value() || undefined - }); + var tags = {}; + + wrap.selectAll('input') + .each(function (field) { + tags['addr:' + field.id] = this.value || undefined; + }); + + event.change(tags); + } + + function updateTags(tags) { + wrap.selectAll('input') + .value(function (field) { + return tags['addr:' + field.id] || ''; + }); } address.entity = function(_) { @@ -30242,14 +30722,17 @@ iD.ui.preset.address = function(field, context) { }; address.tags = function(tags) { - housenumber.value(tags['addr:housenumber'] || ''); - street.value(tags['addr:street'] || ''); - city.value(tags['addr:city'] || ''); - postcode.value(tags['addr:postcode'] || ''); + if (isInitialized) { + updateTags(tags); + } else { + event.on('init', function () { + updateTags(tags); + }); + } }; address.focus = function() { - housenumber.node().focus(); + wrap.selectAll('input').node().focus(); }; return d3.rebind(address, event, 'on'); @@ -32137,7 +32620,7 @@ iD.validate = function(changes, graph) { if ((geometry === 'point' || geometry === 'line' || geometry === 'area') && !change.isUsed(graph)) { warnings.push({ message: t('validations.untagged_' + geometry), - tooltip: t('validations.untagged_tooltip', {geometry: geometry}), + tooltip: t('validations.untagged_' + geometry + '_tooltip'), entity: change }); } @@ -42470,7 +42953,7 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," "name": "Locator Overlay", "type": "tms", "description": "Shows major features to help orient you.", - "template": "http://{switch:a,b,c}.tiles.mapbox.com/v3/openstreetmap.map-btyhiati/{zoom}/{x}/{y}.png", + "template": "http://{switch:a,b,c}.tiles.mapbox.com/v4/openstreetmap.map-inh76ba2/{zoom}/{x}/{y}.png?access_token=pk.eyJ1Ijoib3BlbnN0cmVldG1hcCIsImEiOiJhNVlHd29ZIn0.ti6wATGDWOmCnCYen-Ip7Q", "scaleExtent": [ 0, 16 @@ -42490,7 +42973,7 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," "name": "Mapbox Satellite", "type": "tms", "description": "Satellite and aerial imagery.", - "template": "http://{switch:a,b,c}.tiles.mapbox.com/v3/openstreetmap.map-4wvf9l0l/{zoom}/{x}/{y}.png", + "template": "http://{switch:a,b,c}.tiles.mapbox.com/v4/openstreetmap.map-inh7ifmo/{zoom}/{x}/{y}.png?access_token=pk.eyJ1Ijoib3BlbnN0cmVldG1hcCIsImEiOiJhNVlHd29ZIn0.ti6wATGDWOmCnCYen-Ip7Q", "scaleExtent": [ 0, 19 @@ -66840,7 +67323,8 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," "address", "operator", "opening_hours" - ] + ], + "searchable": false }, "craft/tiler": { "name": "Tiler", @@ -67745,6 +68229,27 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," ], "name": "Stop Sign" }, + "highway/street_lamp": { + "geometry": [ + "point", + "vertex" + ], + "tags": { + "highway": "street_lamp" + }, + "fields": [ + "lamp_type", + "ref" + ], + "terms": [ + "streetlight", + "street light", + "lamp", + "light", + "gaslight" + ], + "name": "Street Lamp" + }, "highway/tertiary": { "icon": "highway-tertiary", "fields": [ @@ -71058,6 +71563,27 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," }, "name": "Supermarket" }, + "shop/tailor": { + "name": "Tailor", + "geometry": [ + "point", + "area" + ], + "terms": [ + "tailor", + "clothes" + ], + "tags": { + "shop": "tailor" + }, + "icon": "clothing-store", + "fields": [ + "building_area", + "address", + "operator", + "opening_hours" + ] + }, "shop/toys": { "icon": "shop", "fields": [ @@ -100820,6 +101346,7 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," "address": { "type": "address", "keys": [ + "addr:housename", "addr:housenumber", "addr:street", "addr:city", @@ -100833,10 +101360,19 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," "label": "Address", "strings": { "placeholders": { - "number": "123", + "housename": "Housename", + "housenumber": "123", "street": "Street", "city": "City", - "postcode": "Postal code" + "postcode": "Postcode", + "place": "Place", + "hamlet": "Hamlet", + "suburb": "Suburb", + "subdistrict": "Subdistrict", + "district": "District", + "province": "Province", + "state": "State", + "country": "Country" } } }, @@ -101248,6 +101784,11 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," } } }, + "lamp_type": { + "key": "lamp_type", + "type": "combo", + "label": "Type" + }, "landuse": { "key": "landuse", "type": "typeCombo", @@ -114129,7 +114670,9 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," "untagged_area": "Untagged area", "many_deletions": "You're deleting {n} objects. Are you sure you want to do this? This will delete them from the map that everyone else sees on openstreetmap.org.", "tag_suggests_area": "The tag {tag} suggests line should be area, but it is not an area", - "untagged_tooltip": "Select a feature type that describes what this {geometry} is.", + "untagged_point_tooltip": "Select a feature type that describes what this point is.", + "untagged_line_tooltip": "Select a feature type that describes what this line is.", + "untagged_area_tooltip": "Select a feature type that describes what this area is.", "deprecated_tags": "Deprecated tags: {tags}" }, "zoom": { @@ -114281,10 +114824,19 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," "address": { "label": "Address", "placeholders": { - "number": "123", + "housename": "Housename", + "housenumber": "123", "street": "Street", "city": "City", - "postcode": "Postal code" + "postcode": "Postcode", + "place": "Place", + "hamlet": "Hamlet", + "suburb": "Suburb", + "subdistrict": "Subdistrict", + "district": "District", + "province": "Province", + "state": "State", + "country": "Country" } }, "admin_level": { @@ -114504,6 +115056,9 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," "terminal": "Terminal" } }, + "lamp_type": { + "label": "Type" + }, "landuse": { "label": "Type" }, @@ -115698,6 +116253,10 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," "name": "Stop Sign", "terms": "stop sign" }, + "highway/street_lamp": { + "name": "Street Lamp", + "terms": "streetlight,street light,lamp,light,gaslight" + }, "highway/tertiary": { "name": "Tertiary Road", "terms": "" @@ -116558,6 +117117,10 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," "name": "Supermarket", "terms": "bazaar,boutique,chain,co-op,cut-rate store,discount store,five-and-dime,flea market,galleria,grocery store,mall,mart,outlet,outlet store,shop,shopping center,shopping centre,shopping plaza,stand,store,supermarket,thrift shop" }, + "shop/tailor": { + "name": "Tailor", + "terms": "tailor,clothes" + }, "shop/toys": { "name": "Toy Store", "terms": "" @@ -121628,5 +122191,151 @@ iD.introGraph = '{"n185954700":{"id":"n185954700","loc":[-85.642244,41.939081]," } } } - } + }, + "addressFormats": [ + { + "format": [ + [ + "housenumber", + "street" + ], + [ + "city", + "postcode" + ] + ] + }, + { + "countryCodes": [ + "gb" + ], + "format": [ + [ + "housename" + ], + [ + "housenumber", + "street" + ], + [ + "city", + "postcode" + ] + ] + }, + { + "countryCodes": [ + "ie" + ], + "format": [ + [ + "housename" + ], + [ + "housenumber", + "street" + ], + [ + "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" + ] + ] + }, + { + "countryCodes": [ + "br" + ], + "format": [ + [ + "street" + ], + [ + "housenumber", + "suburb" + ], + [ + "city", + "postcode" + ] + ] + }, + { + "countryCodes": [ + "vn" + ], + "format": [ + [ + "housenumber", + "street" + ], + [ + "subdistrict" + ], + [ + "district" + ], + [ + "city" + ], + [ + "province", + "postcode" + ] + ] + } + ] }; \ No newline at end of file