From 857a1c4821bf0ae0aa8b78dd118828754d03973e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 27 Nov 2017 12:03:14 -0500 Subject: [PATCH 001/206] Refactor offset control into separate `uiBackgroundOffset` module --- modules/ui/background.js | 266 ++++++-------------------------- modules/ui/background_offset.js | 203 ++++++++++++++++++++++++ modules/ui/index.js | 1 + 3 files changed, 255 insertions(+), 215 deletions(-) create mode 100644 modules/ui/background_offset.js diff --git a/modules/ui/background.js b/modules/ui/background.js index 9b58c79d57..5ad890c3ea 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -7,20 +7,19 @@ import { import { event as d3_event, - select as d3_select, - selectAll as d3_selectAll + select as d3_select } from 'd3-selection'; import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t, textDirection } from '../util/locale'; -import { geoMetersToOffset, geoOffsetToMeters } from '../geo'; -import { utilDetect } from '../util/detect'; -import { utilSetTransform, utilCallWhenIdle } from '../util'; import { svgIcon } from '../svg'; -import { uiMapInMap } from './map_in_map'; +import { uiBackgroundOffset } from './background_offset'; import { uiCmd } from './cmd'; +import { uiMapInMap } from './map_in_map'; import { uiTooltipHtml } from './tooltipHtml'; +import { utilDetect } from '../util/detect'; +import { utilSetTransform, utilCallWhenIdle } from '../util'; import { tooltip } from '../util/tooltip'; @@ -28,16 +27,14 @@ export function uiBackground(context) { var key = t('background.key'), detected = utilDetect(), opacities = [1, 0.75, 0.5, 0.25], - directions = [ - ['right', [0.5, 0]], - ['top', [0, -0.5]], - ['left', [-0.5, 0]], - ['bottom', [0, 0.5]]], opacityDefault = (context.storage('background-opacity') !== null) ? (+context.storage('background-opacity')) : 1.0, customSource = context.background().findSource('custom'), previous; + var backgroundOffset = uiBackgroundOffset(context); + + // Can be 0 from <1.3.0 use or due to issue #1923. if (opacityDefault === 0) opacityDefault = 1.0; @@ -206,119 +203,16 @@ export function uiBackground(context) { function update() { - backgroundList.call(drawList, 'radio', clickSetSource, function(d) { return !d.isHidden() && !d.overlay; }); - overlayList.call(drawList, 'checkbox', clickSetOverlay, function(d) { return !d.isHidden() && d.overlay; }); - - selectLayer(); - updateOffsetVal(); - } - - - function updateOffsetVal() { - var meters = geoOffsetToMeters(context.background().offset()), - x = +meters[0].toFixed(2), - y = +meters[1].toFixed(2); - - d3_selectAll('.nudge-inner-rect') - .select('input') - .classed('error', false) - .property('value', x + ', ' + y); - - d3_selectAll('.nudge-reset') - .classed('disabled', function() { - return (x === 0 && y === 0); - }); - } - - - function resetOffset() { - if (d3_event.button !== 0) return; - context.background().offset([0, 0]); - updateOffsetVal(); - } - - - function nudge(d) { - context.background().nudge(d, context.map().zoom()); - updateOffsetVal(); - } - - - function buttonOffset(d) { - if (d3_event.button !== 0) return; - var timeout = window.setTimeout(function() { - interval = window.setInterval(nudge.bind(null, d), 100); - }, 500), - interval; - - function doneNudge() { - window.clearTimeout(timeout); - window.clearInterval(interval); - d3_select(window) - .on('mouseup.buttonoffset', null, true) - .on('mousedown.buttonoffset', null, true); - } - - d3_select(window) - .on('mouseup.buttonoffset', doneNudge, true) - .on('mousedown.buttonoffset', doneNudge, true); - - nudge(d); - } - - - function inputOffset() { - if (d3_event.button !== 0) return; - var input = d3_select(this); - var d = input.node().value; - - if (d === '') return resetOffset(); - - d = d.replace(/;/g, ',').split(',').map(function(n) { - // if n is NaN, it will always get mapped to false. - return !isNaN(n) && n; - }); - - if (d.length !== 2 || !d[0] || !d[1]) { - input.classed('error', true); - return; - } - - context.background().offset(geoMetersToOffset(d)); - updateOffsetVal(); - } - + backgroundList + .call(drawList, 'radio', clickSetSource, function(d) { return !d.isHidden() && !d.overlay; }); - function dragOffset() { - if (d3_event.button !== 0) return; - var origin = [d3_event.clientX, d3_event.clientY]; + overlayList + .call(drawList, 'checkbox', clickSetOverlay, function(d) { return !d.isHidden() && d.overlay; }); - context.container() - .append('div') - .attr('class', 'nudge-surface'); - - d3_select(window) - .on('mousemove.offset', function() { - var latest = [d3_event.clientX, d3_event.clientY]; - var d = [ - -(origin[0] - latest[0]) / 4, - -(origin[1] - latest[1]) / 4 - ]; - - origin = latest; - nudge(d); - }) - .on('mouseup.offset', function() { - if (d3_event.button !== 0) return; - d3_selectAll('.nudge-surface') - .remove(); - - d3_select(window) - .on('mousemove.offset', null) - .on('mouseup.offset', null); - }); + selectLayer(); - d3_event.preventDefault(); + offsetContainer + .call(backgroundOffset); } @@ -387,26 +281,28 @@ export function uiBackground(context) { var content = selection - .append('div') - .attr('class', 'fillL map-overlay col3 content hide'), - tooltipBehavior = tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - .html(true) - .title(uiTooltipHtml(t('background.description'), key)), - button = selection - .append('button') - .attr('tabindex', -1) - .on('click', toggle) - .call(svgIcon('#icon-layers', 'light')) - .call(tooltipBehavior), - shown = false; + .append('div') + .attr('class', 'fillL map-overlay col3 content hide'); + + var tooltipBehavior = tooltip() + .placement((textDirection === 'rtl') ? 'right' : 'left') + .html(true) + .title(uiTooltipHtml(t('background.description'), key)); + var button = selection + .append('button') + .attr('tabindex', -1) + .on('click', toggle) + .call(svgIcon('#icon-layers', 'light')) + .call(tooltipBehavior); - /* opacity switcher */ + var shown = false; + + /* add opacity switcher */ var opawrap = content - .append('div') - .attr('class', 'opacity-options-wrapper'); + .append('div') + .attr('class', 'opacity-options-wrapper'); opawrap .append('h4') @@ -432,27 +328,26 @@ export function uiBackground(context) { .style('opacity', function(d) { return 1.25 - d; }); - /* background list */ - + /* add background list */ var backgroundList = content .append('ul') .attr('class', 'layer-list') .attr('dir', 'auto'); - content - .append('div') - .attr('class', 'imagery-faq') - .append('a') - .attr('target', '_blank') - .attr('tabindex', -1) - .call(svgIcon('#icon-out-link', 'inline')) - .attr('href', 'https://github.com/openstreetmap/iD/blob/master/FAQ.md#how-can-i-report-an-issue-with-background-imagery') - .append('span') - .text(t('background.imagery_source_faq')); + // "Where does this imagery come from?" + // content + // .append('div') + // .attr('class', 'imagery-faq') + // .append('a') + // .attr('target', '_blank') + // .attr('tabindex', -1) + // .call(svgIcon('#icon-out-link', 'inline')) + // .attr('href', 'https://github.com/openstreetmap/iD/blob/master/FAQ.md#how-can-i-report-an-issue-with-background-imagery') + // .append('span') + // .text(t('background.imagery_source_faq')); - /* overlay list */ - + /* add overlay list */ var overlayList = content .append('ul') .attr('class', 'layer-list'); @@ -462,8 +357,7 @@ export function uiBackground(context) { .attr('class', 'controls-list'); - /* minimap toggle */ - + /* add minimap toggle */ var minimapLabel = controls .append('label') .call(tooltip() @@ -486,71 +380,13 @@ export function uiBackground(context) { .text(t('background.minimap.description')); - /* imagery offset controls */ - - var adjustments = content - .append('div') - .attr('class', 'adjustments'); - - adjustments - .append('a') - .text(t('background.fix_misalignment')) - .attr('href', '#') - .classed('hide-toggle', true) - .classed('expanded', false) - .on('click', function() { - if (d3_event.button !== 0) return; - var exp = d3_select(this).classed('expanded'); - nudgeContainer.style('display', exp ? 'none' : 'block'); - d3_select(this).classed('expanded', !exp); - d3_event.preventDefault(); - }); - - var nudgeContainer = adjustments + /* add offset controls */ + var offsetContainer = content .append('div') - .attr('class', 'nudge-container cf') - .style('display', 'none'); + .attr('class', 'background-offset'); - nudgeContainer - .append('div') - .attr('class', 'nudge-instructions') - .text(t('background.offset')); - - var nudgeRect = nudgeContainer - .append('div') - .attr('class', 'nudge-outer-rect') - .on('mousedown', dragOffset); - - nudgeRect - .append('div') - .attr('class', 'nudge-inner-rect') - .append('input') - .on('change', inputOffset) - .on('mousedown', function() { - if (d3_event.button !== 0) return; - d3_event.stopPropagation(); - }); - - nudgeContainer - .append('div') - .selectAll('button') - .data(directions).enter() - .append('button') - .attr('class', function(d) { return d[0] + ' nudge'; }) - .on('mousedown', function(d) { - if (d3_event.button !== 0) return; - buttonOffset(d[1]); - }); - - nudgeContainer - .append('button') - .attr('title', t('background.reset')) - .attr('class', 'nudge-reset disabled') - .on('click', resetOffset) - .call( - (textDirection === 'rtl') ? svgIcon('#icon-redo') : svgIcon('#icon-undo') - ); + /* add listeners */ context.map() .on('move.background-update', _debounce(utilCallWhenIdle(update), 1000)); diff --git a/modules/ui/background_offset.js b/modules/ui/background_offset.js new file mode 100644 index 0000000000..332cb7f9c0 --- /dev/null +++ b/modules/ui/background_offset.js @@ -0,0 +1,203 @@ +import { + event as d3_event, + select as d3_select, + selectAll as d3_selectAll +} from 'd3-selection'; + +import { t, textDirection } from '../util/locale'; +import { geoMetersToOffset, geoOffsetToMeters } from '../geo'; +import { svgIcon } from '../svg'; + + +export function uiBackgroundOffset(context) { + var directions = [ + ['right', [0.5, 0]], + ['top', [0, -0.5]], + ['left', [-0.5, 0]], + ['bottom', [0, 0.5]] + ]; + + + function updateValue() { + var meters = geoOffsetToMeters(context.background().offset()); + var x = +meters[0].toFixed(2); + var y = +meters[1].toFixed(2); + + d3_selectAll('.nudge-inner-rect') + .select('input') + .classed('error', false) + .property('value', x + ', ' + y); + + d3_selectAll('.nudge-reset') + .classed('disabled', function() { + return (x === 0 && y === 0); + }); + } + + + function resetOffset() { + if (d3_event.button !== 0) return; + context.background().offset([0, 0]); + updateValue(); + } + + + function nudge(d) { + context.background().nudge(d, context.map().zoom()); + updateValue(); + } + + + function buttonOffset(d) { + if (d3_event.button !== 0) return; + var timeout = window.setTimeout(function() { + interval = window.setInterval(nudge.bind(null, d), 100); + }, 500); + var interval; + + function doneNudge() { + window.clearTimeout(timeout); + window.clearInterval(interval); + d3_select(window) + .on('mouseup.buttonoffset', null, true) + .on('mousedown.buttonoffset', null, true); + } + + d3_select(window) + .on('mouseup.buttonoffset', doneNudge, true) + .on('mousedown.buttonoffset', doneNudge, true); + + nudge(d); + } + + + function inputOffset() { + if (d3_event.button !== 0) return; + var input = d3_select(this); + var d = input.node().value; + + if (d === '') return resetOffset(); + + d = d.replace(/;/g, ',').split(',').map(function(n) { + // if n is NaN, it will always get mapped to false. + return !isNaN(n) && n; + }); + + if (d.length !== 2 || !d[0] || !d[1]) { + input.classed('error', true); + return; + } + + context.background().offset(geoMetersToOffset(d)); + updateValue(); + } + + + function dragOffset() { + if (d3_event.button !== 0) return; + var origin = [d3_event.clientX, d3_event.clientY]; + + context.container() + .append('div') + .attr('class', 'nudge-surface'); + + d3_select(window) + .on('mousemove.offset', function() { + var latest = [d3_event.clientX, d3_event.clientY]; + var d = [ + -(origin[0] - latest[0]) / 4, + -(origin[1] - latest[1]) / 4 + ]; + + origin = latest; + nudge(d); + }) + .on('mouseup.offset', function() { + if (d3_event.button !== 0) return; + d3_selectAll('.nudge-surface') + .remove(); + + d3_select(window) + .on('mousemove.offset', null) + .on('mouseup.offset', null); + }); + + d3_event.preventDefault(); + } + + + function backgroundOffset(selection) { + + var adjustments = selection.selectAll('.adjustments') + .data([0]); + + var adjustmentsEnter = adjustments.enter() + .append('div') + .attr('class', 'adjustments'); + + adjustmentsEnter + .append('a') + .text(t('background.fix_misalignment')) + .attr('href', '#') + .classed('hide-toggle', true) + .classed('expanded', false) + .on('click', function() { + if (d3_event.button !== 0) return; + var exp = d3_select(this).classed('expanded'); + nudgeContainer.style('display', exp ? 'none' : 'block'); + d3_select(this).classed('expanded', !exp); + d3_event.preventDefault(); + }); + + var nudgeContainer = adjustmentsEnter + .append('div') + .attr('class', 'nudge-container cf') + .style('display', 'none'); + + nudgeContainer + .append('div') + .attr('class', 'nudge-instructions') + .text(t('background.offset')); + + var nudgeRect = nudgeContainer + .append('div') + .attr('class', 'nudge-outer-rect') + .on('mousedown', dragOffset); + + nudgeRect + .append('div') + .attr('class', 'nudge-inner-rect') + .append('input') + .on('change', inputOffset) + .on('mousedown', function() { + if (d3_event.button !== 0) return; + d3_event.stopPropagation(); + }); + + nudgeContainer + .append('div') + .selectAll('button') + .data(directions).enter() + .append('button') + .attr('class', function(d) { return d[0] + ' nudge'; }) + .on('mousedown', function(d) { + if (d3_event.button !== 0) return; + buttonOffset(d[1]); + }); + + nudgeContainer + .append('button') + .attr('title', t('background.reset')) + .attr('class', 'nudge-reset disabled') + .on('click', resetOffset) + .call(svgIcon('#icon-' + (textDirection === 'rtl' ? 'redo' : 'undo'))); + + updateValue(); + } + + + context.background() + .on('change.backgroundOffset-update', updateValue); + + return backgroundOffset; +} diff --git a/modules/ui/index.js b/modules/ui/index.js index 742aa7b1ca..f1affc4fde 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -2,6 +2,7 @@ export { uiInit } from './init'; export { uiAccount } from './account'; export { uiAttribution } from './attribution'; export { uiBackground } from './background'; +export { uiBackgroundOffset } from './background_offset'; export { uiChangesetEditor } from './changeset_editor'; export { uiCmd } from './cmd'; export { uiCommit } from './commit'; From d2c70938f6c987a6de840c4ac072615e00090368 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 27 Nov 2017 13:54:32 -0500 Subject: [PATCH 002/206] Wrap offset control in uiDisclosure, fix event handling on input field (closes #4553) --- modules/ui/background_offset.js | 82 ++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/modules/ui/background_offset.js b/modules/ui/background_offset.js index 332cb7f9c0..0d9c1f6837 100644 --- a/modules/ui/background_offset.js +++ b/modules/ui/background_offset.js @@ -7,9 +7,11 @@ import { import { t, textDirection } from '../util/locale'; import { geoMetersToOffset, geoOffsetToMeters } from '../geo'; import { svgIcon } from '../svg'; +import { uiDisclosure } from './disclosure'; export function uiBackgroundOffset(context) { + var expandedPreference = (context.storage('background_offset.expanded') !== 'false'); var directions = [ ['right', [0.5, 0]], ['top', [0, -0.5]], @@ -18,6 +20,12 @@ export function uiBackgroundOffset(context) { ]; + function d3_eventCancel() { + d3_event.stopPropagation(); + d3_event.preventDefault(); + } + + function updateValue() { var meters = geoOffsetToMeters(context.background().offset()); var x = +meters[0].toFixed(2); @@ -36,7 +44,6 @@ export function uiBackgroundOffset(context) { function resetOffset() { - if (d3_event.button !== 0) return; context.background().offset([0, 0]); updateValue(); } @@ -48,12 +55,11 @@ export function uiBackgroundOffset(context) { } - function buttonOffset(d) { - if (d3_event.button !== 0) return; + function clickNudgeButton(d) { + var interval; var timeout = window.setTimeout(function() { interval = window.setInterval(nudge.bind(null, d), 100); }, 500); - var interval; function doneNudge() { window.clearTimeout(timeout); @@ -72,7 +78,6 @@ export function uiBackgroundOffset(context) { function inputOffset() { - if (d3_event.button !== 0) return; var input = d3_select(this); var d = input.node().value; @@ -94,7 +99,9 @@ export function uiBackgroundOffset(context) { function dragOffset() { + d3_event.preventDefault(); if (d3_event.button !== 0) return; + var origin = [d3_event.clientX, d3_event.clientY]; context.container() @@ -121,50 +128,28 @@ export function uiBackgroundOffset(context) { .on('mousemove.offset', null) .on('mouseup.offset', null); }); - - d3_event.preventDefault(); } - function backgroundOffset(selection) { - - var adjustments = selection.selectAll('.adjustments') + function render(selection) { + var container = selection.selectAll('.nudge-container') .data([0]); - var adjustmentsEnter = adjustments.enter() + var containerEnter = container.enter() .append('div') - .attr('class', 'adjustments'); - - adjustmentsEnter - .append('a') - .text(t('background.fix_misalignment')) - .attr('href', '#') - .classed('hide-toggle', true) - .classed('expanded', false) - .on('click', function() { - if (d3_event.button !== 0) return; - var exp = d3_select(this).classed('expanded'); - nudgeContainer.style('display', exp ? 'none' : 'block'); - d3_select(this).classed('expanded', !exp); - d3_event.preventDefault(); - }); + .attr('class', 'nudge-container cf'); - var nudgeContainer = adjustmentsEnter - .append('div') - .attr('class', 'nudge-container cf') - .style('display', 'none'); - - nudgeContainer + containerEnter .append('div') .attr('class', 'nudge-instructions') .text(t('background.offset')); - var nudgeRect = nudgeContainer + var nudgeEnter = containerEnter .append('div') .attr('class', 'nudge-outer-rect') .on('mousedown', dragOffset); - nudgeRect + nudgeEnter .append('div') .attr('class', 'nudge-inner-rect') .append('input') @@ -174,28 +159,49 @@ export function uiBackgroundOffset(context) { d3_event.stopPropagation(); }); - nudgeContainer + containerEnter .append('div') .selectAll('button') .data(directions).enter() .append('button') .attr('class', function(d) { return d[0] + ' nudge'; }) + .on('contextmenu', d3_eventCancel) .on('mousedown', function(d) { if (d3_event.button !== 0) return; - buttonOffset(d[1]); + clickNudgeButton(d[1]); }); - nudgeContainer + containerEnter .append('button') .attr('title', t('background.reset')) .attr('class', 'nudge-reset disabled') - .on('click', resetOffset) + .on('contextmenu', d3_eventCancel) + .on('click', function() { + if (d3_event.button !== 0) return; + resetOffset(); + }) .call(svgIcon('#icon-' + (textDirection === 'rtl' ? 'redo' : 'undo'))); updateValue(); } + function backgroundOffset(selection) { + selection + .call(uiDisclosure() + .title(t('background.fix_misalignment')) + .expanded(expandedPreference) + .on('toggled', toggled) + .content(render) + ); + + function toggled(expanded) { + expandedPreference = expanded; + context.storage('background_offset.expanded', expanded); + } + } + + context.background() .on('change.backgroundOffset-update', updateValue); From 34cdab91979d4df96bb9be6dfe9ac0057d4c36a3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 27 Nov 2017 17:13:57 -0500 Subject: [PATCH 003/206] uiDisclosure now handles expanded state in localStorage Now all the places that use a uiDisclosure won't have to do that --- modules/ui/background_offset.js | 16 +--- modules/ui/commit.js | 1 - modules/ui/disclosure.js | 48 +++++++----- modules/ui/preset_editor.js | 10 +-- modules/ui/raw_member_editor.js | 24 +++--- modules/ui/raw_membership_editor.js | 53 ++++++------- modules/ui/raw_tag_editor.js | 114 ++++++++++++++-------------- 7 files changed, 124 insertions(+), 142 deletions(-) diff --git a/modules/ui/background_offset.js b/modules/ui/background_offset.js index 0d9c1f6837..295557de28 100644 --- a/modules/ui/background_offset.js +++ b/modules/ui/background_offset.js @@ -11,7 +11,6 @@ import { uiDisclosure } from './disclosure'; export function uiBackgroundOffset(context) { - var expandedPreference = (context.storage('background_offset.expanded') !== 'false'); var directions = [ ['right', [0.5, 0]], ['top', [0, -0.5]], @@ -153,11 +152,7 @@ export function uiBackgroundOffset(context) { .append('div') .attr('class', 'nudge-inner-rect') .append('input') - .on('change', inputOffset) - .on('mousedown', function() { - if (d3_event.button !== 0) return; - d3_event.stopPropagation(); - }); + .on('change', inputOffset); containerEnter .append('div') @@ -188,17 +183,10 @@ export function uiBackgroundOffset(context) { function backgroundOffset(selection) { selection - .call(uiDisclosure() + .call(uiDisclosure(context, 'background_offset', false) .title(t('background.fix_misalignment')) - .expanded(expandedPreference) - .on('toggled', toggled) .content(render) ); - - function toggled(expanded) { - expandedPreference = expanded; - context.storage('background_offset.expanded', expanded); - } } diff --git a/modules/ui/commit.js b/modules/ui/commit.js index 52696ca304..e7d3837f50 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -269,7 +269,6 @@ export function uiCommit(context) { updateChangeset({ review_requested: (rr ? 'yes' : undefined) }); var expanded = !tagSection.selectAll('a.hide-toggle.expanded').empty(); - tagSection .call(rawTagEditor .expanded(expanded) diff --git a/modules/ui/disclosure.js b/modules/ui/disclosure.js index 82ee9f0b4c..9d84c02f86 100644 --- a/modules/ui/disclosure.js +++ b/modules/ui/disclosure.js @@ -4,11 +4,13 @@ import { utilRebind } from '../util/rebind'; import { uiToggle } from './toggle'; -export function uiDisclosure() { +export function uiDisclosure(context, key, expandedDefault) { var dispatch = d3_dispatch('toggled'), - title, - expanded = false, - content = function () {}; + _preference = (context.storage('disclosure.' + key + '.expanded')), + _expanded = (_preference === null ? !!expandedDefault : (_preference === 'true')), + _title, + _updatePreference = true, + _content = function () {}; var disclosure = function(selection) { @@ -22,9 +24,9 @@ export function uiDisclosure() { .merge(hideToggle); hideToggle - .text(title) + .text(_title) .on('click', toggle) - .classed('expanded', expanded); + .classed('expanded', _expanded); var wrap = selection.selectAll('div') @@ -35,36 +37,46 @@ export function uiDisclosure() { .merge(wrap); wrap - .classed('hide', !expanded) - .call(content); + .classed('hide', !_expanded) + .call(_content); function toggle() { - expanded = !expanded; - hideToggle.classed('expanded', expanded); - wrap.call(uiToggle(expanded)); - dispatch.call('toggled', this, expanded); + _expanded = !_expanded; + if (_updatePreference) { + context.storage('disclosure.' + key + '.expanded', _expanded); + } + hideToggle.classed('expanded', _expanded); + wrap.call(uiToggle(_expanded)); + dispatch.call('toggled', this, _expanded); } }; disclosure.title = function(_) { - if (!arguments.length) return title; - title = _; + if (!arguments.length) return _title; + _title = _; return disclosure; }; disclosure.expanded = function(_) { - if (!arguments.length) return expanded; - expanded = _; + if (!arguments.length) return _expanded; + _expanded = _; + return disclosure; + }; + + + disclosure.updatePreference = function(_) { + if (!arguments.length) return _updatePreference; + _updatePreference = _; return disclosure; }; disclosure.content = function(_) { - if (!arguments.length) return content; - content = _; + if (!arguments.length) return _content; + _content = _; return disclosure; }; diff --git a/modules/ui/preset_editor.js b/modules/ui/preset_editor.js index 9944c8c971..6022785070 100644 --- a/modules/ui/preset_editor.js +++ b/modules/ui/preset_editor.js @@ -16,7 +16,6 @@ import { utilRebind } from '../util'; export function uiPresetEditor(context) { var dispatch = d3_dispatch('change'), formFields = uiFormFields(context), - expandedPreference = (context.storage('preset_fields.expanded') !== 'false'), state, fieldsArr, preset, @@ -25,17 +24,10 @@ export function uiPresetEditor(context) { function presetEditor(selection) { - selection.call(uiDisclosure() + selection.call(uiDisclosure(context, 'preset_fields', true) .title(t('inspector.all_fields')) - .expanded(expandedPreference) - .on('toggled', toggled) .content(render) ); - - function toggled(expanded) { - expandedPreference = expanded; - context.storage('preset_fields.expanded', expanded); - } } diff --git a/modules/ui/raw_member_editor.js b/modules/ui/raw_member_editor.js index 73c7de1591..e8e47b0584 100644 --- a/modules/ui/raw_member_editor.js +++ b/modules/ui/raw_member_editor.js @@ -20,8 +20,8 @@ import { export function uiRawMemberEditor(context) { - var id, - taginfo = services.taginfo; + var taginfo = services.taginfo, + _entityID; function selectMember(d) { @@ -53,7 +53,7 @@ export function uiRawMemberEditor(context) { function rawMemberEditor(selection) { - var entity = context.entity(id), + var entity = context.entity(_entityID), memberships = []; entity.members.slice(0, 1000).forEach(function(member, index) { @@ -68,21 +68,17 @@ export function uiRawMemberEditor(context) { }); var gt = entity.members.length > 1000 ? '>' : ''; - selection.call(uiDisclosure() + selection.call(uiDisclosure(context, 'raw_member_editor', true) .title(t('inspector.all_members') + ' (' + gt + memberships.length + ')') .expanded(true) - .on('toggled', toggled) + .updatePreference(false) + .on('toggled', function(expanded) { + if (expanded) { selection.node().parentNode.scrollTop += 200; } + }) .content(content) ); - function toggled(expanded) { - if (expanded) { - selection.node().parentNode.scrollTop += 200; - } - } - - function content(wrap) { var list = wrap.selectAll('.member-list') .data([0]); @@ -201,8 +197,8 @@ export function uiRawMemberEditor(context) { rawMemberEditor.entityID = function(_) { - if (!arguments.length) return id; - id = _; + if (!arguments.length) return _entityID; + _entityID = _; return rawMemberEditor; }; diff --git a/modules/ui/raw_membership_editor.js b/modules/ui/raw_membership_editor.js index 21952ab08e..9b67c79d60 100644 --- a/modules/ui/raw_membership_editor.js +++ b/modules/ui/raw_membership_editor.js @@ -28,7 +28,8 @@ import { utilDisplayName, utilNoAuto } from '../util'; export function uiRawMembershipEditor(context) { var taginfo = services.taginfo, - id, showBlank; + _entityID, + _showBlank; function selectRelation(d) { @@ -47,11 +48,13 @@ export function uiRawMembershipEditor(context) { function addMembership(d, role) { - showBlank = false; + _showBlank = false; + + var member = { id: _entityID, type: context.entity(_entityID).type, role: role }; if (d.relation) { context.perform( - actionAddMember(d.relation.id, { id: id, type: context.entity(id).type, role: role }), + actionAddMember(d.relation.id, member), t('operations.add_member.annotation') ); @@ -59,7 +62,7 @@ export function uiRawMembershipEditor(context) { var relation = osmRelation(); context.perform( actionAddEntity(relation), - actionAddMember(relation.id, { id: id, type: context.entity(id).type, role: role }), + actionAddMember(relation.id, member), t('operations.add.annotation.relation') ); @@ -77,15 +80,12 @@ export function uiRawMembershipEditor(context) { function relations(q) { - var newRelation = { - relation: null, - value: t('inspector.new_relation') - }, - result = [], - graph = context.graph(); + var newRelation = { relation: null, value: t('inspector.new_relation') }; + var result = []; + var graph = context.graph(); context.intersects(context.extent()).forEach(function(entity) { - if (entity.type !== 'relation' || entity.id === id) + if (entity.type !== 'relation' || entity.id === _entityID) return; var matched = context.presets().match(entity, graph), @@ -96,10 +96,7 @@ export function uiRawMembershipEditor(context) { if (q && value.toLowerCase().indexOf(q.toLowerCase()) === -1) return; - result.push({ - relation: entity, - value: value - }); + result.push({ relation: entity, value: value }); }); result.sort(function(a, b) { @@ -124,7 +121,7 @@ export function uiRawMembershipEditor(context) { function rawMembershipEditor(selection) { - var entity = context.entity(id), + var entity = context.entity(_entityID), parents = context.graph().parentRelations(entity), memberships = []; @@ -137,21 +134,17 @@ export function uiRawMembershipEditor(context) { }); var gt = parents.length > 1000 ? '>' : ''; - selection.call(uiDisclosure() + selection.call(uiDisclosure(context, 'raw_membership_editor', true) .title(t('inspector.all_relations') + ' (' + gt + memberships.length + ')') .expanded(true) - .on('toggled', toggled) + .updatePreference(false) + .on('toggled', function(expanded) { + if (expanded) { selection.node().parentNode.scrollTop += 200; } + }) .content(content) ); - function toggled(expanded) { - if (expanded) { - selection.node().parentNode.scrollTop += 200; - } - } - - function content(wrap) { var list = wrap.selectAll('.member-list') .data([0]); @@ -218,7 +211,7 @@ export function uiRawMembershipEditor(context) { var newrow = list.selectAll('.member-row-new') - .data(showBlank ? [0] : []); + .data(_showBlank ? [0] : []); newrow.exit() .remove(); @@ -272,7 +265,7 @@ export function uiRawMembershipEditor(context) { addrel .call(svgIcon('#icon-plus', 'light')) .on('click', function() { - showBlank = true; + _showBlank = true; content(wrap); list.selectAll('.member-entity-input').node().focus(); }); @@ -308,7 +301,7 @@ export function uiRawMembershipEditor(context) { taginfo.roles({ debounce: true, rtype: rtype || '', - geometry: context.geometry(id), + geometry: context.geometry(_entityID), query: role }, function(err, data) { if (!err) callback(sort(role, data)); @@ -328,8 +321,8 @@ export function uiRawMembershipEditor(context) { rawMembershipEditor.entityID = function(_) { - if (!arguments.length) return id; - id = _; + if (!arguments.length) return _entityID; + _entityID = _; return rawMembershipEditor; }; diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index e3cf7b6e4f..9e5bccc705 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -25,34 +25,36 @@ import { export function uiRawTagEditor(context) { var taginfo = services.taginfo, dispatch = d3_dispatch('change'), - expandedPreference = (context.storage('raw_tag_editor.expanded') === 'true'), - expandedCurrent = expandedPreference, - updatePreference = true, - readOnlyTags = [], - showBlank = false, - newRow, - state, - preset, - tags, - id; + _readOnlyTags = [], + _showBlank = false, + _updatePreference = true, + _expanded = false, + _newRow, + _state, + _preset, + _tags, + _entityID; function rawTagEditor(selection) { - var count = Object.keys(tags).filter(function(d) { return d; }).length; + var count = Object.keys(_tags).filter(function(d) { return d; }).length; - selection.call(uiDisclosure() + var disclosure = uiDisclosure(context, 'raw_tag_editor', false) .title(t('inspector.all_tags') + ' (' + count + ')') - .expanded(expandedCurrent) .on('toggled', toggled) - .content(content) - ); + .updatePreference(_updatePreference) + .content(content); + + // Sometimes we want to force the raw_tag_editor to be opened/closed.. + // When undefined, uiDisclosure will use the user's stored preference. + if (_expanded !== undefined) { + disclosure.expanded(_expanded); + } + + selection.call(disclosure); function toggled(expanded) { - expandedCurrent = expanded; - if (updatePreference) { - expandedPreference = expanded; - context.storage('raw_tag_editor.expanded', expanded); - } + _expanded = expanded; if (expanded) { selection.node().parentNode.scrollTop += 200; } @@ -61,14 +63,14 @@ export function uiRawTagEditor(context) { function content(wrap) { - var entries = _map(tags, function(v, k) { + var entries = _map(_tags, function(v, k) { return { key: k, value: v }; }); - if (!entries.length || showBlank) { - showBlank = false; + if (!entries.length || _showBlank) { + _showBlank = false; entries.push({key: '', value: ''}); - newRow = ''; + _newRow = ''; } var list = wrap.selectAll('.tag-list') @@ -138,8 +140,8 @@ export function uiRawTagEditor(context) { items = items .merge(enter) .sort(function(a, b) { - return (a.key === newRow && b.key !== newRow) ? 1 - : (a.key !== newRow && b.key === newRow) ? -1 + return (a.key === _newRow && b.key !== _newRow) ? 1 + : (a.key !== _newRow && b.key === _newRow) ? -1 : d3_ascending(a.key, b.key); }); @@ -149,11 +151,11 @@ export function uiRawTagEditor(context) { key = row.select('input.key'), // propagate bound data to child value = row.select('input.value'); // propagate bound data to child - if (id && taginfo) { + if (_entityID && taginfo) { bindTypeahead(key, value); } - var isRelation = (id && context.entity(id).type === 'relation'), + var isRelation = (_entityID && context.entity(_entityID).type === 'relation'), reference; if (isRelation && tag.key === 'type') { @@ -162,7 +164,7 @@ export function uiRawTagEditor(context) { reference = uiTagReference({ key: tag.key, value: tag.value }, context); } - if (state === 'hover') { + if (_state === 'hover') { reference.showing(false); } @@ -187,8 +189,8 @@ export function uiRawTagEditor(context) { function isReadOnly(d) { - for (var i = 0; i < readOnlyTags.length; i++) { - if (d.key.match(readOnlyTags[i]) !== null) { + for (var i = 0; i < _readOnlyTags.length; i++) { + if (d.key.match(_readOnlyTags[i]) !== null) { return true; } } @@ -206,7 +208,7 @@ export function uiRawTagEditor(context) { function bindTypeahead(key, value) { if (isReadOnly({ key: key })) return; - var geometry = context.geometry(id); + var geometry = context.geometry(_entityID); key.call(d3_combobox() .container(context.container()) @@ -275,7 +277,7 @@ export function uiRawTagEditor(context) { var match = kNew.match(/^(.*?)(?:_(\d+))?$/), base = match[1], suffix = +(match[2] || 1); - while (tags[kNew]) { // rename key if already in use + while (_tags[kNew]) { // rename key if already in use kNew = base + '_' + suffix++; } } @@ -284,8 +286,8 @@ export function uiRawTagEditor(context) { d.key = kNew; // Maintain DOM identity through the subsequent update. - if (newRow === kOld) { // see if this row is still a new row - newRow = ((d.value === '' || kNew === '') ? kNew : undefined); + if (_newRow === kOld) { // see if this row is still a new row + _newRow = ((d.value === '' || kNew === '') ? kNew : undefined); } this.value = kNew; @@ -298,8 +300,8 @@ export function uiRawTagEditor(context) { var tag = {}; tag[d.key] = this.value; - if (newRow === d.key && d.key !== '' && d.value !== '') { // not a new row anymore - newRow = undefined; + if (_newRow === d.key && d.key !== '' && d.value !== '') { // not a new row anymore + _newRow = undefined; } dispatch.call('change', this, tag); @@ -320,7 +322,7 @@ export function uiRawTagEditor(context) { // handler. Without the setTimeout, the call to `content` would // wipe out the pending value change. setTimeout(function() { - showBlank = true; + _showBlank = true; content(wrap); list.selectAll('li:last-child input.key').node().focus(); }, 0); @@ -329,51 +331,51 @@ export function uiRawTagEditor(context) { rawTagEditor.state = function(_) { - if (!arguments.length) return state; - state = _; + if (!arguments.length) return _state; + _state = _; return rawTagEditor; }; rawTagEditor.preset = function(_) { - if (!arguments.length) return preset; - preset = _; - if (preset.isFallback()) { - expandedCurrent = true; - updatePreference = false; + if (!arguments.length) return _preset; + _preset = _; + if (_preset.isFallback()) { + _expanded = true; + _updatePreference = false; } else { - expandedCurrent = expandedPreference; - updatePreference = true; + _expanded = undefined; + _updatePreference = true; } return rawTagEditor; }; rawTagEditor.tags = function(_) { - if (!arguments.length) return tags; - tags = _; + if (!arguments.length) return _tags; + _tags = _; return rawTagEditor; }; rawTagEditor.entityID = function(_) { - if (!arguments.length) return id; - id = _; + if (!arguments.length) return _entityID; + _entityID = _; return rawTagEditor; }; rawTagEditor.expanded = function(_) { - if (!arguments.length) return expandedCurrent; - expandedCurrent = _; - updatePreference = false; + if (!arguments.length) return _expanded; + _expanded = _; + _updatePreference = false; return rawTagEditor; }; rawTagEditor.readOnlyTags = function(_) { - if (!arguments.length) return readOnlyTags; - readOnlyTags = _; + if (!arguments.length) return _readOnlyTags; + _readOnlyTags = _; return rawTagEditor; }; From 385297d99317607248d9a8b8ba8f143220e2ceff Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 27 Nov 2017 22:03:11 -0500 Subject: [PATCH 004/206] Use uiDisclosure for data pane subsections --- modules/ui/background.js | 6 +- modules/ui/disclosure.js | 5 +- modules/ui/map_data.js | 765 ++++++++++++++++++++------------------- 3 files changed, 393 insertions(+), 383 deletions(-) diff --git a/modules/ui/background.js b/modules/ui/background.js index 5ad890c3ea..732188deb9 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -147,7 +147,7 @@ export function uiBackground(context) { } - function drawList(layerList, type, change, filter) { + function drawListItems(layerList, type, change, filter) { var sources = context.background() .sources(context.map().extent()) .filter(filter); @@ -204,10 +204,10 @@ export function uiBackground(context) { function update() { backgroundList - .call(drawList, 'radio', clickSetSource, function(d) { return !d.isHidden() && !d.overlay; }); + .call(drawListItems, 'radio', clickSetSource, function(d) { return !d.isHidden() && !d.overlay; }); overlayList - .call(drawList, 'checkbox', clickSetOverlay, function(d) { return !d.isHidden() && d.overlay; }); + .call(drawListItems, 'checkbox', clickSetOverlay, function(d) { return !d.isHidden() && d.overlay; }); selectLayer(); diff --git a/modules/ui/disclosure.js b/modules/ui/disclosure.js index 9d84c02f86..45c6315a51 100644 --- a/modules/ui/disclosure.js +++ b/modules/ui/disclosure.js @@ -20,7 +20,7 @@ export function uiDisclosure(context, key, expandedDefault) { hideToggle = hideToggle.enter() .append('a') .attr('href', '#') - .attr('class', 'hide-toggle') + .attr('class', 'hide-toggle hide-toggle-' + key) .merge(hideToggle); hideToggle @@ -29,11 +29,12 @@ export function uiDisclosure(context, key, expandedDefault) { .classed('expanded', _expanded); - var wrap = selection.selectAll('div') + var wrap = selection.selectAll('.disclosure-wrap') .data([0]); wrap = wrap.enter() .append('div') + .attr('class', 'disclosure-wrap disclosure-wrap-' + key) .merge(wrap); wrap diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index fb522c0984..a928bd6495 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -5,380 +5,424 @@ import { import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; -import { t, textDirection } from '../util/locale'; import { svgIcon } from '../svg'; -import { uiTooltipHtml } from './tooltipHtml'; +import { t, textDirection } from '../util/locale'; import { tooltip } from '../util/tooltip'; +import { uiDisclosure } from './disclosure'; +import { uiTooltipHtml } from './tooltipHtml'; export function uiMapData(context) { - var key = t('map_data.key'), - features = context.features().keys(), - layers = context.layers(), - fills = ['wireframe', 'partial', 'full'], - fillDefault = context.storage('area-fill') || 'partial', - fillSelected = fillDefault; + var key = t('map_data.key'); + var features = context.features().keys(); + var layers = context.layers(); + var fills = ['wireframe', 'partial', 'full']; + + var _fillDefault = context.storage('area-fill') || 'partial'; + var _fillSelected = _fillDefault; + var _shown = false; + var _dataLayerContainer = d3_select(null); + var _fillList = d3_select(null); + var _featureList = d3_select(null); + + + + function showsFeature(d) { + return context.features().enabled(d); + } + + + function autoHiddenFeature(d) { + return context.features().autoHidden(d); + } + + + function clickFeature(d) { + context.features().toggle(d); + update(); + } + + + function showsFill(d) { + return _fillSelected === d; + } - function map_data(selection) { + function setFill(d) { + fills.forEach(function(opt) { + context.surface().classed('fill-' + opt, Boolean(opt === d)); + }); - function showsFeature(d) { - return context.features().enabled(d); + _fillSelected = d; + if (d !== 'wireframe') { + _fillDefault = d; + context.storage('area-fill', d); } + update(); + } - function autoHiddenFeature(d) { - return context.features().autoHidden(d); + function showsLayer(which) { + var layer = layers.layer(which); + if (layer) { + return layer.enabled(); } + return false; + } - function clickFeature(d) { - context.features().toggle(d); + function setLayer(which, enabled) { + var layer = layers.layer(which); + if (layer) { + layer.enabled(enabled); update(); } + } - function showsFill(d) { - return fillSelected === d; - } + function toggleLayer(which) { + setLayer(which, !showsLayer(which)); + } - function setFill(d) { - fills.forEach(function(opt) { - context.surface().classed('fill-' + opt, Boolean(opt === d)); - }); + function drawPhotoItems(selection) { + var photoKeys = ['mapillary-images', 'mapillary-signs', 'openstreetcam-images']; + var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; }); + var data = photoLayers.filter(function(obj) { return obj.layer.supported(); }); - fillSelected = d; - if (d !== 'wireframe') { - fillDefault = d; - context.storage('area-fill', d); - } - update(); + function layerSupported(d) { + return d.layer && d.layer.supported(); + } + function layerEnabled(d) { + return layerSupported(d) && d.layer.enabled(); } + var ul = selection + .selectAll('.layer-list-photos') + .data([0]); - function showsLayer(which) { - var layer = layers.layer(which); - if (layer) { - return layer.enabled(); - } - return false; - } + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-photos') + .merge(ul); + + var li = ul.selectAll('.list-item-photos') + .data(data); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { return 'list-item-photos list-item-' + d.id; }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + d3_select(this) + .call(tooltip() + .title(t(d.id.replace('-', '_') + '.tooltip')) + .placement('top') + ); + }); + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function(d) { toggleLayer(d.id); }); - function setLayer(which, enabled) { - var layer = layers.layer(which); - if (layer) { - layer.enabled(enabled); - update(); - } - } + labelEnter + .append('span') + .text(function(d) { return t(d.id.replace('-', '_') + '.title'); }); - function toggleLayer(which) { - setLayer(which, !showsLayer(which)); - } + // Update + li = li + .merge(liEnter); + li + .classed('active', layerEnabled) + .selectAll('input') + .property('checked', layerEnabled); + } - function drawPhotoItems(selection) { - var photoKeys = ['mapillary-images', 'mapillary-signs', 'openstreetcam-images']; - var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; }); - var data = photoLayers.filter(function(obj) { return obj.layer.supported(); }); - function layerSupported(d) { - return d.layer && d.layer.supported(); - } - function layerEnabled(d) { - return layerSupported(d) && d.layer.enabled(); - } + function drawOsmItem(selection) { + var osm = layers.layer('osm'), + showsOsm = osm.enabled(); - var ul = selection - .selectAll('.layer-list-photos') - .data([0]); - - ul = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-photos') - .merge(ul); - - var li = ul.selectAll('.list-item-photos') - .data(data); - - li.exit() - .remove(); - - var liEnter = li.enter() - .append('li') - .attr('class', function(d) { return 'list-item-photos list-item-' + d.id; }); - - var labelEnter = liEnter - .append('label') - .each(function(d) { - d3_select(this) - .call(tooltip() - .title(t(d.id.replace('-', '_') + '.tooltip')) - .placement('top') - ); - }); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function(d) { toggleLayer(d.id); }); - - labelEnter - .append('span') - .text(function(d) { return t(d.id.replace('-', '_') + '.title'); }); - - - // Update - li = li - .merge(liEnter); - - li - .classed('active', layerEnabled) - .selectAll('input') - .property('checked', layerEnabled); - } + var ul = selection + .selectAll('.layer-list-osm') + .data(osm ? [0] : []); + // Exit + ul.exit() + .remove(); - function drawOsmItem(selection) { - var osm = layers.layer('osm'), - showsOsm = osm.enabled(); + // Enter + var ulEnter = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-osm'); + + var liEnter = ulEnter + .append('li') + .attr('class', 'list-item-osm'); + + var labelEnter = liEnter + .append('label') + .call(tooltip() + .title(t('map_data.layers.osm.tooltip')) + .placement('top') + ); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { toggleLayer('osm'); }); + + labelEnter + .append('span') + .text(t('map_data.layers.osm.title')); + + // Update + ul = ul + .merge(ulEnter); + + ul.selectAll('.list-item-osm') + .classed('active', showsOsm) + .selectAll('input') + .property('checked', showsOsm); + } - var ul = selection - .selectAll('.layer-list-osm') - .data(osm ? [0] : []); - // Exit - ul.exit() - .remove(); + function drawGpxItem(selection) { + var gpx = layers.layer('gpx'), + hasGpx = gpx && gpx.hasGpx(), + showsGpx = hasGpx && gpx.enabled(); - // Enter - var ulEnter = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-osm'); + var ul = selection + .selectAll('.layer-list-gpx') + .data(gpx ? [0] : []); - var liEnter = ulEnter - .append('li') - .attr('class', 'list-item-osm'); + // Exit + ul.exit() + .remove(); - var labelEnter = liEnter - .append('label') - .call(tooltip() - .title(t('map_data.layers.osm.tooltip')) - .placement('top') - ); + // Enter + var ulEnter = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-gpx'); - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { toggleLayer('osm'); }); + var liEnter = ulEnter + .append('li') + .attr('class', 'list-item-gpx'); - labelEnter - .append('span') - .text(t('map_data.layers.osm.title')); + liEnter + .append('button') + .attr('class', 'list-item-gpx-extent') + .call(tooltip() + .title(t('gpx.zoom')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + gpx.fitZoom(); + }) + .call(svgIcon('#icon-search')); + + liEnter + .append('button') + .attr('class', 'list-item-gpx-browse') + .call(tooltip() + .title(t('gpx.browse')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', function() { + d3_select(document.createElement('input')) + .attr('type', 'file') + .on('change', function() { + gpx.files(d3_event.target.files); + }) + .node().click(); + }) + .call(svgIcon('#icon-geolocate')); + + var labelEnter = liEnter + .append('label') + .call(tooltip() + .title(t('gpx.drag_drop')) + .placement('top') + ); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { toggleLayer('gpx'); }); + + labelEnter + .append('span') + .text(t('gpx.local_layer')); + + // Update + ul = ul + .merge(ulEnter); + + ul.selectAll('.list-item-gpx') + .classed('active', showsGpx) + .selectAll('label') + .classed('deemphasize', !hasGpx) + .selectAll('input') + .property('disabled', !hasGpx) + .property('checked', showsGpx); + } - // Update - ul = ul - .merge(ulEnter); - ul.selectAll('.list-item-osm') - .classed('active', showsOsm) - .selectAll('input') - .property('checked', showsOsm); - } + function drawListItems(selection, data, type, name, change, active) { + var items = selection.selectAll('li') + .data(data); + // Exit + items.exit() + .remove(); - function drawGpxItem(selection) { - var gpx = layers.layer('gpx'), - hasGpx = gpx && gpx.hasGpx(), - showsGpx = hasGpx && gpx.enabled(); - - var ul = selection - .selectAll('.layer-list-gpx') - .data(gpx ? [0] : []); - - // Exit - ul.exit() - .remove(); - - // Enter - var ulEnter = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-gpx'); - - var liEnter = ulEnter - .append('li') - .attr('class', 'list-item-gpx'); - - liEnter - .append('button') - .attr('class', 'list-item-gpx-extent') - .call(tooltip() - .title(t('gpx.zoom')) - .placement((textDirection === 'rtl') ? 'right' : 'left')) - .on('click', function() { - d3_event.preventDefault(); - d3_event.stopPropagation(); - gpx.fitZoom(); - }) - .call(svgIcon('#icon-search')); - - liEnter - .append('button') - .attr('class', 'list-item-gpx-browse') - .call(tooltip() - .title(t('gpx.browse')) - .placement((textDirection === 'rtl') ? 'right' : 'left') - ) - .on('click', function() { - d3_select(document.createElement('input')) - .attr('type', 'file') - .on('change', function() { - gpx.files(d3_event.target.files); - }) - .node().click(); + // Enter + var enter = items.enter() + .append('li') + .attr('class', 'layer') + .call(tooltip() + .html(true) + .title(function(d) { + var tip = t(name + '.' + d + '.tooltip'), + key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null); + + if (name === 'feature' && autoHiddenFeature(d)) { + var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden'); + tip += '
' + msg + '
'; + } + return uiTooltipHtml(tip, key); }) - .call(svgIcon('#icon-geolocate')); - - var labelEnter = liEnter - .append('label') - .call(tooltip() - .title(t('gpx.drag_drop')) - .placement('top') - ); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { toggleLayer('gpx'); }); - - labelEnter - .append('span') - .text(t('gpx.local_layer')); - - // Update - ul = ul - .merge(ulEnter); - - ul.selectAll('.list-item-gpx') - .classed('active', showsGpx) - .selectAll('label') - .classed('deemphasize', !hasGpx) - .selectAll('input') - .property('disabled', !hasGpx) - .property('checked', showsGpx); - } + .placement('top') + ); + + var label = enter + .append('label'); + + label + .append('input') + .attr('type', type) + .attr('name', name) + .on('change', change); + + label + .append('span') + .text(function(d) { return t(name + '.' + d + '.description'); }); + + // Update + items = items + .merge(enter); + + items + .classed('active', active) + .selectAll('input') + .property('checked', active) + .property('indeterminate', function(d) { + return (name === 'feature' && autoHiddenFeature(d)); + }); + } - function drawList(selection, data, type, name, change, active) { - var items = selection.selectAll('li') - .data(data); - - // Exit - items.exit() - .remove(); - - // Enter - var enter = items.enter() - .append('li') - .attr('class', 'layer') - .call(tooltip() - .html(true) - .title(function(d) { - var tip = t(name + '.' + d + '.tooltip'), - key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null); - - if (name === 'feature' && autoHiddenFeature(d)) { - var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden'); - tip += '
' + msg + '
'; - } - return uiTooltipHtml(tip, key); - }) - .placement('top') - ); - - var label = enter - .append('label'); - - label - .append('input') - .attr('type', type) - .attr('name', name) - .on('change', change); - - label - .append('span') - .text(function(d) { return t(name + '.' + d + '.description'); }); - - // Update - items = items - .merge(enter); - - items - .classed('active', active) - .selectAll('input') - .property('checked', active) - .property('indeterminate', function(d) { - return (name === 'feature' && autoHiddenFeature(d)); - }); - } + function renderDataLayers(selection) { + var container = selection.selectAll('data-layer-container') + .data([0]); + _dataLayerContainer = container.enter() + .append('div') + .attr('class', 'data-layer-container') + .merge(container); + } - function update() { - dataLayerContainer - .call(drawOsmItem) - .call(drawPhotoItems) - .call(drawGpxItem); - fillList - .call(drawList, fills, 'radio', 'area_fill', setFill, showsFill); + function renderFillList(selection) { + var container = selection.selectAll('layer-fill-list') + .data([0]); - featureList - .call(drawList, features, 'checkbox', 'feature', clickFeature, showsFeature); - } + _fillList = container.enter() + .append('ul') + .attr('class', 'layer-list layer-fill-list') + .merge(container); + } - function hidePanel() { - setVisible(false); - } + function renderFeatureList(selection) { + var container = selection.selectAll('layer-feature-list') + .data([0]); + _featureList = container.enter() + .append('ul') + .attr('class', 'layer-list layer-feature-list') + .merge(container); + } - function togglePanel() { - if (d3_event) d3_event.preventDefault(); - tooltipBehavior.hide(button); - setVisible(!button.classed('active')); + + function update() { + _dataLayerContainer + .call(drawOsmItem) + .call(drawPhotoItems) + .call(drawGpxItem); + + _fillList + .call(drawListItems, fills, 'radio', 'area_fill', setFill, showsFill); + + _featureList + .call(drawListItems, features, 'checkbox', 'feature', clickFeature, showsFeature); + } + + + function toggleWireframe() { + if (d3_event) { + d3_event.preventDefault(); + d3_event.stopPropagation(); } + setFill((_fillSelected === 'wireframe' ? _fillDefault : 'wireframe')); + context.map().pan([0,0]); // trigger a redraw + } - function toggleWireframe() { - if (d3_event) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - } - setFill((fillSelected === 'wireframe' ? fillDefault : 'wireframe')); - context.map().pan([0,0]); // trigger a redraw + function mapData(selection) { + + function hidePane() { + setVisible(false); } + function togglePane() { + if (d3_event) d3_event.preventDefault(); + paneTooltip.hide(button); + setVisible(!button.classed('active')); + } function setVisible(show) { - if (show !== shown) { + if (show !== _shown) { button.classed('active', show); - shown = show; + _shown = show; if (show) { update(); + selection.on('mousedown.map_data-inside', function() { return d3_event.stopPropagation(); }); - content.style('display', 'block') + + pane + .style('display', 'block') .style('right', '-300px') .transition() .duration(200) .style('right', '0px'); + } else { - content.style('display', 'block') + pane + .style('display', 'block') .style('right', '0px') .transition() .duration(200) @@ -386,113 +430,78 @@ export function uiMapData(context) { .on('end', function() { d3_select(this).style('display', 'none'); }); + selection.on('mousedown.map_data-inside', null); } } } - var content = selection - .append('div') - .attr('class', 'fillL map-overlay col3 content hide'), - tooltipBehavior = tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - .html(true) - .title(uiTooltipHtml(t('map_data.description'), key)), - button = selection - .append('button') - .attr('tabindex', -1) - .on('click', togglePanel) - .call(svgIcon('#icon-data', 'light')) - .call(tooltipBehavior), - shown = false; - - content - .append('h4') - .text(t('map_data.title')); + var pane = selection + .append('div') + .attr('class', 'fillL map-overlay col3 content hide'); + var paneTooltip = tooltip() + .placement((textDirection === 'rtl') ? 'right' : 'left') + .html(true) + .title(uiTooltipHtml(t('map_data.description'), key)); - // data layers - content - .append('a') - .text(t('map_data.data_layers')) - .attr('href', '#') - .classed('hide-toggle', true) - .classed('expanded', true) - .on('click', function() { - var exp = d3_select(this).classed('expanded'); - dataLayerContainer.style('display', exp ? 'none' : 'block'); - d3_select(this).classed('expanded', !exp); - d3_event.preventDefault(); - }); + var button = selection + .append('button') + .attr('tabindex', -1) + .on('click', togglePane) + .call(svgIcon('#icon-data', 'light')) + .call(paneTooltip); - var dataLayerContainer = content - .append('div') - .attr('class', 'data-data-layers') - .style('display', 'block'); + pane + .append('h3') + .text(t('map_data.title')); - // area fills - content - .append('a') - .text(t('map_data.fill_area')) - .attr('href', '#') - .classed('hide-toggle', true) - .classed('expanded', false) - .on('click', function() { - var exp = d3_select(this).classed('expanded'); - fillContainer.style('display', exp ? 'none' : 'block'); - d3_select(this).classed('expanded', !exp); - d3_event.preventDefault(); - }); - var fillContainer = content + // data layers + pane .append('div') - .attr('class', 'data-area-fills') - .style('display', 'none'); - - var fillList = fillContainer - .append('ul') - .attr('class', 'layer-list layer-fill-list'); + .attr('class', 'map-data-data-layers') + .call(uiDisclosure(context, 'data_layers', true) + .title(t('map_data.data_layers')) + .content(renderDataLayers) + ); + // area fills + pane + .append('div') + .attr('class', 'map-data-area-fills') + .call(uiDisclosure(context, 'fill_area', false) + .title(t('map_data.fill_area')) + .content(renderFillList) + ); // feature filters - content - .append('a') - .text(t('map_data.map_features')) - .attr('href', '#') - .classed('hide-toggle', true) - .classed('expanded', false) - .on('click', function() { - var exp = d3_select(this).classed('expanded'); - featureContainer.style('display', exp ? 'none' : 'block'); - d3_select(this).classed('expanded', !exp); - d3_event.preventDefault(); - }); - - var featureContainer = content + pane .append('div') - .attr('class', 'data-feature-filters') - .style('display', 'none'); - - var featureList = featureContainer - .append('ul') - .attr('class', 'layer-list layer-feature-list'); + .attr('class', 'map-data-feature-filters') + .call(uiDisclosure(context, 'map_features', false) + .title(t('map_data.map_features')) + .content(renderFeatureList) + ); + // add listeners context.features() .on('change.map_data-update', update); - setFill(fillDefault); + update(); + setFill(_fillDefault); var keybinding = d3_keybinding('features') - .on(key, togglePanel) + .on(key, togglePane) .on(t('area_fill.wireframe.key'), toggleWireframe) - .on([t('background.key'), t('help.key')], hidePanel); + .on([t('background.key'), t('help.key')], hidePane); d3_select(document) .call(keybinding); } - return map_data; + return mapData; } From 5a9749c51642f512cc7bea03f4c3f8694d3dff5a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 27 Nov 2017 23:42:02 -0500 Subject: [PATCH 005/206] Fix bug that caused clicking a uiDisclosure to change the url (closes #4570) --- modules/ui/disclosure.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/ui/disclosure.js b/modules/ui/disclosure.js index 45c6315a51..3b4c3a0a4d 100644 --- a/modules/ui/disclosure.js +++ b/modules/ui/disclosure.js @@ -1,4 +1,5 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { event as d3_event } from 'd3-selection'; import { utilRebind } from '../util/rebind'; import { uiToggle } from './toggle'; @@ -43,12 +44,15 @@ export function uiDisclosure(context, key, expandedDefault) { function toggle() { + d3_event.preventDefault(); + _expanded = !_expanded; if (_updatePreference) { context.storage('disclosure.' + key + '.expanded', _expanded); } hideToggle.classed('expanded', _expanded); wrap.call(uiToggle(_expanded)); + dispatch.call('toggled', this, _expanded); } }; From d7e8625d6b7d2f987db87dd363cc2f8f9f2a90db Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 28 Nov 2017 15:06:19 -0500 Subject: [PATCH 006/206] Use uiDisclosure for background pane subsections, move brightness --- css/80_app.css | 41 ++- data/core.yaml | 12 +- dist/locales/en.json | 14 +- modules/ui/background.js | 554 ++++++++++++++++++++++----------------- 4 files changed, 343 insertions(+), 278 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index b8662d8992..2e8b957158 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2307,7 +2307,7 @@ div.full-screen > button:hover { padding: 5px 10px; cursor: pointer; color: #7092FF; - border-radius: 3px; + border-top: 1px solid #ccc; } .minimap-toggle.active { @@ -2463,42 +2463,39 @@ div.full-screen > button:hover { } .background-control .nudge.right::after { - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-left: 5px solid #222; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #222; } .background-control .nudge.left::after { - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-right: 5px solid #222; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #222; } .background-control .nudge.top::after { - border-right: 5px solid transparent; - border-left: 5px solid transparent; - border-bottom: 5px solid #222; + border-right: 5px solid transparent; + border-left: 5px solid transparent; + border-bottom: 5px solid #222; } .background-control .nudge.bottom::after { - border-right: 5px solid transparent; - border-left: 5px solid transparent; - border-top: 5px solid #222; + border-right: 5px solid transparent; + border-left: 5px solid transparent; + border-top: 5px solid #222; +} + +.opacity-options-wrapper { + padding: 10px; } .opacity-options { background: url(img/background-pattern-opacity.png) 0 0 repeat; height: 20px; width: 82px; - position: absolute; - right: 50px; - top: 20px; border: 1px solid #ccc; } -[dir='rtl'] .opacity-options { - left: 50px; - right: auto; -} .opacity-options li { height: 100%; @@ -2513,8 +2510,6 @@ div.full-screen > button:hover { z-index: 9999; } -.map-data-control li:hover .select-box, -.map-data-control li.selected .select-box, .background-control li:hover .select-box, .background-control li.selected .select-box { border: 2px solid #7092ff; @@ -2522,8 +2517,6 @@ div.full-screen > button:hover { opacity: .5; } -.map-data-control li.selected:hover .select-box, -.map-data-control li.selected .select-box, .background-control li.selected:hover .select-box, .background-control li.selected .select-box { opacity: 1; diff --git a/data/core.yaml b/data/core.yaml index e40b52190c..1344b733a7 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -372,21 +372,25 @@ en: title: Background description: Background settings key: B - percent_brightness: "{opacity}% brightness" + backgrounds: Backgrounds none: None best_imagery: Best known imagery source for this location switch: Switch back to this background custom: Custom custom_button: Edit custom background custom_prompt: "Enter a tile URL template. Valid tokens are:\n - {zoom}/{z}, {x}, {y} for Z/X/Y tile scheme\n - {ty} for flipped TMS-style Y coordinates\n - {u} for quadtile scheme\n - {switch:a,b,c} for DNS server multiplexing\n\nExample:\n{example}" - fix_misalignment: Adjust imagery offset + overlays: Overlays imagery_source_faq: Where does this imagery come from? reset: reset - offset: "Drag anywhere in the gray area below to adjust the imagery offset, or enter the offset values in meters." + display_options: Display Options + brightness: Brightness + percent_brightness: "{opacity}% brightness" minimap: - description: Minimap + description: Show Minimap tooltip: Show a zoomed out map to help locate the area currently displayed. key: '/' + fix_misalignment: Adjust imagery offset + offset: "Drag anywhere in the gray area below to adjust the imagery offset, or enter the offset values in meters." map_data: title: Map Data description: Map Data diff --git a/dist/locales/en.json b/dist/locales/en.json index 0cdefe708f..c41d2f0053 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -460,22 +460,26 @@ "title": "Background", "description": "Background settings", "key": "B", - "percent_brightness": "{opacity}% brightness", + "backgrounds": "Backgrounds", "none": "None", "best_imagery": "Best known imagery source for this location", "switch": "Switch back to this background", "custom": "Custom", "custom_button": "Edit custom background", "custom_prompt": "Enter a tile URL template. Valid tokens are:\n - {zoom}/{z}, {x}, {y} for Z/X/Y tile scheme\n - {ty} for flipped TMS-style Y coordinates\n - {u} for quadtile scheme\n - {switch:a,b,c} for DNS server multiplexing\n\nExample:\n{example}", - "fix_misalignment": "Adjust imagery offset", + "overlays": "Overlays", "imagery_source_faq": "Where does this imagery come from?", "reset": "reset", - "offset": "Drag anywhere in the gray area below to adjust the imagery offset, or enter the offset values in meters.", + "display_options": "Display Options", + "brightness": "Brightness", + "percent_brightness": "{opacity}% brightness", "minimap": { - "description": "Minimap", + "description": "Show Minimap", "tooltip": "Show a zoomed out map to help locate the area currently displayed.", "key": "/" - } + }, + "fix_misalignment": "Adjust imagery offset", + "offset": "Drag anywhere in the gray area below to adjust the imagery offset, or enter the offset values in meters." }, "map_data": { "title": "Map Data", diff --git a/modules/ui/background.js b/modules/ui/background.js index 732188deb9..d23287a7fc 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -16,6 +16,7 @@ import { t, textDirection } from '../util/locale'; import { svgIcon } from '../svg'; import { uiBackgroundOffset } from './background_offset'; import { uiCmd } from './cmd'; +import { uiDisclosure } from './disclosure'; import { uiMapInMap } from './map_in_map'; import { uiTooltipHtml } from './tooltipHtml'; import { utilDetect } from '../util/detect'; @@ -24,227 +25,325 @@ import { tooltip } from '../util/tooltip'; export function uiBackground(context) { - var key = t('background.key'), - detected = utilDetect(), - opacities = [1, 0.75, 0.5, 0.25], - opacityDefault = (context.storage('background-opacity') !== null) ? - (+context.storage('background-opacity')) : 1.0, - customSource = context.background().findSource('custom'), - previous; + var key = t('background.key'); + var detected = utilDetect(); + var opacities = [1, 0.75, 0.5, 0.25]; + + var _opacityDefault = (context.storage('background-opacity') !== null) ? + (+context.storage('background-opacity')) : 1.0; + var _customSource = context.background().findSource('custom'); + var _previousBackground; + var _shown = false; + + var _backgroundList = d3_select(null); + var _overlayList = d3_select(null); + var _displayOptions = d3_select(null); + var _offsetContainer = d3_select(null); var backgroundOffset = uiBackgroundOffset(context); // Can be 0 from <1.3.0 use or due to issue #1923. - if (opacityDefault === 0) opacityDefault = 1.0; + if (_opacityDefault === 0) _opacityDefault = 1.0; - function background(selection) { + function setOpacity(d) { + var bg = context.container().selectAll('.layer-background') + .transition() + .style('opacity', d) + .attr('data-opacity', d); - function sortSources(a, b) { - return a.best() && !b.best() ? -1 - : b.best() && !a.best() ? 1 - : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0; + if (!detected.opera) { + utilSetTransform(bg, 0, 0); } + _displayOptions.selectAll('opacity-options li') + .classed('active', function(_) { return _ === d; }); + + context.storage('background-opacity', d); + } - function setOpacity(d) { - var bg = context.container().selectAll('.layer-background') - .transition() - .style('opacity', d) - .attr('data-opacity', d); - if (!detected.opera) { - utilSetTransform(bg, 0, 0); + function setTooltips(selection) { + selection.each(function(d, i, nodes) { + var item = d3_select(this).select('label'), + span = item.select('span'), + placement = (i < nodes.length / 2) ? 'bottom' : 'top', + description = d.description(), + isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); + + if (d === _previousBackground) { + item.call(tooltip() + .placement(placement) + .html(true) + .title(function() { + var tip = '
' + t('background.switch') + '
'; + return uiTooltipHtml(tip, uiCmd('⌘' + key)); + }) + ); + } else if (description || isOverflowing) { + item.call(tooltip() + .placement(placement) + .title(description || d.name()) + ); + } else { + item.call(tooltip().destroy); } + }); + } - opacityList.selectAll('li') - .classed('active', function(_) { return _ === d; }); - context.storage('background-opacity', d); + function updateLayerSelections(selection) { + function active(d) { + return context.background().showsLayer(d); } + selection.selectAll('.layer') + .classed('active', active) + .classed('switch', function(d) { return d === _previousBackground; }) + .call(setTooltips) + .selectAll('input') + .property('checked', active); + } - function setTooltips(selection) { - selection.each(function(d, i, nodes) { - var item = d3_select(this).select('label'), - span = item.select('span'), - placement = (i < nodes.length / 2) ? 'bottom' : 'top', - description = d.description(), - isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); - - if (d === previous) { - item.call(tooltip() - .placement(placement) - .html(true) - .title(function() { - var tip = '
' + t('background.switch') + '
'; - return uiTooltipHtml(tip, uiCmd('⌘' + key)); - }) - ); - } else if (description || isOverflowing) { - item.call(tooltip() - .placement(placement) - .title(description || d.name()) - ); - } else { - item.call(tooltip().destroy); - } - }); + + function chooseBackground(d) { + if (d.id === 'custom' && !d.template()) { + return editCustom(); } + d3_event.preventDefault(); + _previousBackground = context.background().baseLayerSource(); + context.background().baseLayerSource(d); + _backgroundList.call(updateLayerSelections); + document.activeElement.blur(); + } - function selectLayer() { - function active(d) { - return context.background().showsLayer(d); - } - content.selectAll('.layer') - .classed('active', active) - .classed('switch', function(d) { return d === previous; }) - .call(setTooltips) - .selectAll('input') - .property('checked', active); + function editCustom() { + d3_event.preventDefault(); + var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png'; + var template = window.prompt( + t('background.custom_prompt', { example: example }), + _customSource.template() || example + ); + + if (template) { + context.storage('background-custom-template', template); + _customSource.template(template); + chooseBackground(_customSource); + } else { + _backgroundList.call(updateLayerSelections); } + } - function clickSetSource(d) { - if (d.id === 'custom' && !d.template()) { - return editCustom(); - } + function chooseOverlay(d) { + d3_event.preventDefault(); + context.background().toggleOverlayLayer(d); + _overlayList.call(updateLayerSelections); + document.activeElement.blur(); + } - d3_event.preventDefault(); - previous = context.background().baseLayerSource(); - context.background().baseLayerSource(d); - selectLayer(); - document.activeElement.blur(); - } + function drawListItems(layerList, type, change, filter) { + var sources = context.background() + .sources(context.map().extent()) + .filter(filter); - function editCustom() { - d3_event.preventDefault(); - var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png'; - var template = window.prompt( - t('background.custom_prompt', { example: example }), - customSource.template() || example - ); + var layerLinks = layerList.selectAll('li.layer') + .data(sources, function(d) { return d.name(); }); - if (template) { - context.storage('background-custom-template', template); - customSource.template(template); - clickSetSource(customSource); - } else { - selectLayer(); - } - } + layerLinks.exit() + .remove(); + var enter = layerLinks.enter() + .append('li') + .attr('class', 'layer') + .classed('layer-custom', function(d) { return d.id === 'custom'; }) + .classed('best', function(d) { return d.best(); }); - function clickSetOverlay(d) { - d3_event.preventDefault(); - context.background().toggleOverlayLayer(d); - selectLayer(); - document.activeElement.blur(); - } + enter.filter(function(d) { return d.id === 'custom'; }) + .append('button') + .attr('class', 'layer-browse') + .call(tooltip() + .title(t('background.custom_button')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', editCustom) + .call(svgIcon('#icon-search')); + enter.filter(function(d) { return d.best(); }) + .append('div') + .attr('class', 'best') + .call(tooltip() + .title(t('background.best_imagery')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .append('span') + .html('★'); - function drawListItems(layerList, type, change, filter) { - var sources = context.background() - .sources(context.map().extent()) - .filter(filter); - - var layerLinks = layerList.selectAll('li.layer') - .data(sources, function(d) { return d.name(); }); - - layerLinks.exit() - .remove(); - - var enter = layerLinks.enter() - .append('li') - .attr('class', 'layer') - .classed('layer-custom', function(d) { return d.id === 'custom'; }) - .classed('best', function(d) { return d.best(); }); - - enter.filter(function(d) { return d.id === 'custom'; }) - .append('button') - .attr('class', 'layer-browse') - .call(tooltip() - .title(t('background.custom_button')) - .placement((textDirection === 'rtl') ? 'right' : 'left')) - .on('click', editCustom) - .call(svgIcon('#icon-search')); - - enter.filter(function(d) { return d.best(); }) - .append('div') - .attr('class', 'best') - .call(tooltip() - .title(t('background.best_imagery')) - .placement((textDirection === 'rtl') ? 'right' : 'left')) - .append('span') - .html('★'); - - var label = enter - .append('label'); - - label - .append('input') - .attr('type', type) - .attr('name', 'layers') - .on('change', change); - - label - .append('span') - .text(function(d) { return d.name(); }); - - - layerList.selectAll('li.layer') - .sort(sortSources) - .style('display', layerList.selectAll('li.layer').data().length > 0 ? 'block' : 'none'); - } + var label = enter + .append('label'); + label + .append('input') + .attr('type', type) + .attr('name', 'layers') + .on('change', change); - function update() { - backgroundList - .call(drawListItems, 'radio', clickSetSource, function(d) { return !d.isHidden() && !d.overlay; }); + label + .append('span') + .text(function(d) { return d.name(); }); - overlayList - .call(drawListItems, 'checkbox', clickSetOverlay, function(d) { return !d.isHidden() && d.overlay; }); - selectLayer(); + layerList.selectAll('li.layer') + .sort(sortSources) + .style('display', layerList.selectAll('li.layer').data().length > 0 ? 'block' : 'none'); - offsetContainer - .call(backgroundOffset); - } + layerList + .call(updateLayerSelections); - function hide() { - setVisible(false); + function sortSources(a, b) { + return a.best() && !b.best() ? -1 + : b.best() && !a.best() ? 1 + : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0; } + } + + + function renderBackgroundList(selection) { + var container = selection.selectAll('layer-background-list') + .data([0]); + + _backgroundList = container.enter() + .append('ul') + .attr('class', 'layer-list layer-background-list') + .attr('dir', 'auto') + .merge(container); + } + + + function renderOverlayList(selection) { + var container = selection.selectAll('layer-overlay-list') + .data([0]); + + _overlayList = container.enter() + .append('ul') + .attr('class', 'layer-list layer-overlay-list') + .attr('dir', 'auto') + .merge(container); + } + + + function renderDisplayOptions(selection) { + var container = selection.selectAll('display-options-container') + .data([0]); + + var containerEnter = container.enter() + .append('div') + .attr('class', 'display-options-container controls-list'); + + /* add opacity switcher */ + var opacityDivEnter = containerEnter + .append('div') + .attr('class', 'opacity-options-wrapper'); + + opacityDivEnter + .append('h5') + .text(t('background.brightness')); + var opacityUlEnter = opacityDivEnter.append('ul') + .attr('class', 'opacity-options'); - function toggle() { - if (d3_event) { + opacityUlEnter.selectAll('div.opacity') + .data(opacities) + .enter() + .append('li') + .attr('data-original-title', function(d) { + return t('background.percent_brightness', { opacity: (d * 100) }); + }) + .on('click.set-opacity', setOpacity) + .html('
') + .call(tooltip() + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .append('div') + .attr('class', 'opacity') + .style('opacity', function(d) { return 1.25 - d; }); + + + /* add minimap toggle */ + var minimapEnter = containerEnter + .append('div') + .attr('class', 'minimap-toggle-wrap'); + + var minimapLabelEnter = minimapEnter + .append('label') + .call(tooltip() + .html(true) + .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) + .placement('top') + ); + + minimapLabelEnter + .classed('minimap-toggle', true) + .append('input') + .attr('type', 'checkbox') + .on('change', function() { + uiMapInMap.toggle(); d3_event.preventDefault(); - } - tooltipBehavior.hide(button); - setVisible(!button.classed('active')); + }); + + minimapLabelEnter + .append('span') + .text(t('background.minimap.description')); + + _displayOptions = containerEnter + .merge(container); + } + + + function update() { + _backgroundList + .call(drawListItems, 'radio', chooseBackground, function(d) { return !d.isHidden() && !d.overlay; }); + + _overlayList + .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; }); + + _offsetContainer + .call(backgroundOffset); + } + + + function quickSwitch() { + if (d3_event) { + d3_event.stopImmediatePropagation(); + d3_event.preventDefault(); } + if (_previousBackground) { + chooseBackground(_previousBackground); + } + } - function quickSwitch() { - if (d3_event) { - d3_event.stopImmediatePropagation(); - d3_event.preventDefault(); - } - if (previous) { - clickSetSource(previous); - } + function background(selection) { + + function hidePane() { + setVisible(false); } + function togglePane() { + if (d3_event) d3_event.preventDefault(); + paneTooltip.hide(button); + setVisible(!button.classed('active')); + } function setVisible(show) { - if (show !== shown) { + if (show !== _shown) { button.classed('active', show); - shown = show; + _shown = show; if (show) { selection @@ -252,18 +351,18 @@ export function uiBackground(context) { d3_event.stopPropagation(); }); - content + pane .style('display', 'block') .style('right', '-300px') .transition() .duration(200) .style('right', '0px'); - content.selectAll('.layer') + pane.selectAll('.layer') .call(setTooltips); } else { - content + pane .style('display', 'block') .style('right', '0px') .transition() @@ -280,11 +379,11 @@ export function uiBackground(context) { } - var content = selection + var pane = selection .append('div') .attr('class', 'fillL map-overlay col3 content hide'); - var tooltipBehavior = tooltip() + var paneTooltip = tooltip() .placement((textDirection === 'rtl') ? 'right' : 'left') .html(true) .title(uiTooltipHtml(t('background.description'), key)); @@ -292,50 +391,30 @@ export function uiBackground(context) { var button = selection .append('button') .attr('tabindex', -1) - .on('click', toggle) + .on('click', togglePane) .call(svgIcon('#icon-layers', 'light')) - .call(tooltipBehavior); + .call(paneTooltip); - var shown = false; - - - /* add opacity switcher */ - var opawrap = content - .append('div') - .attr('class', 'opacity-options-wrapper'); - - opawrap - .append('h4') + pane + .append('h3') .text(t('background.title')); - var opacityList = opawrap - .append('ul') - .attr('class', 'opacity-options'); - - opacityList.selectAll('div.opacity') - .data(opacities) - .enter() - .append('li') - .attr('data-original-title', function(d) { - return t('background.percent_brightness', { opacity: (d * 100) }); - }) - .on('click.set-opacity', setOpacity) - .html('
') - .call(tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left')) + // background list + pane .append('div') - .attr('class', 'opacity') - .style('opacity', function(d) { return 1.25 - d; }); - + .attr('class', 'background-background-list-container') + .call(uiDisclosure(context, 'background_list', true) + .title(t('background.backgrounds')) + .content(renderBackgroundList) + ); - /* add background list */ - var backgroundList = content - .append('ul') - .attr('class', 'layer-list') - .attr('dir', 'auto'); + // _backgroundList = pane + // .append('ul') + // .attr('class', 'layer-list') + // .attr('dir', 'auto'); // "Where does this imagery come from?" - // content + // pane // .append('div') // .attr('class', 'imagery-faq') // .append('a') @@ -347,46 +426,31 @@ export function uiBackground(context) { // .text(t('background.imagery_source_faq')); - /* add overlay list */ - var overlayList = content - .append('ul') - .attr('class', 'layer-list'); - - var controls = content + // overlay list + pane .append('div') - .attr('class', 'controls-list'); - - - /* add minimap toggle */ - var minimapLabel = controls - .append('label') - .call(tooltip() - .html(true) - .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) - .placement('top') + .attr('class', 'background-overlay-list-container') + .call(uiDisclosure(context, 'overlay_list', true) + .title(t('background.overlays')) + .content(renderOverlayList) ); - minimapLabel - .classed('minimap-toggle', true) - .append('input') - .attr('type', 'checkbox') - .on('change', function() { - uiMapInMap.toggle(); - d3_event.preventDefault(); - }); - - minimapLabel - .append('span') - .text(t('background.minimap.description')); - + // display settings + pane + .append('div') + .attr('class', 'background-display-options-container') + .call(uiDisclosure(context, 'background_display_options', true) + .title(t('background.display_options')) + .content(renderDisplayOptions) + ); - /* add offset controls */ - var offsetContainer = content + // offset controls + _offsetContainer = pane .append('div') .attr('class', 'background-offset'); - /* add listeners */ + // add listeners context.map() .on('move.background-update', _debounce(utilCallWhenIdle(update), 1000)); @@ -395,12 +459,12 @@ export function uiBackground(context) { update(); - setOpacity(opacityDefault); + setOpacity(_opacityDefault); var keybinding = d3_keybinding('background') - .on(key, toggle) + .on(key, togglePane) .on(uiCmd('⌘' + key), quickSwitch) - .on([t('map_data.key'), t('help.key')], hide); + .on([t('map_data.key'), t('help.key')], hidePane); d3_select(document) .call(keybinding); From 61bb9d968dd5c8fa5a986f827c502e7a3619cb25 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 28 Nov 2017 21:33:46 -0500 Subject: [PATCH 007/206] Restyle uiDisclosures, larger text, svg expand/contract icon --- css/80_app.css | 206 ++++++++++++++++++--------------------- modules/ui/disclosure.js | 38 +++++++- 2 files changed, 127 insertions(+), 117 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 2e8b957158..76cb459c31 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -268,7 +268,7 @@ table th { } table.tags, table.tags td, table.tags th { - border: 1px solid #CCC; + border: 1px solid #ccc; padding: 4px; } @@ -304,7 +304,7 @@ ul li { list-style: none;} display: block; height: 30px; background-color: white; - color: #7092FF; + color: #7092ff; cursor: pointer; } @@ -740,6 +740,30 @@ button.save.has-count .count::before { position: absolute; } + +/* Hide-Toggle +------------------------------------------------------- */ + +.hide-toggle .icon.pre-text { + vertical-align: text-top; + width: 16px; + height: 16px; + margin-left: -3px; +} +[dir='rtl'] .hide-toggle .icon.pre-text { + margin-left: 0; + margin-right: -3px; +} + +a:visited.hide-toggle, +a.hide-toggle { + display: inline-block; + font-size: 14px; + font-weight: bold; + padding-bottom: 5px; +} + + /* Inspector ------------------------------------------------------- */ @@ -782,7 +806,6 @@ button.save.has-count .count::before { bottom: 0; } - .feature-list-pane .inspector-body { top: 120px; } @@ -1071,7 +1094,7 @@ button.save.has-count .count::before { .preset-list-item button.tag-reference-button { height: 100%; - border: 1px solid #CCC; + border: 1px solid #ccc; border-radius: 0 3px 3px 0; position: absolute; top: 0; @@ -1143,7 +1166,7 @@ button.save.has-count .count::before { } .preset-editor a.hide-toggle { - margin: 0 20px 10px 20px; + margin: 0 20px 5px 20px; } .preset-editor .form-fields-container { @@ -1218,7 +1241,7 @@ button.save.has-count .count::before { } [dir='rtl'] .form-label button { border-left: none; - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; border-radius: 4px 0 0 0; width: 31px; } @@ -1574,13 +1597,13 @@ input[type=number] { float: left; height: 100%; width: 32px; - border-left: 1px solid #CCC; + border-left: 1px solid #ccc; border-radius: 0; background: rgba(0, 0, 0, 0); } [dir='rtl'] .spin-control button{ border-left: 0; - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } .spin-control button.decrement { @@ -1604,13 +1627,13 @@ input[type=number] { } .spin-control button.decrement::after { - border-top: 5px solid #CCC; + border-top: 5px solid #ccc; border-left: 5px solid transparent; border-right: 5px solid transparent; } .spin-control button.increment::after { - border-bottom: 5px solid #CCC; + border-bottom: 5px solid #ccc; border-left: 5px solid transparent; border-right: 5px solid transparent; } @@ -1622,7 +1645,7 @@ input[type=number] { display: block; background: white; padding: 5px 10px; - color: #7092FF; + color: #7092ff; } .checkselect label:hover { @@ -1720,7 +1743,7 @@ input[type=number] { right: 1px; width: 32px; margin-left: -32px; - border: 1px solid #CCC; + border: 1px solid #ccc; border-top-width: 0; border-right-width: 0; border-radius: 0 0 4px 0; @@ -1916,12 +1939,12 @@ div.combobox { height: 31px; border: 0; border-radius: 0; - border-bottom: 1px solid #CCC; - border-left: 1px solid #CCC; + border-bottom: 1px solid #ccc; + border-left: 1px solid #ccc; } [dir='rtl'] .tag-row input { border-left: none; - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } .tag-row .key-wrap, @@ -1941,14 +1964,14 @@ div.combobox { } .tag-row input.value { - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } [dir='rtl'] .tag-row input.value { - border-left: 1px solid #CCC; + border-left: 1px solid #ccc; } .tag-row:first-child input.key { - border-top: 1px solid #CCC; + border-top: 1px solid #ccc; border-top-left-radius: 4px; } [dir='rtl'] .tag-row:first-child input.key { @@ -1957,14 +1980,14 @@ div.combobox { } .tag-row:first-child input.value { - border-top: 1px solid #CCC; + border-top: 1px solid #ccc; } .tag-row button { position: absolute; height: 31px; right: 10%; - border: 1px solid #CCC; + border: 1px solid #ccc; border-top-width: 0; border-left-width: 0; } @@ -2226,14 +2249,9 @@ div.full-screen > button:hover { margin-bottom: 10px; } -.map-data-control .hide-toggle, -.background-control .hide-toggle { - padding-bottom: 10px; -} - .layer-list, .controls-list { margin-bottom: 10px; - border: 1px solid #CCC; + border: 1px solid #ccc; border-radius: 4px; } @@ -2241,7 +2259,7 @@ div.full-screen > button:hover { position: relative; height: 30px; background-color: white; - color: #7092FF; + color: #7092ff; } .layer-list:empty { @@ -2270,7 +2288,7 @@ div.full-screen > button:hover { .layer-list li.active, .layer-list li.switch { - background: #E8EBFF; + background: #e8ebff; } .layer-list li.best > div.best { @@ -2306,58 +2324,18 @@ div.full-screen > button:hover { display: block; padding: 5px 10px; cursor: pointer; - color: #7092FF; + color: #7092ff; border-top: 1px solid #ccc; } .minimap-toggle.active { - background: #E8EBFF; + background: #e8ebff; } .minimap-toggle:hover { background-color: #ececec; } -.hide-toggle { - display: block; - padding-left: 12px; - position: relative; -} -[dir='rtl'] .hide-toggle { - padding-left: 0; - padding-right: 12px; -} - -.hide-toggle:before { - content: ''; - display: block; - position: absolute; - height: 0; - width: 0; - left: 0; - top: 5px; - border-top: 4px solid transparent; - border-bottom: 4px solid transparent; - border-left: 8px solid #7092ff; -} -[dir='rtl'] .hide-toggle:before { - left: auto; - right: 0; - border-left: none; - border-right: 8px solid #7092ff; -} - -.hide-toggle.expanded:before { - border-top: 8px solid #7092ff; - border-bottom: 0; - border-right: 4px solid transparent; - border-left: 4px solid transparent; -} -[dir='rtl'] .hide-toggle.expanded:before { - border-left: 4px solid transparent; - border-right: 4px solid transparent; -} - /* Adjust Alignment controls */ @@ -2411,7 +2389,7 @@ div.full-screen > button:hover { } .nudge-container input.error { - border: 1px solid #FF7878; + border: 1px solid #ff7878; border-radius: 2px; background: #ffb; } @@ -2523,10 +2501,10 @@ div.full-screen > button:hover { } .background-control .opacity { - background:#222; - display:inline-block; - width:20px; - height:18px; + background: #222; + display: inline-block; + width: 20px; + height: 18px; } .map-data-control .layer-list button, @@ -2534,14 +2512,14 @@ div.full-screen > button:hover { float: right; height: 100%; width: 10%; - border-left: 1px solid #CCC; + border-left: 1px solid #ccc; border-radius: 0; } [dir='rtl'] .map-data-control .layer-list button, [dir='rtl'] .background-control .layer-list button { float: left; border-left: none; - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } .map-data-control .layer-list button .icon, @@ -2574,12 +2552,12 @@ div.full-screen > button:hover { border-radius: 0 0 0 4px; } [dir='rtl'] .geolocate-control button { - border-radius: 0 0 4px 0; + border-radius: 0 0 4px 0; } .map-overlay.content { position: fixed; - top:60px; + top: 60px; bottom: 30px; padding: 20px 50px 20px 20px; right: 0; @@ -2591,13 +2569,17 @@ div.full-screen > button:hover { right: auto !important; } +.map-overlay.content > div { + padding-bottom: 15px; +} + /* Help */ .help-control button { border-radius: 0 0 0 4px; } [dir='rtl'] .help-control button { - border-radius: 0 0 4px 0; + border-radius: 0 0 4px 0; } .help-wrap p { @@ -2626,7 +2608,7 @@ div.full-screen > button:hover { } .help-wrap .toc { - width:40%; + width: 40%; float:right; margin-left: 20px; margin-bottom: 20px; @@ -2703,12 +2685,12 @@ div.full-screen > button:hover { ------------------------------------------------------- */ img.tile { - position:absolute; - transform-origin:0 0; - -ms-transform-origin:0 0; - -webkit-transform-origin:0 0; - -moz-transform-origin:0 0; - -o-transform-origin:0 0; + position: absolute; + transform-origin: 0 0; + -ms-transform-origin: 0 0; + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; -moz-user-select: none; -webkit-user-select: none; @@ -2734,11 +2716,11 @@ img.tile { margin-left: -50px; margin-top: -20px; - transform-origin:0 0; - -ms-transform-origin:0 0; - -webkit-transform-origin:0 0; - -moz-transform-origin:0 0; - -o-transform-origin:0 0; + transform-origin: 0 0; + -ms-transform-origin: 0 0; + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; -moz-user-select: none; -webkit-user-select: none; @@ -2763,10 +2745,10 @@ img.tile-removing { ------------------------------------------------------- */ #map { - position:relative; - overflow:hidden; - height:100%; - background:#000; + position: relative; + overflow: hidden; + height: 100%; + background: #000; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; @@ -2774,11 +2756,11 @@ img.tile-removing { } #supersurface { - transform-origin:0 0; - -ms-transform-origin:0 0; - -webkit-transform-origin:0 0; - -moz-transform-origin:0 0; - -o-transform-origin:0 0; + transform-origin: 0 0; + -ms-transform-origin: 0 0; + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; } #supersurface, .layer { @@ -3307,7 +3289,7 @@ img.tile-removing { .modal-section { padding: 20px; - border-bottom: 1px solid #CCC; + border-bottom: 1px solid #ccc; } .modal-section.header h3 { @@ -3340,8 +3322,8 @@ img.tile-removing { .modal-actions button, .save-success a.button { font-weight: normal; - color: #7092FF; - border-bottom: 1px solid #CCC; + color: #7092ff; + border-bottom: 1px solid #ccc; border-radius: 0; height: 160px; text-align: center; @@ -3361,7 +3343,7 @@ img.tile-removing { } .modal-actions > :first-child { - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } .modal-section:last-child { @@ -3371,7 +3353,7 @@ img.tile-removing { /* Restore Modal ------------------------------------------------------- */ .modal-actions .logo-restore { - color: #7092FF; + color: #7092ff; } .modal-actions .logo-reset { color: #E06C5E; @@ -3389,7 +3371,7 @@ img.tile-removing { padding-top: 15px; } .save-success .logo-osm { - color: #7092FF; + color: #7092ff; margin-bottom: 10px; } .save-success a.button.social { @@ -3399,14 +3381,14 @@ img.tile-removing { .save-success .icon.social { height: 80px; width: 80px; - color: #7092FF; + color: #7092ff; } /* Splash Modal ------------------------------------------------------- */ .modal-actions .logo-walkthrough, .modal-actions .logo-features { - color: #7092FF; + color: #7092ff; } @@ -3597,7 +3579,7 @@ svg.mouseclick use.right { } .mode-save .commit-section .changeset-list button { - border-left: 1px solid #CCC; + border-left: 1px solid #ccc; } .changeset-list li span.count:before { content: '('; } @@ -4154,7 +4136,7 @@ li.hide + li.version .badge .tooltip .tooltip-arrow { .curtain-tooltip .tooltip-inner .instruction { font-weight: bold; display: block; - border-top: 1px solid #CCC; + border-top: 1px solid #ccc; margin-top: 10px; margin-left: -20px; margin-right: -20px; @@ -4232,5 +4214,5 @@ li.hide + li.version .badge .tooltip .tooltip-arrow { .huge-modal-button .illustration { height: 100px; width: 100px; - color: #7092FF; + color: #7092ff; } diff --git a/modules/ui/disclosure.js b/modules/ui/disclosure.js index 3b4c3a0a4d..6c56e37c6a 100644 --- a/modules/ui/disclosure.js +++ b/modules/ui/disclosure.js @@ -1,8 +1,10 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; import { event as d3_event } from 'd3-selection'; +import { svgIcon } from '../svg'; import { utilRebind } from '../util/rebind'; import { uiToggle } from './toggle'; +import { textDirection } from '../util/locale'; export function uiDisclosure(context, key, expandedDefault) { @@ -15,20 +17,36 @@ export function uiDisclosure(context, key, expandedDefault) { var disclosure = function(selection) { - var hideToggle = selection.selectAll('.hide-toggle') + var hideToggle = selection.selectAll('.hide-toggle-' + key) .data([0]); - hideToggle = hideToggle.enter() + // enter + var hideToggleEnter = hideToggle.enter() .append('a') .attr('href', '#') .attr('class', 'hide-toggle hide-toggle-' + key) + .call(svgIcon('', 'pre-text', 'hide-toggle-icon')); + + hideToggleEnter + .append('span') + .attr('class', 'hide-toggle-text'); + + // update + hideToggle = hideToggleEnter .merge(hideToggle); hideToggle - .text(_title) .on('click', toggle) .classed('expanded', _expanded); + hideToggle.selectAll('.hide-toggle-text') + .text(_title); + + hideToggle.selectAll('.hide-toggle-icon') + .attr('xlink:href', _expanded ? '#icon-down' + : (textDirection === 'rtl') ? '#icon-backward' : '#icon-forward' + ); + var wrap = selection.selectAll('.disclosure-wrap') .data([0]); @@ -47,11 +65,21 @@ export function uiDisclosure(context, key, expandedDefault) { d3_event.preventDefault(); _expanded = !_expanded; + if (_updatePreference) { context.storage('disclosure.' + key + '.expanded', _expanded); } - hideToggle.classed('expanded', _expanded); - wrap.call(uiToggle(_expanded)); + + hideToggle + .classed('expanded', _expanded); + + hideToggle.selectAll('.hide-toggle-icon') + .attr('xlink:href', _expanded ? '#icon-down' + : (textDirection === 'rtl') ? '#icon-backward' : '#icon-forward' + ); + + wrap + .call(uiToggle(_expanded)); dispatch.call('toggled', this, _expanded); } From 2e2dd5f02564183a8b2661fa657287789fc9c1c2 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 28 Nov 2017 22:49:46 -0500 Subject: [PATCH 008/206] Larger headings on Map Data and Background panes --- modules/ui/background.js | 2 +- modules/ui/help.js | 25 +++++++++++++++---------- modules/ui/map_data.js | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/modules/ui/background.js b/modules/ui/background.js index d23287a7fc..4b0dbfaf76 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -396,7 +396,7 @@ export function uiBackground(context) { .call(paneTooltip); pane - .append('h3') + .append('h2') .text(t('background.title')); // background list diff --git a/modules/ui/help.js b/modules/ui/help.js index 6b1e6b435a..024515f77a 100644 --- a/modules/ui/help.js +++ b/modules/ui/help.js @@ -260,12 +260,12 @@ export function uiHelp(context) { function help(selection) { - function hide() { + function hidePane() { setVisible(false); } - function toggle() { + function togglePane() { if (d3_event) d3_event.preventDefault(); tooltipBehavior.hide(button); setVisible(!button.classed('active')); @@ -375,13 +375,14 @@ export function uiHelp(context) { .title(uiTooltipHtml(t('help.title'), key)), button = selection.append('button') .attr('tabindex', -1) - .on('click', toggle) + .on('click', togglePane) .call(svgIcon('#icon-help', 'light')) .call(tooltipBehavior), shown = false; - var toc = pane.append('ul') + var toc = pane + .append('ul') .attr('class', 'toc'); var menuItems = toc.selectAll('li') @@ -424,23 +425,27 @@ export function uiHelp(context) { .text(t('splash.walkthrough')); - var content = pane.append('div') + var content = pane + .append('div') .attr('class', 'left-content'); - var doctitle = content.append('h2') + var doctitle = content + .append('h2') .text(t('help.title')); - var body = content.append('div') + var body = content + .append('div') .attr('class', 'body'); - var nav = content.append('div') + var nav = content + .append('div') .attr('class', 'nav'); clickHelp(docs[0], 0); var keybinding = d3_keybinding('help') - .on(key, toggle) - .on([t('background.key'), t('map_data.key')], hide); + .on(key, togglePane) + .on([t('background.key'), t('map_data.key')], hidePane); d3_select(document) .call(keybinding); diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index a928bd6495..2cff0c842c 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -455,7 +455,7 @@ export function uiMapData(context) { pane - .append('h3') + .append('h2') .text(t('map_data.title')); From 17809545c6b29516286d11a381a31d0ea982f25b Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 28 Nov 2017 23:21:52 -0500 Subject: [PATCH 009/206] Ensure only one pane shown at a time, remove unnecessary handlers --- modules/ui/background.js | 19 +++++++++---------- modules/ui/help.js | 14 ++++++++++---- modules/ui/map_data.js | 15 ++++++++------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/modules/ui/background.js b/modules/ui/background.js index 4b0dbfaf76..680775eca0 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -17,6 +17,8 @@ import { svgIcon } from '../svg'; import { uiBackgroundOffset } from './background_offset'; import { uiCmd } from './cmd'; import { uiDisclosure } from './disclosure'; +import { uiHelp } from './help'; +import { uiMapData } from './map_data'; import { uiMapInMap } from './map_in_map'; import { uiTooltipHtml } from './tooltipHtml'; import { utilDetect } from '../util/detect'; @@ -346,10 +348,9 @@ export function uiBackground(context) { _shown = show; if (show) { - selection - .on('mousedown.background-inside', function() { - d3_event.stopPropagation(); - }); + uiMapData.hidePane(); + uiHelp.hidePane(); + update(); pane .style('display', 'block') @@ -358,9 +359,6 @@ export function uiBackground(context) { .duration(200) .style('right', '0px'); - pane.selectAll('.layer') - .call(setTooltips); - } else { pane .style('display', 'block') @@ -371,9 +369,6 @@ export function uiBackground(context) { .on('end', function() { d3_select(this).style('display', 'none'); }); - - selection - .on('mousedown.background-inside', null); } } } @@ -468,6 +463,10 @@ export function uiBackground(context) { d3_select(document) .call(keybinding); + + uiBackground.hidePane = hidePane; + uiBackground.togglePane = togglePane; + uiBackground.setVisible = setVisible; } return background; diff --git a/modules/ui/help.js b/modules/ui/help.js index 024515f77a..01196b3a75 100644 --- a/modules/ui/help.js +++ b/modules/ui/help.js @@ -9,7 +9,9 @@ import marked from 'marked'; import { t, textDirection } from '../util/locale'; import { svgIcon } from '../svg'; import { uiCmd } from './cmd'; +import { uiBackground } from './background'; import { uiIntro } from './intro'; +import { uiMapData } from './map_data'; import { uiShortcuts } from './shortcuts'; import { uiTooltipHtml } from './tooltipHtml'; import { tooltip } from '../util/tooltip'; @@ -278,14 +280,15 @@ export function uiHelp(context) { shown = show; if (show) { - selection.on('mousedown.help-inside', function() { - return d3_event.stopPropagation(); - }); + uiBackground.hidePane(); + uiMapData.hidePane(); + pane.style('display', 'block') .style('right', '-500px') .transition() .duration(200) .style('right', '0px'); + } else { pane.style('right', '0px') .transition() @@ -294,7 +297,6 @@ export function uiHelp(context) { .on('end', function() { d3_select(this).style('display', 'none'); }); - selection.on('mousedown.help-inside', null); } } } @@ -449,6 +451,10 @@ export function uiHelp(context) { d3_select(document) .call(keybinding); + + uiHelp.hidePane = hidePane; + uiHelp.togglePane = togglePane; + uiHelp.setVisible = setVisible; } return help; diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index 2cff0c842c..aef91c01b3 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -8,7 +8,9 @@ import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { svgIcon } from '../svg'; import { t, textDirection } from '../util/locale'; import { tooltip } from '../util/tooltip'; +import { uiBackground } from './background'; import { uiDisclosure } from './disclosure'; +import { uiHelp } from './help'; import { uiTooltipHtml } from './tooltipHtml'; @@ -26,7 +28,6 @@ export function uiMapData(context) { var _featureList = d3_select(null); - function showsFeature(d) { return context.features().enabled(d); } @@ -407,12 +408,10 @@ export function uiMapData(context) { _shown = show; if (show) { + uiBackground.hidePane(); + uiHelp.hidePane(); update(); - selection.on('mousedown.map_data-inside', function() { - return d3_event.stopPropagation(); - }); - pane .style('display', 'block') .style('right', '-300px') @@ -430,8 +429,6 @@ export function uiMapData(context) { .on('end', function() { d3_select(this).style('display', 'none'); }); - - selection.on('mousedown.map_data-inside', null); } } } @@ -501,6 +498,10 @@ export function uiMapData(context) { d3_select(document) .call(keybinding); + + uiMapData.hidePane = hidePane; + uiMapData.togglePane = togglePane; + uiMapData.setVisible = setVisible; } return mapData; From d24e2663f2a2e6d264feb914ac8b9b28a46c3971 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 29 Nov 2017 16:49:58 -0500 Subject: [PATCH 010/206] Replace confusing opacity buttons with opacity slider --- css/80_app.css | 40 +------------ data/core.yaml | 1 - dist/img/background-pattern-1.png | Bin 89 -> 0 bytes dist/img/background-pattern-opacity.png | Bin 90 -> 0 bytes dist/locales/en.json | 1 - modules/ui/background.js | 71 +++++++++++------------- 6 files changed, 33 insertions(+), 80 deletions(-) delete mode 100644 dist/img/background-pattern-1.png delete mode 100644 dist/img/background-pattern-opacity.png diff --git a/css/80_app.css b/css/80_app.css index 76cb459c31..9c8045f4b4 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -130,7 +130,6 @@ a, button, input, textarea { a, button, .checkselect label:hover, -.opacity-options li, .radial-menu-item { cursor: pointer; } @@ -2468,43 +2467,8 @@ div.full-screen > button:hover { padding: 10px; } -.opacity-options { - background: url(img/background-pattern-opacity.png) 0 0 repeat; - height: 20px; - width: 82px; - border: 1px solid #ccc; -} - -.opacity-options li { - height: 100%; - display: block; - float: left; -} - -.opacity-options li .select-box{ - position: absolute; - width: 20px; - height: 18px; - z-index: 9999; -} - -.background-control li:hover .select-box, -.background-control li.selected .select-box { - border: 2px solid #7092ff; - background: rgba(89, 123, 231, .5); - opacity: .5; -} - -.background-control li.selected:hover .select-box, -.background-control li.selected .select-box { - opacity: 1; -} - -.background-control .opacity { - background: #222; - display: inline-block; - width: 20px; - height: 18px; +.opacity-options-wrapper .opacity-value { + margin: 5px; } .map-data-control .layer-list button, diff --git a/data/core.yaml b/data/core.yaml index 1344b733a7..3404c64548 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -384,7 +384,6 @@ en: reset: reset display_options: Display Options brightness: Brightness - percent_brightness: "{opacity}% brightness" minimap: description: Show Minimap tooltip: Show a zoomed out map to help locate the area currently displayed. diff --git a/dist/img/background-pattern-1.png b/dist/img/background-pattern-1.png deleted file mode 100644 index d2f2bcb125924fa13e23a9e4d657ef10198a6a5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1|-8Yw(bW~3Z5>GAr-fBk8k8bP0l+XkKz}Xtc diff --git a/dist/img/background-pattern-opacity.png b/dist/img/background-pattern-opacity.png deleted file mode 100644 index f24376283025091b6fbe5b8e64b2dd4a38d470d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^Y#_`5A|IT2?*XI~JzX3_DsIi~G6X6UX!`a4N&oJb l2X-EC)Nzo{=MbCQR-mw') - .call(tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - ) - .append('div') - .attr('class', 'opacity') - .style('opacity', function(d) { return 1.25 - d; }); - + .text(t('background.brightness')) + .append('span') + .attr('class', 'opacity-value') + .text(Math.floor(_opacity * 100) + '%'); - /* add minimap toggle */ + opacityDivEnter + .append('input') + .attr('class', 'opacity-input') + .attr('type', 'range') + .attr('min', '0.25') + .attr('max', '1') + .attr('step', '0.05') + .property('value', _opacity) + .on('input.set-opacity', setOpacity); + + // add minimap toggle var minimapEnter = containerEnter .append('div') .attr('class', 'minimap-toggle-wrap'); @@ -403,11 +399,6 @@ export function uiBackground(context) { .content(renderBackgroundList) ); - // _backgroundList = pane - // .append('ul') - // .attr('class', 'layer-list') - // .attr('dir', 'auto'); - // "Where does this imagery come from?" // pane // .append('div') @@ -454,7 +445,7 @@ export function uiBackground(context) { update(); - setOpacity(_opacityDefault); + setOpacity(_opacity); var keybinding = d3_keybinding('background') .on(key, togglePane) From 2946774e60ed0cff9afefc89707fd916bb7c117d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 30 Nov 2017 01:56:23 -0500 Subject: [PATCH 011/206] WIP: Add unsharp-mask filter layer to sharpen background imagery --- modules/renderer/background.js | 118 ++++++++++++++++++++++++++------- modules/ui/background.js | 12 ++-- 2 files changed, 101 insertions(+), 29 deletions(-) diff --git a/modules/renderer/background.js b/modules/renderer/background.js index d1d59042e4..8fd9720be6 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -12,24 +12,79 @@ import { utilRebind } from '../util/rebind'; export function rendererBackground(context) { - var dispatch = d3_dispatch('change'), - baseLayer = rendererTileLayer(context).projection(context.projection), - overlayLayers = [], - backgroundSources; + var dispatch = d3_dispatch('change'); + var baseLayer = rendererTileLayer(context).projection(context.projection); + var _overlayLayers = []; + var _backgroundSources = []; + var _brightness = 1; + var _sharpness = 1; function background(selection) { + var baseFilter = ''; + if (_brightness !== 1) { + baseFilter += 'brightness(' + _brightness + ')'; + } + if (_sharpness !== 1) { + baseFilter += 'contrast(1.25)';// + _sharpness + ')'; + } + var base = selection.selectAll('.layer-background') .data([0]); - base.enter() + base = base.enter() .insert('div', '.layer-data') .attr('class', 'layer layer-background') .merge(base) + .style('-webkit-filter', baseFilter || null) + .style('filter', baseFilter || null); + + + var imagery = base.selectAll('.layer-imagery') + .data([0]); + + imagery.enter() + .append('div') + .attr('class', 'layer layer-imagery') + .merge(imagery) .call(baseLayer); + + var maskFilter = ''; + var mixBlendMode = ''; + if (_sharpness !== 1) { + var blur = Math.abs(_sharpness - 1) * 10; + maskFilter += 'blur(' + blur + 'px)'; + + if (_sharpness > 1) { + maskFilter += 'invert(1)'; + mixBlendMode = 'overlay'; + } else { + mixBlendMode = 'normal'; + } + + // var contrast = 1 / (_sharpness || 0.1); + maskFilter += 'contrast(0.75)';// + contrast + ')'; + } + + var mask = base.selectAll('.layer-unsharp-mask') + .data(_sharpness !== 1 ? [0] : []); + + mask.exit() + .remove(); + + mask.enter() + .append('div') + .attr('class', 'layer layer-unsharp-mask') + .merge(mask) + .call(baseLayer) + .style('-webkit-filter', maskFilter || null) + .style('filter', maskFilter || null) + .style('mix-blend-mode', mixBlendMode || null); + + var overlays = selection.selectAll('.layer-overlay') - .data(overlayLayers, function(d) { return d.source().name(); }); + .data(_overlayLayers, function(d) { return d.source().name(); }); overlays.exit() .remove(); @@ -46,7 +101,7 @@ export function rendererBackground(context) { if (context.inIntro()) return; var b = background.baseLayerSource(), - o = overlayLayers + o = _overlayLayers .filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); }) .map(function (d) { return d.source().id; }) .join(','), @@ -85,7 +140,7 @@ export function rendererBackground(context) { var imageryUsed = [b.imageryUsed()]; - overlayLayers + _overlayLayers .filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); }) .forEach(function (d) { imageryUsed.push(d.source().imageryUsed()); }); @@ -117,7 +172,7 @@ export function rendererBackground(context) { background.sources = function(extent) { - return backgroundSources.filter(function(source) { + return _backgroundSources.filter(function(source) { return source.intersects(extent); }); }; @@ -127,7 +182,7 @@ export function rendererBackground(context) { if (!_) return; baseLayer.dimensions(_); - overlayLayers.forEach(function(layer) { + _overlayLayers.forEach(function(layer) { layer.dimensions(_); }); }; @@ -172,7 +227,7 @@ export function rendererBackground(context) { background.findSource = function(id) { - return _find(backgroundSources, function(d) { + return _find(_backgroundSources, function(d) { return d.id && d.id === id; }); }; @@ -185,22 +240,22 @@ export function rendererBackground(context) { background.showsLayer = function(d) { return d.id === baseLayer.source().id || - overlayLayers.some(function(layer) { return d.id === layer.source().id; }); + _overlayLayers.some(function(layer) { return d.id === layer.source().id; }); }; background.overlayLayerSources = function() { - return overlayLayers.map(function (l) { return l.source(); }); + return _overlayLayers.map(function (l) { return l.source(); }); }; background.toggleOverlayLayer = function(d) { var layer; - for (var i = 0; i < overlayLayers.length; i++) { - layer = overlayLayers[i]; + for (var i = 0; i < _overlayLayers.length; i++) { + layer = _overlayLayers[i]; if (layer.source() === d) { - overlayLayers.splice(i, 1); + _overlayLayers.splice(i, 1); dispatch.call('change'); background.updateImagery(); return; @@ -210,9 +265,10 @@ export function rendererBackground(context) { layer = rendererTileLayer(context) .source(d) .projection(context.projection) - .dimensions(baseLayer.dimensions()); + .dimensions(baseLayer.dimensions() + ); - overlayLayers.push(layer); + _overlayLayers.push(layer); dispatch.call('change'); background.updateImagery(); }; @@ -235,6 +291,22 @@ export function rendererBackground(context) { }; + background.brightness = function(d) { + if (!arguments.length) return _brightness; + _brightness = d; + if (context.mode()) dispatch.call('change'); + return background; + }; + + + background.sharpness = function(d) { + if (!arguments.length) return _sharpness; + _sharpness = d; + if (context.mode()) dispatch.call('change'); + return background; + }; + + background.init = function() { function parseMap(qmap) { if (!qmap) return false; @@ -251,7 +323,7 @@ export function rendererBackground(context) { best; // Add all the available imagery sources - backgroundSources = dataImagery.map(function(source) { + _backgroundSources = dataImagery.map(function(source) { if (source.type === 'bing') { return rendererBackgroundSource.Bing(source, dispatch); } else if (source.id === 'EsriWorldImagery') { @@ -261,15 +333,15 @@ export function rendererBackground(context) { } }); - first = backgroundSources.length && backgroundSources[0]; + first = _backgroundSources.length && _backgroundSources[0]; // Add 'None' - backgroundSources.unshift(rendererBackgroundSource.None()); + _backgroundSources.unshift(rendererBackgroundSource.None()); // Add 'Custom' var template = context.storage('background-custom-template') || ''; var custom = rendererBackgroundSource.Custom(template); - backgroundSources.unshift(custom); + _backgroundSources.unshift(custom); // Decide which background layer to display @@ -290,7 +362,7 @@ export function rendererBackground(context) { ); } - var locator = _find(backgroundSources, function(d) { + var locator = _find(_backgroundSources, function(d) { return d.overlay && d.default; }); diff --git a/modules/ui/background.js b/modules/ui/background.js index e2ecfe678e..3777975d60 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -51,12 +51,12 @@ export function uiBackground(context) { // Can be 0 from <1.3.0 use or due to issue #1923. if (!d) d = 1.0; - var bg = context.container().selectAll('.layer-background') - .style('opacity', d); - - if (!detected.opera) { - utilSetTransform(bg, 0, 0); - } + // var bg = context.container().selectAll('.layer-background') + // .style('opacity', d); + // if (!detected.opera) { + // utilSetTransform(bg, 0, 0); + // } + context.background().brightness(d); _displayOptions.selectAll('.opacity-input') .property('value', d); From b1efcf83b29b9489d4bc8adf7641b6647745dee3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 30 Nov 2017 08:50:32 -0500 Subject: [PATCH 012/206] Add Sharpness slider --- css/80_app.css | 4 +-- data/core.yaml | 1 + dist/locales/en.json | 1 + modules/ui/background.js | 75 +++++++++++++++++++++++++++++----------- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 9c8045f4b4..0435e46c07 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2463,11 +2463,11 @@ div.full-screen > button:hover { border-top: 5px solid #222; } -.opacity-options-wrapper { +.display-controls-wrapper { padding: 10px; } -.opacity-options-wrapper .opacity-value { +.display-controls-wrapper h5 span { margin: 5px; } diff --git a/data/core.yaml b/data/core.yaml index 3404c64548..1cd24fb735 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -384,6 +384,7 @@ en: reset: reset display_options: Display Options brightness: Brightness + sharpness: Sharpness minimap: description: Show Minimap tooltip: Show a zoomed out map to help locate the area currently displayed. diff --git a/dist/locales/en.json b/dist/locales/en.json index 9d1872d602..e41ba48493 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -472,6 +472,7 @@ "reset": "reset", "display_options": "Display Options", "brightness": "Brightness", + "sharpness": "Sharpness", "minimap": { "description": "Show Minimap", "tooltip": "Show a zoomed out map to help locate the area currently displayed.", diff --git a/modules/ui/background.js b/modules/ui/background.js index 3777975d60..0b4251c88b 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -30,8 +30,9 @@ export function uiBackground(context) { var key = t('background.key'); var detected = utilDetect(); - var _opacity = (context.storage('background-opacity') !== null) ? + var _brightness = (context.storage('background-opacity') !== null) ? (+context.storage('background-opacity')) : 1.0; + var _sharpness = 1; var _customSource = context.background().findSource('custom'); var _previousBackground; var _shown = false; @@ -43,19 +44,15 @@ export function uiBackground(context) { var backgroundOffset = uiBackgroundOffset(context); + function clamp(x, min, max) { return Math.max(min, Math.min(x, max)); } - function setOpacity(d) { + + function setBrightness(d) { if (!d && d3_event && d3_event.target) { d = d3_event.target.value; } - // Can be 0 from <1.3.0 use or due to issue #1923. - if (!d) d = 1.0; - - // var bg = context.container().selectAll('.layer-background') - // .style('opacity', d); - // if (!detected.opera) { - // utilSetTransform(bg, 0, 0); - // } + + d = clamp(d, 0.25, 2); context.background().brightness(d); _displayOptions.selectAll('.opacity-input') @@ -65,7 +62,25 @@ export function uiBackground(context) { .text(Math.floor(d * 100) + '%'); context.storage('background-opacity', d); - _opacity = d; + _brightness = d; + } + + + function setSharpness(d) { + if (!d && d3_event && d3_event.target) { + d = d3_event.target.value; + } + + d = clamp(d, 0.5, 2); + context.background().sharpness(d); + + _displayOptions.selectAll('.sharpness-input') + .property('value', d); + + _displayOptions.selectAll('.sharpness-value') + .text(Math.floor(d * 100) + '%'); + + _sharpness = d; } @@ -250,27 +265,45 @@ export function uiBackground(context) { .append('div') .attr('class', 'display-options-container controls-list'); - // add opacity switcher - var opacityDivEnter = containerEnter + // brightness + var controlsEnter = containerEnter .append('div') - .attr('class', 'opacity-options-wrapper'); + .attr('class', 'display-controls-wrapper'); - opacityDivEnter + controlsEnter .append('h5') .text(t('background.brightness')) .append('span') .attr('class', 'opacity-value') - .text(Math.floor(_opacity * 100) + '%'); + .text(Math.floor(_brightness * 100) + '%'); - opacityDivEnter + controlsEnter .append('input') .attr('class', 'opacity-input') .attr('type', 'range') .attr('min', '0.25') - .attr('max', '1') + .attr('max', '2') + .attr('step', '0.05') + .property('value', _brightness) + .on('input.set-brightness', setBrightness); + + // sharpness + controlsEnter + .append('h5') + .text(t('background.sharpness')) + .append('span') + .attr('class', 'sharpness-value') + .text(Math.floor(_brightness * 100) + '%'); + + controlsEnter + .append('input') + .attr('class', 'sharpness-input') + .attr('type', 'range') + .attr('min', '0.5') + .attr('max', '2') .attr('step', '0.05') - .property('value', _opacity) - .on('input.set-opacity', setOpacity); + .property('value', _sharpness) + .on('input.set-sharpness', setSharpness); // add minimap toggle var minimapEnter = containerEnter @@ -445,7 +478,7 @@ export function uiBackground(context) { update(); - setOpacity(_opacity); + setBrightness(_brightness); var keybinding = d3_keybinding('background') .on(key, togglePane) From 53225f08ec7a7287646cecf955054e6d992f5df3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 30 Nov 2017 16:19:06 -0500 Subject: [PATCH 013/206] more tweaks to unsharp mask --- css/80_app.css | 18 ++++++++++-------- modules/renderer/background.js | 33 ++++++++++++++++++--------------- modules/ui/background.js | 5 +++-- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 0435e46c07..743d698bba 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2662,7 +2662,17 @@ img.tile { user-select: none; opacity: 0; +} + +img.tile-loaded { + opacity: 1; +} + +img.tile-removing { + opacity: 0; +} +div:not(.layer-mask) > img.tile { -webkit-transition: opacity 200ms linear; transition: opacity 200ms linear; -moz-transition: opacity 200ms linear; @@ -2696,14 +2706,6 @@ img.tile-debug { outline: 1px solid red; } -img.tile-loaded { - opacity: 1; -} - -img.tile-removing { - opacity: 0; -} - /* Map ------------------------------------------------------- */ diff --git a/modules/renderer/background.js b/modules/renderer/background.js index 8fd9720be6..bf3940e849 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -1,6 +1,7 @@ import _find from 'lodash-es/find'; import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate'; import { select as d3_select } from 'd3-selection'; import { data } from '../../data'; @@ -22,11 +23,17 @@ export function rendererBackground(context) { function background(selection) { var baseFilter = ''; + var blur, contrast; + if (_brightness !== 1) { baseFilter += 'brightness(' + _brightness + ')'; } - if (_sharpness !== 1) { - baseFilter += 'contrast(1.25)';// + _sharpness + ')'; + if (_sharpness < 1) { // gaussian blur + blur = d3_interpolateNumber(0.5, 5)(1 - _sharpness); + baseFilter += 'blur(' + blur + 'px)'; + } else if (_sharpness > 1) { + contrast = d3_interpolateNumber(1, 1.5)(_sharpness - 1); + baseFilter += 'contrast(' + contrast + ')'; } var base = selection.selectAll('.layer-background') @@ -52,30 +59,26 @@ export function rendererBackground(context) { var maskFilter = ''; var mixBlendMode = ''; - if (_sharpness !== 1) { - var blur = Math.abs(_sharpness - 1) * 10; - maskFilter += 'blur(' + blur + 'px)'; + if (_sharpness > 1) { // apply unsharp mask + mixBlendMode = 'overlay'; + maskFilter = 'saturate(0)'; - if (_sharpness > 1) { - maskFilter += 'invert(1)'; - mixBlendMode = 'overlay'; - } else { - mixBlendMode = 'normal'; - } + blur = d3_interpolateNumber(3, 0.5)(_sharpness - 1); + maskFilter += 'blur(' + blur + 'px) invert(1)'; - // var contrast = 1 / (_sharpness || 0.1); - maskFilter += 'contrast(0.75)';// + contrast + ')'; + contrast = d3_interpolateNumber(0, 1)(_sharpness - 1); + maskFilter += 'contrast(' + contrast + ')'; } var mask = base.selectAll('.layer-unsharp-mask') - .data(_sharpness !== 1 ? [0] : []); + .data(_sharpness > 1 ? [0] : []); mask.exit() .remove(); mask.enter() .append('div') - .attr('class', 'layer layer-unsharp-mask') + .attr('class', 'layer layer-mask layer-unsharp-mask') .merge(mask) .call(baseLayer) .style('-webkit-filter', maskFilter || null) diff --git a/modules/ui/background.js b/modules/ui/background.js index 0b4251c88b..0739ba4076 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -10,6 +10,7 @@ import { select as d3_select } from 'd3-selection'; + import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t, textDirection } from '../util/locale'; @@ -71,7 +72,7 @@ export function uiBackground(context) { d = d3_event.target.value; } - d = clamp(d, 0.5, 2); + d = clamp(d, 0.25, 2); context.background().sharpness(d); _displayOptions.selectAll('.sharpness-input') @@ -299,7 +300,7 @@ export function uiBackground(context) { .append('input') .attr('class', 'sharpness-input') .attr('type', 'range') - .attr('min', '0.5') + .attr('min', '0.25') .attr('max', '2') .attr('step', '0.05') .property('value', _sharpness) From 50c1b58bda19531328c4211287e11d6d7365953f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 30 Nov 2017 23:24:55 -0500 Subject: [PATCH 014/206] Added saturation slider, simplify code, improved sharpen parameters --- css/80_app.css | 14 +++-- data/core.yaml | 2 + dist/locales/en.json | 2 + modules/renderer/background.js | 42 ++++++++++----- modules/ui/background.js | 94 +++++++++++++--------------------- 5 files changed, 75 insertions(+), 79 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 743d698bba..e0c851e13c 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2463,11 +2463,11 @@ div.full-screen > button:hover { border-top: 5px solid #222; } -.display-controls-wrapper { +.display-options-container { padding: 10px; } -.display-controls-wrapper h5 span { +.display-options-container h5 span { margin: 5px; } @@ -2662,6 +2662,10 @@ img.tile { user-select: none; opacity: 0; + + -webkit-transition: opacity 200ms linear; + -moz-transition: opacity 200ms linear; + transition: opacity 200ms linear; } img.tile-loaded { @@ -2672,12 +2676,6 @@ img.tile-removing { opacity: 0; } -div:not(.layer-mask) > img.tile { - -webkit-transition: opacity 200ms linear; - transition: opacity 200ms linear; - -moz-transition: opacity 200ms linear; -} - .tile-label-debug { font-size: 10px; background: rgba(0, 0, 0, 0.7); diff --git a/data/core.yaml b/data/core.yaml index 1cd24fb735..7b593ce3d4 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -384,6 +384,8 @@ en: reset: reset display_options: Display Options brightness: Brightness + contrast: Contrast + saturation: Saturation sharpness: Sharpness minimap: description: Show Minimap diff --git a/dist/locales/en.json b/dist/locales/en.json index e41ba48493..306c8ac3d0 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -472,6 +472,8 @@ "reset": "reset", "display_options": "Display Options", "brightness": "Brightness", + "contrast": "Contrast", + "saturation": "Saturation", "sharpness": "Sharpness", "minimap": { "description": "Show Minimap", diff --git a/modules/renderer/background.js b/modules/renderer/background.js index bf3940e849..3d9f645b48 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -18,22 +18,26 @@ export function rendererBackground(context) { var _overlayLayers = []; var _backgroundSources = []; var _brightness = 1; + var _contrast = 1; + var _saturation = 1; var _sharpness = 1; function background(selection) { var baseFilter = ''; - var blur, contrast; if (_brightness !== 1) { baseFilter += 'brightness(' + _brightness + ')'; } + if (_contrast !== 1) { + baseFilter += 'contrast(' + _contrast + ')'; + } + if (_saturation !== 1) { + baseFilter += 'saturate(' + _saturation + ')'; + } if (_sharpness < 1) { // gaussian blur - blur = d3_interpolateNumber(0.5, 5)(1 - _sharpness); + var blur = d3_interpolateNumber(0.5, 5)(1 - _sharpness); baseFilter += 'blur(' + blur + 'px)'; - } else if (_sharpness > 1) { - contrast = d3_interpolateNumber(1, 1.5)(_sharpness - 1); - baseFilter += 'contrast(' + contrast + ')'; } var base = selection.selectAll('.layer-background') @@ -43,7 +47,6 @@ export function rendererBackground(context) { .insert('div', '.layer-data') .attr('class', 'layer layer-background') .merge(base) - .style('-webkit-filter', baseFilter || null) .style('filter', baseFilter || null); @@ -61,13 +64,13 @@ export function rendererBackground(context) { var mixBlendMode = ''; if (_sharpness > 1) { // apply unsharp mask mixBlendMode = 'overlay'; - maskFilter = 'saturate(0)'; + maskFilter = 'saturate(0) blur(3px) invert(1)'; - blur = d3_interpolateNumber(3, 0.5)(_sharpness - 1); - maskFilter += 'blur(' + blur + 'px) invert(1)'; + var contrast = _sharpness - 1; + maskFilter += ' contrast(' + contrast + ')'; - contrast = d3_interpolateNumber(0, 1)(_sharpness - 1); - maskFilter += 'contrast(' + contrast + ')'; + var brightness = d3_interpolateNumber(1, 0.85)(_sharpness - 1); + maskFilter += ' brightness(' + brightness + ')'; } var mask = base.selectAll('.layer-unsharp-mask') @@ -81,7 +84,6 @@ export function rendererBackground(context) { .attr('class', 'layer layer-mask layer-unsharp-mask') .merge(mask) .call(baseLayer) - .style('-webkit-filter', maskFilter || null) .style('filter', maskFilter || null) .style('mix-blend-mode', mixBlendMode || null); @@ -302,6 +304,22 @@ export function rendererBackground(context) { }; + background.contrast = function(d) { + if (!arguments.length) return _contrast; + _contrast = d; + if (context.mode()) dispatch.call('change'); + return background; + }; + + + background.saturation = function(d) { + if (!arguments.length) return _saturation; + _saturation = d; + if (context.mode()) dispatch.call('change'); + return background; + }; + + background.sharpness = function(d) { if (!arguments.length) return _sharpness; _sharpness = d; diff --git a/modules/ui/background.js b/modules/ui/background.js index 0739ba4076..2ddf0d81c7 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -31,9 +31,13 @@ export function uiBackground(context) { var key = t('background.key'); var detected = utilDetect(); - var _brightness = (context.storage('background-opacity') !== null) ? - (+context.storage('background-opacity')) : 1.0; - var _sharpness = 1; + var _options = { + brightness: (context.storage('background-opacity') !== null) ? + (+context.storage('background-opacity')) : 1.0, + contrast: 1, + saturation: 1, + sharpness: 1 + }; var _customSource = context.background().findSource('custom'); var _previousBackground; var _shown = false; @@ -48,40 +52,23 @@ export function uiBackground(context) { function clamp(x, min, max) { return Math.max(min, Math.min(x, max)); } - function setBrightness(d) { - if (!d && d3_event && d3_event.target) { - d = d3_event.target.value; + function setDisplayOption(d, val) { + if (!val && d3_event && d3_event.target) { + val = d3_event.target.value; } - d = clamp(d, 0.25, 2); - context.background().brightness(d); + val = clamp(val, 0.25, 2); + _displayOptions.selectAll('.' + d + '-input') + .property('value', val); + _displayOptions.selectAll('.' + d + '-value') + .text(Math.floor(val * 100) + '%'); - _displayOptions.selectAll('.opacity-input') - .property('value', d); + _options[d] = val; + context.background()[d](val); - _displayOptions.selectAll('.opacity-value') - .text(Math.floor(d * 100) + '%'); - - context.storage('background-opacity', d); - _brightness = d; - } - - - function setSharpness(d) { - if (!d && d3_event && d3_event.target) { - d = d3_event.target.value; + if (d === 'brightness') { + context.storage('background-opacity', val); } - - d = clamp(d, 0.25, 2); - context.background().sharpness(d); - - _displayOptions.selectAll('.sharpness-input') - .property('value', d); - - _displayOptions.selectAll('.sharpness-value') - .text(Math.floor(d * 100) + '%'); - - _sharpness = d; } @@ -266,45 +253,34 @@ export function uiBackground(context) { .append('div') .attr('class', 'display-options-container controls-list'); - // brightness - var controlsEnter = containerEnter - .append('div') - .attr('class', 'display-controls-wrapper'); + var sliders = ['brightness', 'contrast', 'saturation', 'sharpness']; - controlsEnter - .append('h5') - .text(t('background.brightness')) - .append('span') - .attr('class', 'opacity-value') - .text(Math.floor(_brightness * 100) + '%'); + var controls = containerEnter.selectAll('.display-control') + .data(sliders); - controlsEnter - .append('input') - .attr('class', 'opacity-input') - .attr('type', 'range') - .attr('min', '0.25') - .attr('max', '2') - .attr('step', '0.05') - .property('value', _brightness) - .on('input.set-brightness', setBrightness); + var controlsEnter = controls.enter() + .append('div') + .attr('class', function(d) { return 'display-control display-control-' + d; }); - // sharpness controlsEnter .append('h5') - .text(t('background.sharpness')) + .text(function(d) { return t('background.' + d); }) .append('span') - .attr('class', 'sharpness-value') - .text(Math.floor(_brightness * 100) + '%'); + .attr('class', function(d) { return d + '-value'; }); controlsEnter .append('input') - .attr('class', 'sharpness-input') + .attr('class', function(d) { return d + '-input'; }) .attr('type', 'range') .attr('min', '0.25') .attr('max', '2') .attr('step', '0.05') - .property('value', _sharpness) - .on('input.set-sharpness', setSharpness); + .property('value', function(d) { return _options[d]; }) + .on('input', function(d) { + var val = d3_select(this).property('value'); + setDisplayOption(d, val); + }); + // add minimap toggle var minimapEnter = containerEnter @@ -479,7 +455,7 @@ export function uiBackground(context) { update(); - setBrightness(_brightness); + setDisplayOption('brightness', _options.brightness); var keybinding = d3_keybinding('background') .on(key, togglePane) From b79b6ca97a8fe80004dfb4015df651967818818d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 7 Dec 2017 16:44:18 -0500 Subject: [PATCH 015/206] Support rendering `direction` on vertices (stop sign, traffic_signals, etc) (closes #3815) --- css/20_map.css | 4 +- modules/svg/defs.js | 26 ++++++++++-- modules/svg/vertices.js | 87 +++++++++++++++++++++++++++++++---------- 3 files changed, 91 insertions(+), 26 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 4a66b00dfb..1554ce0116 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -180,11 +180,11 @@ text { fill: #002F35; } -path.oneway { +.directiongroup path.directional, +.onewaygroup path.oneway { stroke-width: 6px; } - text.arealabel-halo, text.linelabel-halo, text.pointlabel-halo, diff --git a/modules/svg/defs.js b/modules/svg/defs.js index 93d9663a9e..25d2c8db1f 100644 --- a/modules/svg/defs.js +++ b/modules/svg/defs.js @@ -26,12 +26,12 @@ export function svgDefs(context) { return function drawDefs(selection) { var defs = selection.append('defs'); - // marker + // oneway marker defs.append('marker') .attr('id', 'oneway-marker') - .attr('viewBox', '0 0 10 10') - .attr('refY', 2.5) + .attr('viewBox', '0 0 10 5') .attr('refX', 5) + .attr('refY', 2.5) .attr('markerWidth', 2) .attr('markerHeight', 2) .attr('markerUnits', 'strokeWidth') @@ -39,11 +39,29 @@ export function svgDefs(context) { .append('path') .attr('class', 'oneway') - .attr('d', 'M 5 3 L 0 3 L 0 2 L 5 2 L 5 0 L 10 2.5 L 5 5 z') + .attr('d', 'M 5,3 L 0,3 L 0,2 L 5,2 L 5,0 L 10,2.5 L 5,5 z') .attr('stroke', 'none') .attr('fill', '#000') .attr('opacity', '0.75'); + defs.append('marker') + .attr('id', 'directional-marker') + .attr('viewBox', '0 0 15 5') + .attr('refX', 5.5) + .attr('refY', 2.5) + .attr('markerWidth', 7) + .attr('markerHeight', 7) + .attr('markerUnits', 'strokeWidth') + .attr('orient', 'auto') + + .append('path') + .attr('class', 'directional') + .attr('d', 'M 10,2.5 L 9,0 L 14,2.5 L 9,5 z') + .attr('stroke', '#fff') + .attr('fill', '#333') + .attr('stroke-width', '0.5px') + .attr('stroke-opacity', '0.75'); + // patterns var patterns = defs.selectAll('pattern') .data([ diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 053fc53023..01544165c1 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,6 +1,7 @@ import _values from 'lodash-es/values'; import { dataFeatureIcons } from '../../data'; +import { geoAngle } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform } from './index'; @@ -56,27 +57,64 @@ export function svgVertices(projection, context) { function draw(selection, vertices, klass, graph, zoom, siblings) { + siblings = siblings || {}; + var icons = {}; + var directions = {}; + var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); + - function icon(entity) { + function getIcon(entity) { if (entity.id in icons) return icons[entity.id]; + icons[entity.id] = entity.hasInterestingTags() && context.presets().match(entity, graph).icon; return icons[entity.id]; } + function getDirections(entity) { + if (entity.id in directions) return directions[entity.id]; + + var dir = (entity.tags['traffic_signals:direction'] || entity.tags.direction || '').toLowerCase(); + var stop = (entity.tags.stop || '').toLowerCase(); + var goBackward = (dir === 'backward' || dir === 'both' || dir === 'all' || stop === 'all'); + var goForward = (dir === 'forward' || dir === 'both' || dir === 'all' || stop === 'all'); + if (!goForward && !goBackward) return; + + var nodeIds = {}; + graph.parentWays(entity).forEach(function (parent) { + var nodes = parent.nodes; + for (var i = 0; i < nodes.length; i++) { + if (nodes[i] === entity.id) { // match current entity + if (goBackward && i > 0) { + nodeIds[nodes[i - 1]] = true; + } + if (goForward && i < nodes.length - 1) { + nodeIds[nodes[i + 1]] = true; + } + } + } + }); + + var dirAngles = Object.keys(nodeIds).map(function (nodeId) { + return geoAngle(entity, graph.entity(nodeId), projection) * (180 / Math.PI); + }); + directions[entity.id] = dirAngles; + return directions[entity.id]; + } + function setClass(klass) { return function(entity) { this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id); }; } - function setAttributes(selection) { + function updateAttributes(selection) { ['shadow','stroke','fill'].forEach(function(klass) { var rads = radiuses[klass]; selection.selectAll('.' + klass) .each(function(entity) { - var i = z && icon(entity), + var i = z && getIcon(entity), c = i ? 0.5 : 0, r = rads[i ? 3 : z]; @@ -97,20 +135,12 @@ export function svgVertices(projection, context) { }); selection.selectAll('use') - .each(function() { - if (z) { - this.removeAttribute('visibility'); - } else { - this.setAttribute('visibility', 'hidden'); - } - }); - } - + .attr('visibility', (z === 0 ? 'hidden' : null)); - siblings = siblings || {}; + selection.selectAll('.directiongroup') + .attr('visibility', (zoom < 18 ? 'hidden' : null)); + } - var icons = {}, - z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); var groups = selection .data(vertices, osmEntity.key); @@ -122,18 +152,34 @@ export function svgVertices(projection, context) { .append('g') .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); - enter.append('circle') + // Directional vertices get arrows + var directionsEnter = enter.filter(function(d) { return getDirections(d); }) + .append('g') + .each(setClass('directiongroup')); + + directionsEnter.selectAll('.directional') + .data(function(d) { return getDirections(d); }) + .enter() + .append('path') + .attr('class', 'directional') + .attr('transform', function(d) { return 'rotate(' + d + ')'; }) + .attr('d', 'M0,0H0') + .attr('marker-start', 'url(#directional-marker)'); + + enter + .append('circle') .each(setClass('shadow')); - enter.append('circle') + enter + .append('circle') .each(setClass('stroke')); // Vertices with icons get a `use`. - enter.filter(function(d) { return icon(d); }) + enter.filter(function(d) { return getIcon(d); }) .append('use') .attr('transform', 'translate(-5, -6)') .attr('xlink:href', function(d) { - var picon = icon(d), + var picon = getIcon(d), isMaki = dataFeatureIcons.indexOf(picon) !== -1; return '#' + picon + (isMaki ? '-11' : ''); }) @@ -146,13 +192,14 @@ export function svgVertices(projection, context) { .append('circle') .each(setClass('fill')); + // Update groups .merge(enter) .attr('transform', svgPointTransform(projection)) .classed('sibling', function(entity) { return entity.id in siblings; }) .classed('shared', function(entity) { return graph.isShared(entity); }) .classed('endpoint', function(entity) { return entity.isEndpoint(graph); }) - .call(setAttributes); + .call(updateAttributes); } From 6aba27c84a6b64c37e7fa1b9f3e864d4c49be39c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Dec 2017 14:29:56 -0500 Subject: [PATCH 016/206] Display directional vertex with a viewfield, not an arrow --- css/20_map.css | 4 +-- modules/svg/defs.js | 68 ++++++++++++++++++----------------------- modules/svg/vertices.js | 12 ++++---- 3 files changed, 37 insertions(+), 47 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 1554ce0116..6d3a39037c 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -180,8 +180,8 @@ text { fill: #002F35; } -.directiongroup path.directional, -.onewaygroup path.oneway { +.onewaygroup path.oneway, +.viewfieldgroup path.viewfield { stroke-width: 6px; } diff --git a/modules/svg/defs.js b/modules/svg/defs.js index 25d2c8db1f..6f43537693 100644 --- a/modules/svg/defs.js +++ b/modules/svg/defs.js @@ -26,8 +26,9 @@ export function svgDefs(context) { return function drawDefs(selection) { var defs = selection.append('defs'); - // oneway marker - defs.append('marker') + // markers + defs + .append('marker') .attr('id', 'oneway-marker') .attr('viewBox', '0 0 10 5') .attr('refX', 5) @@ -36,7 +37,6 @@ export function svgDefs(context) { .attr('markerHeight', 2) .attr('markerUnits', 'strokeWidth') .attr('orient', 'auto') - .append('path') .attr('class', 'oneway') .attr('d', 'M 5,3 L 0,3 L 0,2 L 5,2 L 5,0 L 10,2.5 L 5,5 z') @@ -44,21 +44,22 @@ export function svgDefs(context) { .attr('fill', '#000') .attr('opacity', '0.75'); - defs.append('marker') - .attr('id', 'directional-marker') - .attr('viewBox', '0 0 15 5') - .attr('refX', 5.5) - .attr('refY', 2.5) - .attr('markerWidth', 7) - .attr('markerHeight', 7) + defs + .append('marker') + .attr('id', 'viewfield-marker') + .attr('viewBox', '0 0 16 16') + .attr('refX', 8) + .attr('refY', 16) + .attr('markerWidth', 4) + .attr('markerHeight', 4) .attr('markerUnits', 'strokeWidth') .attr('orient', 'auto') - .append('path') - .attr('class', 'directional') - .attr('d', 'M 10,2.5 L 9,0 L 14,2.5 L 9,5 z') - .attr('stroke', '#fff') + .attr('class', 'viewfield') + .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z') .attr('fill', '#333') + .attr('fill-opacity', '0.75') + .attr('stroke', '#fff') .attr('stroke-width', '0.5px') .attr('stroke-opacity', '0.75'); @@ -77,23 +78,21 @@ export function svgDefs(context) { ]) .enter() .append('pattern') - .attr('id', function (d) { - return 'pattern-' + d[0]; - }) + .attr('id', function (d) { return 'pattern-' + d[0]; }) .attr('width', 32) .attr('height', 32) .attr('patternUnits', 'userSpaceOnUse'); - patterns.append('rect') + patterns + .append('rect') .attr('x', 0) .attr('y', 0) .attr('width', 32) .attr('height', 32) - .attr('class', function (d) { - return 'pattern-color-' + d[0]; - }); + .attr('class', function (d) { return 'pattern-color-' + d[0]; }); - patterns.append('image') + patterns + .append('image') .attr('x', 0) .attr('y', 0) .attr('width', 32) @@ -103,29 +102,20 @@ export function svgDefs(context) { }); // clip paths - defs.selectAll() + defs.selectAll('clipPath') .data([12, 18, 20, 32, 45]) .enter() .append('clipPath') - .attr('id', function (d) { - return 'clip-square-' + d; - }) + .attr('id', function (d) { return 'clip-square-' + d; }) .append('rect') .attr('x', 0) .attr('y', 0) - .attr('width', function (d) { - return d; - }) - .attr('height', function (d) { - return d; - }); - - defs.call(SVGSpriteDefinition( - 'iD-sprite', - context.imagePath('iD-sprite.svg'))); + .attr('width', function (d) { return d; }) + .attr('height', function (d) { return d; }); - defs.call(SVGSpriteDefinition( - 'maki-sprite', - context.imagePath('maki-sprite.svg'))); + // symbol spritesheets + defs + .call(SVGSpriteDefinition('iD-sprite', context.imagePath('iD-sprite.svg'))) + .call(SVGSpriteDefinition('maki-sprite', context.imagePath('maki-sprite.svg'))); }; } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 01544165c1..fa449fdc21 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -152,19 +152,19 @@ export function svgVertices(projection, context) { .append('g') .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); - // Directional vertices get arrows + // Directional vertices get viewfields var directionsEnter = enter.filter(function(d) { return getDirections(d); }) .append('g') - .each(setClass('directiongroup')); + .each(setClass('viewfieldgroup')); - directionsEnter.selectAll('.directional') + directionsEnter.selectAll('.viewfield') .data(function(d) { return getDirections(d); }) .enter() .append('path') - .attr('class', 'directional') - .attr('transform', function(d) { return 'rotate(' + d + ')'; }) + .attr('class', 'viewfield') + .attr('transform', function(d) { return 'rotate(' + (d + 90) + ')'; }) // +90 because marker is oriented along Y not X .attr('d', 'M0,0H0') - .attr('marker-start', 'url(#directional-marker)'); + .attr('marker-start', 'url(#viewfield-marker)'); enter .append('circle') From 515094cb56f500cfaefa82bd68ccf90f3f906ed3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Dec 2017 14:36:45 -0500 Subject: [PATCH 017/206] Only add viewfields for an all-way stop on a highway intersection. Sometimes people tag all-way stop signs at the junction node, othertimes people tag all-way stop signs at the stop sign location. What we're doing here is: - if `stop=all` tagged at the junction, show viewfield in all directions - if `stop=all` tagged at the sign location, show viewfield according to `direction=` tag --- modules/svg/vertices.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index fa449fdc21..dc8eb4dc1e 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -76,7 +76,7 @@ export function svgVertices(projection, context) { if (entity.id in directions) return directions[entity.id]; var dir = (entity.tags['traffic_signals:direction'] || entity.tags.direction || '').toLowerCase(); - var stop = (entity.tags.stop || '').toLowerCase(); + var stop = ((entity.isHighwayIntersection(graph) && entity.tags.stop) || '').toLowerCase(); var goBackward = (dir === 'backward' || dir === 'both' || dir === 'all' || stop === 'all'); var goForward = (dir === 'forward' || dir === 'both' || dir === 'all' || stop === 'all'); if (!goForward && !goBackward) return; From 9c649b74cd3289c7b6c0d31e856cae1ac4e9c4b7 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Dec 2017 14:54:26 -0500 Subject: [PATCH 018/206] After switching from arrows to viewfields, forward/backward is different --- modules/svg/vertices.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index dc8eb4dc1e..c0704b9bc1 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -86,11 +86,11 @@ export function svgVertices(projection, context) { var nodes = parent.nodes; for (var i = 0; i < nodes.length; i++) { if (nodes[i] === entity.id) { // match current entity - if (goBackward && i > 0) { - nodeIds[nodes[i - 1]] = true; + if (goForward && i > 0) { + nodeIds[nodes[i - 1]] = true; // viewfield point back to prev node } - if (goForward && i < nodes.length - 1) { - nodeIds[nodes[i + 1]] = true; + if (goBackward && i < nodes.length - 1) { + nodeIds[nodes[i + 1]] = true; // viewfield point ahead to next node } } } From 42043b2ce1dfbc5d169503fab9775ef3700774d3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Dec 2017 16:23:35 -0500 Subject: [PATCH 019/206] Support more tags, cardinal and numeric directions --- modules/svg/vertices.js | 60 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index c0704b9bc1..8a57b19edd 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -72,13 +72,60 @@ export function svgVertices(projection, context) { return icons[entity.id]; } + function getDirections(entity) { if (entity.id in directions) return directions[entity.id]; - var dir = (entity.tags['traffic_signals:direction'] || entity.tags.direction || '').toLowerCase(); - var stop = ((entity.isHighwayIntersection(graph) && entity.tags.stop) || '').toLowerCase(); - var goBackward = (dir === 'backward' || dir === 'both' || dir === 'all' || stop === 'all'); - var goForward = (dir === 'forward' || dir === 'both' || dir === 'all' || stop === 'all'); + var dir = ''; + + if (entity.isHighwayIntersection(graph) && (entity.tags.stop || '').toLowerCase() === 'all') { + // all-way stop tag on a highway intersection + dir = 'all'; + } else { + // direction tag + dir = ( + entity.tags['railway:signal:direction'] || + entity.tags['traffic_signals:direction'] || + entity.tags.direction || + '' + ).toLowerCase(); + } + + // swap cardinal for numeric directions + var cardinal = { + north: 0, n: 0, + northnortheast: 22, nne: 22, + northeast: 45, ne: 45, + eastnortheast: 67, ene: 67, + east: 90, e: 90, + eastsoutheast: 112, ese: 112, + southeast: 135, se: 135, + southsoutheast: 157, sse: 157, + south: 180, s: 180, + southsouthwest: 202, ssw: 202, + southwest: 225, sw: 225, + westsouthwest: 247, wsw: 247, + west: 270, w: 270, + westnorthwest: 292, wnw: 292, + northwest: 315, nw: 315, + northnorthwest: 337, nnw: 337 + }; + if (cardinal[dir] !== undefined) { + dir = cardinal[dir]; + } + + // if direction tag is numeric, return early + if (dir !== '' && !isNaN(+dir)) { + directions[entity.id] = [(+dir) - 90]; // -90 because marker is oriented along Y not X + return directions[entity.id]; + } + + // determine which direction(s) this feature points + var goBackward = + (entity.tags['traffic_sign:backward'] || dir === 'backward' || dir === 'both' || dir === 'all'); + var goForward = + (entity.tags['traffic_sign:forward'] || dir === 'forward' || dir === 'both' || dir === 'all'); + if (!goForward && !goBackward) return; var nodeIds = {}; @@ -87,10 +134,10 @@ export function svgVertices(projection, context) { for (var i = 0; i < nodes.length; i++) { if (nodes[i] === entity.id) { // match current entity if (goForward && i > 0) { - nodeIds[nodes[i - 1]] = true; // viewfield point back to prev node + nodeIds[nodes[i - 1]] = true; // viewfield points back to prev node } if (goBackward && i < nodes.length - 1) { - nodeIds[nodes[i + 1]] = true; // viewfield point ahead to next node + nodeIds[nodes[i + 1]] = true; // viewfield points ahead to next node } } } @@ -99,6 +146,7 @@ export function svgVertices(projection, context) { var dirAngles = Object.keys(nodeIds).map(function (nodeId) { return geoAngle(entity, graph.entity(nodeId), projection) * (180 / Math.PI); }); + directions[entity.id] = dirAngles; return directions[entity.id]; } From 34c98b94e67b9ef8f1def8fabfccc1c50ddd4064 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 9 Dec 2017 12:53:08 -0500 Subject: [PATCH 020/206] Move directions code from vertices to node.js --- modules/osm/node.js | 76 +++++++++++++++++++++++++++++++++- modules/svg/vertices.js | 91 +++++------------------------------------ 2 files changed, 86 insertions(+), 81 deletions(-) diff --git a/modules/osm/node.js b/modules/osm/node.js index 59d299dbb2..38b882c417 100644 --- a/modules/osm/node.js +++ b/modules/osm/node.js @@ -3,7 +3,7 @@ import _map from 'lodash-es/map'; import _some from 'lodash-es/some'; import { osmEntity } from './entity'; -import { geoExtent } from '../geo'; +import { geoAngle, geoExtent } from '../geo'; export function osmNode() { @@ -49,6 +49,80 @@ _extend(osmNode.prototype, { }, + // Inspect tags and geometry to determine which direction(s) this node/vertex points + directions: function(resolver, projection) { + var val; + + if (this.isHighwayIntersection(resolver) && (this.tags.stop || '').toLowerCase() === 'all') { + // all-way stop tag on a highway intersection + val = 'all'; + } else { + // direction tag + val = ( + this.tags['railway:signal:direction'] || + this.tags['traffic_signals:direction'] || + this.tags.direction || + '' + ).toLowerCase(); + } + + // swap cardinal for numeric directions + var cardinal = { + north: 0, n: 0, + northnortheast: 22, nne: 22, + northeast: 45, ne: 45, + eastnortheast: 67, ene: 67, + east: 90, e: 90, + eastsoutheast: 112, ese: 112, + southeast: 135, se: 135, + southsoutheast: 157, sse: 157, + south: 180, s: 180, + southsouthwest: 202, ssw: 202, + southwest: 225, sw: 225, + westsouthwest: 247, wsw: 247, + west: 270, w: 270, + westnorthwest: 292, wnw: 292, + northwest: 315, nw: 315, + northnorthwest: 337, nnw: 337 + }; + if (cardinal[val] !== undefined) { + val = cardinal[val]; + } + + // if direction is numeric, return early + if (val !== '' && !isNaN(+val)) { + return [(+val)]; + } + + var lookBackward = + (this.tags['traffic_sign:backward'] || val === 'backward' || val === 'both' || val === 'all'); + var lookForward = + (this.tags['traffic_sign:forward'] || val === 'forward' || val === 'both' || val === 'all'); + + if (!lookForward && !lookBackward) return null; + + var nodeIds = {}; + resolver.parentWays(this).forEach(function(parent) { + var nodes = parent.nodes; + for (var i = 0; i < nodes.length; i++) { + if (nodes[i] === this.id) { // match current entity + if (lookForward && i > 0) { + nodeIds[nodes[i - 1]] = true; // look back to prev node + } + if (lookBackward && i < nodes.length - 1) { + nodeIds[nodes[i + 1]] = true; // look ahead to next node + } + } + } + }, this); + + return Object.keys(nodeIds).map(function(nodeId) { + // +90 because geoAngle returns angle from X axis, not Y (north) + return (geoAngle(this, resolver.entity(nodeId), projection) * (180 / Math.PI)) + 90; + }, this); + }, + + isEndpoint: function(resolver) { return resolver.transient(this, 'isEndpoint', function() { var id = this.id; diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 8a57b19edd..170c4ba2e2 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,7 +1,6 @@ import _values from 'lodash-es/values'; import { dataFeatureIcons } from '../../data'; -import { geoAngle } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform } from './index'; @@ -14,7 +13,7 @@ export function svgVertices(projection, context) { fill: [1, 1.5, 1.5, 1.5] }; - var hover; + var _hover; function siblingAndChildVertices(ids, graph, extent) { @@ -76,87 +75,19 @@ export function svgVertices(projection, context) { function getDirections(entity) { if (entity.id in directions) return directions[entity.id]; - var dir = ''; - - if (entity.isHighwayIntersection(graph) && (entity.tags.stop || '').toLowerCase() === 'all') { - // all-way stop tag on a highway intersection - dir = 'all'; - } else { - // direction tag - dir = ( - entity.tags['railway:signal:direction'] || - entity.tags['traffic_signals:direction'] || - entity.tags.direction || - '' - ).toLowerCase(); - } - - // swap cardinal for numeric directions - var cardinal = { - north: 0, n: 0, - northnortheast: 22, nne: 22, - northeast: 45, ne: 45, - eastnortheast: 67, ene: 67, - east: 90, e: 90, - eastsoutheast: 112, ese: 112, - southeast: 135, se: 135, - southsoutheast: 157, sse: 157, - south: 180, s: 180, - southsouthwest: 202, ssw: 202, - southwest: 225, sw: 225, - westsouthwest: 247, wsw: 247, - west: 270, w: 270, - westnorthwest: 292, wnw: 292, - northwest: 315, nw: 315, - northnorthwest: 337, nnw: 337 - }; - if (cardinal[dir] !== undefined) { - dir = cardinal[dir]; - } - - // if direction tag is numeric, return early - if (dir !== '' && !isNaN(+dir)) { - directions[entity.id] = [(+dir) - 90]; // -90 because marker is oriented along Y not X - return directions[entity.id]; - } - - // determine which direction(s) this feature points - var goBackward = - (entity.tags['traffic_sign:backward'] || dir === 'backward' || dir === 'both' || dir === 'all'); - var goForward = - (entity.tags['traffic_sign:forward'] || dir === 'forward' || dir === 'both' || dir === 'all'); - - if (!goForward && !goBackward) return; - - var nodeIds = {}; - graph.parentWays(entity).forEach(function (parent) { - var nodes = parent.nodes; - for (var i = 0; i < nodes.length; i++) { - if (nodes[i] === entity.id) { // match current entity - if (goForward && i > 0) { - nodeIds[nodes[i - 1]] = true; // viewfield points back to prev node - } - if (goBackward && i < nodes.length - 1) { - nodeIds[nodes[i + 1]] = true; // viewfield points ahead to next node - } - } - } - }); - - var dirAngles = Object.keys(nodeIds).map(function (nodeId) { - return geoAngle(entity, graph.entity(nodeId), projection) * (180 / Math.PI); - }); - - directions[entity.id] = dirAngles; - return directions[entity.id]; + var angles = entity.directions(graph, projection); + if (angles) directions[entity.id] = angles; + return angles; } + function setClass(klass) { return function(entity) { this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id); }; } + function updateAttributes(selection) { ['shadow','stroke','fill'].forEach(function(klass) { var rads = radiuses[klass]; @@ -185,7 +116,7 @@ export function svgVertices(projection, context) { selection.selectAll('use') .attr('visibility', (z === 0 ? 'hidden' : null)); - selection.selectAll('.directiongroup') + selection.selectAll('.viewfieldgroup') .attr('visibility', (zoom < 18 ? 'hidden' : null)); } @@ -210,7 +141,7 @@ export function svgVertices(projection, context) { .enter() .append('path') .attr('class', 'viewfield') - .attr('transform', function(d) { return 'rotate(' + (d + 90) + ')'; }) // +90 because marker is oriented along Y not X + .attr('transform', function(d) { return 'rotate(' + d + ')'; }) .attr('d', 'M0,0H0') .attr('marker-start', 'url(#viewfield-marker)'); @@ -286,7 +217,7 @@ export function svgVertices(projection, context) { function drawHover(selection, graph, extent, zoom) { - var hovered = hover ? siblingAndChildVertices([hover.id], graph, extent) : {}; + var hovered = _hover ? siblingAndChildVertices([_hover.id], graph, extent) : {}; var layer = selection.selectAll('.layer-hit'); layer.selectAll('g.vertex.vertex-hover') @@ -295,8 +226,8 @@ export function svgVertices(projection, context) { drawVertices.drawHover = function(selection, graph, target, extent, zoom) { - if (target === hover) return; - hover = target; + if (target === _hover) return; + _hover = target; drawHover(selection, graph, extent, zoom); }; From 2a8bf6c7bf9607eb9bd6d43ed9de8e2e1d008111 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 10 Dec 2017 00:43:46 -0500 Subject: [PATCH 021/206] Don't draw viewfields on hover, rearrange code --- modules/svg/vertices.js | 50 ++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 170c4ba2e2..0e58d8dcb4 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -124,27 +124,15 @@ export function svgVertices(projection, context) { var groups = selection .data(vertices, osmEntity.key); + // exit groups.exit() .remove(); + // enter var enter = groups.enter() .append('g') .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); - // Directional vertices get viewfields - var directionsEnter = enter.filter(function(d) { return getDirections(d); }) - .append('g') - .each(setClass('viewfieldgroup')); - - directionsEnter.selectAll('.viewfield') - .data(function(d) { return getDirections(d); }) - .enter() - .append('path') - .attr('class', 'viewfield') - .attr('transform', function(d) { return 'rotate(' + d + ')'; }) - .attr('d', 'M0,0H0') - .attr('marker-start', 'url(#viewfield-marker)'); - enter .append('circle') .each(setClass('shadow')); @@ -171,14 +159,44 @@ export function svgVertices(projection, context) { .append('circle') .each(setClass('fill')); - // Update - groups + // update + groups = groups .merge(enter) .attr('transform', svgPointTransform(projection)) .classed('sibling', function(entity) { return entity.id in siblings; }) .classed('shared', function(entity) { return graph.isShared(entity); }) .classed('endpoint', function(entity) { return entity.isEndpoint(graph); }) .call(updateAttributes); + + + // Directional vertices get viewfields + var dgroups = groups.filter(function(d) { return getDirections(d); }) + .selectAll('.viewfieldgroup') + .data(function(d) { return klass === 'vertex-hover' ? [] : [d]; }, osmEntity.key); + + // exit + dgroups.exit() + .remove(); + + // enter + var dgroupsEnter = dgroups.enter() + .insert('g', '.shadow') + .each(setClass('viewfieldgroup')); + + dgroupsEnter + .selectAll('.viewfield') + .data(getDirections, function key(d) { return d; }) + .enter() + .append('path') + .attr('class', 'viewfield') + .attr('d', 'M0,0H0') + .attr('marker-start', 'url(#viewfield-marker)'); + + // update + dgroups + .merge(dgroupsEnter) + .selectAll('.viewfield') + .attr('transform', function(d) { return 'rotate(' + d + ')'; }); } From b42c096fe53adec9711320360c6219b4f6b65567 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 10 Dec 2017 14:34:50 -0500 Subject: [PATCH 022/206] Make sure viewfields actually update on update selection --- modules/svg/vertices.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 0e58d8dcb4..80638b7bd8 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -178,24 +178,26 @@ export function svgVertices(projection, context) { dgroups.exit() .remove(); - // enter - var dgroupsEnter = dgroups.enter() + // enter/update + dgroups = dgroups.enter() .insert('g', '.shadow') - .each(setClass('viewfieldgroup')); + .each(setClass('viewfieldgroup')) + .merge(dgroups); + + var viewfields = dgroups.selectAll('.viewfield') + .data(getDirections, function key(d) { return d; }); - dgroupsEnter - .selectAll('.viewfield') - .data(getDirections, function key(d) { return d; }) - .enter() + // exit + viewfields.exit() + .remove(); + + // enter/update + viewfields.enter() .append('path') .attr('class', 'viewfield') .attr('d', 'M0,0H0') - .attr('marker-start', 'url(#viewfield-marker)'); - - // update - dgroups - .merge(dgroupsEnter) - .selectAll('.viewfield') + .attr('marker-start', 'url(#viewfield-marker)') + .merge(viewfields) .attr('transform', function(d) { return 'rotate(' + d + ')'; }); } From 8e19474293108b77a1edddeb27ad15249b353deb Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 10:48:04 -0500 Subject: [PATCH 023/206] Render directional points (e.g. benches, cameras, signs) as vertices --- modules/osm/node.js | 2 +- modules/svg/points.js | 14 +++++++++----- modules/svg/vertices.js | 5 +++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/osm/node.js b/modules/osm/node.js index 38b882c417..cd3c8a79a2 100644 --- a/modules/osm/node.js +++ b/modules/osm/node.js @@ -99,7 +99,7 @@ _extend(osmNode.prototype, { var lookForward = (this.tags['traffic_sign:forward'] || val === 'forward' || val === 'both' || val === 'all'); - if (!lookForward && !lookBackward) return null; + if (!lookForward && !lookBackward) return []; var nodeIds = {}; resolver.parentWays(this).forEach(function(parent) { diff --git a/modules/svg/points.js b/modules/svg/points.js index e48f31163a..054a7359e1 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -22,7 +22,7 @@ export function svgPoints(projection, context) { return function drawPoints(selection, graph, entities, filter) { var wireframe = context.surface().classed('fill-wireframe'), points = wireframe ? [] : _filter(entities, function(e) { - return e.geometry(graph) === 'point'; + return e.geometry(graph) === 'point' && !e.directions(graph, projection).length; }); points.sort(sortY); @@ -41,20 +41,24 @@ export function svgPoints(projection, context) { .attr('class', function(d) { return 'node point ' + d.id; }) .order(); - enter.append('path') + enter + .append('path') .call(markerPath, 'shadow'); - enter.append('ellipse') + enter + .append('ellipse') .attr('cx', 0.5) .attr('cy', 1) .attr('rx', 6.5) .attr('ry', 3) .attr('class', 'stroke'); - enter.append('path') + enter + .append('path') .call(markerPath, 'stroke'); - enter.append('use') + enter + .append('use') .attr('transform', 'translate(-5, -19)') .attr('class', 'icon') .attr('width', '11px') diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 80638b7bd8..e7fa7a7769 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -72,11 +72,12 @@ export function svgVertices(projection, context) { } + // memoize directions results, return false for empty arrays (for use in filter) function getDirections(entity) { if (entity.id in directions) return directions[entity.id]; var angles = entity.directions(graph, projection); - if (angles) directions[entity.id] = angles; + directions[entity.id] = angles.length ? angles : false; return angles; } @@ -211,7 +212,7 @@ export function svgVertices(projection, context) { var entity = entities[i], geometry = entity.geometry(graph); - if (wireframe && geometry === 'point') { + if ((geometry === 'point') && (wireframe || entity.directions(graph, projection).length)) { vertices.push(entity); continue; } From ee3083b113ef8bfbbd8d3e669a4e54fb94e5d427 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 11:46:11 -0500 Subject: [PATCH 024/206] Support rendering `camera:direction` --- modules/osm/node.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/osm/node.js b/modules/osm/node.js index cd3c8a79a2..362b4448c7 100644 --- a/modules/osm/node.js +++ b/modules/osm/node.js @@ -59,6 +59,7 @@ _extend(osmNode.prototype, { } else { // direction tag val = ( + this.tags['camera:direction'] || this.tags['railway:signal:direction'] || this.tags['traffic_signals:direction'] || this.tags.direction || From 6b9ccdb45acbc1d776c32c6b2c55b7abbfe7cf13 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 14:33:22 -0500 Subject: [PATCH 025/206] Add tests for osmNode#direction --- test/spec/osm/node.js | 555 +++++++++++++++++++++++++++++++++++------- 1 file changed, 471 insertions(+), 84 deletions(-) diff --git a/test/spec/osm/node.js b/test/spec/osm/node.js index 53de624dc2..dd005c8895 100644 --- a/test/spec/osm/node.js +++ b/test/spec/osm/node.js @@ -1,66 +1,66 @@ describe('iD.osmNode', function () { it('returns a node', function () { - expect(iD.Node()).to.be.an.instanceOf(iD.Node); - expect(iD.Node().type).to.equal('node'); + expect(iD.osmNode()).to.be.an.instanceOf(iD.osmNode); + expect(iD.osmNode().type).to.equal('node'); }); it('defaults tags to an empty object', function () { - expect(iD.Node().tags).to.eql({}); + expect(iD.osmNode().tags).to.eql({}); }); it('sets tags as specified', function () { - expect(iD.Node({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'}); + expect(iD.osmNode({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'}); }); describe('#extent', function() { it('returns a point extent', function() { - expect(iD.Node({loc: [5, 10]}).extent().equals([[5, 10], [5, 10]])).to.be.ok; + expect(iD.osmNode({loc: [5, 10]}).extent().equals([[5, 10], [5, 10]])).to.be.ok; }); }); describe('#intersects', function () { it('returns true for a node within the given extent', function () { - expect(iD.Node({loc: [0, 0]}).intersects([[-5, -5], [5, 5]])).to.equal(true); + expect(iD.osmNode({loc: [0, 0]}).intersects([[-5, -5], [5, 5]])).to.equal(true); }); it('returns false for a node outside the given extend', function () { - expect(iD.Node({loc: [6, 6]}).intersects([[-5, -5], [5, 5]])).to.equal(false); + expect(iD.osmNode({loc: [6, 6]}).intersects([[-5, -5], [5, 5]])).to.equal(false); }); }); describe('#geometry', function () { it('returns \'vertex\' if the node is a member of any way', function () { - var node = iD.Node(), - way = iD.Way({nodes: [node.id]}), - graph = iD.Graph([node, way]); + var node = iD.osmNode(), + way = iD.osmWay({nodes: [node.id]}), + graph = iD.coreGraph([node, way]); expect(node.geometry(graph)).to.equal('vertex'); }); it('returns \'point\' if the node is not a member of any way', function () { - var node = iD.Node(), - graph = iD.Graph([node]); + var node = iD.osmNode(), + graph = iD.coreGraph([node]); expect(node.geometry(graph)).to.equal('point'); }); }); describe('#isEndpoint', function () { it('returns true for a node at an endpoint along a linear way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c']}), + graph = iD.coreGraph([a, b, c, w]); expect(a.isEndpoint(graph)).to.equal(true, 'linear way, beginning node'); expect(b.isEndpoint(graph)).to.equal(false, 'linear way, middle node'); expect(c.isEndpoint(graph)).to.equal(true, 'linear way, ending node'); }); it('returns false for nodes along a circular way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c', 'a']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c', 'a']}), + graph = iD.coreGraph([a, b, c, w]); expect(a.isEndpoint(graph)).to.equal(false, 'circular way, connector node'); expect(b.isEndpoint(graph)).to.equal(false, 'circular way, middle node'); expect(c.isEndpoint(graph)).to.equal(false, 'circular way, ending node'); @@ -69,120 +69,507 @@ describe('iD.osmNode', function () { describe('#isConnected', function () { it('returns true for a node with multiple parent ways, at least one interesting', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id]}), - w2 = iD.Way({nodes: [node.id], tags: { highway: 'residential' }}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id]}), + w2 = iD.osmWay({nodes: [node.id], tags: { highway: 'residential' }}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isConnected(graph)).to.equal(true); }); it('returns false for a node with only area parent ways', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: { area: 'yes' }}), - w2 = iD.Way({nodes: [node.id], tags: { area: 'yes' }}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: { area: 'yes' }}), + w2 = iD.osmWay({nodes: [node.id], tags: { area: 'yes' }}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isConnected(graph)).to.equal(false); }); it('returns false for a node with only uninteresting parent ways', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id]}), - w2 = iD.Way({nodes: [node.id]}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id]}), + w2 = iD.osmWay({nodes: [node.id]}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isConnected(graph)).to.equal(false); }); it('returns false for a standalone node on a single parent way', function () { - var node = iD.Node(), - way = iD.Way({nodes: [node.id]}), - graph = iD.Graph([node, way]); + var node = iD.osmNode(), + way = iD.osmWay({nodes: [node.id]}), + graph = iD.coreGraph([node, way]); expect(node.isConnected(graph)).to.equal(false); }); it('returns true for a self-intersecting node on a single parent way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c', 'b']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c', 'b']}), + graph = iD.coreGraph([a, b, c, w]); expect(b.isConnected(graph)).to.equal(true); }); it('returns false for the connecting node of a closed way', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}), - c = iD.Node({id: 'c'}), - w = iD.Way({nodes: ['a', 'b', 'c', 'a']}), - graph = iD.Graph([a, b, c, w]); + var a = iD.osmNode({id: 'a'}), + b = iD.osmNode({id: 'b'}), + c = iD.osmNode({id: 'c'}), + w = iD.osmWay({nodes: ['a', 'b', 'c', 'a']}), + graph = iD.coreGraph([a, b, c, w]); expect(a.isConnected(graph)).to.equal(false); }); }); describe('#isIntersection', function () { it('returns true for a node shared by more than one highway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - w2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isIntersection(graph)).to.equal(true); }); it('returns true for a node shared by more than one waterway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - w2 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isIntersection(graph)).to.equal(true); }); }); describe('#isHighwayIntersection', function () { it('returns true for a node shared by more than one highway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - w2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isHighwayIntersection(graph)).to.equal(true); }); it('returns false for a node shared by more than one waterway', function () { - var node = iD.Node(), - w1 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - w2 = iD.Way({nodes: [node.id], tags: {waterway: 'river'}}), - graph = iD.Graph([node, w1, w2]); + var node = iD.osmNode(), + w1 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + w2 = iD.osmWay({nodes: [node.id], tags: {waterway: 'river'}}), + graph = iD.coreGraph([node, w1, w2]); expect(node.isHighwayIntersection(graph)).to.equal(false); }); }); describe('#isDegenerate', function () { it('returns true if node has invalid loc', function () { - expect(iD.Node().isDegenerate()).to.be.equal(true, 'no loc'); - expect(iD.Node({loc: ''}).isDegenerate()).to.be.equal(true, 'empty string loc'); - expect(iD.Node({loc: []}).isDegenerate()).to.be.equal(true, 'empty array loc'); - expect(iD.Node({loc: [0]}).isDegenerate()).to.be.equal(true, '1-array loc'); - expect(iD.Node({loc: [0, 0, 0]}).isDegenerate()).to.be.equal(true, '3-array loc'); - expect(iD.Node({loc: [-181, 0]}).isDegenerate()).to.be.equal(true, '< min lon'); - expect(iD.Node({loc: [181, 0]}).isDegenerate()).to.be.equal(true, '> max lon'); - expect(iD.Node({loc: [0, -91]}).isDegenerate()).to.be.equal(true, '< min lat'); - expect(iD.Node({loc: [0, 91]}).isDegenerate()).to.be.equal(true, '> max lat'); - expect(iD.Node({loc: [Infinity, 0]}).isDegenerate()).to.be.equal(true, 'Infinity lon'); - expect(iD.Node({loc: [0, Infinity]}).isDegenerate()).to.be.equal(true, 'Infinity lat'); - expect(iD.Node({loc: [NaN, 0]}).isDegenerate()).to.be.equal(true, 'NaN lon'); - expect(iD.Node({loc: [0, NaN]}).isDegenerate()).to.be.equal(true, 'NaN lat'); + expect(iD.osmNode().isDegenerate()).to.be.equal(true, 'no loc'); + expect(iD.osmNode({loc: ''}).isDegenerate()).to.be.equal(true, 'empty string loc'); + expect(iD.osmNode({loc: []}).isDegenerate()).to.be.equal(true, 'empty array loc'); + expect(iD.osmNode({loc: [0]}).isDegenerate()).to.be.equal(true, '1-array loc'); + expect(iD.osmNode({loc: [0, 0, 0]}).isDegenerate()).to.be.equal(true, '3-array loc'); + expect(iD.osmNode({loc: [-181, 0]}).isDegenerate()).to.be.equal(true, '< min lon'); + expect(iD.osmNode({loc: [181, 0]}).isDegenerate()).to.be.equal(true, '> max lon'); + expect(iD.osmNode({loc: [0, -91]}).isDegenerate()).to.be.equal(true, '< min lat'); + expect(iD.osmNode({loc: [0, 91]}).isDegenerate()).to.be.equal(true, '> max lat'); + expect(iD.osmNode({loc: [Infinity, 0]}).isDegenerate()).to.be.equal(true, 'Infinity lon'); + expect(iD.osmNode({loc: [0, Infinity]}).isDegenerate()).to.be.equal(true, 'Infinity lat'); + expect(iD.osmNode({loc: [NaN, 0]}).isDegenerate()).to.be.equal(true, 'NaN lon'); + expect(iD.osmNode({loc: [0, NaN]}).isDegenerate()).to.be.equal(true, 'NaN lat'); }); it('returns false if node has valid loc', function () { - expect(iD.Node({loc: [0, 0]}).isDegenerate()).to.be.equal(false, '2-array loc'); - expect(iD.Node({loc: [-180, 0]}).isDegenerate()).to.be.equal(false, 'min lon'); - expect(iD.Node({loc: [180, 0]}).isDegenerate()).to.be.equal(false, 'max lon'); - expect(iD.Node({loc: [0, -90]}).isDegenerate()).to.be.equal(false, 'min lat'); - expect(iD.Node({loc: [0, 90]}).isDegenerate()).to.be.equal(false, 'max lat'); + expect(iD.osmNode({loc: [0, 0]}).isDegenerate()).to.be.equal(false, '2-array loc'); + expect(iD.osmNode({loc: [-180, 0]}).isDegenerate()).to.be.equal(false, 'min lon'); + expect(iD.osmNode({loc: [180, 0]}).isDegenerate()).to.be.equal(false, 'max lon'); + expect(iD.osmNode({loc: [0, -90]}).isDegenerate()).to.be.equal(false, 'min lat'); + expect(iD.osmNode({loc: [0, 90]}).isDegenerate()).to.be.equal(false, 'max lat'); }); }); + describe('#directions', function () { + var projection = function (_) { return _; }; + it('returns empty array if no direction tag', function () { + var node1 = iD.osmNode({ loc: [0, 0], tags: {}}); + var graph = iD.coreGraph([node1]); + expect(node1.directions(graph, projection)).to.eql([], 'no direction tag'); + }); + + it('returns empty array if nonsense direction tag', function () { + var node1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'blah' }}); + var node2 = iD.osmNode({ loc: [0, 0], tags: { direction: '' }}); + var node3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NaN' }}); + var node4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'eastwest' }}); + var graph = iD.coreGraph([node1, node2, node3, node4]); + + expect(node1.directions(graph, projection)).to.eql([], 'nonsense direction tag'); + expect(node2.directions(graph, projection)).to.eql([], 'empty string direction tag'); + expect(node3.directions(graph, projection)).to.eql([], 'NaN direction tag'); + expect(node4.directions(graph, projection)).to.eql([], 'eastwest direction tag'); + }); + + it('supports numeric direction tag', function () { + var node1 = iD.osmNode({ loc: [0, 0], tags: { direction: '0' }}); + var node2 = iD.osmNode({ loc: [0, 0], tags: { direction: '45' }}); + var node3 = iD.osmNode({ loc: [0, 0], tags: { direction: '-45' }}); + var node4 = iD.osmNode({ loc: [0, 0], tags: { direction: '360' }}); + var node5 = iD.osmNode({ loc: [0, 0], tags: { direction: '1000' }}); + var graph = iD.coreGraph([node1, node2, node3, node4, node5]); + + expect(node1.directions(graph, projection)).to.eql([0], 'numeric 0'); + expect(node2.directions(graph, projection)).to.eql([45], 'numeric 45'); + expect(node3.directions(graph, projection)).to.eql([-45], 'numeric -45'); + expect(node4.directions(graph, projection)).to.eql([360], 'numeric 360'); + expect(node5.directions(graph, projection)).to.eql([1000], 'numeric 1000'); + }); + + it('supports cardinal direction tags (test abbreviated and mixed case)', function () { + var nodeN1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'n' }}); + var nodeN2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'N' }}); + var nodeN3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'north' }}); + var nodeN4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NOrth' }}); + + var nodeNNE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nne' }}); + var nodeNNE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NnE' }}); + var nodeNNE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northnortheast' }}); + var nodeNNE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NOrthnorTHEast' }}); + + var nodeNE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ne' }}); + var nodeNE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nE' }}); + var nodeNE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northeast' }}); + var nodeNE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'norTHEast' }}); + + var nodeENE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ene' }}); + var nodeENE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EnE' }}); + var nodeENE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'eastnortheast' }}); + var nodeENE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EAstnorTHEast' }}); + + var nodeE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'e' }}); + var nodeE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'E' }}); + var nodeE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'east' }}); + var nodeE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EAst' }}); + + var nodeESE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ese' }}); + var nodeESE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EsE' }}); + var nodeESE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'eastsoutheast' }}); + var nodeESE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'EAstsouTHEast' }}); + + var nodeSE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'se' }}); + var nodeSE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sE' }}); + var nodeSE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southeast' }}); + var nodeSE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'souTHEast' }}); + + var nodeSSE1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sse' }}); + var nodeSSE2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SsE' }}); + var nodeSSE3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southsoutheast' }}); + var nodeSSE4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SOuthsouTHEast' }}); + + var nodeS1 = iD.osmNode({ loc: [0, 0], tags: { direction: 's' }}); + var nodeS2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'S' }}); + var nodeS3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'south' }}); + var nodeS4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SOuth' }}); + + var nodeSSW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'ssw' }}); + var nodeSSW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SsW' }}); + var nodeSSW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southsouthwest' }}); + var nodeSSW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'SOuthsouTHWest' }}); + + var nodeSW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sw' }}); + var nodeSW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'sW' }}); + var nodeSW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'southwest' }}); + var nodeSW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'souTHWest' }}); + + var nodeWSW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'wsw' }}); + var nodeWSW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WsW' }}); + var nodeWSW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'westsouthwest' }}); + var nodeWSW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WEstsouTHWest' }}); + + var nodeW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'w' }}); + var nodeW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'W' }}); + var nodeW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'west' }}); + var nodeW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WEst' }}); + + var nodeWNW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'wnw' }}); + var nodeWNW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WnW' }}); + var nodeWNW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'westnorthwest' }}); + var nodeWNW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'WEstnorTHWest' }}); + + var nodeNW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nw' }}); + var nodeNW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nW' }}); + var nodeNW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northwest' }}); + var nodeNW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'norTHWest' }}); + + var nodeNNW1 = iD.osmNode({ loc: [0, 0], tags: { direction: 'nnw' }}); + var nodeNNW2 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NnW' }}); + var nodeNNW3 = iD.osmNode({ loc: [0, 0], tags: { direction: 'northnorthwest' }}); + var nodeNNW4 = iD.osmNode({ loc: [0, 0], tags: { direction: 'NOrthnorTHWest' }}); + + var graph = iD.coreGraph([ + nodeN1, nodeN2, nodeN3, nodeN4, + nodeNNE1, nodeNNE2, nodeNNE3, nodeNNE4, + nodeNE1, nodeNE2, nodeNE3, nodeNE4, + nodeENE1, nodeENE2, nodeENE3, nodeENE4, + nodeE1, nodeE2, nodeE3, nodeE4, + nodeESE1, nodeESE2, nodeESE3, nodeESE4, + nodeSE1, nodeSE2, nodeSE3, nodeSE4, + nodeSSE1, nodeSSE2, nodeSSE3, nodeSSE4, + nodeS1, nodeS2, nodeS3, nodeS4, + nodeSSW1, nodeSSW2, nodeSSW3, nodeSSW4, + nodeSW1, nodeSW2, nodeSW3, nodeSW4, + nodeWSW1, nodeWSW2, nodeWSW3, nodeWSW4, + nodeW1, nodeW2, nodeW3, nodeW4, + nodeWNW1, nodeWNW2, nodeWNW3, nodeWNW4, + nodeNW1, nodeNW2, nodeNW3, nodeNW4, + nodeNNW1, nodeNNW2, nodeNNW3, nodeNNW4 + ]); + + expect(nodeN1.directions(graph, projection)).to.eql([0], 'cardinal n'); + expect(nodeN2.directions(graph, projection)).to.eql([0], 'cardinal N'); + expect(nodeN3.directions(graph, projection)).to.eql([0], 'cardinal north'); + expect(nodeN4.directions(graph, projection)).to.eql([0], 'cardinal NOrth'); + + expect(nodeNNE1.directions(graph, projection)).to.eql([22], 'cardinal nne'); + expect(nodeNNE2.directions(graph, projection)).to.eql([22], 'cardinal NnE'); + expect(nodeNNE3.directions(graph, projection)).to.eql([22], 'cardinal northnortheast'); + expect(nodeNNE4.directions(graph, projection)).to.eql([22], 'cardinal NOrthnorTHEast'); + + expect(nodeNE1.directions(graph, projection)).to.eql([45], 'cardinal ne'); + expect(nodeNE2.directions(graph, projection)).to.eql([45], 'cardinal nE'); + expect(nodeNE3.directions(graph, projection)).to.eql([45], 'cardinal northeast'); + expect(nodeNE4.directions(graph, projection)).to.eql([45], 'cardinal norTHEast'); + + expect(nodeENE1.directions(graph, projection)).to.eql([67], 'cardinal ene'); + expect(nodeENE2.directions(graph, projection)).to.eql([67], 'cardinal EnE'); + expect(nodeENE3.directions(graph, projection)).to.eql([67], 'cardinal eastnortheast'); + expect(nodeENE4.directions(graph, projection)).to.eql([67], 'cardinal EAstnorTHEast'); + + expect(nodeE1.directions(graph, projection)).to.eql([90], 'cardinal e'); + expect(nodeE2.directions(graph, projection)).to.eql([90], 'cardinal E'); + expect(nodeE3.directions(graph, projection)).to.eql([90], 'cardinal east'); + expect(nodeE4.directions(graph, projection)).to.eql([90], 'cardinal EAst'); + + expect(nodeESE1.directions(graph, projection)).to.eql([112], 'cardinal ese'); + expect(nodeESE2.directions(graph, projection)).to.eql([112], 'cardinal EsE'); + expect(nodeESE3.directions(graph, projection)).to.eql([112], 'cardinal eastsoutheast'); + expect(nodeESE4.directions(graph, projection)).to.eql([112], 'cardinal EAstsouTHEast'); + + expect(nodeSE1.directions(graph, projection)).to.eql([135], 'cardinal se'); + expect(nodeSE2.directions(graph, projection)).to.eql([135], 'cardinal sE'); + expect(nodeSE3.directions(graph, projection)).to.eql([135], 'cardinal southeast'); + expect(nodeSE4.directions(graph, projection)).to.eql([135], 'cardinal souTHEast'); + + expect(nodeSSE1.directions(graph, projection)).to.eql([157], 'cardinal sse'); + expect(nodeSSE2.directions(graph, projection)).to.eql([157], 'cardinal SsE'); + expect(nodeSSE3.directions(graph, projection)).to.eql([157], 'cardinal southsoutheast'); + expect(nodeSSE4.directions(graph, projection)).to.eql([157], 'cardinal SouthsouTHEast'); + + expect(nodeS1.directions(graph, projection)).to.eql([180], 'cardinal s'); + expect(nodeS2.directions(graph, projection)).to.eql([180], 'cardinal S'); + expect(nodeS3.directions(graph, projection)).to.eql([180], 'cardinal south'); + expect(nodeS4.directions(graph, projection)).to.eql([180], 'cardinal SOuth'); + + expect(nodeSSW1.directions(graph, projection)).to.eql([202], 'cardinal ssw'); + expect(nodeSSW2.directions(graph, projection)).to.eql([202], 'cardinal SsW'); + expect(nodeSSW3.directions(graph, projection)).to.eql([202], 'cardinal southsouthwest'); + expect(nodeSSW4.directions(graph, projection)).to.eql([202], 'cardinal SouthsouTHWest'); + + expect(nodeSW1.directions(graph, projection)).to.eql([225], 'cardinal sw'); + expect(nodeSW2.directions(graph, projection)).to.eql([225], 'cardinal sW'); + expect(nodeSW3.directions(graph, projection)).to.eql([225], 'cardinal southwest'); + expect(nodeSW4.directions(graph, projection)).to.eql([225], 'cardinal souTHWest'); + + expect(nodeWSW1.directions(graph, projection)).to.eql([247], 'cardinal wsw'); + expect(nodeWSW2.directions(graph, projection)).to.eql([247], 'cardinal WsW'); + expect(nodeWSW3.directions(graph, projection)).to.eql([247], 'cardinal westsouthwest'); + expect(nodeWSW4.directions(graph, projection)).to.eql([247], 'cardinal WEstsouTHWest'); + + expect(nodeW1.directions(graph, projection)).to.eql([270], 'cardinal w'); + expect(nodeW2.directions(graph, projection)).to.eql([270], 'cardinal W'); + expect(nodeW3.directions(graph, projection)).to.eql([270], 'cardinal west'); + expect(nodeW4.directions(graph, projection)).to.eql([270], 'cardinal WEst'); + + expect(nodeWNW1.directions(graph, projection)).to.eql([292], 'cardinal wnw'); + expect(nodeWNW2.directions(graph, projection)).to.eql([292], 'cardinal WnW'); + expect(nodeWNW3.directions(graph, projection)).to.eql([292], 'cardinal westnorthwest'); + expect(nodeWNW4.directions(graph, projection)).to.eql([292], 'cardinal WEstnorTHWest'); + + expect(nodeNW1.directions(graph, projection)).to.eql([315], 'cardinal nw'); + expect(nodeNW2.directions(graph, projection)).to.eql([315], 'cardinal nW'); + expect(nodeNW3.directions(graph, projection)).to.eql([315], 'cardinal northwest'); + expect(nodeNW4.directions(graph, projection)).to.eql([315], 'cardinal norTHWest'); + + expect(nodeNNW1.directions(graph, projection)).to.eql([337], 'cardinal nnw'); + expect(nodeNNW2.directions(graph, projection)).to.eql([337], 'cardinal NnW'); + expect(nodeNNW3.directions(graph, projection)).to.eql([337], 'cardinal northnorthwest'); + expect(nodeNNW4.directions(graph, projection)).to.eql([337], 'cardinal NOrthnorTHWest'); + }); + + it('supports direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports traffic_signals:direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports traffic_signals:direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports traffic_signals:direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports traffic_signals:direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'traffic_signals:direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports railway:signal:direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports railway:signal:direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports railway:signal:direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports railway:signal:direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'railway:signal:direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports camera:direction=forward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'forward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([270]); + }); + + it('supports camera:direction=backward', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'backward' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.eql([90]); + }); + + it('supports camera:direction=both', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'both' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('supports camera:direction=all', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'camera:direction': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var way = iD.osmWay({ nodes: ['n1','n2','n3'] }); + var graph = iD.coreGraph([node1, node2, node3, way]); + expect(node2.directions(graph, projection)).to.have.members([90, 270]); + }); + + it('returns directions for an all-way stop at a highway interstction', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0] }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0], tags: { 'highway': 'stop', 'stop': 'all' }}); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0] }); + var node4 = iD.osmNode({ id: 'n4', loc: [0, -1] }); + var node5 = iD.osmNode({ id: 'n5', loc: [0, 1] }); + var way1 = iD.osmWay({ id: 'w1', nodes: ['n1','n2','n3'], tags: { 'highway': 'residential' } }); + var way2 = iD.osmWay({ id: 'w2', nodes: ['n4','n2','n5'], tags: { 'highway': 'residential' } }); + var graph = iD.coreGraph([node1, node2, node3, node4, node5, way1, way2]); + expect(node2.directions(graph, projection)).to.have.members([0, 90, 180, 270]); + }); + + it('does not return directions for an all-way stop not at a highway interstction', function () { + var node1 = iD.osmNode({ id: 'n1', loc: [-1, 0], tags: { 'highway': 'stop', 'stop': 'all' } }); + var node2 = iD.osmNode({ id: 'n2', loc: [0, 0] }); + var node3 = iD.osmNode({ id: 'n3', loc: [1, 0], tags: { 'highway': 'stop', 'stop': 'all' } }); + var node4 = iD.osmNode({ id: 'n4', loc: [0, -1], tags: { 'highway': 'stop', 'stop': 'all' } }); + var node5 = iD.osmNode({ id: 'n5', loc: [0, 1], tags: { 'highway': 'stop', 'stop': 'all' } }); + var way1 = iD.osmWay({ id: 'w1', nodes: ['n1','n2','n3'], tags: { 'highway': 'residential' } }); + var way2 = iD.osmWay({ id: 'w2', nodes: ['n4','n2','n5'], tags: { 'highway': 'residential' } }); + var graph = iD.coreGraph([node1, node2, node3, node4, node5, way1, way2]); + expect(node2.directions(graph, projection)).to.eql([]); + }); + + }); + describe('#asJXON', function () { it('converts a node to jxon', function() { - var node = iD.Node({id: 'n-1', loc: [-77, 38], tags: {amenity: 'cafe'}}); + var node = iD.osmNode({id: 'n-1', loc: [-77, 38], tags: {amenity: 'cafe'}}); expect(node.asJXON()).to.eql({node: { '@id': '-1', '@lon': -77, @@ -192,13 +579,13 @@ describe('iD.osmNode', function () { }); it('includes changeset if provided', function() { - expect(iD.Node({loc: [0, 0]}).asJXON('1234').node['@changeset']).to.equal('1234'); + expect(iD.osmNode({loc: [0, 0]}).asJXON('1234').node['@changeset']).to.equal('1234'); }); }); describe('#asGeoJSON', function () { it('converts to a GeoJSON Point geometry', function () { - var node = iD.Node({tags: {amenity: 'cafe'}, loc: [1, 2]}), + var node = iD.osmNode({tags: {amenity: 'cafe'}, loc: [1, 2]}), json = node.asGeoJSON(); expect(json.type).to.equal('Point'); From 2edbcc4b82bf95bdd124dcfb5deac5c9f7abeb5c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 15:17:17 -0500 Subject: [PATCH 026/206] Match any `*:direction` key, rather than hardcoding a list --- modules/actions/reverse.js | 3 ++- modules/osm/node.js | 24 +++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/modules/actions/reverse.js b/modules/actions/reverse.js index 051f3e64be..3f92fcd91c 100644 --- a/modules/actions/reverse.js +++ b/modules/actions/reverse.js @@ -87,7 +87,8 @@ export function actionReverse(wayId, options) { // Update the direction based tags as appropriate then return an updated node return node.update({tags: _transform(node.tags, function(acc, tagValue, tagKey) { // See if this is a direction tag and reverse (or use existing value if not recognised) - if (tagKey.match(/direction$/) !== null) { + var re = /direction$/; + if (re.test(tagKey)) { acc[tagKey] = {forward: 'backward', backward: 'forward', left: 'right', right: 'left'}[tagValue] || tagValue; } else { // Use the reverseKey method to cater for situations such as traffic_sign:forward=stop diff --git a/modules/osm/node.js b/modules/osm/node.js index 362b4448c7..5083caada2 100644 --- a/modules/osm/node.js +++ b/modules/osm/node.js @@ -52,19 +52,25 @@ _extend(osmNode.prototype, { // Inspect tags and geometry to determine which direction(s) this node/vertex points directions: function(resolver, projection) { var val; + var i; + // which tag to use? if (this.isHighwayIntersection(resolver) && (this.tags.stop || '').toLowerCase() === 'all') { // all-way stop tag on a highway intersection val = 'all'; } else { - // direction tag - val = ( - this.tags['camera:direction'] || - this.tags['railway:signal:direction'] || - this.tags['traffic_signals:direction'] || - this.tags.direction || - '' - ).toLowerCase(); + // generic direction tag + val = (this.tags.direction || '').toLowerCase(); + + // better suffix-style direction tag + var re = /:direction$/i; + var keys = Object.keys(this.tags); + for (i = 0; i < keys.length; i++) { + if (re.test(keys[i])) { + val = this.tags[keys[i]].toLowerCase(); + break; + } + } } // swap cardinal for numeric directions @@ -105,7 +111,7 @@ _extend(osmNode.prototype, { var nodeIds = {}; resolver.parentWays(this).forEach(function(parent) { var nodes = parent.nodes; - for (var i = 0; i < nodes.length; i++) { + for (i = 0; i < nodes.length; i++) { if (nodes[i] === this.id) { // match current entity if (lookForward && i > 0) { nodeIds[nodes[i - 1]] = true; // look back to prev node From 4b5260a5abdafb9f2cd47897fa28a8b78a7146ff Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 15:48:08 -0500 Subject: [PATCH 027/206] Add wireframe viewfield marker (styling marker fill in svg is not currently supported) --- modules/svg/defs.js | 18 ++++++++++++++++++ modules/svg/vertices.js | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/modules/svg/defs.js b/modules/svg/defs.js index 6f43537693..65f37a7ba3 100644 --- a/modules/svg/defs.js +++ b/modules/svg/defs.js @@ -63,6 +63,24 @@ export function svgDefs(context) { .attr('stroke-width', '0.5px') .attr('stroke-opacity', '0.75'); + defs + .append('marker') + .attr('id', 'viewfield-marker-wireframe') + .attr('viewBox', '0 0 16 16') + .attr('refX', 8) + .attr('refY', 16) + .attr('markerWidth', 4) + .attr('markerHeight', 4) + .attr('markerUnits', 'strokeWidth') + .attr('orient', 'auto') + .append('path') + .attr('class', 'viewfield') + .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z') + .attr('fill', 'none') + .attr('stroke', '#fff') + .attr('stroke-width', '0.5px') + .attr('stroke-opacity', '0.75'); + // patterns var patterns = defs.selectAll('pattern') .data([ diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index e7fa7a7769..c0425154c0 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -59,6 +59,7 @@ export function svgVertices(projection, context) { siblings = siblings || {}; var icons = {}; var directions = {}; + var wireframe = context.surface().classed('fill-wireframe'); var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); @@ -197,8 +198,8 @@ export function svgVertices(projection, context) { .append('path') .attr('class', 'viewfield') .attr('d', 'M0,0H0') - .attr('marker-start', 'url(#viewfield-marker)') .merge(viewfields) + .attr('marker-start', 'url(#viewfield-marker' + (wireframe ? '-wireframe' : '') + ')') .attr('transform', function(d) { return 'rotate(' + d + ')'; }); } From a5bbc21728270ce51e5fe386a6e0db46a8fcb423 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 17:34:01 -0500 Subject: [PATCH 028/206] Remove unnecessary zoom parameter --- modules/renderer/map.js | 8 ++--- modules/svg/vertices.js | 56 +++++++++++++++++-------------- modules/ui/fields/restrictions.js | 2 +- test/spec/svg/vertices.js | 27 ++++++++------- 4 files changed, 50 insertions(+), 43 deletions(-) diff --git a/modules/renderer/map.js b/modules/renderer/map.js index e53297ff49..1538f5a227 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -183,7 +183,7 @@ export function rendererMap(context) { if (map.editable() && !transformed) { var hover = d3_event.target.__data__; surface.selectAll('.data-layer-osm') - .call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom()); + .call(drawVertices.drawHover, context.graph(), hover, map.extent()); dispatch.call('drawn', this, {full: false}); } }) @@ -191,7 +191,7 @@ export function rendererMap(context) { if (map.editable() && !transformed) { var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; surface.selectAll('.data-layer-osm') - .call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom()); + .call(drawVertices.drawHover, context.graph(), hover, map.extent()); dispatch.call('drawn', this, {full: false}); } }); @@ -207,7 +207,7 @@ export function rendererMap(context) { all = context.features().filter(all, graph); surface.selectAll('.data-layer-osm') - .call(drawVertices, graph, all, filter, map.extent(), map.zoom()) + .call(drawVertices, graph, all, filter, map.extent()) .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); dispatch.call('drawn', this, {full: false}); } @@ -297,7 +297,7 @@ export function rendererMap(context) { data = features.filter(data, graph); surface.selectAll('.data-layer-osm') - .call(drawVertices, graph, data, filter, map.extent(), map.zoom()) + .call(drawVertices, graph, data, filter, map.extent()) .call(drawLines, graph, data, filter) .call(drawAreas, graph, data, filter) .call(drawMidpoints, graph, data, filter, map.trimmedExtent()) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index c0425154c0..a26f9c08f3 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,10 +1,16 @@ import _values from 'lodash-es/values'; +import { select as d3_select } from 'd3-selection'; + import { dataFeatureIcons } from '../../data'; import { osmEntity } from '../osm'; import { svgPointTransform } from './index'; +var TAU = 2 * Math.PI; +function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } + + export function svgVertices(projection, context) { var radiuses = { // z16-, z17, z18+, tagged @@ -55,11 +61,12 @@ export function svgVertices(projection, context) { } - function draw(selection, vertices, klass, graph, zoom, siblings) { + function draw(selection, vertices, klass, graph, siblings) { siblings = siblings || {}; var icons = {}; var directions = {}; var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); @@ -95,23 +102,20 @@ export function svgVertices(projection, context) { var rads = radiuses[klass]; selection.selectAll('.' + klass) .each(function(entity) { - var i = z && getIcon(entity), - c = i ? 0.5 : 0, - r = rads[i ? 3 : z]; + var i = z && getIcon(entity); + var c = i ? 0.5 : 0; + var r = rads[i ? 3 : z]; // slightly increase the size of unconnected endpoints #3775 if (entity.isEndpoint(graph) && !entity.isConnected(graph)) { r += 1.5; } - this.setAttribute('cx', c); - this.setAttribute('cy', -c); - this.setAttribute('r', r); - if (i && klass === 'fill') { - this.setAttribute('visibility', 'hidden'); - } else { - this.removeAttribute('visibility'); - } + d3_select(this) + .attr('cx', c) + .attr('cy', -c) + .attr('r', r) + .attr('visibility', ((i && klass === 'fill') ? 'hidden' : null)); }); }); @@ -148,8 +152,8 @@ export function svgVertices(projection, context) { .append('use') .attr('transform', 'translate(-5, -6)') .attr('xlink:href', function(d) { - var picon = getIcon(d), - isMaki = dataFeatureIcons.indexOf(picon) !== -1; + var picon = getIcon(d); + var isMaki = dataFeatureIcons.indexOf(picon) !== -1; return '#' + picon + (isMaki ? '-11' : ''); }) .attr('width', '11px') @@ -204,14 +208,14 @@ export function svgVertices(projection, context) { } - function drawVertices(selection, graph, entities, filter, extent, zoom) { - var siblings = siblingAndChildVertices(context.selectedIDs(), graph, extent), - wireframe = context.surface().classed('fill-wireframe'), - vertices = []; + function drawVertices(selection, graph, entities, filter, extent) { + var siblings = siblingAndChildVertices(context.selectedIDs(), graph, extent); + var wireframe = context.surface().classed('fill-wireframe'); + var vertices = []; for (var i = 0; i < entities.length; i++) { - var entity = entities[i], - geometry = entity.geometry(graph); + var entity = entities[i]; + var geometry = entity.geometry(graph); if ((geometry === 'point') && (wireframe || entity.directions(graph, projection).length)) { vertices.push(entity); @@ -232,25 +236,25 @@ export function svgVertices(projection, context) { var layer = selection.selectAll('.layer-hit'); layer.selectAll('g.vertex.vertex-persistent') .filter(filter) - .call(draw, vertices, 'vertex-persistent', graph, zoom, siblings); + .call(draw, vertices, 'vertex-persistent', graph, siblings); - drawHover(selection, graph, extent, zoom); + drawHover(selection, graph, extent); } - function drawHover(selection, graph, extent, zoom) { + function drawHover(selection, graph, extent) { var hovered = _hover ? siblingAndChildVertices([_hover.id], graph, extent) : {}; var layer = selection.selectAll('.layer-hit'); layer.selectAll('g.vertex.vertex-hover') - .call(draw, _values(hovered), 'vertex-hover', graph, zoom); + .call(draw, _values(hovered), 'vertex-hover', graph); } - drawVertices.drawHover = function(selection, graph, target, extent, zoom) { + drawVertices.drawHover = function(selection, graph, target, extent) { if (target === _hover) return; _hover = target; - drawHover(selection, graph, extent, zoom); + drawHover(selection, graph, extent); }; return drawVertices; diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index f70b47f488..579e17aa5b 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -115,7 +115,7 @@ export function uiFieldRestrictions(field, context) { surface .call(utilSetDimensions, d) - .call(drawVertices, graph, [vertex], filter, extent, z) + .call(drawVertices, graph, [vertex], filter, extent) .call(drawLines, graph, intersection.ways, filter) .call(drawTurns, graph, intersection.turns(fromNodeID)); diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index f539db5fad..2c5104d22d 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -1,9 +1,14 @@ describe('iD.svgVertices', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]); + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context; + var surface; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); + beforeEach(function () { context = iD.Context(); @@ -15,14 +20,12 @@ describe('iD.svgVertices', function () { it('adds the .shared class to vertices that are members of two or more ways', function () { - var zoom = 17, - node = iD.Node({loc: [0, 0]}), - way1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - way2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}), - graph = iD.Graph([node, way1, way2]); - - surface.call(iD.svgVertices(projection, context), graph, [node], zoom); + var node = iD.Node({loc: [0, 0]}); + var way1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}); + var way2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}); + var graph = iD.Graph([node, way1, way2]); + surface.call(iD.svgVertices(projection, context), graph, [node]); expect(surface.select('.vertex').classed('shared')).to.be.true; }); }); From b394cb6dfa10edb27d7c2b75d9ddce985d7a8c2e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Dec 2017 17:46:15 -0500 Subject: [PATCH 029/206] Variable cleanup, elminiate lodash _filter --- modules/svg/points.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/modules/svg/points.js b/modules/svg/points.js index 054a7359e1..24c8941746 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -1,5 +1,3 @@ -import _filter from 'lodash-es/filter'; - import { dataFeatureIcons } from '../../data'; import { osmEntity } from '../osm'; import { svgPointTransform, svgTagClasses } from './index'; @@ -20,10 +18,10 @@ export function svgPoints(projection, context) { return function drawPoints(selection, graph, entities, filter) { - var wireframe = context.surface().classed('fill-wireframe'), - points = wireframe ? [] : _filter(entities, function(e) { - return e.geometry(graph) === 'point' && !e.directions(graph, projection).length; - }); + var wireframe = context.surface().classed('fill-wireframe'); + var points = wireframe ? [] : entities.filter(function(e) { + return e.geometry(graph) === 'point' && !e.directions(graph, projection).length; + }); points.sort(sortY); @@ -75,8 +73,8 @@ export function svgPoints(projection, context) { groups.select('.stroke'); groups.select('.icon') .attr('xlink:href', function(entity) { - var preset = context.presets().match(entity, graph), - picon = preset && preset.icon; + var preset = context.presets().match(entity, graph); + var picon = preset && preset.icon; if (!picon) return ''; From 899abc7ef524465bfd29553acc3cfc4b7a4ca94c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 12 Dec 2017 17:39:36 -0500 Subject: [PATCH 030/206] WIP to render vertices while dragging (re: #3003 / #4602) For now, drawHover is commented out. Still not sure what I will do with it. This means that things flicker a bit when dragging, also connecting nodes (and closing lines) does not currently work. There was lot going on preventing the vertices from rendering while dragging. 1. `modeDragNode` needed a proper `selectedIDs()` function that works like other modes.. Many other places in iD (including the vertex renderer) call `context.selectedIDs()`.. This means that `modeDragNode` needs a new function `restoreSelectedIDs()` to do what `selectedIDs()` was previously doing (a place to store selectedIDs so that we can reselect those entities after the user is done dragging a node in select mode) 2. Just so many things in svg/vertices.js - siblingAndChildVertices was missing some things for points that we render as vertices (points in wireframe, points with directions) - the sibling vertices weren't being included in the `filter` function, so would disappear when doing differenced/extent redraws - probably some other things --- css/20_map.css | 6 +- css/70_fills.css | 4 +- modules/modes/drag_node.js | 88 +++++++++++--------- modules/modes/select.js | 2 +- modules/renderer/map.js | 49 +++++------ modules/svg/vertices.js | 164 ++++++++++++++++++++----------------- 6 files changed, 169 insertions(+), 144 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 6d3a39037c..a29a1c85a4 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -88,7 +88,7 @@ g.midpoint .shadow { fill-opacity: 0; } -g.vertex.vertex-hover { +/*g.vertex.vertex-hover { display: none; } @@ -109,7 +109,7 @@ g.vertex.vertex-hover { .mode-drag-node .hover-disabled g.vertex.vertex-hover { display: none; } - +*/ g.vertex.related:not(.selected) .shadow, g.vertex.hover:not(.selected) .shadow, g.midpoint.related:not(.selected) .shadow, @@ -126,7 +126,7 @@ g.vertex.selected .shadow { .mode-add-area g.midpoint, .mode-add-line g.midpoint, .mode-add-point g.midpoint { - display: none; + display: none; } /* lines */ diff --git a/css/70_fills.css b/css/70_fills.css index 9402eec5d1..c0cb1b167b 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -38,12 +38,12 @@ /* Modes */ -.mode-draw-line .vertex.active, +/*.mode-draw-line .vertex.active, .mode-draw-area .vertex.active, .mode-drag-node .vertex.active { display: none; } - +*/ .mode-draw-line .way.active, .mode-draw-area .way.active, .mode-drag-node .active { diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index baed024aee..50cd8231b3 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -1,5 +1,3 @@ -import _map from 'lodash-es/map'; - import { event as d3_event, select as d3_select @@ -36,15 +34,16 @@ export function modeDragNode(context) { id: 'drag-node', button: 'browse' }; + var hover = behaviorHover(context).altDisables(true).on('hover', context.ui().sidebar.hover); + var edit = behaviorEdit(context); - var nudgeInterval, - activeIDs, - wasMidpoint, - isCancelled, - lastLoc, - selectedIDs = [], - hover = behaviorHover(context).altDisables(true).on('hover', context.ui().sidebar.hover), - edit = behaviorEdit(context); + var _nudgeInterval; + var _restoreSelectedIDs = []; + var _activeIDs = []; + var _wasMidpoint = false; + var _isCancelled = false; + var _dragEntity; + var _lastLoc; function vecSub(a, b) { @@ -52,9 +51,9 @@ export function modeDragNode(context) { } function edge(point, size) { - var pad = [80, 20, 50, 20], // top, right, bottom, left - x = 0, - y = 0; + var pad = [80, 20, 50, 20]; // top, right, bottom, left + var x = 0; + var y = 0; if (point[0] > size[0] - pad[1]) x = -10; @@ -74,8 +73,8 @@ export function modeDragNode(context) { function startNudge(entity, nudge) { - if (nudgeInterval) window.clearInterval(nudgeInterval); - nudgeInterval = window.setInterval(function() { + if (_nudgeInterval) window.clearInterval(_nudgeInterval); + _nudgeInterval = window.setInterval(function() { context.pan(nudge); doMove(entity, nudge); }, 50); @@ -83,9 +82,9 @@ export function modeDragNode(context) { function stopNudge() { - if (nudgeInterval) { - window.clearInterval(nudgeInterval); - nudgeInterval = null; + if (_nudgeInterval) { + window.clearInterval(_nudgeInterval); + _nudgeInterval = null; } } @@ -106,19 +105,19 @@ export function modeDragNode(context) { function start(entity) { - wasMidpoint = entity.type === 'midpoint'; + _wasMidpoint = entity.type === 'midpoint'; var hasHidden = context.features().hasHiddenConnections(entity, context.graph()); - isCancelled = d3_event.sourceEvent.shiftKey || hasHidden; + _isCancelled = d3_event.sourceEvent.shiftKey || hasHidden; - if (isCancelled) { + if (_isCancelled) { if (hasHidden) { uiFlash().text(t('modes.drag_node.connected_to_hidden'))(); } return behavior.cancel(); } - if (wasMidpoint) { + if (_wasMidpoint) { var midpoint = entity; entity = osmNode(); context.perform(actionAddMidpoint(midpoint, entity)); @@ -130,10 +129,13 @@ export function modeDragNode(context) { context.perform(actionNoop()); } + _dragEntity = entity; + // activeIDs generate no pointer events. This prevents the node or vertex // being dragged from trying to connect to itself or its parent element. - activeIDs = _map(context.graph().parentWays(entity), 'id'); - activeIDs.push(entity.id); + _activeIDs = context.graph().parentWays(entity) + .map(function(parent) { return parent.id; }); + _activeIDs.push(entity.id); setActiveElements(); context.enter(mode); @@ -153,12 +155,12 @@ export function modeDragNode(context) { function doMove(entity, nudge) { nudge = nudge || [0, 0]; - var currPoint = (d3_event && d3_event.point) || context.projection(lastLoc), - currMouse = vecSub(currPoint, nudge), - loc = context.projection.invert(currMouse), - d = datum(); + var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); + var currMouse = vecSub(currPoint, nudge); + var loc = context.projection.invert(currMouse); + var d = datum(); - if (!nudgeInterval) { + if (!_nudgeInterval) { if (d.type === 'node' && d.id !== entity.id) { loc = d.loc; } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { @@ -171,14 +173,15 @@ export function modeDragNode(context) { moveAnnotation(entity) ); - lastLoc = loc; + _lastLoc = loc; } function move(entity) { - if (isCancelled) return; + if (_isCancelled) return; + d3_event.sourceEvent.stopPropagation(); - lastLoc = context.projection.invert(d3_event.point); + _lastLoc = context.projection.invert(d3_event.point); doMove(entity); var nudge = edge(d3_event.point, context.map().dimensions()); @@ -191,7 +194,7 @@ export function modeDragNode(context) { function end(entity) { - if (isCancelled) return; + if (_isCancelled) return; var d = datum(); @@ -208,7 +211,7 @@ export function modeDragNode(context) { connectAnnotation(d) ); - } else if (wasMidpoint) { + } else if (_wasMidpoint) { context.replace( actionNoop(), t('operations.add.annotation.vertex') @@ -221,7 +224,7 @@ export function modeDragNode(context) { ); } - var reselection = selectedIDs.filter(function(id) { + var reselection = _restoreSelectedIDs.filter(function(id) { return context.graph().hasEntity(id); }); @@ -240,7 +243,7 @@ export function modeDragNode(context) { function setActiveElements() { - context.surface().selectAll(utilEntitySelector(activeIDs)) + context.surface().selectAll(utilEntitySelector(_activeIDs)) .classed('active', true); } @@ -287,9 +290,16 @@ export function modeDragNode(context) { }; - mode.selectedIDs = function(_) { - if (!arguments.length) return selectedIDs; - selectedIDs = _; + mode.selectedIDs = function() { + if (!arguments.length) return _dragEntity ? [_dragEntity.id] : []; + // no assign + return mode; + }; + + + mode.restoreSelectedIDs = function(_) { + if (!arguments.length) return _restoreSelectedIDs; + _restoreSelectedIDs = _; return mode; }; diff --git a/modules/modes/select.js b/modules/modes/select.js index 281e77709e..fa5a3ddd92 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -63,7 +63,7 @@ export function modeSelect(context, selectedIDs) { behaviorHover(context), behaviorSelect(context), behaviorLasso(context), - modeDragNode(context).selectedIDs(selectedIDs).behavior + modeDragNode(context).restoreSelectedIDs(selectedIDs).behavior ], inspector, editMenu, diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 1538f5a227..0240844df3 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -178,38 +178,38 @@ export function rendererMap(context) { }) .on('mousemove.map', function() { mousemove = d3_event; - }) - .on('mouseover.vertices', function() { - if (map.editable() && !transformed) { - var hover = d3_event.target.__data__; - surface.selectAll('.data-layer-osm') - .call(drawVertices.drawHover, context.graph(), hover, map.extent()); - dispatch.call('drawn', this, {full: false}); - } - }) - .on('mouseout.vertices', function() { - if (map.editable() && !transformed) { - var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; - surface.selectAll('.data-layer-osm') - .call(drawVertices.drawHover, context.graph(), hover, map.extent()); - dispatch.call('drawn', this, {full: false}); - } }); + // .on('mouseover.vertices', function() { + // if (map.editable() && !transformed) { + // var hover = d3_event.target.__data__; + // surface.selectAll('.data-layer-osm') + // .call(drawVertices.drawHover, context.graph(), hover, map.extent()); + // dispatch.call('drawn', this, { full: false }); + // } + // }) + // .on('mouseout.vertices', function() { + // if (map.editable() && !transformed) { + // var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; + // surface.selectAll('.data-layer-osm') + // .call(drawVertices.drawHover, context.graph(), hover, map.extent()); + // dispatch.call('drawn', this, { full: false }); + // } + // }); supersurface .call(context.background()); context.on('enter.map', function() { if (map.editable() && !transformed) { - var all = context.intersects(map.extent()), - filter = utilFunctor(true), - graph = context.graph(); + var all = context.intersects(map.extent()); + var filter = utilFunctor(true); + var graph = context.graph(); all = context.features().filter(all, graph); surface.selectAll('.data-layer-osm') .call(drawVertices, graph, all, filter, map.extent()) .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); - dispatch.call('drawn', this, {full: false}); + dispatch.call('drawn', this, { full: false }); } }); @@ -265,10 +265,11 @@ export function rendererMap(context) { function drawVector(difference, extent) { - var graph = context.graph(), - features = context.features(), - all = context.intersects(map.extent()), - data, filter; + var graph = context.graph(); + var features = context.features(); + var all = context.intersects(map.extent()); + var data; + var filter; if (difference) { var complete = difference.complete(map.extent()); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index a26f9c08f3..ad0c41786b 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,3 +1,4 @@ +import _clone from 'lodash-es/clone'; import _values from 'lodash-es/values'; import { select as d3_select } from 'd3-selection'; @@ -13,55 +14,16 @@ function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } export function svgVertices(projection, context) { var radiuses = { - // z16-, z17, z18+, tagged - shadow: [6, 7.5, 7.5, 11.5], - stroke: [2.5, 3.5, 3.5, 7], - fill: [1, 1.5, 1.5, 1.5] + // z16-, z17, z18+, tagged + shadow: [6, 7.5, 7.5, 11.5], + stroke: [2.5, 3.5, 3.5, 7], + fill: [1, 1.5, 1.5, 1.5] }; var _hover; - function siblingAndChildVertices(ids, graph, extent) { - var vertices = {}; - - function addChildVertices(entity) { - if (!context.features().isHiddenFeature(entity, graph, entity.geometry(graph))) { - var i; - if (entity.type === 'way') { - for (i = 0; i < entity.nodes.length; i++) { - addChildVertices(graph.entity(entity.nodes[i])); - } - } else if (entity.type === 'relation') { - for (i = 0; i < entity.members.length; i++) { - var member = context.hasEntity(entity.members[i].id); - if (member) { - addChildVertices(member); - } - } - } else if (entity.intersects(extent, graph)) { - vertices[entity.id] = entity; - } - } - } - - ids.forEach(function(id) { - var entity = context.hasEntity(id); - if (entity && entity.type === 'node') { - vertices[entity.id] = entity; - context.graph().parentWays(entity).forEach(function(entity) { - addChildVertices(entity); - }); - } else if (entity) { - addChildVertices(entity); - } - }); - - return vertices; - } - - - function draw(selection, vertices, klass, graph, siblings) { + function draw(selection, vertices, klass, graph, siblings, filter) { siblings = siblings || {}; var icons = {}; var directions = {}; @@ -127,7 +89,8 @@ export function svgVertices(projection, context) { } - var groups = selection + var groups = selection.selectAll('.vertex.' + klass) + .filter(filter) .data(vertices, osmEntity.key); // exit @@ -178,7 +141,7 @@ export function svgVertices(projection, context) { // Directional vertices get viewfields var dgroups = groups.filter(function(d) { return getDirections(d); }) .selectAll('.viewfieldgroup') - .data(function(d) { return klass === 'vertex-hover' ? [] : [d]; }, osmEntity.key); + .data(function(d) { return /*klass === 'vertex-hover' ? [] : */[d]; }, osmEntity.key); // exit dgroups.exit() @@ -209,53 +172,104 @@ export function svgVertices(projection, context) { function drawVertices(selection, graph, entities, filter, extent) { - var siblings = siblingAndChildVertices(context.selectedIDs(), graph, extent); var wireframe = context.surface().classed('fill-wireframe'); - var vertices = []; + var siblings = {}; + getSiblingAndChildVertices(context.selectedIDs(), graph, extent); + + // always render selected and sibling vertices.. + var vertices = _clone(siblings); + var filterWithSiblings = function(d) { return d.id in siblings || filter(d); }; + + // also render important vertices from the `entities` list.. for (var i = 0; i < entities.length; i++) { var entity = entities[i]; var geometry = entity.geometry(graph); - if ((geometry === 'point') && (wireframe || entity.directions(graph, projection).length)) { - vertices.push(entity); - continue; + if ((geometry === 'point') && renderAsVertex(entity)) { + vertices[entity.id] = entity; + + } else if ((geometry === 'vertex') && + (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) ) { + vertices[entity.id] = entity; } + } - if (geometry !== 'vertex') - continue; - if (entity.id in siblings || - entity.hasInterestingTags() || - entity.isEndpoint(graph) || - entity.isConnected(graph)) { - vertices.push(entity); - } + selection.selectAll('.layer-hit') + .call(draw, _values(vertices), 'vertex-persistent', graph, siblings, filterWithSiblings); + +// drawHover(selection, graph, extent, true); + + + function renderAsVertex(entity) { + var geometry = entity.geometry(graph); + return (geometry === 'vertex') || + (geometry === 'point' && (wireframe || entity.directions(graph, projection).length)); } - var layer = selection.selectAll('.layer-hit'); - layer.selectAll('g.vertex.vertex-persistent') - .filter(filter) - .call(draw, vertices, 'vertex-persistent', graph, siblings); - drawHover(selection, graph, extent); - } + function getSiblingAndChildVertices(ids, graph, extent) { + + function addChildVertices(entity) { + var geometry = entity.geometry(graph); + if (!context.features().isHiddenFeature(entity, graph, geometry)) { + var i; + if (entity.type === 'way') { + for (i = 0; i < entity.nodes.length; i++) { + var child = context.hasEntity(entity.nodes[i]); + if (child) { + addChildVertices(child); + } + } + } else if (entity.type === 'relation') { + for (i = 0; i < entity.members.length; i++) { + var member = context.hasEntity(entity.members[i].id); + if (member) { + addChildVertices(member); + } + } + } else if (renderAsVertex(entity) && entity.intersects(extent, graph)) { + siblings[entity.id] = entity; + } + } + } + ids.forEach(function(id) { + var entity = context.hasEntity(id); + if (!entity) return; - function drawHover(selection, graph, extent) { - var hovered = _hover ? siblingAndChildVertices([_hover.id], graph, extent) : {}; - var layer = selection.selectAll('.layer-hit'); + if (entity.type === 'node') { + if (renderAsVertex(entity)) { + siblings[entity.id] = entity; + graph.parentWays(entity).forEach(function(entity) { + addChildVertices(entity); + }); + } + } else { // way, relation + addChildVertices(entity); + } + }); - layer.selectAll('g.vertex.vertex-hover') - .call(draw, _values(hovered), 'vertex-hover', graph); + } } - drawVertices.drawHover = function(selection, graph, target, extent) { - if (target === _hover) return; - _hover = target; - drawHover(selection, graph, extent); - }; +// function drawHover(selection, graph, extent, follow) { +// var hovered = _hover ? siblingAndChildVertices([_hover.id], graph, extent) : {}; +// var wireframe = context.surface().classed('fill-wireframe'); +// var layer = selection.selectAll('.layer-hit'); +// +// layer.selectAll('g.vertex.vertex-hover') +// .call(draw, _values(hovered), 'vertex-hover', graph, {}, false); +// } +// +// +// drawVertices.drawHover = function(selection, graph, target, extent) { +// if (target === _hover) return; +// _hover = target; +// drawHover(selection, graph, extent); +// }; return drawVertices; } From 789f1e5f6f380cd8eb5879e5a8b44ddb05860911 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 08:57:43 -0500 Subject: [PATCH 031/206] Make sure points/vertices render the right things at different zooms --- modules/svg/points.js | 17 ++++++++++++++--- modules/svg/vertices.js | 14 ++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/modules/svg/points.js b/modules/svg/points.js index 24c8941746..909533f3a1 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -3,6 +3,10 @@ import { osmEntity } from '../osm'; import { svgPointTransform, svgTagClasses } from './index'; +var TAU = 2 * Math.PI; +function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } + + export function svgPoints(projection, context) { function markerPath(selection, klass) { @@ -19,9 +23,16 @@ export function svgPoints(projection, context) { return function drawPoints(selection, graph, entities, filter) { var wireframe = context.surface().classed('fill-wireframe'); - var points = wireframe ? [] : entities.filter(function(e) { - return e.geometry(graph) === 'point' && !e.directions(graph, projection).length; - }); + var zoom = ktoz(projection.scale()); + + // points with a direction will render as vertices at higher zooms + function renderAsPoint(entity) { + return entity.geometry(graph) === 'point' && + !(zoom >= 18 && entity.directions(graph, projection).length); + } + + // all points will render as vertices in wireframe mode too + var points = wireframe ? [] : entities.filter(renderAsPoint); points.sort(sortY); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index ad0c41786b..7f2d5a49c6 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -83,9 +83,6 @@ export function svgVertices(projection, context) { selection.selectAll('use') .attr('visibility', (z === 0 ? 'hidden' : null)); - - selection.selectAll('.viewfieldgroup') - .attr('visibility', (zoom < 18 ? 'hidden' : null)); } @@ -141,7 +138,7 @@ export function svgVertices(projection, context) { // Directional vertices get viewfields var dgroups = groups.filter(function(d) { return getDirections(d); }) .selectAll('.viewfieldgroup') - .data(function(d) { return /*klass === 'vertex-hover' ? [] : */[d]; }, osmEntity.key); + .data(function data(d) { return zoom < 18 ? [] : [d]; }, osmEntity.key); // exit dgroups.exit() @@ -173,6 +170,7 @@ export function svgVertices(projection, context) { function drawVertices(selection, graph, entities, filter, extent) { var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); var siblings = {}; getSiblingAndChildVertices(context.selectedIDs(), graph, extent); @@ -202,10 +200,14 @@ export function svgVertices(projection, context) { // drawHover(selection, graph, extent, true); + // Points can also render as vertices: + // 1. in wireframe mode or + // 2. at higher zooms if they have a direction function renderAsVertex(entity) { var geometry = entity.geometry(graph); - return (geometry === 'vertex') || - (geometry === 'point' && (wireframe || entity.directions(graph, projection).length)); + return geometry === 'vertex' || (geometry === 'point' && ( + wireframe || (zoom > 18 && entity.directions(graph, projection).length) + )); } From 5d5546d54dd5f07d87b219f7592e66e78a6ccdeb Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 09:57:14 -0500 Subject: [PATCH 032/206] Minor code formatting --- modules/svg/labels.js | 182 +++++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/modules/svg/labels.js b/modules/svg/labels.js index a420227211..4c4cb70ced 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -28,13 +28,13 @@ import { export function svgLabels(projection, context) { - var path = d3_geoPath(projection), - detected = utilDetect(), - baselineHack = (detected.ie || detected.browser.toLowerCase() === 'edge'), - rdrawn = rbush(), - rskipped = rbush(), - textWidthCache = {}, - entitybboxes = {}; + var path = d3_geoPath(projection); + var detected = utilDetect(); + var baselineHack = (detected.ie || detected.browser.toLowerCase() === 'edge'); + var _rdrawn = rbush(); + var _rskipped = rbush(); + var _textWidthCache = {}; + var _entitybboxes = {}; // Listed from highest to lowest priority var labelStack = [ @@ -87,8 +87,8 @@ export function svgLabels(projection, context) { function textWidth(text, size, elem) { - var c = textWidthCache[size]; - if (!c) c = textWidthCache[size] = {}; + var c = _textWidthCache[size]; + if (!c) c = _textWidthCache[size] = {}; if (c[text]) { return c[text]; @@ -207,12 +207,12 @@ export function svgLabels(projection, context) { icons .attr('transform', get(labels, 'transform')) .attr('xlink:href', function(d) { - var preset = context.presets().match(d, context.graph()), - picon = preset && preset.icon; + var preset = context.presets().match(d, context.graph()); + var picon = preset && preset.icon; - if (!picon) + if (!picon) { return ''; - else { + } else { var isMaki = dataFeatureIcons.indexOf(picon) !== -1; return '#' + picon + (isMaki ? '-15' : ''); } @@ -221,12 +221,11 @@ export function svgLabels(projection, context) { function drawCollisionBoxes(selection, rtree, which) { - var showDebug = context.getDebug('collision'), - classes = 'debug ' + which + ' ' + - (which === 'debug-skipped' ? 'orange' : 'yellow'); + var showDebug = context.getDebug('collision'); + var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow'); var debug = selection.selectAll('.layer-label-debug') - .data(showDebug ? [true] : []); + .data(showDebug ? [true] : []); debug.exit() .remove(); @@ -266,26 +265,27 @@ export function svgLabels(projection, context) { function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) { var lowZoom = context.surface().classed('low-zoom'); + var labelable = [] + var i, j, k, entity, geometry; - var labelable = [], i, j, k, entity, geometry; for (i = 0; i < labelStack.length; i++) { labelable.push([]); } if (fullRedraw) { - rdrawn.clear(); - rskipped.clear(); - entitybboxes = {}; + _rdrawn.clear(); + _rskipped.clear(); + _entitybboxes = {}; } else { for (i = 0; i < entities.length; i++) { entity = entities[i]; var toRemove = [] - .concat(entitybboxes[entity.id] || []) - .concat(entitybboxes[entity.id + 'I'] || []); + .concat(_entitybboxes[entity.id] || []) + .concat(_entitybboxes[entity.id + 'I'] || []); for (j = 0; j < toRemove.length; j++) { - rdrawn.remove(toRemove[j]); - rskipped.remove(toRemove[j]); + _rdrawn.remove(toRemove[j]); + _rskipped.remove(toRemove[j]); } } } @@ -296,17 +296,17 @@ export function svgLabels(projection, context) { geometry = entity.geometry(graph); if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point - var preset = geometry === 'area' && context.presets().match(entity, graph), - icon = preset && !blacklisted(preset) && preset.icon; + var preset = geometry === 'area' && context.presets().match(entity, graph); + var icon = preset && !blacklisted(preset) && preset.icon; if (!icon && !utilDisplayName(entity)) continue; for (k = 0; k < labelStack.length; k++) { - var matchGeom = labelStack[k][0], - matchKey = labelStack[k][1], - matchVal = labelStack[k][2], - hasVal = entity.tags[matchKey]; + var matchGeom = labelStack[k][0]; + var matchKey = labelStack[k][1]; + var matchVal = labelStack[k][2]; + var hasVal = entity.tags[matchKey]; if (geometry === matchGeom && hasVal && (matchVal === '*' || matchVal === hasVal)) { labelable[k].push(entity); @@ -330,14 +330,15 @@ export function svgLabels(projection, context) { // Try and find a valid label for labellable entities for (k = 0; k < labelable.length; k++) { var fontSize = labelStack[k][3]; + for (i = 0; i < labelable[k].length; i++) { entity = labelable[k][i]; geometry = entity.geometry(graph); - var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName, - name = getName(entity), - width = name && textWidth(name, fontSize), - p = null; + var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName; + var name = getName(entity); + var width = name && textWidth(name, fontSize); + var p = null; if (geometry === 'point') { p = getPointLabel(entity, width, fontSize, geometry); @@ -361,24 +362,24 @@ export function svgLabels(projection, context) { function getPointLabel(entity, width, height, geometry) { - var y = (geometry === 'point' ? -12 : 0), - pointOffsets = { - ltr: [15, y, 'start'], - rtl: [-15, y, 'end'] - }; - - var coord = projection(entity.loc), - margin = 2, - offset = pointOffsets[textDirection], - p = { - height: height, - width: width, - x: coord[0] + offset[0], - y: coord[1] + offset[1], - textAnchor: offset[2] - }, - bbox; - + var y = (geometry === 'point' ? -12 : 0); + var pointOffsets = { + ltr: [15, y, 'start'], + rtl: [-15, y, 'end'] + }; + + var coord = projection(entity.loc); + var margin = 2; + var offset = pointOffsets[textDirection]; + var p = { + height: height, + width: width, + x: coord[0] + offset[0], + y: coord[1] + offset[1], + textAnchor: offset[2] + }; + + var bbox; if (textDirection === 'rtl') { bbox = { minX: p.x - width - margin, @@ -402,9 +403,9 @@ export function svgLabels(projection, context) { function getLineLabel(entity, width, height) { - var viewport = geoExtent(context.projection.clipExtent()).polygon(), - nodes = _map(graph.childNodes(entity), 'loc').map(projection), - length = geoPathLength(nodes); + var viewport = geoExtent(context.projection.clipExtent()).polygon(); + var nodes = _map(graph.childNodes(entity), 'loc').map(projection); + var length = geoPathLength(nodes); if (length < width + 20) return; @@ -414,9 +415,9 @@ export function svgLabels(projection, context) { var margin = 3; for (var i = 0; i < lineOffsets.length; i++) { - var offset = lineOffsets[i], - middle = offset / 100 * length, - start = middle - width / 2; + var offset = lineOffsets[i]; + var middle = offset / 100 * length; + var start = middle - width / 2; if (start < 0 || start + width > length) continue; @@ -431,8 +432,8 @@ export function svgLabels(projection, context) { sub = sub.reverse(); } - var bboxes = [], - boxsize = (height + 2) / 2; + var bboxes = []; + var boxsize = (height + 2) / 2; for (var j = 0; j < sub.length - 1; j++) { var a = sub[j]; @@ -474,12 +475,12 @@ export function svgLabels(projection, context) { } function subpath(nodes, from, to) { - var sofar = 0, - start, end, i0, i1; + var sofar = 0; + var start, end, i0, i1; for (var i = 0; i < nodes.length - 1; i++) { - var a = nodes[i], - b = nodes[i + 1]; + var a = nodes[i]; + var b = nodes[i + 1]; var current = geoEuclideanDistance(a, b); var portion; if (!start && sofar + current >= from) { @@ -510,17 +511,17 @@ export function svgLabels(projection, context) { function getAreaLabel(entity, width, height) { - var centroid = path.centroid(entity.asGeoJSON(graph, true)), - extent = entity.extent(graph), - areaWidth = projection(extent[1])[0] - projection(extent[0])[0]; + var centroid = path.centroid(entity.asGeoJSON(graph, true)); + var extent = entity.extent(graph); + var areaWidth = projection(extent[1])[0] - projection(extent[0])[0]; if (isNaN(centroid[0]) || areaWidth < 20) return; - var preset = context.presets().match(entity, context.graph()), - picon = preset && preset.icon, - iconSize = 17, - margin = 2, - p = {}; + var preset = context.presets().match(entity, context.graph()); + var picon = preset && preset.icon; + var iconSize = 17; + var margin = 2; + var p = {}; if (picon) { // icon and label.. if (addIcon()) { @@ -576,11 +577,10 @@ export function svgLabels(projection, context) { function tryInsert(bboxes, id, saveSkipped) { - var skipped = false, - bbox; + var skipped = false; for (var i = 0; i < bboxes.length; i++) { - bbox = bboxes[i]; + var bbox = bboxes[i]; bbox.id = id; // Check that label is visible @@ -588,28 +588,28 @@ export function svgLabels(projection, context) { skipped = true; break; } - if (rdrawn.collides(bbox)) { + if (_rdrawn.collides(bbox)) { skipped = true; break; } } - entitybboxes[id] = bboxes; + _entitybboxes[id] = bboxes; if (skipped) { if (saveSkipped) { - rskipped.load(bboxes); + _rskipped.load(bboxes); } } else { - rdrawn.load(bboxes); + _rdrawn.load(bboxes); } return !skipped; } - var label = selection.selectAll('.layer-label'), - halo = selection.selectAll('.layer-halo'); + var label = selection.selectAll('.layer-label'); + var halo = selection.selectAll('.layer-halo'); // points drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point); @@ -627,8 +627,8 @@ export function svgLabels(projection, context) { drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo', positions.area); // debug - drawCollisionBoxes(label, rskipped, 'debug-skipped'); - drawCollisionBoxes(label, rdrawn, 'debug-drawn'); + drawCollisionBoxes(label, _rskipped, 'debug-skipped'); + drawCollisionBoxes(label, _rdrawn, 'debug-drawn'); selection.call(filterLabels); } @@ -641,17 +641,17 @@ export function svgLabels(projection, context) { layers.selectAll('.proximate') .classed('proximate', false); - var mouse = context.mouse(), - graph = context.graph(), - selectedIDs = context.selectedIDs(), - ids = [], - pad, bbox; + var mouse = context.mouse(); + var graph = context.graph(); + var selectedIDs = context.selectedIDs(); + var ids = []; + var pad, bbox; // hide labels near the mouse if (mouse) { pad = 20; bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad }; - ids.push.apply(ids, _map(rdrawn.search(bbox), 'id')); + ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); } // hide labels along selected ways, or near selected vertices @@ -666,7 +666,7 @@ export function svgLabels(projection, context) { var point = context.projection(entity.loc); pad = 10; bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad }; - ids.push.apply(ids, _map(rdrawn.search(bbox), 'id')); + ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); } } From 450392d2e59e1ea3ff23a6cb8ef311d149fb0c8f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 10:23:25 -0500 Subject: [PATCH 033/206] Fix label placement for directional points rendered as vertices --- modules/svg/labels.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 4c4cb70ced..ccf2425fa4 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -27,6 +27,10 @@ import { } from '../util'; +var TAU = 2 * Math.PI; +function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } + + export function svgLabels(projection, context) { var path = d3_geoPath(projection); var detected = utilDetect(); @@ -264,8 +268,10 @@ export function svgLabels(projection, context) { function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) { - var lowZoom = context.surface().classed('low-zoom'); - var labelable = [] + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); + + var labelable = []; var i, j, k, entity, geometry; for (i = 0; i < labelStack.length; i++) { @@ -340,13 +346,23 @@ export function svgLabels(projection, context) { var width = name && textWidth(name, fontSize); var p = null; - if (geometry === 'point') { - p = getPointLabel(entity, width, fontSize, geometry); - } else if (geometry === 'vertex' && !lowZoom) { - // don't label vertices at low zoom because they don't have icons - p = getPointLabel(entity, width, fontSize, geometry); + if (geometry === 'point' || geometry === 'vertex') { + if (wireframe) continue; // no point or vertex labels in wireframe + + var hasDirections = entity.directions(graph, projection).length; + var renderAs; + + if (geometry === 'point' && !(zoom >= 18 && hasDirections)) { + renderAs = 'point'; + } else { + if (zoom < 17) continue; // no vertex labels at low zooms (vertices have no icons) + renderAs = 'vertex'; + } + p = getPointLabel(entity, width, fontSize, renderAs); + } else if (geometry === 'line') { p = getLineLabel(entity, width, fontSize); + } else if (geometry === 'area') { p = getAreaLabel(entity, width, fontSize); } From 006ee691bfa04d9939105334580132e4a7226d93 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 14:31:17 -0500 Subject: [PATCH 034/206] Avoid placing labels in interesting points/vertices (closes #4271) --- modules/svg/labels.js | 121 +++++++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 38 deletions(-) diff --git a/modules/svg/labels.js b/modules/svg/labels.js index ccf2425fa4..e6db96a052 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -272,6 +272,7 @@ export function svgLabels(projection, context) { var zoom = ktoz(projection.scale()); var labelable = []; + var renderNodeAs = {}; var i, j, k, entity, geometry; for (i = 0; i < labelStack.length; i++) { @@ -282,6 +283,7 @@ export function svgLabels(projection, context) { _rdrawn.clear(); _rskipped.clear(); _entitybboxes = {}; + } else { for (i = 0; i < entities.length; i++) { entity = entities[i]; @@ -296,12 +298,42 @@ export function svgLabels(projection, context) { } } - // Split entities into groups specified by labelStack + // Loop through all the entities to do some preprocessing for (i = 0; i < entities.length; i++) { entity = entities[i]; geometry = entity.geometry(graph); - if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point + // Insert collision boxes around interesting points/vertices + if (geometry === 'point' || (geometry === 'vertex' && entity.hasInterestingTags())) { + var hasDirections = entity.directions(graph, projection).length; + var markerPadding; + + if (!wireframe && geometry === 'point' && !(zoom >= 18 && hasDirections)) { + renderNodeAs[entity.id] = 'point'; + markerPadding = 20; // extra y for marker height + } else { + renderNodeAs[entity.id] = 'vertex'; + markerPadding = 0; + } + + var coord = projection(entity.loc); + var nodePadding = 10; + var bbox = { + minX: coord[0] - nodePadding, + minY: coord[1] - nodePadding - markerPadding, + maxX: coord[0] + nodePadding, + maxY: coord[1] + nodePadding + }; + + doInsert(bbox, entity.id + 'P'); + } + + // From here on, treat vertices like points + if (geometry === 'vertex') { + geometry = 'point'; + } + + // Determine which entities are label-able var preset = geometry === 'area' && context.presets().match(entity, graph); var icon = preset && !blacklisted(preset) && preset.icon; @@ -347,17 +379,12 @@ export function svgLabels(projection, context) { var p = null; if (geometry === 'point' || geometry === 'vertex') { - if (wireframe) continue; // no point or vertex labels in wireframe - - var hasDirections = entity.directions(graph, projection).length; - var renderAs; + // no point or vertex labels in wireframe mode + // no vertex labels at low zooms (vertices have no icons) + if (wireframe) continue; + var renderAs = renderNodeAs[entity.id]; + if (renderAs === 'vertex' && zoom < 17) continue; - if (geometry === 'point' && !(zoom >= 18 && hasDirections)) { - renderAs = 'point'; - } else { - if (zoom < 17) continue; // no vertex labels at low zooms (vertices have no icons) - renderAs = 'vertex'; - } p = getPointLabel(entity, width, fontSize, renderAs); } else if (geometry === 'line') { @@ -385,7 +412,7 @@ export function svgLabels(projection, context) { }; var coord = projection(entity.loc); - var margin = 2; + var textPadding = 2; var offset = pointOffsets[textDirection]; var p = { height: height, @@ -395,20 +422,21 @@ export function svgLabels(projection, context) { textAnchor: offset[2] }; + // insert a collision box for the text label.. var bbox; if (textDirection === 'rtl') { bbox = { - minX: p.x - width - margin, - minY: p.y - (height / 2) - margin, - maxX: p.x + margin, - maxY: p.y + (height / 2) + margin + minX: p.x - width - textPadding, + minY: p.y - (height / 2) - textPadding, + maxX: p.x + textPadding, + maxY: p.y + (height / 2) + textPadding }; } else { bbox = { - minX: p.x - margin, - minY: p.y - (height / 2) - margin, - maxX: p.x + width + margin, - maxY: p.y + (height / 2) + margin + minX: p.x - textPadding, + minY: p.y - (height / 2) - textPadding, + maxX: p.x + width + textPadding, + maxY: p.y + (height / 2) + textPadding }; } @@ -428,7 +456,7 @@ export function svgLabels(projection, context) { // % along the line to attempt to place the label var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70, 25, 75, 20, 80, 15, 95, 10, 90, 5, 95]; - var margin = 3; + var padding = 3; for (var i = 0; i < lineOffsets.length; i++) { var offset = lineOffsets[i]; @@ -454,14 +482,16 @@ export function svgLabels(projection, context) { for (var j = 0; j < sub.length - 1; j++) { var a = sub[j]; var b = sub[j + 1]; + + // split up the text into small collision boxes var num = Math.max(1, Math.floor(geoEuclideanDistance(a, b) / boxsize / 2)); for (var box = 0; box < num; box++) { var p = geoInterp(a, b, box / num); - var x0 = p[0] - boxsize - margin; - var y0 = p[1] - boxsize - margin; - var x1 = p[0] + boxsize + margin; - var y1 = p[1] + boxsize + margin; + var x0 = p[0] - boxsize - padding; + var y0 = p[1] - boxsize - padding; + var x1 = p[0] + boxsize + padding; + var y1 = p[1] + boxsize + padding; bboxes.push({ minX: Math.min(x0, x1), @@ -536,12 +566,12 @@ export function svgLabels(projection, context) { var preset = context.presets().match(entity, context.graph()); var picon = preset && preset.icon; var iconSize = 17; - var margin = 2; + var padding = 2; var p = {}; if (picon) { // icon and label.. if (addIcon()) { - addLabel(iconSize + margin); + addLabel(iconSize + padding); return p; } } else { // label only.. @@ -573,10 +603,10 @@ export function svgLabels(projection, context) { var labelX = centroid[0]; var labelY = centroid[1] + yOffset; var bbox = { - minX: labelX - (width / 2) - margin, - minY: labelY - (height / 2) - margin, - maxX: labelX + (width / 2) + margin, - maxY: labelY + (height / 2) + margin + minX: labelX - (width / 2) - padding, + minY: labelY - (height / 2) - padding, + maxX: labelX + (width / 2) + padding, + maxY: labelY + (height / 2) + padding }; if (tryInsert([bbox], entity.id, true)) { @@ -592,6 +622,20 @@ export function svgLabels(projection, context) { } + // force insert a singular bounding box + // singular box only, no array, id better be unique + function doInsert(bbox, id) { + bbox.id = id; + + var oldbox = _entitybboxes[id]; + if (oldbox) { + _rdrawn.remove(oldbox); + } + _entitybboxes[id] = bbox; + _rdrawn.insert(bbox); + } + + function tryInsert(bboxes, id, saveSkipped) { var skipped = false; @@ -676,14 +720,15 @@ export function svgLabels(projection, context) { if (!entity) continue; var geometry = entity.geometry(graph); - if (geometry === 'line') { + if (geometry === 'line' || geometry === 'point' || geometry === 'vertex') { ids.push(selectedIDs[i]); - } else if (geometry === 'vertex') { - var point = context.projection(entity.loc); - pad = 10; - bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad }; - ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); } + // } else if (geometry === 'vertex') { + // var point = context.projection(entity.loc); + // pad = 20; + // bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad }; + // ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); + // } } layers.selectAll(utilEntitySelector(ids)) From d9e3367836bd7c9158b06ab139c827c21e6d707d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 18:05:06 -0500 Subject: [PATCH 035/206] More improvements to label vertex avoidance (re: #4271, #3636) - better classification of "interesting" vertices (include tagged, selected, or child of selected) - now we can draw labels on selected lines again (revert #3636) because the labels will avoid the vertices - if debugging is on, draw a collision box for the mouse --- css/20_map.css | 2 +- modules/svg/labels.js | 64 +++++++++++++++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index a29a1c85a4..371b51d5d4 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -217,7 +217,7 @@ text.pointlabel { stroke-miterlimit: 1; } -text.proximate { +text.nolabel { opacity: 0; } diff --git a/modules/svg/labels.js b/modules/svg/labels.js index e6db96a052..b36895e1dc 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -304,7 +304,7 @@ export function svgLabels(projection, context) { geometry = entity.geometry(graph); // Insert collision boxes around interesting points/vertices - if (geometry === 'point' || (geometry === 'vertex' && entity.hasInterestingTags())) { + if (geometry === 'point' || (geometry === 'vertex' && isInterestingVertex(entity))) { var hasDirections = entity.directions(graph, projection).length; var markerPadding; @@ -404,6 +404,19 @@ export function svgLabels(projection, context) { } + function isInterestingVertex(entity) { + var selectedIDs = context.selectedIDs(); + + return entity.hasInterestingTags() || + entity.isEndpoint(graph) || + entity.isConnected(graph) || + selectedIDs.indexOf(entity.id) !== -1 || + _some(graph.parentWays(entity), function(parent) { + return selectedIDs.indexOf(parent.id) !== -1; + }); + } + + function getPointLabel(entity, width, height, geometry) { var y = (geometry === 'point' ? -12 : 0); var pointOffsets = { @@ -698,8 +711,8 @@ export function svgLabels(projection, context) { var layers = selection .selectAll('.layer-label, .layer-halo'); - layers.selectAll('.proximate') - .classed('proximate', false); + layers.selectAll('.nolabel') + .classed('nolabel', false); var mouse = context.mouse(); var graph = context.graph(); @@ -714,25 +727,46 @@ export function svgLabels(projection, context) { ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); } - // hide labels along selected ways, or near selected vertices + // hide labels on selected nodes (they look weird when dragging / haloed) for (var i = 0; i < selectedIDs.length; i++) { var entity = graph.hasEntity(selectedIDs[i]); - if (!entity) continue; - var geometry = entity.geometry(graph); - - if (geometry === 'line' || geometry === 'point' || geometry === 'vertex') { + if (entity && entity.type === 'node') { ids.push(selectedIDs[i]); } - // } else if (geometry === 'vertex') { - // var point = context.projection(entity.loc); - // pad = 20; - // bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad }; - // ids.push.apply(ids, _map(_rdrawn.search(bbox), 'id')); - // } } layers.selectAll(utilEntitySelector(ids)) - .classed('proximate', true); + .classed('nolabel', true); + + + // draw the mouse bbox if debugging is on.. + if (context.getDebug('collision')) { + var gj = bbox ? [{ + type: 'Polygon', + coordinates: [[ + [bbox.minX, bbox.minY], + [bbox.maxX, bbox.minY], + [bbox.maxX, bbox.maxY], + [bbox.minX, bbox.maxY], + [bbox.minX, bbox.minY] + ]] + }] : []; + + var debug = selection.selectAll('.layer-label-debug'); + var debugMouse = debug.selectAll('.debug-mouse') + .data(gj); + + // exit + debugMouse.exit() + .remove(); + + // enter/update + debugMouse.enter() + .append('path') + .attr('class', 'debug debug-mouse yellow') + .merge(debugMouse) + .attr('d', d3_geoPath()); + } } From 24baa5390ec49960a579e8e2d719be66b5158d0e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 13 Dec 2017 18:31:37 -0500 Subject: [PATCH 036/206] Adjust some variable names to better match what they do - `nodes` are for osm nodes - `points` are the projected locations of those node `loc` in screen space --- modules/svg/labels.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/svg/labels.js b/modules/svg/labels.js index b36895e1dc..7d16c12563 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -461,8 +461,8 @@ export function svgLabels(projection, context) { function getLineLabel(entity, width, height) { var viewport = geoExtent(context.projection.clipExtent()).polygon(); - var nodes = _map(graph.childNodes(entity), 'loc').map(projection); - var length = geoPathLength(nodes); + var points = _map(graph.childNodes(entity), 'loc').map(projection); + var length = geoPathLength(points); if (length < width + 20) return; @@ -479,7 +479,7 @@ export function svgLabels(projection, context) { if (start < 0 || start + width > length) continue; // generate subpath and ignore paths that are invalid or don't cross viewport. - var sub = subpath(nodes, start, start + width); + var sub = subpath(points, start, start + width); if (!sub || !geoPolygonIntersectsPolygon(viewport, sub, true)) { continue; } @@ -515,7 +515,7 @@ export function svgLabels(projection, context) { } } - if (tryInsert(bboxes, entity.id, false)) { + if (tryInsert(bboxes, entity.id, false)) { // accept this one return { 'font-size': height + 2, lineString: lineString(sub), @@ -529,17 +529,17 @@ export function svgLabels(projection, context) { return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2); } - function lineString(nodes) { - return 'M' + nodes.join('L'); + function lineString(points) { + return 'M' + points.join('L'); } - function subpath(nodes, from, to) { + function subpath(points, from, to) { var sofar = 0; var start, end, i0, i1; - for (var i = 0; i < nodes.length - 1; i++) { - var a = nodes[i]; - var b = nodes[i + 1]; + for (var i = 0; i < points.length - 1; i++) { + var a = points[i]; + var b = points[i + 1]; var current = geoEuclideanDistance(a, b); var portion; if (!start && sofar + current >= from) { @@ -561,10 +561,10 @@ export function svgLabels(projection, context) { sofar += current; } - var ret = nodes.slice(i0, i1); - ret.unshift(start); - ret.push(end); - return ret; + var result = points.slice(i0, i1); + result.unshift(start); + result.push(end); + return result; } } From bfaf17538ea8dbec65ae529ac7f0fee6c9a46939 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 14 Dec 2017 12:32:28 -0500 Subject: [PATCH 037/206] Move text to single group with subgroups for halo,label,debug --- css/20_map.css | 2 +- modules/svg/labels.js | 121 ++++++++++++++++++++++-------------------- modules/svg/osm.js | 2 +- test/spec/svg/osm.js | 5 +- 4 files changed, 68 insertions(+), 62 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 371b51d5d4..8453897781 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -210,7 +210,7 @@ text.pointlabel { dominant-baseline: auto; } -.layer-halo text { +.layer-labels-halo text { opacity: 0.7; stroke: #fff; stroke-width: 5px; diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 7d16c12563..432959dcad 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -117,9 +117,11 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit paths.exit() .remove(); + // enter/update paths.enter() .append('path') .style('stroke-width', get(labels, 'font-size')) @@ -135,9 +137,11 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit texts.exit() .remove(); + // enter texts.enter() .append('text') .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; }) @@ -145,9 +149,8 @@ export function svgLabels(projection, context) { .append('textPath') .attr('class', 'textpath'); - texts = selection.selectAll('text.' + classes); - - texts.selectAll('.textpath') + // update + selection.selectAll('text.' + classes).selectAll('.textpath') .filter(filter) .data(entities, osmEntity.key) .attr('startOffset', '50%') @@ -161,17 +164,17 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit texts.exit() .remove(); - texts = texts.enter() + // enter/update + texts.enter() .append('text') .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; }) - .merge(texts); - - texts + .merge(texts) .attr('x', get(labels, 'x')) .attr('y', get(labels, 'y')) .style('text-anchor', get(labels, 'textAnchor')) @@ -198,17 +201,17 @@ export function svgLabels(projection, context) { .filter(filter) .data(entities, osmEntity.key); + // exit icons.exit() .remove(); - icons = icons.enter() + // enter/update + icons.enter() .append('use') .attr('class', 'icon ' + classes) .attr('width', '17px') .attr('height', '17px') - .merge(icons); - - icons + .merge(icons) .attr('transform', get(labels, 'transform')) .attr('xlink:href', function(d) { var preset = context.presets().match(d, context.graph()); @@ -225,22 +228,11 @@ export function svgLabels(projection, context) { function drawCollisionBoxes(selection, rtree, which) { - var showDebug = context.getDebug('collision'); var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow'); - var debug = selection.selectAll('.layer-label-debug') - .data(showDebug ? [true] : []); - - debug.exit() - .remove(); - - debug = debug.enter() - .append('g') - .attr('class', 'layer-label-debug') - .merge(debug); - - if (showDebug) { - var gj = rtree.all().map(function(d) { + var gj = []; + if (context.getDebug('collision')) { + gj = rtree.all().map(function(d) { return { type: 'Polygon', coordinates: [[ [d.minX, d.minY], [d.maxX, d.minY], @@ -249,21 +241,21 @@ export function svgLabels(projection, context) { [d.minX, d.minY] ]]}; }); + } - var debugboxes = debug.selectAll('.' + which) - .data(gj); - - debugboxes.exit() - .remove(); + var boxes = selection.selectAll('.' + which) + .data(gj); - debugboxes = debugboxes.enter() - .append('path') - .attr('class', classes) - .merge(debugboxes); + // exit + boxes.exit() + .remove(); - debugboxes - .attr('d', d3_geoPath()); - } + // enter/update + boxes.enter() + .append('path') + .attr('class', classes) + .merge(boxes) + .attr('d', d3_geoPath()); } @@ -466,6 +458,8 @@ export function svgLabels(projection, context) { if (length < width + 20) return; + // todo: properly clip points to viewport + // % along the line to attempt to place the label var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70, 25, 75, 20, 80, 15, 95, 10, 90, 5, 95]; @@ -681,8 +675,20 @@ export function svgLabels(projection, context) { } - var label = selection.selectAll('.layer-label'); - var halo = selection.selectAll('.layer-halo'); + var layer = selection.selectAll('.layer-labels'); + + var groups = layer.selectAll('.layer-labels-group') + .data(['halo','label','debug']); + + groups = groups.enter() + .append('g') + .attr('class', function(d) { return 'layer-labels-group layer-labels-' + d; }) + .merge(groups); + + var halo = layer.selectAll('.layer-labels-halo'); + var label = layer.selectAll('.layer-labels-label'); + var debug = layer.selectAll('.layer-labels-debug'); + // points drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point); @@ -700,16 +706,16 @@ export function svgLabels(projection, context) { drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo', positions.area); // debug - drawCollisionBoxes(label, _rskipped, 'debug-skipped'); - drawCollisionBoxes(label, _rdrawn, 'debug-drawn'); + drawCollisionBoxes(debug, _rskipped, 'debug-skipped'); + drawCollisionBoxes(debug, _rdrawn, 'debug-drawn'); - selection.call(filterLabels); + layer.call(filterLabels); } function filterLabels(selection) { var layers = selection - .selectAll('.layer-label, .layer-halo'); + .selectAll('.layer-labels-label, .layer-labels-halo'); layers.selectAll('.nolabel') .classed('nolabel', false); @@ -740,8 +746,10 @@ export function svgLabels(projection, context) { // draw the mouse bbox if debugging is on.. + var debug = selection.selectAll('.layer-labels-debug'); + var gj = []; if (context.getDebug('collision')) { - var gj = bbox ? [{ + gj = bbox ? [{ type: 'Polygon', coordinates: [[ [bbox.minX, bbox.minY], @@ -751,22 +759,21 @@ export function svgLabels(projection, context) { [bbox.minX, bbox.minY] ]] }] : []; + } - var debug = selection.selectAll('.layer-label-debug'); - var debugMouse = debug.selectAll('.debug-mouse') - .data(gj); + var box = debug.selectAll('.debug-mouse') + .data(gj); - // exit - debugMouse.exit() - .remove(); + // exit + box.exit() + .remove(); - // enter/update - debugMouse.enter() - .append('path') - .attr('class', 'debug debug-mouse yellow') - .merge(debugMouse) - .attr('d', d3_geoPath()); - } + // enter/update + box.enter() + .append('path') + .attr('class', 'debug debug-mouse yellow') + .merge(box) + .attr('d', d3_geoPath()); } diff --git a/modules/svg/osm.js b/modules/svg/osm.js index 2a3b6ff727..d133bfbd07 100644 --- a/modules/svg/osm.js +++ b/modules/svg/osm.js @@ -4,7 +4,7 @@ export function svgOsm(projection, context, dispatch) { function drawOsm(selection) { selection.selectAll('.layer-osm') - .data(['areas', 'lines', 'hit', 'halo', 'label']) + .data(['areas', 'lines', 'hit', 'labels']) .enter() .append('g') .attr('class', function(d) { return 'layer-osm layer-' + d; }); diff --git a/test/spec/svg/osm.js b/test/spec/svg/osm.js index e532d45809..fe51c3741f 100644 --- a/test/spec/svg/osm.js +++ b/test/spec/svg/osm.js @@ -8,12 +8,11 @@ describe('iD.svgOsm', function () { it('creates default osm layers', function () { container.call(iD.svgOsm()); var nodes = container.selectAll('.layer-osm').nodes(); - expect(nodes.length).to.eql(5); + expect(nodes.length).to.eql(4); expect(d3.select(nodes[0]).classed('layer-areas')).to.be.true; expect(d3.select(nodes[1]).classed('layer-lines')).to.be.true; expect(d3.select(nodes[2]).classed('layer-hit')).to.be.true; - expect(d3.select(nodes[3]).classed('layer-halo')).to.be.true; - expect(d3.select(nodes[4]).classed('layer-label')).to.be.true; + expect(d3.select(nodes[3]).classed('layer-labels')).to.be.true; }); }); From b9e48d1682e854daf825c3bdcdca173f49cc2716 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 14 Dec 2017 17:38:43 -0500 Subject: [PATCH 038/206] WIP: Move layer-hit to layer-points with explict sublayers, update tests This is more work to further isolate the layers that entities draw to. It makes it easier to debug what is going on, and can eventually lead to deferred drawing, if each draw function is in its own place and not dependant on anything else. I've started to replace the vertex-hover with an explicit layer for touch targets. Also had to change a lot of the svg tests, which are really brittle. Things would happen like - the surface would be created, it would kick of a deferred redraw, which would notice that the zoom was 0 and call editOff, which would remove the osm layers that were just created and that the tests were trying to draw to. These tests need proper zoom and projection otherwise nothing works. --- css/20_map.css | 15 +++- css/70_fills.css | 6 +- modules/renderer/map.js | 32 ++++---- modules/svg/labels.js | 10 --- modules/svg/midpoints.js | 2 +- modules/svg/osm.js | 14 +++- modules/svg/points.js | 3 +- modules/svg/turns.js | 3 +- modules/svg/vertices.js | 149 ++++++++++++++++++++--------------- test/spec/svg/areas.js | 142 +++++++++++++++++---------------- test/spec/svg/layers.js | 15 ++-- test/spec/svg/lines.js | 90 +++++++++++---------- test/spec/svg/midpoints.js | 126 ++++++++++++++--------------- test/spec/svg/osm.js | 32 ++++++-- test/spec/svg/points.js | 21 ++--- test/spec/svg/tag_classes.js | 56 ++++++------- test/spec/svg/vertices.js | 12 +-- 17 files changed, 397 insertions(+), 331 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 8453897781..34061453fb 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -45,6 +45,7 @@ g.point.selected .shadow { stroke-opacity: 0.7; } +g.vertex.active, g.vertex.active *, g.point.active, g.point.active * { pointer-events: none; } @@ -88,6 +89,12 @@ g.midpoint .shadow { fill-opacity: 0; } +/*debug*/ +.vertex.target { + fill: #f00; + fill-opacity: 0.5; +} + /*g.vertex.vertex-hover { display: none; } @@ -99,6 +106,7 @@ g.midpoint .shadow { .mode-add-point g.vertex.vertex-hover, .mode-drag-node g.vertex.vertex-hover { display: block; + color: #f00; } .mode-draw-area .hover-disabled g.vertex.vertex-hover, @@ -110,8 +118,9 @@ g.midpoint .shadow { display: none; } */ + g.vertex.related:not(.selected) .shadow, -g.vertex.hover:not(.selected) .shadow, +/*g.vertex.hover:not(.selected) .shadow,*/ g.midpoint.related:not(.selected) .shadow, g.midpoint.hover:not(.selected) .shadow { fill-opacity: 0.5; @@ -262,11 +271,11 @@ g.turn circle { } path.gpx { - stroke: #FF26D4; + stroke: #ff26d4; stroke-width: 2; fill: none; } text.gpx { - fill: #FF26D4; + fill: #ff26d4; } diff --git a/css/70_fills.css b/css/70_fills.css index c0cb1b167b..ffb437d686 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -44,12 +44,12 @@ display: none; } */ -.mode-draw-line .way.active, -.mode-draw-area .way.active, +/*.mode-draw-line .active, +.mode-draw-area .active, .mode-drag-node .active { pointer-events: none; } - +*/ /* Ensure drawing doesn't interact with area fills. */ .mode-add-point path.area.fill, .mode-draw-line path.area.fill, diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 0240844df3..f9de7d4607 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -178,23 +178,23 @@ export function rendererMap(context) { }) .on('mousemove.map', function() { mousemove = d3_event; + }) + .on('mouseover.vertices', function() { + if (map.editable() && !transformed) { + var hover = d3_event.target.__data__; + surface.selectAll('.data-layer-osm') + .call(drawVertices.drawHover, context.graph(), hover, map.extent()); + dispatch.call('drawn', this, { full: false }); + } + }) + .on('mouseout.vertices', function() { + if (map.editable() && !transformed) { + var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; + surface.selectAll('.data-layer-osm') + .call(drawVertices.drawHover, context.graph(), hover, map.extent()); + dispatch.call('drawn', this, { full: false }); + } }); - // .on('mouseover.vertices', function() { - // if (map.editable() && !transformed) { - // var hover = d3_event.target.__data__; - // surface.selectAll('.data-layer-osm') - // .call(drawVertices.drawHover, context.graph(), hover, map.extent()); - // dispatch.call('drawn', this, { full: false }); - // } - // }) - // .on('mouseout.vertices', function() { - // if (map.editable() && !transformed) { - // var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__; - // surface.selectAll('.data-layer-osm') - // .call(drawVertices.drawHover, context.graph(), hover, map.extent()); - // dispatch.call('drawn', this, { full: false }); - // } - // }); supersurface .call(context.background()); diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 432959dcad..2e65d593ab 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -676,20 +676,10 @@ export function svgLabels(projection, context) { var layer = selection.selectAll('.layer-labels'); - - var groups = layer.selectAll('.layer-labels-group') - .data(['halo','label','debug']); - - groups = groups.enter() - .append('g') - .attr('class', function(d) { return 'layer-labels-group layer-labels-' + d; }) - .merge(groups); - var halo = layer.selectAll('.layer-labels-halo'); var label = layer.selectAll('.layer-labels-label'); var debug = layer.selectAll('.layer-labels-debug'); - // points drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point); drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo', positions.point); diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index 101625feed..27e548ca46 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -16,7 +16,7 @@ import { export function svgMidpoints(projection, context) { return function drawMidpoints(selection, graph, entities, filter, extent) { - var layer = selection.selectAll('.layer-hit'); + var layer = selection.selectAll('.layer-points .layer-points-midpoints'); var mode = context.mode(); if (mode && mode.id !== 'select') { diff --git a/modules/svg/osm.js b/modules/svg/osm.js index d133bfbd07..8a7defe943 100644 --- a/modules/svg/osm.js +++ b/modules/svg/osm.js @@ -4,10 +4,22 @@ export function svgOsm(projection, context, dispatch) { function drawOsm(selection) { selection.selectAll('.layer-osm') - .data(['areas', 'lines', 'hit', 'labels']) + .data(['areas', 'lines', 'points', 'labels']) .enter() .append('g') .attr('class', function(d) { return 'layer-osm layer-' + d; }); + + selection.selectAll('.layer-points').selectAll('.layer-points-group') + .data(['points', 'midpoints', 'vertices', 'turns', 'targets']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-points-group layer-points-' + d; }); + + selection.selectAll('.layer-labels').selectAll('.layer-labels-group') + .data(['halo', 'label', 'debug']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-labels-group layer-labels-' + d; }); } diff --git a/modules/svg/points.js b/modules/svg/points.js index 909533f3a1..ab8f2361e5 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -36,7 +36,8 @@ export function svgPoints(projection, context) { points.sort(sortY); - var layer = selection.selectAll('.layer-hit'); + + var layer = selection.selectAll('.layer-points .layer-points-points'); var groups = layer.selectAll('g.point') .filter(filter) diff --git a/modules/svg/turns.js b/modules/svg/turns.js index 537ccf5fff..cd16bd0dd9 100644 --- a/modules/svg/turns.js +++ b/modules/svg/turns.js @@ -18,7 +18,8 @@ export function svgTurns(projection) { (!turn.indirect_restriction && /^only_/.test(restriction) ? 'only' : 'no') + u; } - var groups = selection.selectAll('.layer-hit').selectAll('g.turn') + var layer = selection.selectAll('.layer-points .layer-points-turns'); + var groups = layer.selectAll('g.turn') .data(turns, key); groups.exit() diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 7f2d5a49c6..707ed2414e 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -54,7 +54,8 @@ export function svgVertices(projection, context) { function setClass(klass) { return function(entity) { - this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id); + d3_select(this) + .attr('class', 'node vertex ' + klass + ' ' + entity.id); }; } @@ -171,9 +172,7 @@ export function svgVertices(projection, context) { function drawVertices(selection, graph, entities, filter, extent) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); - - var siblings = {}; - getSiblingAndChildVertices(context.selectedIDs(), graph, extent); + var siblings = getSiblingAndChildVertices(context.selectedIDs(), graph, extent, wireframe, zoom); // always render selected and sibling vertices.. var vertices = _clone(siblings); @@ -184,7 +183,7 @@ export function svgVertices(projection, context) { var entity = entities[i]; var geometry = entity.geometry(graph); - if ((geometry === 'point') && renderAsVertex(entity)) { + if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { vertices[entity.id] = entity; } else if ((geometry === 'vertex') && @@ -193,85 +192,105 @@ export function svgVertices(projection, context) { } } - - selection.selectAll('.layer-hit') + selection.selectAll('.layer-points .layer-points-vertices') .call(draw, _values(vertices), 'vertex-persistent', graph, siblings, filterWithSiblings); -// drawHover(selection, graph, extent, true); + drawTargets(selection, graph, _values(vertices), filter, extent); + } + + + function drawTargets(selection, graph, entities, filter, extent) { +// todo coming soon +return; + var layer = selection.selectAll('.layer-points .layer-points-targets'); + + var targets = layer.selectAll('g.vertex.target') + .data(entities, osmEntity.key); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('circle') + .attr('r', radiuses.shadow[3]) // just use the biggest one for now + .attr('class', function(d) { return 'node vertex target ' + d.id; }) + .merge(targets) + .attr('transform', svgPointTransform(projection)); + } - // Points can also render as vertices: - // 1. in wireframe mode or - // 2. at higher zooms if they have a direction - function renderAsVertex(entity) { - var geometry = entity.geometry(graph); - return geometry === 'vertex' || (geometry === 'point' && ( - wireframe || (zoom > 18 && entity.directions(graph, projection).length) - )); - } + + // Points can also render as vertices: + // 1. in wireframe mode or + // 2. at higher zooms if they have a direction + function renderAsVertex(entity, graph, wireframe, zoom) { + var geometry = entity.geometry(graph); + return geometry === 'vertex' || (geometry === 'point' && ( + wireframe || (zoom > 18 && entity.directions(graph, projection).length) + )); + } - function getSiblingAndChildVertices(ids, graph, extent) { + function getSiblingAndChildVertices(ids, graph, extent, wireframe, zoom) { + var results = {}; - function addChildVertices(entity) { - var geometry = entity.geometry(graph); - if (!context.features().isHiddenFeature(entity, graph, geometry)) { - var i; - if (entity.type === 'way') { - for (i = 0; i < entity.nodes.length; i++) { - var child = context.hasEntity(entity.nodes[i]); - if (child) { - addChildVertices(child); - } + function addChildVertices(entity) { + var geometry = entity.geometry(graph); + if (!context.features().isHiddenFeature(entity, graph, geometry)) { + var i; + if (entity.type === 'way') { + for (i = 0; i < entity.nodes.length; i++) { + var child = context.hasEntity(entity.nodes[i]); + if (child) { + addChildVertices(child); } - } else if (entity.type === 'relation') { - for (i = 0; i < entity.members.length; i++) { - var member = context.hasEntity(entity.members[i].id); - if (member) { - addChildVertices(member); - } + } + } else if (entity.type === 'relation') { + for (i = 0; i < entity.members.length; i++) { + var member = context.hasEntity(entity.members[i].id); + if (member) { + addChildVertices(member); } - } else if (renderAsVertex(entity) && entity.intersects(extent, graph)) { - siblings[entity.id] = entity; } + } else if (renderAsVertex(entity, graph, wireframe, zoom) && entity.intersects(extent, graph)) { + results[entity.id] = entity; } } + } - ids.forEach(function(id) { - var entity = context.hasEntity(id); - if (!entity) return; + ids.forEach(function(id) { + var entity = context.hasEntity(id); + if (!entity) return; - if (entity.type === 'node') { - if (renderAsVertex(entity)) { - siblings[entity.id] = entity; - graph.parentWays(entity).forEach(function(entity) { - addChildVertices(entity); - }); - } - } else { // way, relation - addChildVertices(entity); + if (entity.type === 'node') { + if (renderAsVertex(entity, graph, wireframe, zoom)) { + results[entity.id] = entity; + graph.parentWays(entity).forEach(function(entity) { + addChildVertices(entity); + }); } - }); + } else { // way, relation + addChildVertices(entity); + } + }); - } + return results; } -// function drawHover(selection, graph, extent, follow) { -// var hovered = _hover ? siblingAndChildVertices([_hover.id], graph, extent) : {}; -// var wireframe = context.surface().classed('fill-wireframe'); -// var layer = selection.selectAll('.layer-hit'); -// -// layer.selectAll('g.vertex.vertex-hover') -// .call(draw, _values(hovered), 'vertex-hover', graph, {}, false); -// } -// -// -// drawVertices.drawHover = function(selection, graph, target, extent) { -// if (target === _hover) return; -// _hover = target; -// drawHover(selection, graph, extent); -// }; + drawVertices.drawHover = function(selection, graph, target, extent) { + if (target === _hover) return; + _hover = target; + + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); + var hovered = _hover ? getSiblingAndChildVertices([_hover.id], graph, extent, wireframe, zoom) : {}; + var filter = function() { return true; }; + + drawTargets(selection, graph, _values(hovered), filter, extent); + }; return drawVertices; } diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 7397682bd2..140576398e 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -1,17 +1,21 @@ describe('iD.svgAreas', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]), - all = function() { return true; }, - none = function() { return false; }; + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context, surface; + var all = function() { return true; }; + var none = function() { return false; }; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); + beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); iD.setAreaKeys({ @@ -22,13 +26,13 @@ describe('iD.svgAreas', function () { }); it('adds way and area classes', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 0]}), - iD.Node({id: 'c', loc: [1, 1]}), - iD.Node({id: 'd', loc: [0, 1]}), - iD.Way({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0]}), + iD.osmNode({id: 'c', loc: [1, 1]}), + iD.osmNode({id: 'd', loc: [0, 1]}), + iD.osmWay({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) + ]); surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], none); @@ -37,13 +41,13 @@ describe('iD.svgAreas', function () { }); it('adds tag classes', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 0]}), - iD.Node({id: 'c', loc: [1, 1]}), - iD.Node({id: 'd', loc: [0, 1]}), - iD.Way({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0]}), + iD.osmNode({id: 'c', loc: [1, 1]}), + iD.osmNode({id: 'd', loc: [0, 1]}), + iD.osmWay({id: 'w', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'a']}) + ]); surface.call(iD.svgAreas(projection, context), graph, [graph.entity('w')], none); @@ -52,14 +56,14 @@ describe('iD.svgAreas', function () { }); it('handles deletion of a way and a member vertex (#1903)', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 0]}), - iD.Node({id: 'c', loc: [1, 1]}), - iD.Node({id: 'd', loc: [1, 1]}), - iD.Way({id: 'w', tags: {area: 'yes'}, nodes: ['a', 'b', 'c', 'a']}), - iD.Way({id: 'x', tags: {area: 'yes'}, nodes: ['a', 'b', 'd', 'a']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 0]}), + iD.osmNode({id: 'c', loc: [1, 1]}), + iD.osmNode({id: 'd', loc: [1, 1]}), + iD.osmWay({id: 'w', tags: {area: 'yes'}, nodes: ['a', 'b', 'c', 'a']}), + iD.osmWay({id: 'x', tags: {area: 'yes'}, nodes: ['a', 'b', 'd', 'a']}) + ]); surface.call(iD.svgAreas(projection, context), graph, [graph.entity('x')], all); graph = graph.remove(graph.entity('x')).remove(graph.entity('d')); @@ -69,18 +73,18 @@ describe('iD.svgAreas', function () { }); describe('z-indexing', function() { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [-0.0002, 0.0001]}), - iD.Node({id: 'b', loc: [ 0.0002, 0.0001]}), - iD.Node({id: 'c', loc: [ 0.0002, -0.0001]}), - iD.Node({id: 'd', loc: [-0.0002, -0.0001]}), - iD.Node({id: 'e', loc: [-0.0004, 0.0002]}), - iD.Node({id: 'f', loc: [ 0.0004, 0.0002]}), - iD.Node({id: 'g', loc: [ 0.0004, -0.0002]}), - iD.Node({id: 'h', loc: [-0.0004, -0.0002]}), - iD.Way({id: 's', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}), - iD.Way({id: 'l', tags: {landuse: 'park'}, nodes: ['e', 'f', 'g', 'h', 'e']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [-0.0002, 0.0001]}), + iD.osmNode({id: 'b', loc: [ 0.0002, 0.0001]}), + iD.osmNode({id: 'c', loc: [ 0.0002, -0.0001]}), + iD.osmNode({id: 'd', loc: [-0.0002, -0.0001]}), + iD.osmNode({id: 'e', loc: [-0.0004, 0.0002]}), + iD.osmNode({id: 'f', loc: [ 0.0004, 0.0002]}), + iD.osmNode({id: 'g', loc: [ 0.0004, -0.0002]}), + iD.osmNode({id: 'h', loc: [-0.0004, -0.0002]}), + iD.osmWay({id: 's', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}), + iD.osmWay({id: 'l', tags: {landuse: 'park'}, nodes: ['e', 'f', 'g', 'h', 'e']}) + ]); it('stacks smaller areas above larger ones in a single render', function () { surface.call(iD.svgAreas(projection, context), graph, [graph.entity('s'), graph.entity('l')], none); @@ -114,13 +118,13 @@ describe('iD.svgAreas', function () { }); it('renders fills for multipolygon areas', function () { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), - graph = iD.Graph([a, b, c, w, r]), - areas = [w, r]; + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}); + var graph = iD.coreGraph([a, b, c, w, r]); + var areas = [w, r]; surface.call(iD.svgAreas(projection, context), graph, areas, none); @@ -128,13 +132,13 @@ describe('iD.svgAreas', function () { }); it('renders no strokes for multipolygon areas', function () { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), - graph = iD.Graph([a, b, c, w, r]), - areas = [w, r]; + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}); + var graph = iD.coreGraph([a, b, c, w, r]); + var areas = [w, r]; surface.call(iD.svgAreas(projection, context), graph, areas, none); @@ -142,12 +146,12 @@ describe('iD.svgAreas', function () { }); it('renders fill for a multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, w, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, w, r]); surface.call(iD.svgAreas(projection, context), graph, [w, r], none); @@ -157,12 +161,12 @@ describe('iD.svgAreas', function () { }); it('renders no strokes for a multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, w, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: w.id, type: 'way'}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, w, r]); surface.call(iD.svgAreas(projection, context), graph, [w, r], none); diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index e3cb29a894..fa70b6565f 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -1,12 +1,15 @@ describe('iD.svgLayers', function () { - var context, container, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]); + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context, container; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); container = d3.select(document.createElement('div')); }); diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index c1748eeff7..2a414afcda 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -1,26 +1,30 @@ describe('iD.svgLines', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]), - all = function() { return true; }, - none = function() { return false; }; + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context, surface; + var all = function() { return true; }; + var none = function() { return false; }; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); + beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); }); it('adds way and line classes', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [1, 1]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 1]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); surface.call(iD.svgLines(projection, context), graph, [line], all); @@ -29,10 +33,10 @@ describe('iD.svgLines', function () { }); it('adds tag classes', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [1, 1]}), - line = iD.Way({nodes: [a.id, b.id], tags: {highway: 'residential'}}), - graph = iD.Graph([a, b, line]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 1]}); + var line = iD.osmWay({nodes: [a.id, b.id], tags: {highway: 'residential'}}); + var graph = iD.coreGraph([a, b, line]); surface.call(iD.svgLines(projection, context), graph, [line], all); @@ -41,11 +45,11 @@ describe('iD.svgLines', function () { }); it('adds stroke classes for the tags of the parent relation of multipolygon members', function() { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [1, 1]}), - line = iD.Way({nodes: [a.id, b.id]}), - relation = iD.Relation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}), - graph = iD.Graph([a, b, line, relation]); + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 1]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var relation = iD.osmRelation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}); + var graph = iD.coreGraph([a, b, line, relation]); surface.call(iD.svgLines(projection, context), graph, [line], all); @@ -53,12 +57,12 @@ describe('iD.svgLines', function () { }); it('renders stroke for outer way of multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - w = iD.Way({id: 'w-1', tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: w.id}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, w, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var w = iD.osmWay({id: 'w-1', tags: {natural: 'wood'}, nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: w.id}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, w, r]); surface.call(iD.svgLines(projection, context), graph, [w], all); @@ -67,13 +71,13 @@ describe('iD.svgLines', function () { }); it('adds stroke classes for the tags of the outer way of multipolygon with tags on the outer way', function() { - var a = iD.Node({loc: [1, 1]}), - b = iD.Node({loc: [2, 2]}), - c = iD.Node({loc: [3, 3]}), - o = iD.Way({id: 'w-1', nodes: [a.id, b.id, c.id, a.id], tags: {natural: 'wood'}}), - i = iD.Way({id: 'w-2', nodes: [a.id, b.id, c.id, a.id]}), - r = iD.Relation({members: [{id: o.id, role: 'outer'}, {id: i.id, role: 'inner'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([a, b, c, o, i, r]); + var a = iD.osmNode({loc: [1, 1]}); + var b = iD.osmNode({loc: [2, 2]}); + var c = iD.osmNode({loc: [3, 3]}); + var o = iD.osmWay({id: 'w-1', nodes: [a.id, b.id, c.id, a.id], tags: {natural: 'wood'}}); + var i = iD.osmWay({id: 'w-2', nodes: [a.id, b.id, c.id, a.id]}); + var r = iD.osmRelation({members: [{id: o.id, role: 'outer'}, {id: i.id, role: 'inner'}], tags: {type: 'multipolygon'}}); + var graph = iD.coreGraph([a, b, c, o, i, r]); surface.call(iD.svgLines(projection, context), graph, [i, o], all); @@ -84,14 +88,14 @@ describe('iD.svgLines', function () { }); describe('z-indexing', function() { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [1, 1]}), - iD.Node({id: 'c', loc: [0, 0]}), - iD.Node({id: 'd', loc: [1, 1]}), - iD.Way({id: 'lo', tags: {highway: 'residential', tunnel: 'yes'}, nodes: ['a', 'b']}), - iD.Way({id: 'hi', tags: {highway: 'residential', bridge: 'yes'}, nodes: ['c', 'd']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [1, 1]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [1, 1]}), + iD.osmWay({id: 'lo', tags: {highway: 'residential', tunnel: 'yes'}, nodes: ['a', 'b']}), + iD.osmWay({id: 'hi', tags: {highway: 'residential', bridge: 'yes'}, nodes: ['c', 'd']}) + ]); it('stacks higher lines above lower ones in a single render', function () { surface.call(iD.svgLines(projection, context), graph, [graph.entity('lo'), graph.entity('hi')], none); diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js index 92266c5c6c..bb4442415b 100644 --- a/test/spec/svg/midpoints.js +++ b/test/spec/svg/midpoints.js @@ -1,103 +1,103 @@ describe('iD.svgMidpoints', function () { - var context, surface, - selectedIDs = [], - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]), - filter = function() { return true; }; + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context, surface; + var _selectedIDs = []; + var filter = function() { return true; }; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { - context = iD.Context(); - context.mode = function() { - return { - id: 'select', - selectedIDs: function() { return selectedIDs; } - }; - }; - d3.select(document.createElement('div')) + context = iD.coreContext(); + context.enter({ + id: 'select', + enter: function() { }, + exit: function() { }, + selectedIDs: function() { return _selectedIDs; } + }); + + var map = d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); + surface = context.surface(); }); it('creates midpoint on segment completely within the extent', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [50, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); - - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [1, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); + + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); - expect(surface.selectAll('.midpoint').datum().loc).to.eql([25, 0]); + expect(surface.selectAll('.midpoint').datum().loc).to.eql([0.5, 0]); }); it('doesn\'t create midpoint on segment with pixel length less than 40', function () { - var a = iD.Node({loc: [0, 0]}), - b = iD.Node({loc: [39, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); - - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + var a = iD.osmNode({loc: [0, 0]}); + var b = iD.osmNode({loc: [0.0001, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); + + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); expect(surface.selectAll('.midpoint').nodes()).to.have.length(0); }); it('doesn\'t create midpoint on segment completely outside of the extent', function () { - var a = iD.Node({loc: [-100, 0]}), - b = iD.Node({loc: [-50, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); - - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + var a = iD.osmNode({loc: [-1, 0]}); + var b = iD.osmNode({loc: [-0.5, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); + + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); expect(surface.selectAll('.midpoint').nodes()).to.have.length(0); }); it('creates midpoint on extent edge for segment partially outside of the extent', function () { - var a = iD.Node({loc: [50, 0]}), - b = iD.Node({loc: [500, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); - - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + var a = iD.osmNode({loc: [0.5, 0]}); + var b = iD.osmNode({loc: [2, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); + + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); - expect(surface.selectAll('.midpoint').datum().loc).to.eql([100, 0]); + expect(surface.selectAll('.midpoint').datum().loc).to.eql([1, 0]); }); it('doesn\'t create midpoint on extent edge for segment with pixel length less than 20', function () { - var a = iD.Node({loc: [81, 0]}), - b = iD.Node({loc: [500, 0]}), - line = iD.Way({nodes: [a.id, b.id]}), - graph = iD.Graph([a, b, line]), - extent = iD.geoExtent([0, 0], [100, 100]); - - selectedIDs = [line.id]; - context.selectedIDs = function() { return selectedIDs; }; + var a = iD.osmNode({loc: [0.9999, 0]}); + var b = iD.osmNode({loc: [2, 0]}); + var line = iD.osmWay({nodes: [a.id, b.id]}); + var graph = iD.coreGraph([a, b, line]); + var extent = iD.geoExtent([0, 0], [1, 1]); + + _selectedIDs = [line.id]; context.entity = function(id) { return graph.entity(id); }; - context.hasEntity = context.entity; + context.hasEntity = function(id) { return graph.entities[id]; }; surface.call(iD.svgMidpoints(projection, context), graph, [line], filter, extent); expect(surface.selectAll('.midpoint').nodes()).to.have.length(0); diff --git a/test/spec/svg/osm.js b/test/spec/svg/osm.js index fe51c3741f..43358b11f8 100644 --- a/test/spec/svg/osm.js +++ b/test/spec/svg/osm.js @@ -7,12 +7,32 @@ describe('iD.svgOsm', function () { it('creates default osm layers', function () { container.call(iD.svgOsm()); - var nodes = container.selectAll('.layer-osm').nodes(); - expect(nodes.length).to.eql(4); - expect(d3.select(nodes[0]).classed('layer-areas')).to.be.true; - expect(d3.select(nodes[1]).classed('layer-lines')).to.be.true; - expect(d3.select(nodes[2]).classed('layer-hit')).to.be.true; - expect(d3.select(nodes[3]).classed('layer-labels')).to.be.true; + var layers = container.selectAll('g.layer-osm').nodes(); + expect(layers.length).to.eql(4); + expect(d3.select(layers[0]).classed('layer-areas')).to.be.true; + expect(d3.select(layers[1]).classed('layer-lines')).to.be.true; + expect(d3.select(layers[2]).classed('layer-points')).to.be.true; + expect(d3.select(layers[3]).classed('layer-labels')).to.be.true; + }); + + it('creates default osm point layers', function () { + container.call(iD.svgOsm()); + var layers = container.selectAll('g.layer-points g.layer-points-group').nodes(); + expect(layers.length).to.eql(5); + expect(d3.select(layers[0]).classed('layer-points-points')).to.be.true; + expect(d3.select(layers[1]).classed('layer-points-midpoints')).to.be.true; + expect(d3.select(layers[2]).classed('layer-points-vertices')).to.be.true; + expect(d3.select(layers[3]).classed('layer-points-turns')).to.be.true; + expect(d3.select(layers[4]).classed('layer-points-targets')).to.be.true; + }); + + it('creates default osm label layers', function () { + container.call(iD.svgOsm()); + var layers = container.selectAll('g.layer-labels g.layer-labels-group').nodes(); + expect(layers.length).to.eql(3); + expect(d3.select(layers[0]).classed('layer-labels-halo')).to.be.true; + expect(d3.select(layers[1]).classed('layer-labels-label')).to.be.true; + expect(d3.select(layers[2]).classed('layer-labels-debug')).to.be.true; }); }); diff --git a/test/spec/svg/points.js b/test/spec/svg/points.js index dccca9d117..8faaa54c22 100644 --- a/test/spec/svg/points.js +++ b/test/spec/svg/points.js @@ -1,22 +1,25 @@ describe('iD.svgPoints', function () { - var context, surface, - projection = d3.geoProjection(function(x, y) { return [x, -y]; }) - .translate([0, 0]) - .scale(180 / Math.PI) - .clipExtent([[0, 0], [Infinity, Infinity]]); + var TAU = 2 * Math.PI; + function ztok(z) { return 256 * Math.pow(2, z) / TAU; } + + var context, surface; + var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) + .translate([0, 0]) + .scale(ztok(17)) + .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); }); it('adds tag classes', function () { - var point = iD.Node({tags: {amenity: 'cafe'}, loc: [0, 0]}), - graph = iD.Graph([point]); + var point = iD.osmNode({tags: {amenity: 'cafe'}, loc: [0, 0]}); + var graph = iD.coreGraph([point]); surface.call(iD.svgPoints(projection, context), graph, [point]); diff --git a/test/spec/svg/tag_classes.js b/test/spec/svg/tag_classes.js index dffb8e14d4..d454300d7e 100644 --- a/test/spec/svg/tag_classes.js +++ b/test/spec/svg/tag_classes.js @@ -7,156 +7,156 @@ describe('iD.svgTagClasses', function () { it('adds no classes to elements whose datum has no tags', function() { selection - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(null); }); it('adds classes for primary tag key and key-value', function() { selection - .datum(iD.Entity({tags: {highway: 'primary'}})) + .datum(iD.osmEntity({tags: {highway: 'primary'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); it('adds only one primary tag', function() { selection - .datum(iD.Entity({tags: {highway: 'primary', railway: 'rail'}})) + .datum(iD.osmEntity({tags: {highway: 'primary', railway: 'rail'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); it('orders primary tags', function() { selection - .datum(iD.Entity({tags: {railway: 'rail', highway: 'primary'}})) + .datum(iD.osmEntity({tags: {railway: 'rail', highway: 'primary'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); it('adds status tag when status in primary value (`railway=abandoned`)', function() { selection - .datum(iD.Entity({tags: {railway: 'abandoned'}})) + .datum(iD.osmEntity({tags: {railway: 'abandoned'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-status tag-status-abandoned'); }); it('adds status tag when status in key and value matches "yes" (railway=rail + abandoned=yes)', function() { selection - .datum(iD.Entity({tags: {railway: 'rail', abandoned: 'yes'}})) + .datum(iD.osmEntity({tags: {railway: 'rail', abandoned: 'yes'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-railway-rail tag-status tag-status-abandoned'); }); it('adds status tag when status in key and value matches primary (railway=rail + abandoned=railway)', function() { selection - .datum(iD.Entity({tags: {railway: 'rail', abandoned: 'railway'}})) + .datum(iD.osmEntity({tags: {railway: 'rail', abandoned: 'railway'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-railway-rail tag-status tag-status-abandoned'); }); it('adds primary and status tag when status in key and no primary (abandoned=railway)', function() { selection - .datum(iD.Entity({tags: {abandoned: 'railway'}})) + .datum(iD.osmEntity({tags: {abandoned: 'railway'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-railway tag-status tag-status-abandoned'); }); it('does not add status tag for different primary tag (highway=path + abandoned=railway)', function() { selection - .datum(iD.Entity({tags: {highway: 'path', abandoned: 'railway'}})) + .datum(iD.osmEntity({tags: {highway: 'path', abandoned: 'railway'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-path'); }); it('adds secondary tags', function() { selection - .datum(iD.Entity({tags: {highway: 'primary', bridge: 'yes'}})) + .datum(iD.osmEntity({tags: {highway: 'primary', bridge: 'yes'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary tag-bridge tag-bridge-yes'); }); it('adds no bridge=no tags', function() { selection - .datum(iD.Entity({tags: {bridge: 'no'}})) + .datum(iD.osmEntity({tags: {bridge: 'no'}})) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(null); }); it('adds tag-unpaved for highway=track with no surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'track'}})) + .datum(iD.osmEntity({tags: {highway: 'track'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; }); it('does not add tag-unpaved for highway=track with explicit paved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'track', surface: 'asphalt'}})) + .datum(iD.osmEntity({tags: {highway: 'track', surface: 'asphalt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {highway: 'track', tracktype: 'grade1'}})) + .datum(iD.osmEntity({tags: {highway: 'track', tracktype: 'grade1'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); it('adds tag-unpaved for highway=track with explicit unpaved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'track', surface: 'dirt'}})) + .datum(iD.osmEntity({tags: {highway: 'track', surface: 'dirt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; selection - .datum(iD.Entity({tags: {highway: 'track', tracktype: 'grade3'}})) + .datum(iD.osmEntity({tags: {highway: 'track', tracktype: 'grade3'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; }); it('does not add tag-unpaved for other highway types with no surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'tertiary'}})) + .datum(iD.osmEntity({tags: {highway: 'tertiary'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {highway: 'foo'}})) + .datum(iD.osmEntity({tags: {highway: 'foo'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); it('does not add tag-unpaved for other highway types with explicit paved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'tertiary', surface: 'asphalt'}})) + .datum(iD.osmEntity({tags: {highway: 'tertiary', surface: 'asphalt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {highway: 'foo', tracktype: 'grade1'}})) + .datum(iD.osmEntity({tags: {highway: 'foo', tracktype: 'grade1'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); it('adds tag-unpaved for other highway types with explicit unpaved surface tagging', function() { selection - .datum(iD.Entity({tags: {highway: 'tertiary', surface: 'dirt'}})) + .datum(iD.osmEntity({tags: {highway: 'tertiary', surface: 'dirt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; selection - .datum(iD.Entity({tags: {highway: 'foo', tracktype: 'grade3'}})) + .datum(iD.osmEntity({tags: {highway: 'foo', tracktype: 'grade3'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.true; }); it('does not add tag-unpaved for non-highways', function() { selection - .datum(iD.Entity({tags: {railway: 'abandoned', surface: 'gravel'}})) + .datum(iD.osmEntity({tags: {railway: 'abandoned', surface: 'gravel'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; selection - .datum(iD.Entity({tags: {amenity: 'parking', surface: 'dirt'}})) + .datum(iD.osmEntity({tags: {amenity: 'parking', surface: 'dirt'}})) .call(iD.svgTagClasses()); expect(selection.classed('tag-unpaved')).to.be.false; }); @@ -164,7 +164,7 @@ describe('iD.svgTagClasses', function () { it('adds tags based on the result of the `tags` accessor', function() { var primary = function () { return { highway: 'primary'}; }; selection - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses().tags(primary)); expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); @@ -172,7 +172,7 @@ describe('iD.svgTagClasses', function () { it('removes classes for tags that are no longer present', function() { selection .attr('class', 'tag-highway tag-highway-primary') - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(''); }); @@ -180,7 +180,7 @@ describe('iD.svgTagClasses', function () { it('preserves existing non-"tag-"-prefixed classes', function() { selection .attr('class', 'selected') - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal('selected'); }); @@ -188,7 +188,7 @@ describe('iD.svgTagClasses', function () { it('works on SVG elements', function() { selection = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'g')); selection - .datum(iD.Entity()) + .datum(iD.osmEntity()) .call(iD.svgTagClasses()); expect(selection.attr('class')).to.equal(null); }); diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index 2c5104d22d..6ab4d4e3bf 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -11,19 +11,19 @@ describe('iD.svgVertices', function () { beforeEach(function () { - context = iD.Context(); + context = iD.coreContext(); d3.select(document.createElement('div')) .attr('id', 'map') - .call(context.map()); + .call(context.map().centerZoom([0, 0], 17)); surface = context.surface(); }); it('adds the .shared class to vertices that are members of two or more ways', function () { - var node = iD.Node({loc: [0, 0]}); - var way1 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}); - var way2 = iD.Way({nodes: [node.id], tags: {highway: 'residential'}}); - var graph = iD.Graph([node, way1, way2]); + var node = iD.osmNode({loc: [0, 0]}); + var way1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}); + var way2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}); + var graph = iD.coreGraph([node, way1, way2]); surface.call(iD.svgVertices(projection, context), graph, [node]); expect(surface.select('.vertex').classed('shared')).to.be.true; From ba5b3eee9c21c1908c84771a50a8cdd6e360218d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 00:26:37 -0500 Subject: [PATCH 039/206] More work on vertex drawing, add debug for touch targets --- css/20_map.css | 21 ++++---- modules/behavior/hover.js | 61 +++++++++++---------- modules/core/context.js | 11 ++-- modules/svg/debug.js | 35 ++++++------ modules/svg/vertices.js | 109 ++++++++++++++++++++++---------------- 5 files changed, 131 insertions(+), 106 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 34061453fb..4ab4dae1cf 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -11,9 +11,10 @@ use { pointer-events: none; } #turn-no-shape2, #turn-no-u-shape2 { fill: #E06D5F; } /* FF turn-no, turn-no-u */ #turn-yes-shape2, #turn-yes-u-shape2 { fill: #8CD05F; } /* FF turn-yes, turn-yes-u */ -g.point .shadow, -g.vertex .shadow, -g.midpoint .shadow { +.layer-points-group * { + pointer-events: none; +} +.layer-points-group.layer-points-targets * { pointer-events: all; } @@ -45,11 +46,11 @@ g.point.selected .shadow { stroke-opacity: 0.7; } -g.vertex.active, g.vertex.active *, +/*g.vertex.active, g.vertex.active *, g.point.active, g.point.active * { pointer-events: none; } - +*/ g.point ellipse.stroke { display: none; } @@ -89,10 +90,10 @@ g.midpoint .shadow { fill-opacity: 0; } -/*debug*/ -.vertex.target { - fill: #f00; - fill-opacity: 0.5; +.target { + color: rgba(0,0,0,0); + fill-opacity: 0.8; + fill: currentColor; } /*g.vertex.vertex-hover { @@ -120,7 +121,7 @@ g.midpoint .shadow { */ g.vertex.related:not(.selected) .shadow, -/*g.vertex.hover:not(.selected) .shadow,*/ +g.vertex.hover:not(.selected) .shadow, g.midpoint.related:not(.selected) .shadow, g.midpoint.hover:not(.selected) .shadow { fill-opacity: 0.5; diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index f7ea0b4ebb..b9c4bfa5cc 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -20,16 +20,16 @@ import { utilRebind } from '../util/rebind'; have the .hover class. */ export function behaviorHover(context) { - var dispatch = d3_dispatch('hover'), - _selection = d3_select(null), - newId = null, - buttonDown, - altDisables, - target; + var dispatch = d3_dispatch('hover'); + var _selection = d3_select(null); + var _newId = null; + var _buttonDown; + var _altDisables; + var _target; function keydown() { - if (altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { + if (_altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { _selection.selectAll('.hover') .classed('hover-suppressed', true) .classed('hover', false); @@ -43,7 +43,7 @@ export function behaviorHover(context) { function keyup() { - if (altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { + if (_altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) { _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false) .classed('hover', true); @@ -51,14 +51,14 @@ export function behaviorHover(context) { _selection .classed('hover-disabled', false); - dispatch.call('hover', this, target ? target.id : null); + dispatch.call('hover', this, _target ? _target.id : null); } } var hover = function(selection) { _selection = selection; - newId = null; + _newId = null; _selection .on('mouseover.hover', mouseover) @@ -71,65 +71,65 @@ export function behaviorHover(context) { function mouseover() { - if (buttonDown) return; - var target = d3_event.target; - enter(target ? target.__data__ : null); + if (_buttonDown) return; + var _target = d3_event.target; + enter(_target ? _target.__data__ : null); } function mouseout() { - if (buttonDown) return; - var target = d3_event.relatedTarget; - enter(target ? target.__data__ : null); + if (_buttonDown) return; + var _target = d3_event.relatedTarget; + enter(_target ? _target.__data__ : null); } function mousedown() { - buttonDown = true; + _buttonDown = true; d3_select(window) .on('mouseup.hover', mouseup, true); } function mouseup() { - buttonDown = false; + _buttonDown = false; d3_select(window) .on('mouseup.hover', null, true); } function enter(d) { - if (d === target) return; - target = d; + if (d === _target) return; + _target = d; _selection.selectAll('.hover') .classed('hover', false); _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - if (target instanceof osmEntity && target.id !== newId) { + if (_target instanceof osmEntity && _target.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 var mode = context.mode() && context.mode().id; - if ((mode === 'draw-line' || mode === 'draw-area') && !newId && target.type === 'node') { - newId = target.id; + if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && _target.type === 'node') { + _newId = _target.id; return; } - var selector = '.' + target.id; + var selector = '.' + _target.id; - if (target.type === 'relation') { - target.members.forEach(function(member) { + if (_target.type === 'relation') { + _target.members.forEach(function(member) { selector += ', .' + member.id; }); } - var suppressed = altDisables && d3_event && d3_event.altKey; + var suppressed = _altDisables && d3_event && d3_event.altKey; _selection.selectAll(selector) .classed(suppressed ? 'hover-suppressed' : 'hover', true); - dispatch.call('hover', this, !suppressed && target.id); + dispatch.call('hover', this, !suppressed && _target.id); } else { dispatch.call('hover', this, null); @@ -147,7 +147,6 @@ export function behaviorHover(context) { selection .classed('hover-disabled', false); - selection .on('mouseover.hover', null) .on('mouseout.hover', null) @@ -160,8 +159,8 @@ export function behaviorHover(context) { hover.altDisables = function(_) { - if (!arguments.length) return altDisables; - altDisables = _; + if (!arguments.length) return _altDisables; + _altDisables = _; return hover; }; diff --git a/modules/core/context.js b/modules/core/context.js index ce0cf57870..95146e2480 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -310,11 +310,12 @@ export function coreContext() { /* Debug */ var debugFlags = { - tile: false, - collision: false, - imagery: false, - imperial: false, - driveLeft: false + tile: false, // tile boundaries + collision: false, // label collision bounding boxes + imagery: false, // imagery bounding polygons + imperial: false, // imperial (not metric) bounding polygons + driveLeft: false, // driveLeft bounding polygons + target: false // touch targets }; context.debugFlags = function() { return debugFlags; diff --git a/modules/svg/debug.js b/modules/svg/debug.js index 9e1c82cb0f..8ab5617d42 100644 --- a/modules/svg/debug.js +++ b/modules/svg/debug.js @@ -21,12 +21,13 @@ export function svgDebug(projection, context) { } function drawDebug(selection) { - var showsTile = context.getDebug('tile'), - showsCollision = context.getDebug('collision'), - showsImagery = context.getDebug('imagery'), - showsImperial = context.getDebug('imperial'), - showsDriveLeft = context.getDebug('driveLeft'), - path = d3_geoPath(projection); + var showsTile = context.getDebug('tile'); + var showsCollision = context.getDebug('collision'); + var showsImagery = context.getDebug('imagery'); + var showsImperial = context.getDebug('imperial'); + var showsDriveLeft = context.getDebug('driveLeft'); + var showsTouchTargets = context.getDebug('target'); + var path = d3_geoPath(projection); var debugData = []; @@ -45,6 +46,9 @@ export function svgDebug(projection, context) { if (showsDriveLeft) { debugData.push({ class: 'green', label: 'driveLeft' }); } + if (showsTouchTargets) { + debugData.push({ class: 'pink', label: 'touchTargets' }); + } var legend = d3_select('#content') @@ -84,14 +88,14 @@ export function svgDebug(projection, context) { .merge(layer); - var extent = context.map().extent(), - dataImagery = data.imagery || [], - availableImagery = showsImagery && multipolygons(dataImagery.filter(function(source) { - if (!source.polygon) return false; - return source.polygon.some(function(polygon) { - return geoPolygonIntersectsPolygon(polygon, extent, true); - }); - })); + var extent = context.map().extent(); + var dataImagery = data.imagery || []; + var availableImagery = showsImagery && multipolygons(dataImagery.filter(function(source) { + if (!source.polygon) return false; + return source.polygon.some(function(polygon) { + return geoPolygonIntersectsPolygon(polygon, extent, true); + }); + })); var imagery = layer.selectAll('path.debug-imagery') .data(showsImagery ? availableImagery : []); @@ -142,7 +146,8 @@ export function svgDebug(projection, context) { context.getDebug('collision') || context.getDebug('imagery') || context.getDebug('imperial') || - context.getDebug('driveLeft'); + context.getDebug('driveLeft') || + context.getDebug('target'); } else { return this; } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 707ed2414e..85ab774cac 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,3 +1,4 @@ +import _assign from 'lodash-es/assign'; import _clone from 'lodash-es/clone'; import _values from 'lodash-es/values'; @@ -20,10 +21,11 @@ export function svgVertices(projection, context) { fill: [1, 1.5, 1.5, 1.5] }; - var _hover; + var _currHover; + var _currHoverSiblings = {}; - function draw(selection, vertices, klass, graph, siblings, filter) { + function draw(selection, graph, vertices, klass, siblings, filter) { siblings = siblings || {}; var icons = {}; var directions = {}; @@ -169,43 +171,9 @@ export function svgVertices(projection, context) { } - function drawVertices(selection, graph, entities, filter, extent) { - var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); - var siblings = getSiblingAndChildVertices(context.selectedIDs(), graph, extent, wireframe, zoom); - - // always render selected and sibling vertices.. - var vertices = _clone(siblings); - var filterWithSiblings = function(d) { return d.id in siblings || filter(d); }; - - // also render important vertices from the `entities` list.. - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - var geometry = entity.geometry(graph); - - if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { - vertices[entity.id] = entity; - - } else if ((geometry === 'vertex') && - (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) ) { - vertices[entity.id] = entity; - } - } - - selection.selectAll('.layer-points .layer-points-vertices') - .call(draw, _values(vertices), 'vertex-persistent', graph, siblings, filterWithSiblings); - - drawTargets(selection, graph, _values(vertices), filter, extent); - - } - - - function drawTargets(selection, graph, entities, filter, extent) { -// todo coming soon -return; - var layer = selection.selectAll('.layer-points .layer-points-targets'); - - var targets = layer.selectAll('g.vertex.target') + function drawTargets(selection, graph, entities, filter) { + var debugClass = 'pink'; + var targets = selection.selectAll('.target') .data(entities, osmEntity.key); // exit @@ -218,7 +186,8 @@ return; .attr('r', radiuses.shadow[3]) // just use the biggest one for now .attr('class', function(d) { return 'node vertex target ' + d.id; }) .merge(targets) - .attr('transform', svgPointTransform(projection)); + .attr('transform', svgPointTransform(projection)) + .classed(debugClass, context.getDebug('target')); } @@ -280,16 +249,66 @@ return; } + function drawVertices(selection, graph, entities, filter, extent) { + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); + + var selected = getSiblingAndChildVertices(context.selectedIDs(), graph, extent, wireframe, zoom); + + // interesting vertices from the `entities` list.. + var interesting = {}; + for (var i = 0; i < entities.length; i++) { + var entity = entities[i]; + var geometry = entity.geometry(graph); + + if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { + interesting[entity.id] = entity; + + } else if ((geometry === 'vertex') && + (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) ) { + interesting[entity.id] = entity; + } + } + + // 3 sets of vertices to consider + // - selected + siblings + // - hovered + siblings + // - interesting entities passed in + var all = _assign(selected, interesting, _currHoverSiblings); + + var filterWithSiblings = function(d) { + return d.id in selected || d.id in _currHoverSiblings || filter(d); + }; + selection.selectAll('.layer-points .layer-points-vertices') + .call(draw, graph, _values(all), 'vertex-persistent', {}, filterWithSiblings); + + + // draw touch targets for the hovered items only + var filterWithHover = function(d) { + return d.id in _currHoverSiblings || filter(d); + }; + selection.selectAll('.layer-points .layer-points-targets') + .call(drawTargets, graph, _values(_currHoverSiblings), filterWithHover); + } + + drawVertices.drawHover = function(selection, graph, target, extent) { - if (target === _hover) return; - _hover = target; + if (target === _currHover) return; var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); - var hovered = _hover ? getSiblingAndChildVertices([_hover.id], graph, extent, wireframe, zoom) : {}; - var filter = function() { return true; }; + var prevHoverSiblings = _currHoverSiblings || {}; + var filter = function(d) { return d.id in prevHoverSiblings; }; + + _currHover = target; + + if (_currHover) { + _currHoverSiblings = getSiblingAndChildVertices([_currHover.id], graph, extent, wireframe, zoom); + } else { + _currHoverSiblings = {}; + } - drawTargets(selection, graph, _values(hovered), filter, extent); + drawVertices(selection, graph, _values(prevHoverSiblings), filter, extent); }; return drawVertices; From 89d8d37576b813b31e94eb44ab470b23522fcca2 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 17:28:20 -0500 Subject: [PATCH 040/206] Drawing all the correct vertices now where I want them, simplify classes Some highlights - `getSiblingAndChildVertices` are expensive, so they're saved and called less frequently - draw touch targets for all the visible vertices - remove redundant css classes and `setClass` function --- css/20_map.css | 25 +------ modules/modes/drag_node.js | 2 +- modules/renderer/map.js | 8 +- modules/svg/vertices.js | 149 +++++++++++++++++++++++-------------- test/spec/svg/midpoints.js | 2 +- 5 files changed, 100 insertions(+), 86 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 4ab4dae1cf..cbff2a7f52 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -14,6 +14,7 @@ use { pointer-events: none; } .layer-points-group * { pointer-events: none; } +.layer-points-group.layer-points-midpoints *, .layer-points-group.layer-points-targets * { pointer-events: all; } @@ -96,30 +97,6 @@ g.midpoint .shadow { fill: currentColor; } -/*g.vertex.vertex-hover { - display: none; -} - -.mode-draw-area g.vertex.vertex-hover, -.mode-draw-line g.vertex.vertex-hover, -.mode-add-area g.vertex.vertex-hover, -.mode-add-line g.vertex.vertex-hover, -.mode-add-point g.vertex.vertex-hover, -.mode-drag-node g.vertex.vertex-hover { - display: block; - color: #f00; -} - -.mode-draw-area .hover-disabled g.vertex.vertex-hover, -.mode-draw-line .hover-disabled g.vertex.vertex-hover, -.mode-add-area .hover-disabled g.vertex.vertex-hover, -.mode-add-line .hover-disabled g.vertex.vertex-hover, -.mode-add-point .hover-disabled g.vertex.vertex-hover, -.mode-drag-node .hover-disabled g.vertex.vertex-hover { - display: none; -} -*/ - g.vertex.related:not(.selected) .shadow, g.vertex.hover:not(.selected) .shadow, g.midpoint.related:not(.selected) .shadow, diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 50cd8231b3..bd5bb5a238 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -249,7 +249,7 @@ export function modeDragNode(context) { var behavior = behaviorDrag() - .selector('g.node, g.point, g.midpoint') + .selector('g.node, g.midpoint') .surface(d3_select('#map').node()) .origin(origin) .on('start', start) diff --git a/modules/renderer/map.js b/modules/renderer/map.js index f9de7d4607..39996d04fc 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -207,7 +207,7 @@ export function rendererMap(context) { all = context.features().filter(all, graph); surface.selectAll('.data-layer-osm') - .call(drawVertices, graph, all, filter, map.extent()) + .call(drawVertices.drawSelected, graph, all, map.extent()) .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); dispatch.call('drawn', this, { full: false }); } @@ -268,6 +268,7 @@ export function rendererMap(context) { var graph = context.graph(); var features = context.features(); var all = context.intersects(map.extent()); + var fullRedraw = false; var data; var filter; @@ -291,6 +292,7 @@ export function rendererMap(context) { } else { data = all; + fullRedraw = true; filter = utilFunctor(true); } } @@ -298,11 +300,11 @@ export function rendererMap(context) { data = features.filter(data, graph); surface.selectAll('.data-layer-osm') - .call(drawVertices, graph, data, filter, map.extent()) + .call(drawVertices, graph, data, filter, map.extent(), fullRedraw) .call(drawLines, graph, data, filter) .call(drawAreas, graph, data, filter) .call(drawMidpoints, graph, data, filter, map.trimmedExtent()) - .call(drawLabels, graph, data, filter, dimensions, !difference && !extent) + .call(drawLabels, graph, data, filter, dimensions, fullRedraw) .call(drawPoints, graph, data, filter); dispatch.call('drawn', this, {full: true}); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 85ab774cac..e09eb40eaf 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -1,5 +1,4 @@ import _assign from 'lodash-es/assign'; -import _clone from 'lodash-es/clone'; import _values from 'lodash-es/values'; import { select as d3_select } from 'd3-selection'; @@ -21,12 +20,21 @@ export function svgVertices(projection, context) { fill: [1, 1.5, 1.5, 1.5] }; - var _currHover; - var _currHoverSiblings = {}; + var _currHoverTarget; + var _currPersistent = {}; + var _currHover = {}; + var _prevHover = {}; + var _currSelected = {}; + var _prevSelected = {}; - function draw(selection, graph, vertices, klass, siblings, filter) { - siblings = siblings || {}; + function sortY(a, b) { + return b.loc[1] - a.loc[1]; + } + + + function draw(selection, graph, vertices, sets, filter) { + sets = sets || { selected: {}, important: {}, hovered: {} }; var icons = {}; var directions = {}; var wireframe = context.surface().classed('fill-wireframe'); @@ -54,16 +62,8 @@ export function svgVertices(projection, context) { } - function setClass(klass) { - return function(entity) { - d3_select(this) - .attr('class', 'node vertex ' + klass + ' ' + entity.id); - }; - } - - function updateAttributes(selection) { - ['shadow','stroke','fill'].forEach(function(klass) { + ['shadow', 'stroke', 'fill'].forEach(function(klass) { var rads = radiuses[klass]; selection.selectAll('.' + klass) .each(function(entity) { @@ -88,8 +88,9 @@ export function svgVertices(projection, context) { .attr('visibility', (z === 0 ? 'hidden' : null)); } + vertices.sort(sortY); - var groups = selection.selectAll('.vertex.' + klass) + var groups = selection.selectAll('g.vertex') .filter(filter) .data(vertices, osmEntity.key); @@ -100,41 +101,42 @@ export function svgVertices(projection, context) { // enter var enter = groups.enter() .append('g') - .attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; }); + .attr('class', function(d) { return 'node vertex ' + d.id; }) + .order(); enter .append('circle') - .each(setClass('shadow')); + .attr('class', 'shadow'); enter .append('circle') - .each(setClass('stroke')); + .attr('class', 'stroke'); // Vertices with icons get a `use`. enter.filter(function(d) { return getIcon(d); }) .append('use') + .attr('class', 'icon') + .attr('width', '11px') + .attr('height', '11px') .attr('transform', 'translate(-5, -6)') .attr('xlink:href', function(d) { var picon = getIcon(d); var isMaki = dataFeatureIcons.indexOf(picon) !== -1; return '#' + picon + (isMaki ? '-11' : ''); - }) - .attr('width', '11px') - .attr('height', '11px') - .each(setClass('icon')); + }); // Vertices with tags get a fill. enter.filter(function(d) { return d.hasInterestingTags(); }) .append('circle') - .each(setClass('fill')); + .attr('class', 'fill'); // update groups = groups .merge(enter) .attr('transform', svgPointTransform(projection)) - .classed('sibling', function(entity) { return entity.id in siblings; }) - .classed('shared', function(entity) { return graph.isShared(entity); }) - .classed('endpoint', function(entity) { return entity.isEndpoint(graph); }) + .classed('sibling', function(d) { return d.id in sets.selected; }) + .classed('shared', function(d) { return graph.isShared(d); }) + .classed('endpoint', function(d) { return d.isEndpoint(graph); }) .call(updateAttributes); @@ -150,7 +152,7 @@ export function svgVertices(projection, context) { // enter/update dgroups = dgroups.enter() .insert('g', '.shadow') - .each(setClass('viewfieldgroup')) + .attr('class', 'viewfieldgroup') .merge(dgroups); var viewfields = dgroups.selectAll('.viewfield') @@ -174,6 +176,7 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { var debugClass = 'pink'; var targets = selection.selectAll('.target') + .filter(filter) .data(entities, osmEntity.key); // exit @@ -202,7 +205,7 @@ export function svgVertices(projection, context) { } - function getSiblingAndChildVertices(ids, graph, extent, wireframe, zoom) { + function getSiblingAndChildVertices(ids, graph, wireframe, zoom) { var results = {}; function addChildVertices(entity) { @@ -223,7 +226,7 @@ export function svgVertices(projection, context) { addChildVertices(member); } } - } else if (renderAsVertex(entity, graph, wireframe, zoom) && entity.intersects(extent, graph)) { + } else if (renderAsVertex(entity, graph, wireframe, zoom)) { results[entity.id] = entity; } } @@ -249,66 +252,98 @@ export function svgVertices(projection, context) { } - function drawVertices(selection, graph, entities, filter, extent) { + function drawVertices(selection, graph, entities, filter, extent, fullRedraw) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); + var mode = context.mode(); + var isDrawing = mode && /^(add|draw|drag)/.test(mode.id); - var selected = getSiblingAndChildVertices(context.selectedIDs(), graph, extent, wireframe, zoom); + // Collect important vertices from the `entities` list.. + // (during a paritial redraw, it will not contain everything) + if (fullRedraw) { + _currPersistent = {}; + } - // interesting vertices from the `entities` list.. - var interesting = {}; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; var geometry = entity.geometry(graph); if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { - interesting[entity.id] = entity; + _currPersistent[entity.id] = entity; } else if ((geometry === 'vertex') && (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) ) { - interesting[entity.id] = entity; + _currPersistent[entity.id] = entity; + + } else if (!fullRedraw) { + delete _currPersistent[entity.id]; } } - // 3 sets of vertices to consider - // - selected + siblings - // - hovered + siblings - // - interesting entities passed in - var all = _assign(selected, interesting, _currHoverSiblings); - - var filterWithSiblings = function(d) { - return d.id in selected || d.id in _currHoverSiblings || filter(d); + // 3 sets of vertices to consider: + var sets = { + persistent: _currPersistent, // persistent = important vertices (render always) + selected: _currSelected, // selected + siblings of selected (render always) + hovered: _currHover // hovered + siblings of hovered (render only in draw modes) }; - selection.selectAll('.layer-points .layer-points-vertices') - .call(draw, graph, _values(all), 'vertex-persistent', {}, filterWithSiblings); + var all = _assign({}, _currPersistent, _currSelected, (isDrawing ? _currHover : {})); - // draw touch targets for the hovered items only - var filterWithHover = function(d) { - return d.id in _currHoverSiblings || filter(d); + // Draw the vertices.. + // The filter function controls the scope of what objects d3 will touch (exit/enter/update) + // It's important to adjust the filter function to expand the scope beyond whatever entities were passed in. + var filterRendered = function(d) { + return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d); }; + selection.selectAll('.layer-points .layer-points-vertices') + .call(draw, graph, visible(all), sets, filterRendered); + + // Draw touch targets.. selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, _values(_currHoverSiblings), filterWithHover); + .call(drawTargets, graph, visible(all), filterRendered); + + + function visible(which) { + return _values(which).filter(function (entity) { + return entity.intersects(extent, graph); + }); + } } + // partial redraw - only update the selected items.. + drawVertices.drawSelected = function(selection, graph, target, extent) { + var wireframe = context.surface().classed('fill-wireframe'); + var zoom = ktoz(projection.scale()); + + _prevSelected = _currSelected || {}; + _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom); + + // note that drawVertices will add `_currSelected` automatically if needed.. + var filter = function(d) { return d.id in _prevSelected; }; + drawVertices(selection, graph, _values(_prevSelected), filter, extent, false); + }; + + + // partial redraw - only update the hovered items.. drawVertices.drawHover = function(selection, graph, target, extent) { - if (target === _currHover) return; + if (target === _currHoverTarget) return; // continue only if something changed var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); - var prevHoverSiblings = _currHoverSiblings || {}; - var filter = function(d) { return d.id in prevHoverSiblings; }; - _currHover = target; + _prevHover = _currHover || {}; + _currHoverTarget = target; - if (_currHover) { - _currHoverSiblings = getSiblingAndChildVertices([_currHover.id], graph, extent, wireframe, zoom); + if (_currHoverTarget) { + _currHover = getSiblingAndChildVertices([_currHoverTarget.id], graph, wireframe, zoom); } else { - _currHoverSiblings = {}; + _currHover = {}; } - drawVertices(selection, graph, _values(prevHoverSiblings), filter, extent); + // note that drawVertices will add `_currHover` automatically if needed.. + var filter = function(d) { return d.id in _prevHover; }; + drawVertices(selection, graph, _values(_prevHover), filter, extent, false); }; return drawVertices; diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js index bb4442415b..230ad8446c 100644 --- a/test/spec/svg/midpoints.js +++ b/test/spec/svg/midpoints.js @@ -20,7 +20,7 @@ describe('iD.svgMidpoints', function () { selectedIDs: function() { return _selectedIDs; } }); - var map = d3.select(document.createElement('div')) + d3.select(document.createElement('div')) .attr('id', 'map') .call(context.map().centerZoom([0, 0], 17)); From f0e2d3fbfe8a5a73843287324244c7d762650ae3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 17:50:00 -0500 Subject: [PATCH 041/206] Slightly adjust numbers for iconified vertices for better centering (maybe not changed after we switched to maki2 which has 11px icons) --- modules/svg/vertices.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index e09eb40eaf..fa6634304d 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -15,8 +15,8 @@ function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } export function svgVertices(projection, context) { var radiuses = { // z16-, z17, z18+, tagged - shadow: [6, 7.5, 7.5, 11.5], - stroke: [2.5, 3.5, 3.5, 7], + shadow: [6, 7.5, 7.5, 12], + stroke: [2.5, 3.5, 3.5, 8], fill: [1, 1.5, 1.5, 1.5] }; @@ -68,7 +68,6 @@ export function svgVertices(projection, context) { selection.selectAll('.' + klass) .each(function(entity) { var i = z && getIcon(entity); - var c = i ? 0.5 : 0; var r = rads[i ? 3 : z]; // slightly increase the size of unconnected endpoints #3775 @@ -77,8 +76,6 @@ export function svgVertices(projection, context) { } d3_select(this) - .attr('cx', c) - .attr('cy', -c) .attr('r', r) .attr('visibility', ((i && klass === 'fill') ? 'hidden' : null)); }); @@ -118,7 +115,7 @@ export function svgVertices(projection, context) { .attr('class', 'icon') .attr('width', '11px') .attr('height', '11px') - .attr('transform', 'translate(-5, -6)') + .attr('transform', 'translate(-5.5, -5.5)') .attr('xlink:href', function(d) { var picon = getIcon(d); var isMaki = dataFeatureIcons.indexOf(picon) !== -1; From 6644c9db6b4129e0647df8ea33ad50b56533f49d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 18:17:12 -0500 Subject: [PATCH 042/206] Fix context.js and vertices.js tests --- test/spec/core/context.js | 3 ++- test/spec/svg/vertices.js | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test/spec/core/context.js b/test/spec/core/context.js index 04f665400b..df3fbfdbf4 100644 --- a/test/spec/core/context.js +++ b/test/spec/core/context.js @@ -59,7 +59,8 @@ describe('iD.Context', function() { collision: false, imagery: false, imperial: false, - driveLeft: false + driveLeft: false, + target: false }; expect(context.debugFlags()).to.eql(flags); diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index 6ab4d4e3bf..3f7789643d 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -24,8 +24,10 @@ describe('iD.svgVertices', function () { var way1 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}); var way2 = iD.osmWay({nodes: [node.id], tags: {highway: 'residential'}}); var graph = iD.coreGraph([node, way1, way2]); + var filter = function() { return true; }; + var extent = iD.geoExtent([0, 0], [1, 1]); - surface.call(iD.svgVertices(projection, context), graph, [node]); + surface.call(iD.svgVertices(projection, context), graph, [node], filter, extent); expect(surface.select('.vertex').classed('shared')).to.be.true; }); }); From ba7437b4cc62f9a1854863aade47ef947273c3b5 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 21:21:48 -0500 Subject: [PATCH 043/206] Fix order of vertex in _assign, always get latest entity (selected or hovered entities are old, if we're moving vertices around) --- modules/modes/drag_node.js | 2 +- modules/svg/vertices.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index bd5bb5a238..748bc97b9f 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -249,7 +249,7 @@ export function modeDragNode(context) { var behavior = behaviorDrag() - .selector('g.node, g.midpoint') + .selector('.vertex.target, g.point, g.midpoint') .surface(d3_select('#map').node()) .origin(origin) .on('start', start) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index fa6634304d..f494e5328c 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -284,7 +284,7 @@ export function svgVertices(projection, context) { hovered: _currHover // hovered + siblings of hovered (render only in draw modes) }; - var all = _assign({}, _currPersistent, _currSelected, (isDrawing ? _currHover : {})); + var all = _assign({}, (isDrawing ? _currHover : {}), _currSelected, _currPersistent); // Draw the vertices.. // The filter function controls the scope of what objects d3 will touch (exit/enter/update) @@ -293,17 +293,17 @@ export function svgVertices(projection, context) { return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d); }; selection.selectAll('.layer-points .layer-points-vertices') - .call(draw, graph, visible(all), sets, filterRendered); + .call(draw, graph, currentVisible(all), sets, filterRendered); // Draw touch targets.. selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, visible(all), filterRendered); + .call(drawTargets, graph, currentVisible(all), filterRendered); - function visible(which) { - return _values(which).filter(function (entity) { - return entity.intersects(extent, graph); - }); + function currentVisible(which) { + return Object.keys(which) + .map(context.hasEntity) // the current version of this entity + .filter(function (entity) { return entity && entity.intersects(extent, graph); }); } } From 9d42d470caf61f599a1bdff372c7bd79b47163ef Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 21:26:58 -0500 Subject: [PATCH 044/206] Touch targets can be keyed on entity.id instead of osmEntity.key to avoid excessive exit/enter flickering --- modules/svg/vertices.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index f494e5328c..d80302a0ba 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -174,7 +174,7 @@ export function svgVertices(projection, context) { var debugClass = 'pink'; var targets = selection.selectAll('.target') .filter(filter) - .data(entities, osmEntity.key); + .data(entities, function key(d) { return d.id; }); // exit targets.exit() From 5cb5456869a07c0de38af4e16be50f907c3210cb Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 15 Dec 2017 22:50:55 -0500 Subject: [PATCH 045/206] Implement touch targets for midpoints and points --- css/20_map.css | 22 +++--------- modules/modes/drag_node.js | 2 +- modules/svg/midpoints.js | 69 ++++++++++++++++++++++++++++---------- modules/svg/points.js | 36 ++++++++++++++++++-- modules/svg/vertices.js | 12 +++---- 5 files changed, 98 insertions(+), 43 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index cbff2a7f52..505b93f9b2 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -7,19 +7,19 @@ use { pointer-events: none; } /* the above fill: none rule affects paths in shadow dom only in Firefox */ .layer-osm use.icon path { fill: #333; } /* FF svg Maki icons */ .layer-osm .turn use path { fill: #000; } /* FF turn restriction icons */ -#turn-only-shape2, #turn-only-u-shape2 { fill: #7092FF; } /* FF turn-only, turn-only-u */ -#turn-no-shape2, #turn-no-u-shape2 { fill: #E06D5F; } /* FF turn-no, turn-no-u */ -#turn-yes-shape2, #turn-yes-u-shape2 { fill: #8CD05F; } /* FF turn-yes, turn-yes-u */ +#turn-only-shape2, #turn-only-u-shape2 { fill: #7092ff; } /* FF turn-only, turn-only-u */ +#turn-no-shape2, #turn-no-u-shape2 { fill: #e06d5f; } /* FF turn-no, turn-no-u */ +#turn-yes-shape2, #turn-yes-u-shape2 { fill: #8cd05f; } /* FF turn-yes, turn-yes-u */ .layer-points-group * { pointer-events: none; } -.layer-points-group.layer-points-midpoints *, .layer-points-group.layer-points-targets * { pointer-events: all; } -path.shadow { +.layer-areas path.shadow, +.layer-lines path.shadow { pointer-events: stroke; } @@ -47,11 +47,6 @@ g.point.selected .shadow { stroke-opacity: 0.7; } -/*g.vertex.active, g.vertex.active *, -g.point.active, g.point.active * { - pointer-events: none; -} -*/ g.point ellipse.stroke { display: none; } @@ -108,13 +103,6 @@ g.vertex.selected .shadow { fill-opacity: 0.7; } -.mode-draw-area g.midpoint, -.mode-draw-line g.midpoint, -.mode-add-area g.midpoint, -.mode-add-line g.midpoint, -.mode-add-point g.midpoint { - display: none; -} /* lines */ diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 748bc97b9f..81bc6c22a5 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -249,7 +249,7 @@ export function modeDragNode(context) { var behavior = behaviorDrag() - .selector('.vertex.target, g.point, g.midpoint') + .selector('.layer-points-targets .target') .surface(d3_select('#map').node()) .origin(origin) .on('start', start) diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index 27e548ca46..d6aaa0ab95 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -15,17 +15,44 @@ import { export function svgMidpoints(projection, context) { - return function drawMidpoints(selection, graph, entities, filter, extent) { + + function drawTargets(selection, graph, entities, filter) { + var debugClass = 'pink'; + var targets = selection.selectAll('.midpoint.target') + .filter(filter) + .data(entities, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('circle') + .attr('r', 12) + .attr('class', function(d) { return 'midpoint target ' + d.id; }) + .merge(targets) + .attr('transform', svgPointTransform(projection)) + .classed(debugClass, context.getDebug('target')); + } + + + function drawMidpoints(selection, graph, entities, filter, extent) { var layer = selection.selectAll('.layer-points .layer-points-midpoints'); var mode = context.mode(); if (mode && mode.id !== 'select') { - layer.selectAll('g.midpoint').remove(); + layer.selectAll('g.midpoint') + .remove(); + + selection.selectAll('.layer-points .layer-points-targets .midpoint.target') + .remove(); + return; } - var poly = extent.polygon(), - midpoints = {}; + var poly = extent.polygon(); + var midpoints = {}; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -40,16 +67,16 @@ export function svgMidpoints(projection, context) { var nodes = graph.childNodes(entity); for (var j = 0; j < nodes.length - 1; j++) { - var a = nodes[j], - b = nodes[j + 1], - id = [a.id, b.id].sort().join('-'); + var a = nodes[j]; + var b = nodes[j + 1]; + var id = [a.id, b.id].sort().join('-'); if (midpoints[id]) { midpoints[id].parents.push(entity); } else { if (geoEuclideanDistance(projection(a.loc), projection(b.loc)) > 40) { - var point = geoInterp(a.loc, b.loc, 0.5), - loc = null; + var point = geoInterp(a.loc, b.loc, 0.5); + var loc = null; if (extent.intersects(point)) { loc = point; @@ -107,22 +134,24 @@ export function svgMidpoints(projection, context) { .insert('g', ':first-child') .attr('class', 'midpoint'); - enter.append('polygon') + enter + .append('polygon') .attr('points', '-6,8 10,0 -6,-8') .attr('class', 'shadow'); - enter.append('polygon') + enter + .append('polygon') .attr('points', '-3,4 5,0 -3,-4') .attr('class', 'fill'); groups = groups .merge(enter) .attr('transform', function(d) { - var translate = svgPointTransform(projection), - a = graph.entity(d.edge[0]), - b = graph.entity(d.edge[1]), - angleVal = Math.round(geoAngle(a, b, projection) * (180 / Math.PI)); - return translate(d) + ' rotate(' + angleVal + ')'; + var translate = svgPointTransform(projection); + var a = graph.entity(d.edge[0]); + var b = graph.entity(d.edge[1]); + var angle = geoAngle(a, b, projection) * (180 / Math.PI); + return translate(d) + ' rotate(' + angle + ')'; }) .call(svgTagClasses().tags( function(d) { return d.parents[0].tags; } @@ -132,5 +161,11 @@ export function svgMidpoints(projection, context) { groups.select('polygon.shadow'); groups.select('polygon.fill'); - }; + + // Draw touch targets.. + selection.selectAll('.layer-points .layer-points-targets') + .call(drawTargets, graph, _values(midpoints), midpointFilter); + } + + return drawMidpoints; } diff --git a/modules/svg/points.js b/modules/svg/points.js index ab8f2361e5..8f38733e89 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -21,7 +21,31 @@ export function svgPoints(projection, context) { } - return function drawPoints(selection, graph, entities, filter) { + function drawTargets(selection, graph, entities, filter) { + var debugClass = 'pink'; + var targets = selection.selectAll('.point.target') + .filter(filter) + .data(entities, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('rect') + .attr('x', -15) + .attr('y', -30) + .attr('width', 30) + .attr('height', 36) + .attr('class', function(d) { return 'node point target ' + d.id; }) + .merge(targets) + .attr('transform', svgPointTransform(projection)) + .classed(debugClass, context.getDebug('target')); + } + + + function drawPoints(selection, graph, entities, filter) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); @@ -95,5 +119,13 @@ export function svgPoints(projection, context) { return '#' + picon + (isMaki ? '-11' : ''); } }); - }; + + + // touch targets + selection.selectAll('.layer-points .layer-points-targets') + .call(drawTargets, graph, points, filter); + } + + + return drawPoints; } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index d80302a0ba..e2231b8c47 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -14,7 +14,7 @@ function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } export function svgVertices(projection, context) { var radiuses = { - // z16-, z17, z18+, tagged + // z16-, z17, z18+, w/icon shadow: [6, 7.5, 7.5, 12], stroke: [2.5, 3.5, 3.5, 8], fill: [1, 1.5, 1.5, 1.5] @@ -172,7 +172,7 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { var debugClass = 'pink'; - var targets = selection.selectAll('.target') + var targets = selection.selectAll('.vertex.target') .filter(filter) .data(entities, function key(d) { return d.id; }); @@ -211,14 +211,14 @@ export function svgVertices(projection, context) { var i; if (entity.type === 'way') { for (i = 0; i < entity.nodes.length; i++) { - var child = context.hasEntity(entity.nodes[i]); + var child = graph.hasEntity(entity.nodes[i]); if (child) { addChildVertices(child); } } } else if (entity.type === 'relation') { for (i = 0; i < entity.members.length; i++) { - var member = context.hasEntity(entity.members[i].id); + var member = graph.hasEntity(entity.members[i].id); if (member) { addChildVertices(member); } @@ -230,7 +230,7 @@ export function svgVertices(projection, context) { } ids.forEach(function(id) { - var entity = context.hasEntity(id); + var entity = graph.hasEntity(id); if (!entity) return; if (entity.type === 'node') { @@ -302,7 +302,7 @@ export function svgVertices(projection, context) { function currentVisible(which) { return Object.keys(which) - .map(context.hasEntity) // the current version of this entity + .map(graph.hasEntity, graph) // the current version of this entity .filter(function (entity) { return entity && entity.intersects(extent, graph); }); } } From 5a4faa84a1083a7d530c9f4d149bc6db56f72a61 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 16 Dec 2017 01:33:12 -0500 Subject: [PATCH 046/206] Fixed some of the point/vertex/midpoint snapping issues Still working on snapping to lines/areas, and making sure drawing lines/areas will complete --- css/20_map.css | 7 +++ css/70_fills.css | 12 ---- modules/behavior/drag.js | 110 +++++++++++++++++++------------------ modules/behavior/draw.js | 88 +++++++++++++++-------------- modules/modes/drag_node.js | 7 ++- 5 files changed, 115 insertions(+), 109 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 505b93f9b2..dc01e7a640 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -18,6 +18,13 @@ use { pointer-events: none; } pointer-events: all; } +/*.mode-draw-line .active, +.mode-draw-area .active, +*/ +.mode-drag-node .active { + pointer-events: none; +} + .layer-areas path.shadow, .layer-lines path.shadow { pointer-events: stroke; diff --git a/css/70_fills.css b/css/70_fills.css index ffb437d686..ec8f78d82c 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -38,18 +38,6 @@ /* Modes */ -/*.mode-draw-line .vertex.active, -.mode-draw-area .vertex.active, -.mode-drag-node .vertex.active { - display: none; -} -*/ -/*.mode-draw-line .active, -.mode-draw-area .active, -.mode-drag-node .active { - pointer-events: none; -} -*/ /* Ensure drawing doesn't interact with area fills. */ .mode-add-point path.area.fill, .mode-draw-line path.area.fill, diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index ebd2ea706b..52e9fb7ffd 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -33,17 +33,19 @@ import { */ export function behaviorDrag() { - var event = d3_dispatch('start', 'move', 'end'), - origin = null, - selector = '', - filter = null, - event_, target, surface; - - - var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect'), - d3_event_userSelectSuppress = function() { - var selection = d3_selection(), - select = selection.style(d3_event_userSelectProperty); + var dispatch = d3_dispatch('start', 'move', 'end'); + var _origin = null; + var _selector = ''; + var _filter = null; + var _event; + var _target; + var _surface; + + + var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect'); + var d3_event_userSelectSuppress = function() { + var selection = d3_selection(); + var select = selection.style(d3_event_userSelectProperty); selection.style(d3_event_userSelectProperty, 'none'); return function() { selection.style(d3_event_userSelectProperty, select); @@ -60,29 +62,29 @@ export function behaviorDrag() { function eventOf(thiz, argumentz) { return function(e1) { e1.target = drag; - d3_customEvent(e1, event.apply, event, [e1.type, thiz, argumentz]); + d3_customEvent(e1, dispatch.apply, dispatch, [e1.type, thiz, argumentz]); }; } function dragstart() { - target = this; - event_ = eventOf(target, arguments); + _target = this; + _event = eventOf(_target, arguments); - var eventTarget = d3_event.target, - touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null, - offset, - origin_ = point(), - started = false, - selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag'); + var eventTarget = d3_event.target; + var touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null; + var offset; + var startOrigin = point(); + var started = false; + var selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag'); d3_select(window) .on(touchId !== null ? 'touchmove.drag-' + touchId : 'mousemove.drag', dragmove) .on(touchId !== null ? 'touchend.drag-' + touchId : 'mouseup.drag', dragend, true); - if (origin) { - offset = origin.apply(target, arguments); - offset = [offset[0] - origin_[0], offset[1] - origin_[1]]; + if (_origin) { + offset = _origin.apply(_target, arguments); + offset = [offset[0] - startOrigin[0], offset[1] - startOrigin[1]]; } else { offset = [0, 0]; } @@ -93,7 +95,7 @@ export function behaviorDrag() { function point() { - var p = surface || target.parentNode; + var p = _surface || _target.parentNode; return touchId !== null ? d3_touches(p).filter(function(p) { return p.identifier === touchId; })[0] : d3_mouse(p); @@ -101,22 +103,22 @@ export function behaviorDrag() { function dragmove() { - var p = point(), - dx = p[0] - origin_[0], - dy = p[1] - origin_[1]; + var p = point(); + var dx = p[0] - startOrigin[0]; + var dy = p[1] - startOrigin[1]; if (dx === 0 && dy === 0) return; if (!started) { started = true; - event_({ type: 'start' }); + _event({ type: 'start' }); } - origin_ = p; + startOrigin = p; d3_eventCancel(); - event_({ + _event({ type: 'move', point: [p[0] + offset[0], p[1] + offset[1]], delta: [dx, dy] @@ -126,7 +128,7 @@ export function behaviorDrag() { function dragend() { if (started) { - event_({ type: 'end' }); + _event({ type: 'end' }); d3_eventCancel(); if (d3_event.target === eventTarget) { @@ -152,16 +154,16 @@ export function behaviorDrag() { function drag(selection) { - var matchesSelector = utilPrefixDOMProperty('matchesSelector'), - delegate = dragstart; + var matchesSelector = utilPrefixDOMProperty('matchesSelector'); + var delegate = dragstart; - if (selector) { + if (_selector) { delegate = function() { - var root = this, - target = d3_event.target; + var root = this; + var target = d3_event.target; for (; target && target !== root; target = target.parentNode) { - if (target[matchesSelector](selector) && - (!filter || filter(target.__data__))) { + if (target[matchesSelector](_selector) && + (!_filter || _filter(target.__data__))) { return dragstart.call(target, target.__data__); } } @@ -169,35 +171,35 @@ export function behaviorDrag() { } selection - .on('mousedown.drag' + selector, delegate) - .on('touchstart.drag' + selector, delegate); + .on('mousedown.drag' + _selector, delegate) + .on('touchstart.drag' + _selector, delegate); } drag.off = function(selection) { selection - .on('mousedown.drag' + selector, null) - .on('touchstart.drag' + selector, null); + .on('mousedown.drag' + _selector, null) + .on('touchstart.drag' + _selector, null); }; drag.selector = function(_) { - if (!arguments.length) return selector; - selector = _; + if (!arguments.length) return _selector; + _selector = _; return drag; }; drag.filter = function(_) { - if (!arguments.length) return origin; - filter = _; + if (!arguments.length) return _filter; + _filter = _; return drag; }; drag.origin = function (_) { - if (!arguments.length) return origin; - origin = _; + if (!arguments.length) return _origin; + _origin = _; return drag; }; @@ -211,19 +213,19 @@ export function behaviorDrag() { drag.target = function() { - if (!arguments.length) return target; - target = arguments[0]; - event_ = eventOf(target, Array.prototype.slice.call(arguments, 1)); + if (!arguments.length) return _target; + _target = arguments[0]; + _event = eventOf(_target, Array.prototype.slice.call(arguments, 1)); return drag; }; drag.surface = function() { - if (!arguments.length) return surface; - surface = arguments[0]; + if (!arguments.length) return _surface; + _surface = arguments[0]; return drag; }; - return utilRebind(drag, event, 'on'); + return utilRebind(drag, dispatch, 'on'); } diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 62c6031f6f..f4629d7f59 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -20,31 +20,35 @@ import { import { utilRebind } from '../util/rebind'; -var usedTails = {}; -var disableSpace = false; -var lastSpace = null; +var _usedTails = {}; +var _disableSpace = false; +var _lastSpace = null; export function behaviorDraw(context) { - var dispatch = d3_dispatch('move', 'click', 'clickWay', - 'clickNode', 'undo', 'cancel', 'finish'), - keybinding = d3_keybinding('draw'), - hover = behaviorHover(context) - .altDisables(true) - .on('hover', context.ui().sidebar.hover), - tail = behaviorTail(), - edit = behaviorEdit(context), - closeTolerance = 4, - tolerance = 12, - mouseLeave = false, - lastMouse = null; + var dispatch = d3_dispatch( + 'move', 'click', 'clickWay', 'clickNode', 'undo', 'cancel', 'finish' + ); + + var keybinding = d3_keybinding('draw'); + + var hover = behaviorHover(context) + .altDisables(true) + .on('hover', context.ui().sidebar.hover); + var tail = behaviorTail(); + var edit = behaviorEdit(context); + + var closeTolerance = 4; + var tolerance = 12; + var _mouseLeave = false; + var _lastMouse = null; function datum() { if (d3_event.altKey) return {}; if (d3_event.type === 'keydown') { - return (lastMouse && lastMouse.target.__data__) || {}; + return (_lastMouse && _lastMouse.target.__data__) || {}; } else { return d3_event.target.__data__ || {}; } @@ -60,17 +64,17 @@ export function behaviorDraw(context) { })[0] : d3_mouse(p); } - var element = d3_select(this), - touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null, - t1 = +new Date(), - p1 = point(); + var element = d3_select(this); + var touchId = d3_event.touches ? d3_event.changedTouches[0].identifier : null; + var t1 = +new Date(); + var p1 = point(); element.on('mousemove.draw', null); d3_select(window).on('mouseup.draw', function() { - var t2 = +new Date(), - p2 = point(), - dist = geoEuclideanDistance(p1, p2); + var t2 = +new Date(); + var p2 = point(); + var dist = geoEuclideanDistance(p1, p2); element.on('mousemove.draw', mousemove); d3_select(window).on('mouseup.draw', null); @@ -95,33 +99,33 @@ export function behaviorDraw(context) { function mousemove() { - lastMouse = d3_event; + _lastMouse = d3_event; dispatch.call('move', this, datum()); } function mouseenter() { - mouseLeave = false; + _mouseLeave = false; } function mouseleave() { - mouseLeave = true; + _mouseLeave = true; } function click() { var d = datum(); if (d.type === 'way') { - var dims = context.map().dimensions(), - mouse = context.mouse(), - pad = 5, - trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad && + var dims = context.map().dimensions(); + var mouse = context.mouse(); + var pad = 5; + var trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad && mouse[1] > pad && mouse[1] < dims[1] - pad; if (trySnap) { - var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection), - edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; + var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); + var edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; dispatch.call('clickWay', this, choice.loc, edge); } else { dispatch.call('click', this, context.map().mouseCoordinates()); @@ -141,23 +145,23 @@ export function behaviorDraw(context) { d3_event.stopPropagation(); var currSpace = context.mouse(); - if (disableSpace && lastSpace) { - var dist = geoEuclideanDistance(lastSpace, currSpace); + if (_disableSpace && _lastSpace) { + var dist = geoEuclideanDistance(_lastSpace, currSpace); if (dist > tolerance) { - disableSpace = false; + _disableSpace = false; } } - if (disableSpace || mouseLeave || !lastMouse) return; + if (_disableSpace || _mouseLeave || !_lastMouse) return; // user must move mouse or release space bar to allow another click - lastSpace = currSpace; - disableSpace = true; + _lastSpace = currSpace; + _disableSpace = true; d3_select(window).on('keyup.space-block', function() { d3_event.preventDefault(); d3_event.stopPropagation(); - disableSpace = false; + _disableSpace = false; d3_select(window).on('keyup.space-block', null); }); @@ -187,7 +191,7 @@ export function behaviorDraw(context) { context.install(hover); context.install(edit); - if (!context.inIntro() && !usedTails[tail.text()]) { + if (!context.inIntro() && !_usedTails[tail.text()]) { context.install(tail); } @@ -217,9 +221,9 @@ export function behaviorDraw(context) { context.uninstall(hover); context.uninstall(edit); - if (!context.inIntro() && !usedTails[tail.text()]) { + if (!context.inIntro() && !_usedTails[tail.text()]) { context.uninstall(tail); - usedTails[tail.text()] = true; + _usedTails[tail.text()] = true; } selection diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 81bc6c22a5..2a5824170c 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -161,10 +161,15 @@ export function modeDragNode(context) { var d = datum(); if (!_nudgeInterval) { + // try to snap if (d.type === 'node' && d.id !== entity.id) { loc = d.loc; } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { - loc = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection).loc; + var childNodes = context.childNodes(d); + var childIDs = childNodes.map(function(node) { return node.id; }); + if (childIDs.indexOf(entity.id) === -1) { + loc = geoChooseEdge(childNodes, context.mouse(), context.projection).loc; + } } } From 5e99f3cbacaadd3cf6566614a4389d8f9b6bd080 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 16 Dec 2017 11:32:26 -0500 Subject: [PATCH 047/206] CSS pointer-events cleanup, also round linejoins --- css/20_map.css | 41 ++++++++++++++++++++--------------------- css/70_fills.css | 13 ------------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index dc01e7a640..9f399ce4ef 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -1,34 +1,36 @@ - -use { pointer-events: none; } - /* base styles */ .layer-osm path:not(.oneway) { fill: none; } /* IE needs :not(.oneway) */ /* the above fill: none rule affects paths in shadow dom only in Firefox */ .layer-osm use.icon path { fill: #333; } /* FF svg Maki icons */ .layer-osm .turn use path { fill: #000; } /* FF turn restriction icons */ -#turn-only-shape2, #turn-only-u-shape2 { fill: #7092ff; } /* FF turn-only, turn-only-u */ -#turn-no-shape2, #turn-no-u-shape2 { fill: #e06d5f; } /* FF turn-no, turn-no-u */ -#turn-yes-shape2, #turn-yes-u-shape2 { fill: #8cd05f; } /* FF turn-yes, turn-yes-u */ +#turn-only-shape2, #turn-only-u-shape2 { fill: #7092ff; } /* FF turn-only, turn-only-u */ +#turn-no-shape2, #turn-no-u-shape2 { fill: #e06d5f; } /* FF turn-no, turn-no-u */ +#turn-yes-shape2, #turn-yes-u-shape2 { fill: #8cd05f; } /* FF turn-yes, turn-yes-u */ -.layer-points-group * { - pointer-events: none; -} -.layer-points-group.layer-points-targets * { - pointer-events: all; -} -/*.mode-draw-line .active, -.mode-draw-area .active, -*/ -.mode-drag-node .active { +/* No interactivity except what we specifically allow */ +.layer-osm * { pointer-events: none; } +/* Line/Area shadows and point/vertex targets are interactive */ +/* They can be picked up, clicked, hovered, or things can connect to them */ .layer-areas path.shadow, .layer-lines path.shadow { pointer-events: stroke; } +.layer-points-targets * { + pointer-events: all; +} + +/* .active objects (currently being drawn or dragged) are not interactive */ +/* This is important to allow the events to drop through to whatever is */ +/* below them on the map, so you can still hover and connect to other things. */ +.layer-osm .active { + pointer-events: none !important; +} + /* points */ @@ -120,7 +122,7 @@ g.vertex.selected .shadow { path.line { stroke-linecap: round; - stroke-linejoin: bevel; + stroke-linejoin: round; } path.stroke { @@ -152,8 +154,7 @@ path.line.stroke { /* Labels / Markers */ text { - font-size:10px; - pointer-events: none; + font-size: 10px; color: #222; opacity: 1; } @@ -178,7 +179,6 @@ text.pointlabel { font-size: 12px; font-weight: bold; fill: #333; - pointer-events: none; -webkit-transition: opacity 100ms linear; transition: opacity 100ms linear; -moz-transition: opacity 100ms linear; @@ -229,7 +229,6 @@ g.turn circle { } .form-field-restrictions .vertex { - pointer-events: none; cursor: auto !important; } diff --git a/css/70_fills.css b/css/70_fills.css index ec8f78d82c..34947b2329 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -33,17 +33,4 @@ .fill-partial path.area.fill { fill-opacity: 0; stroke-width: 60px; - pointer-events: visibleStroke; -} - -/* Modes */ - -/* Ensure drawing doesn't interact with area fills. */ -.mode-add-point path.area.fill, -.mode-draw-line path.area.fill, -.mode-draw-area path.area.fill, -.mode-add-line path.area.fill, -.mode-add-area path.area.fill, -.mode-drag-node path.area.fill { - pointer-events: none; } From aa68b21d7a033c714577e2d1d343d68c9177dac1 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 17 Dec 2017 22:53:58 -0500 Subject: [PATCH 048/206] Add touch targets for line/area --- css/20_map.css | 27 +++++++++---------- css/70_fills.css | 1 + css/80_app.css | 1 + modules/behavior/draw.js | 3 +++ modules/modes/drag_node.js | 24 +++++++++++------ modules/svg/areas.js | 35 ++++++++++++++++++++++--- modules/svg/lines.js | 53 ++++++++++++++++++++++++++++---------- modules/svg/midpoints.js | 7 +++-- modules/svg/osm.js | 12 +++++++++ modules/svg/points.js | 15 +++++------ modules/svg/vertices.js | 7 +++-- 11 files changed, 131 insertions(+), 54 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 9f399ce4ef..38106a2236 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -14,17 +14,24 @@ pointer-events: none; } -/* Line/Area shadows and point/vertex targets are interactive */ +/* `.target` objects are interactive */ /* They can be picked up, clicked, hovered, or things can connect to them */ -.layer-areas path.shadow, -.layer-lines path.shadow { - pointer-events: stroke; +.node.target { + pointer-events: fill; + fill-opacity: 0.8; + fill: currentColor; + stroke: none; } -.layer-points-targets * { - pointer-events: all; + +.way.target { + pointer-events: stroke; + fill: none; + stroke-width: 10; + stroke-opacity: 0.8; + stroke: currentColor; } -/* .active objects (currently being drawn or dragged) are not interactive */ +/* `.active` objects (currently being drawn or dragged) are not interactive */ /* This is important to allow the events to drop through to whatever is */ /* below them on the map, so you can still hover and connect to other things. */ .layer-osm .active { @@ -95,12 +102,6 @@ g.midpoint .shadow { fill-opacity: 0; } -.target { - color: rgba(0,0,0,0); - fill-opacity: 0.8; - fill: currentColor; -} - g.vertex.related:not(.selected) .shadow, g.vertex.hover:not(.selected) .shadow, g.midpoint.related:not(.selected) .shadow, diff --git a/css/70_fills.css b/css/70_fills.css index 34947b2329..3587bbee8c 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -33,4 +33,5 @@ .fill-partial path.area.fill { fill-opacity: 0; stroke-width: 60px; + pointer-events: visibleStroke; } diff --git a/css/80_app.css b/css/80_app.css index 83205fb0b5..bc2f268c0f 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2864,6 +2864,7 @@ img.tile-removing { stroke-width: 1; } +.nocolor { color: rgba(0, 0, 0, 0); } .red { color: rgba(255, 0, 0, 0.75); } .green { color: rgba(0, 255, 0, 0.75); } .blue { color: rgba(0, 0, 255, 0.75); } diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index f4629d7f59..6a6ed2b2b0 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -116,6 +116,9 @@ export function behaviorDraw(context) { function click() { var d = datum(); + + // Try to snap.. + // See also: `modes/drag_node.js doMove()` if (d.type === 'way') { var dims = context.map().dimensions(); var mouse = context.mouse(); diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 2a5824170c..90298a4853 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -131,8 +131,8 @@ export function modeDragNode(context) { _dragEntity = entity; - // activeIDs generate no pointer events. This prevents the node or vertex - // being dragged from trying to connect to itself or its parent element. + // `.active` elements have `pointer-events: none`. + // This prevents the node or vertex being dragged from trying to connect to itself. _activeIDs = context.graph().parentWays(entity) .map(function(parent) { return parent.id; }); _activeIDs.push(entity.id); @@ -158,17 +158,25 @@ export function modeDragNode(context) { var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); var currMouse = vecSub(currPoint, nudge); var loc = context.projection.invert(currMouse); - var d = datum(); if (!_nudgeInterval) { - // try to snap + // If we're not nudging at the edge of the viewport, try to snap.. + // See also `behavior/draw.js click()` + var d = datum(); + + // Snap to a node (not self) if (d.type === 'node' && d.id !== entity.id) { loc = d.loc; + + // Snap to a way (not an area fill) } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { - var childNodes = context.childNodes(d); - var childIDs = childNodes.map(function(node) { return node.id; }); - if (childIDs.indexOf(entity.id) === -1) { - loc = geoChooseEdge(childNodes, context.mouse(), context.projection).loc; + + // var childNodes = context.childNodes(d); + // var childIDs = childNodes.map(function(node) { return node.id; }); + var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); + // (not along a segment adjacent to self) + if (entity.id !== d.nodes[choice.index - 1] && entity.id !== d.nodes[choice.index]) { + loc = choice.loc; } } } diff --git a/modules/svg/areas.js b/modules/svg/areas.js index 0f219738ee..d540a08a0c 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -41,7 +41,29 @@ export function svgAreas(projection, context) { } - return function drawAreas(selection, graph, entities, filter) { + function drawTargets(selection, graph, entities, filter) { + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var getPath = svgPath(projection, graph); + + var targets = selection.selectAll('.area.target') + .filter(filter) + .data(entities, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('path') + .merge(targets) + .attr('d', getPath) + .attr('class', function(d) { return 'way area target ' + fillClass + d.id; }); + } + + + + function drawAreas(selection, graph, entities, filter) { var path = svgPath(projection, graph, true), areas = {}, multipolygon; @@ -99,7 +121,7 @@ export function svgAreas(projection, context) { .attr('d', path); - var layer = selection.selectAll('.layer-areas'); + var layer = selection.selectAll('.layer-areas .layer-areas-areas'); var areagroup = layer .selectAll('g.areagroup') @@ -145,5 +167,12 @@ export function svgAreas(projection, context) { }) .call(svgTagClasses()) .attr('d', path); - }; + + + // touch targets + selection.selectAll('.layer-areas .layer-areas-targets') + .call(drawTargets, graph, data.stroke, filter); + } + + return drawAreas; } diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 1646f083b3..caf11c294d 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -36,13 +36,33 @@ export function svgLines(projection, context) { }; - function drawLines(selection, graph, entities, filter) { + function drawTargets(selection, graph, entities, filter) { + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var getPath = svgPath(projection, graph); + + var targets = selection.selectAll('.line.target') + .filter(filter) + .data(entities, function key(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('path') + .merge(targets) + .attr('d', getPath) + .attr('class', function(d) { return 'way line target ' + fillClass + d.id; }); + } + + function drawLines(selection, graph, entities, filter) { function waystack(a, b) { - var selected = context.selectedIDs(), - scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0, - scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0; + var selected = context.selectedIDs(); + var scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0; + var scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0; if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; } if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; } @@ -91,15 +111,15 @@ export function svgLines(projection, context) { } - var getPath = svgPath(projection, graph), - ways = [], - pathdata = {}, - onewaydata = {}, - oldMultiPolygonOuters = {}; + var getPath = svgPath(projection, graph); + var ways = []; + var pathdata = {}; + var onewaydata = {}; + var oldMultiPolygonOuters = {}; for (var i = 0; i < entities.length; i++) { - var entity = entities[i], - outer = osmSimpleMultipolygonOuterMember(entity, graph); + var entity = entities[i]; + var outer = osmSimpleMultipolygonOuterMember(entity, graph); if (outer) { ways.push(entity.mergeTags(outer.tags)); oldMultiPolygonOuters[outer.id] = true; @@ -117,7 +137,7 @@ export function svgLines(projection, context) { }); - var layer = selection.selectAll('.layer-lines'); + var layer = selection.selectAll('.layer-lines .layer-lines-lines'); var layergroup = layer .selectAll('g.layergroup') @@ -164,8 +184,8 @@ export function svgLines(projection, context) { .selectAll('path') .filter(filter) .data( - function() { return onewaydata[this.parentNode.__data__] || []; }, - function(d) { return [d.id, d.index]; } + function data() { return onewaydata[this.parentNode.__data__] || []; }, + function key(d) { return [d.id, d.index]; } ); oneways.exit() @@ -181,6 +201,11 @@ export function svgLines(projection, context) { if (detected.ie) { oneways.each(function() { this.parentNode.insertBefore(this, this); }); } + + + // touch targets + selection.selectAll('.layer-lines .layer-lines-targets') + .call(drawTargets, graph, ways, filter); } diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index d6aaa0ab95..60ff03de82 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -17,7 +17,7 @@ export function svgMidpoints(projection, context) { function drawTargets(selection, graph, entities, filter) { - var debugClass = 'pink'; + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var targets = selection.selectAll('.midpoint.target') .filter(filter) .data(entities, function key(d) { return d.id; }); @@ -30,10 +30,9 @@ export function svgMidpoints(projection, context) { targets.enter() .append('circle') .attr('r', 12) - .attr('class', function(d) { return 'midpoint target ' + d.id; }) .merge(targets) - .attr('transform', svgPointTransform(projection)) - .classed(debugClass, context.getDebug('target')); + .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; }) + .attr('transform', svgPointTransform(projection)); } diff --git a/modules/svg/osm.js b/modules/svg/osm.js index 8a7defe943..db9a26a125 100644 --- a/modules/svg/osm.js +++ b/modules/svg/osm.js @@ -9,6 +9,18 @@ export function svgOsm(projection, context, dispatch) { .append('g') .attr('class', function(d) { return 'layer-osm layer-' + d; }); + selection.selectAll('.layer-areas').selectAll('.layer-areas-group') + .data(['areas', 'targets']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-areas-group layer-areas-' + d; }); + + selection.selectAll('.layer-lines').selectAll('.layer-lines-group') + .data(['lines', 'targets']) + .enter() + .append('g') + .attr('class', function(d) { return 'layer-lines-group layer-lines-' + d; }); + selection.selectAll('.layer-points').selectAll('.layer-points-group') .data(['points', 'midpoints', 'vertices', 'turns', 'targets']) .enter() diff --git a/modules/svg/points.js b/modules/svg/points.js index 8f38733e89..dfa32e5756 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -22,7 +22,7 @@ export function svgPoints(projection, context) { function drawTargets(selection, graph, entities, filter) { - var debugClass = 'pink'; + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var targets = selection.selectAll('.point.target') .filter(filter) .data(entities, function key(d) { return d.id; }); @@ -34,14 +34,13 @@ export function svgPoints(projection, context) { // enter/update targets.enter() .append('rect') - .attr('x', -15) - .attr('y', -30) - .attr('width', 30) - .attr('height', 36) - .attr('class', function(d) { return 'node point target ' + d.id; }) + .attr('x', -10) + .attr('y', -26) + .attr('width', 20) + .attr('height', 30) .merge(targets) - .attr('transform', svgPointTransform(projection)) - .classed(debugClass, context.getDebug('target')); + .attr('class', function(d) { return 'node point target ' + fillClass + d.id; }) + .attr('transform', svgPointTransform(projection)); } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index e2231b8c47..35a1ac208e 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -171,7 +171,7 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { - var debugClass = 'pink'; + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var targets = selection.selectAll('.vertex.target') .filter(filter) .data(entities, function key(d) { return d.id; }); @@ -184,10 +184,9 @@ export function svgVertices(projection, context) { targets.enter() .append('circle') .attr('r', radiuses.shadow[3]) // just use the biggest one for now - .attr('class', function(d) { return 'node vertex target ' + d.id; }) .merge(targets) - .attr('transform', svgPointTransform(projection)) - .classed(debugClass, context.getDebug('target')); + .attr('class', function(d) { return 'node vertex target ' + fillClass + d.id; }) + .attr('transform', svgPointTransform(projection)); } From 57ba6a98629e3e48b6cbda20e14f24844cfe2896 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 00:11:08 -0500 Subject: [PATCH 049/206] Add selectedIDs() for modeMove and modeRotate This allows the label collision boxes for the vertices to update during these modes. Otherwise the labels.js `isInterestingVertex()` function doesn't consider them interesting enough to update. --- modules/modes/move.js | 7 +++++++ modules/modes/rotate.js | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/modules/modes/move.js b/modules/modes/move.js index a6571d3326..b9ce5b7b3f 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -192,5 +192,12 @@ export function modeMove(context, entityIDs, baseGraph) { }; + mode.selectedIDs = function() { + if (!arguments.length) return entityIDs; + // no assign + return mode; + }; + + return mode; } diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index 4efd701dd6..6448d53335 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -155,5 +155,12 @@ export function modeRotate(context, entityIDs) { }; + mode.selectedIDs = function() { + if (!arguments.length) return entityIDs; + // no assign + return mode; + }; + + return mode; } From b5eaa76d1a66cb2787d86667742cb60df1553482 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 00:13:16 -0500 Subject: [PATCH 050/206] Optimization: when moving stuff around, don't exit/enter nodes This avoids thrashing the DOM. The positions of the nodes will still get updated by the update selection. --- modules/svg/points.js | 11 ++++++++++- modules/svg/vertices.js | 14 +++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/modules/svg/points.js b/modules/svg/points.js index dfa32e5756..124dba5bc2 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -21,6 +21,15 @@ export function svgPoints(projection, context) { } + // Avoid exit/enter if we're just moving stuff around. + // The node will get a new version but we only need to run the update selection. + function fastEntityKey(d) { + var mode = context.mode(); + var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); + return isMoving ? d.id : osmEntity.key(d); + } + + function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var targets = selection.selectAll('.point.target') @@ -64,7 +73,7 @@ export function svgPoints(projection, context) { var groups = layer.selectAll('g.point') .filter(filter) - .data(points, osmEntity.key); + .data(points, fastEntityKey); groups.exit() .remove(); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 35a1ac208e..06ab1e7b6f 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -32,6 +32,14 @@ export function svgVertices(projection, context) { return b.loc[1] - a.loc[1]; } + // Avoid exit/enter if we're just moving stuff around. + // The node will get a new version but we only need to run the update selection. + function fastEntityKey(d) { + var mode = context.mode(); + var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); + return isMoving ? d.id : osmEntity.key(d); + } + function draw(selection, graph, vertices, sets, filter) { sets = sets || { selected: {}, important: {}, hovered: {} }; @@ -89,7 +97,7 @@ export function svgVertices(projection, context) { var groups = selection.selectAll('g.vertex') .filter(filter) - .data(vertices, osmEntity.key); + .data(vertices, fastEntityKey); // exit groups.exit() @@ -252,7 +260,7 @@ export function svgVertices(projection, context) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = ktoz(projection.scale()); var mode = context.mode(); - var isDrawing = mode && /^(add|draw|drag)/.test(mode.id); + var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); // Collect important vertices from the `entities` list.. // (during a paritial redraw, it will not contain everything) @@ -283,7 +291,7 @@ export function svgVertices(projection, context) { hovered: _currHover // hovered + siblings of hovered (render only in draw modes) }; - var all = _assign({}, (isDrawing ? _currHover : {}), _currSelected, _currPersistent); + var all = _assign({}, (isMoving ? _currHover : {}), _currSelected, _currPersistent); // Draw the vertices.. // The filter function controls the scope of what objects d3 will touch (exit/enter/update) From bcd511573f096e3e3acea1fcf37f0fdf440e4c4e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 00:34:54 -0500 Subject: [PATCH 051/206] Fix extent in turn restrictions viewer, so vertices will render --- modules/ui/fields/restrictions.js | 47 ++++++++++++++++--------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index 579e17aa5b..a89c7a9b50 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -46,12 +46,12 @@ import { export function uiFieldRestrictions(field, context) { - var dispatch = d3_dispatch('change'), - breathe = behaviorBreathe(context), - hover = behaviorHover(context), - initialized = false, - vertexID, - fromNodeID; + var dispatch = d3_dispatch('change'); + var breathe = behaviorBreathe(context); + var hover = behaviorHover(context); + var initialized = false; + var vertexID; + var fromNodeID; function restrictions(selection) { @@ -73,16 +73,15 @@ export function uiFieldRestrictions(field, context) { .attr('class', 'restriction-help'); - var intersection = osmIntersection(context.graph(), vertexID), - graph = intersection.graph, - vertex = graph.entity(vertexID), - filter = utilFunctor(true), - extent = geoExtent(), - projection = geoRawMercator(); + var intersection = osmIntersection(context.graph(), vertexID); + var graph = intersection.graph; + var vertex = graph.entity(vertexID); + var filter = utilFunctor(true); + var projection = geoRawMercator(); - var d = utilGetDimensions(wrap.merge(enter)), - c = [d[0] / 2, d[1] / 2], - z = 24; + var d = utilGetDimensions(wrap.merge(enter)); + var c = [d[0] / 2, d[1] / 2]; + var z = 24; projection .scale(256 * Math.pow(2, z) / (2 * Math.PI)); @@ -93,10 +92,12 @@ export function uiFieldRestrictions(field, context) { .translate([c[0] - s[0], c[1] - s[1]]) .clipExtent([[0, 0], d]); - var drawLayers = svgLayers(projection, context).only('osm').dimensions(d), - drawVertices = svgVertices(projection, context), - drawLines = svgLines(projection, context), - drawTurns = svgTurns(projection, context); + var extent = geoExtent(projection.invert([0, d[1]]), projection.invert([d[0], 0])); + + var drawLayers = svgLayers(projection, context).only('osm').dimensions(d); + var drawVertices = svgVertices(projection, context); + var drawLines = svgLines(projection, context); + var drawTurns = svgTurns(projection, context); enter .call(drawLayers); @@ -115,7 +116,7 @@ export function uiFieldRestrictions(field, context) { surface .call(utilSetDimensions, d) - .call(drawVertices, graph, [vertex], filter, extent) + .call(drawVertices, graph, [vertex], filter, extent, true) .call(drawLines, graph, intersection.ways, filter) .call(drawTurns, graph, intersection.turns(fromNodeID)); @@ -174,9 +175,9 @@ export function uiFieldRestrictions(field, context) { function mouseover() { var datum = d3_event.target.__data__; if (datum instanceof osmTurn) { - var graph = context.graph(), - presets = context.presets(), - preset; + var graph = context.graph(); + var presets = context.presets(); + var preset; if (datum.restriction) { preset = presets.match(graph.entity(datum.restriction), graph); From 18c97d52c80fe9ebfe46b793556b94e3726b3016 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 09:50:17 -0500 Subject: [PATCH 052/206] Extract viewport nudging code from several places to geoViewportEdge --- modules/behavior/draw.js | 40 ++++++++--------- modules/geo/geo.js | 24 ++++++++++ modules/geo/index.js | 1 + modules/modes/drag_node.js | 25 +---------- modules/modes/move.js | 92 +++++++++++++++----------------------- modules/modes/rotate.js | 75 ++++++++++++++++--------------- test/spec/geo/geo.js | 32 +++++++++++++ 7 files changed, 152 insertions(+), 137 deletions(-) diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 6a6ed2b2b0..61ca00af55 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -14,7 +14,8 @@ import { behaviorTail } from './tail'; import { geoChooseEdge, - geoEuclideanDistance + geoEuclideanDistance, + geoViewportEdge } from '../geo'; import { utilRebind } from '../util/rebind'; @@ -115,31 +116,28 @@ export function behaviorDraw(context) { function click() { - var d = datum(); - - // Try to snap.. - // See also: `modes/drag_node.js doMove()` - if (d.type === 'way') { - var dims = context.map().dimensions(); - var mouse = context.mouse(); - var pad = 5; - var trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad && - mouse[1] > pad && mouse[1] < dims[1] - pad; - - if (trySnap) { + var trySnap = geoViewportEdge(context.mouse, context.map().dimensions()) !== null; + + if (trySnap) { + // If we're not at the edge of the viewport, try to snap.. + // See also: `modes/drag_node.js doMove()` + var d = datum(); + + // Snap to a node + if (d.type === 'node') { + dispatch.call('clickNode', this, d); + return; + + // Snap to a way (not an area fill) + } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); var edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; dispatch.call('clickWay', this, choice.loc, edge); - } else { - dispatch.call('click', this, context.map().mouseCoordinates()); + return; } - - } else if (d.type === 'node') { - dispatch.call('clickNode', this, d); - - } else { - dispatch.call('click', this, context.map().mouseCoordinates()); } + + dispatch.call('click', this, context.map().mouseCoordinates()); } diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 3d083d219c..2f74eb40a9 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -275,3 +275,27 @@ export function geoPathLength(path) { } return length; } + + +// If the given point is at the edge of the padded viewport, +// return a vector that will nudge the viewport in that direction +export function geoViewportEdge(point, dimensions) { + var pad = [80, 20, 50, 20]; // top, right, bottom, left + var x = 0; + var y = 0; + + if (point[0] > dimensions[0] - pad[1]) + x = -10; + if (point[0] < pad[3]) + x = 10; + if (point[1] > dimensions[1] - pad[2]) + y = -10; + if (point[1] < pad[0]) + y = 10; + + if (x || y) { + return [x, y]; + } else { + return null; + } +} diff --git a/modules/geo/index.js b/modules/geo/index.js index 5e37b16fe0..8966c218bc 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -21,3 +21,4 @@ export { geoPointInPolygon } from './geo.js'; export { geoPolygonContainsPolygon } from './geo.js'; export { geoPolygonIntersectsPolygon } from './geo.js'; export { geoSphericalDistance } from './geo.js'; +export { geoViewportEdge } from './geo.js'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 90298a4853..b52c2da976 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -23,7 +23,7 @@ import { modeSelect } from './index'; -import { geoChooseEdge } from '../geo'; +import { geoChooseEdge, geoViewportEdge } from '../geo'; import { osmNode } from '../osm'; import { utilEntitySelector } from '../util'; import { uiFlash } from '../ui'; @@ -50,27 +50,6 @@ export function modeDragNode(context) { return [a[0] - b[0], a[1] - b[1]]; } - function edge(point, size) { - var pad = [80, 20, 50, 20]; // top, right, bottom, left - var x = 0; - var y = 0; - - if (point[0] > size[0] - pad[1]) - x = -10; - if (point[0] < pad[3]) - x = 10; - if (point[1] > size[1] - pad[2]) - y = -10; - if (point[1] < pad[0]) - y = 10; - - if (x || y) { - return [x, y]; - } else { - return null; - } - } - function startNudge(entity, nudge) { if (_nudgeInterval) window.clearInterval(_nudgeInterval); @@ -197,7 +176,7 @@ export function modeDragNode(context) { _lastLoc = context.projection.invert(d3_event.point); doMove(entity); - var nudge = edge(d3_event.point, context.map().dimensions()); + var nudge = geoViewportEdge(d3_event.point, context.map().dimensions()); if (nudge) { startNudge(entity, nudge); } else { diff --git a/modules/modes/move.js b/modules/modes/move.js index b9ce5b7b3f..e91439b1b2 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -8,6 +8,7 @@ import { t } from '../util/locale'; import { actionMove } from '../actions'; import { behaviorEdit } from '../behavior'; +import { geoViewportEdge } from '../geo'; import { modeBrowse, @@ -30,23 +31,24 @@ export function modeMove(context, entityIDs, baseGraph) { button: 'browse' }; - var keybinding = d3_keybinding('move'), - behaviors = [ - behaviorEdit(context), - operationCircularize(entityIDs, context).behavior, - operationDelete(entityIDs, context).behavior, - operationOrthogonalize(entityIDs, context).behavior, - operationReflectLong(entityIDs, context).behavior, - operationReflectShort(entityIDs, context).behavior, - operationRotate(entityIDs, context).behavior - ], - annotation = entityIDs.length === 1 ? - t('operations.move.annotation.' + context.geometry(entityIDs[0])) : - t('operations.move.annotation.multiple'), - prevGraph, - cache, - origin, - nudgeInterval; + var keybinding = d3_keybinding('move'); + var behaviors = [ + behaviorEdit(context), + operationCircularize(entityIDs, context).behavior, + operationDelete(entityIDs, context).behavior, + operationOrthogonalize(entityIDs, context).behavior, + operationReflectLong(entityIDs, context).behavior, + operationReflectShort(entityIDs, context).behavior, + operationRotate(entityIDs, context).behavior + ]; + var annotation = entityIDs.length === 1 ? + t('operations.move.annotation.' + context.geometry(entityIDs[0])) : + t('operations.move.annotation.multiple'); + + var _prevGraph; + var _cache; + var _origin; + var _nudgeInterval; function vecSub(a, b) { @@ -54,52 +56,30 @@ export function modeMove(context, entityIDs, baseGraph) { } - function edge(point, size) { - var pad = [80, 20, 50, 20], // top, right, bottom, left - x = 0, - y = 0; - - if (point[0] > size[0] - pad[1]) - x = -10; - if (point[0] < pad[3]) - x = 10; - if (point[1] > size[1] - pad[2]) - y = -10; - if (point[1] < pad[0]) - y = 10; - - if (x || y) { - return [x, y]; - } else { - return null; - } - } - - function doMove(nudge) { nudge = nudge || [0, 0]; var fn; - if (prevGraph !== context.graph()) { - cache = {}; - origin = context.map().mouseCoordinates(); + if (_prevGraph !== context.graph()) { + _cache = {}; + _origin = context.map().mouseCoordinates(); fn = context.perform; } else { fn = context.overwrite; } - var currMouse = context.mouse(), - origMouse = context.projection(origin), - delta = vecSub(vecSub(currMouse, origMouse), nudge); + var currMouse = context.mouse(); + var origMouse = context.projection(_origin); + var delta = vecSub(vecSub(currMouse, origMouse), nudge); - fn(actionMove(entityIDs, delta, context.projection, cache), annotation); - prevGraph = context.graph(); + fn(actionMove(entityIDs, delta, context.projection, _cache), annotation); + _prevGraph = context.graph(); } function startNudge(nudge) { - if (nudgeInterval) window.clearInterval(nudgeInterval); - nudgeInterval = window.setInterval(function() { + if (_nudgeInterval) window.clearInterval(_nudgeInterval); + _nudgeInterval = window.setInterval(function() { context.pan(nudge); doMove(nudge); }, 50); @@ -107,16 +87,16 @@ export function modeMove(context, entityIDs, baseGraph) { function stopNudge() { - if (nudgeInterval) { - window.clearInterval(nudgeInterval); - nudgeInterval = null; + if (_nudgeInterval) { + window.clearInterval(_nudgeInterval); + _nudgeInterval = null; } } function move() { doMove(); - var nudge = edge(context.mouse(), context.map().dimensions()); + var nudge = geoViewportEdge(context.mouse(), context.map().dimensions()); if (nudge) { startNudge(nudge); } else { @@ -150,9 +130,9 @@ export function modeMove(context, entityIDs, baseGraph) { mode.enter = function() { - origin = context.map().mouseCoordinates(); - prevGraph = null; - cache = {}; + _origin = context.map().mouseCoordinates(); + _prevGraph = null; + _cache = {}; behaviors.forEach(function(behavior) { context.install(behavior); diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index 6448d53335..d889a174b1 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -38,66 +38,67 @@ export function modeRotate(context, entityIDs) { button: 'browse' }; - var keybinding = d3_keybinding('rotate'), - behaviors = [ - behaviorEdit(context), - operationCircularize(entityIDs, context).behavior, - operationDelete(entityIDs, context).behavior, - operationMove(entityIDs, context).behavior, - operationOrthogonalize(entityIDs, context).behavior, - operationReflectLong(entityIDs, context).behavior, - operationReflectShort(entityIDs, context).behavior - ], - annotation = entityIDs.length === 1 ? - t('operations.rotate.annotation.' + context.geometry(entityIDs[0])) : - t('operations.rotate.annotation.multiple'), - prevGraph, - prevAngle, - prevTransform, - pivot; + var keybinding = d3_keybinding('rotate'); + var behaviors = [ + behaviorEdit(context), + operationCircularize(entityIDs, context).behavior, + operationDelete(entityIDs, context).behavior, + operationMove(entityIDs, context).behavior, + operationOrthogonalize(entityIDs, context).behavior, + operationReflectLong(entityIDs, context).behavior, + operationReflectShort(entityIDs, context).behavior + ]; + var annotation = entityIDs.length === 1 ? + t('operations.rotate.annotation.' + context.geometry(entityIDs[0])) : + t('operations.rotate.annotation.multiple'); + + var _prevGraph; + var _prevAngle; + var _prevTransform; + var _pivot; function doRotate() { var fn; - if (context.graph() !== prevGraph) { + if (context.graph() !== _prevGraph) { fn = context.perform; } else { fn = context.replace; } - // projection changed, recalculate pivot + // projection changed, recalculate _pivot var projection = context.projection; var currTransform = projection.transform(); - if (!prevTransform || - currTransform.k !== prevTransform.k || - currTransform.x !== prevTransform.x || - currTransform.y !== prevTransform.y) { + if (!_prevTransform || + currTransform.k !== _prevTransform.k || + currTransform.x !== _prevTransform.x || + currTransform.y !== _prevTransform.y) { - var nodes = utilGetAllNodes(entityIDs, context.graph()), - points = nodes.map(function(n) { return projection(n.loc); }); + var nodes = utilGetAllNodes(entityIDs, context.graph()); + var points = nodes.map(function(n) { return projection(n.loc); }); if (points.length === 1) { // degenerate case - pivot = points[0]; + _pivot = points[0]; } else if (points.length === 2) { - pivot = geoInterp(points[0], points[1], 0.5); + _pivot = geoInterp(points[0], points[1], 0.5); } else { - pivot = d3_polygonCentroid(d3_polygonHull(points)); + _pivot = d3_polygonCentroid(d3_polygonHull(points)); } - prevAngle = undefined; + _prevAngle = undefined; } - var currMouse = context.mouse(), - currAngle = Math.atan2(currMouse[1] - pivot[1], currMouse[0] - pivot[0]); + var currMouse = context.mouse(); + var currAngle = Math.atan2(currMouse[1] - _pivot[1], currMouse[0] - _pivot[0]); - if (typeof prevAngle === 'undefined') prevAngle = currAngle; - var delta = currAngle - prevAngle; + if (typeof _prevAngle === 'undefined') _prevAngle = currAngle; + var delta = currAngle - _prevAngle; - fn(actionRotate(entityIDs, pivot, delta, projection), annotation); + fn(actionRotate(entityIDs, _pivot, delta, projection), annotation); - prevTransform = currTransform; - prevAngle = currAngle; - prevGraph = context.graph(); + _prevTransform = currTransform; + _prevAngle = currAngle; + _prevGraph = context.graph(); } diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 6b2e0cb855..b4ac874c64 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -411,4 +411,36 @@ describe('iD.geo', function() { expect(iD.geoPathLength(path)).to.eql(0); }); }); + + describe('geoViewportEdge', function() { + var dimensions = [1000, 1000]; + it('returns null if the point is not at the edge', function() { + expect(iD.geoViewportEdge([500, 500], dimensions)).to.be.null; + }); + it('nudges top edge', function() { + expect(iD.geoViewportEdge([500, 5], dimensions)).to.eql([0, 10]); + }); + it('nudges top-right corner', function() { + expect(iD.geoViewportEdge([995, 5], dimensions)).to.eql([-10, 10]); + }); + it('nudges right edge', function() { + expect(iD.geoViewportEdge([995, 500], dimensions)).to.eql([-10, 0]); + }); + it('nudges bottom-right corner', function() { + expect(iD.geoViewportEdge([995, 995], dimensions)).to.eql([-10, -10]); + }); + it('nudges bottom edge', function() { + expect(iD.geoViewportEdge([500, 995], dimensions)).to.eql([0, -10]); + }); + it('nudges bottom-left corner', function() { + expect(iD.geoViewportEdge([5, 995], dimensions)).to.eql([10, -10]); + }); + it('nudges left edge', function() { + expect(iD.geoViewportEdge([5, 500], dimensions)).to.eql([10, 0]); + }); + it('nudges top-left corner', function() { + expect(iD.geoViewportEdge([5, 5], dimensions)).to.eql([10, 10]); + }); + }); + }); From 2e2b037e36c51427ce0a329cd668c79b4ba114e3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 15:05:42 -0500 Subject: [PATCH 053/206] Move a bunch of commonly used vector and projection math functions into geo - geoVecAdd - geoVecSubtract - geoVecScale - geoZoomToScale - geoScaleToZoom --- modules/actions/move.js | 88 ++++----- modules/geo/geo.js | 180 ++++++++++-------- modules/geo/index.js | 8 +- modules/modes/drag_node.js | 14 +- modules/osm/way.js | 15 +- modules/renderer/tile_layer.js | 114 ++++++------ modules/services/osm.js | 28 +-- modules/svg/labels.js | 8 +- modules/svg/points.js | 7 +- modules/svg/vertices.js | 13 +- modules/ui/edit_menu.js | 4 +- modules/ui/fields/restrictions.js | 5 +- modules/ui/map_in_map.js | 119 ++++++------ modules/ui/radial_menu.js | 4 +- test/spec/geo/geo.js | 291 +++++++++++++++++------------- test/spec/svg/areas.js | 5 +- test/spec/svg/layers.js | 5 +- test/spec/svg/lines.js | 5 +- test/spec/svg/midpoints.js | 5 +- test/spec/svg/points.js | 5 +- test/spec/svg/vertices.js | 5 +- 21 files changed, 487 insertions(+), 441 deletions(-) diff --git a/modules/actions/move.js b/modules/actions/move.js index ddc1da8a8e..baa75fb328 100644 --- a/modules/actions/move.js +++ b/modules/actions/move.js @@ -11,22 +11,21 @@ import _without from 'lodash-es/without'; import { osmNode } from '../osm'; import { - geoChooseEdge, geoAngle, + geoChooseEdge, geoInterp, geoPathIntersections, geoPathLength, - geoSphericalDistance + geoSphericalDistance, + geoVecAdd, + geoVecSubtract } from '../geo'; // https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as export function actionMove(moveIds, tryDelta, projection, cache) { - var delta = tryDelta; - - function vecAdd(a, b) { return [a[0] + b[0], a[1] + b[1]]; } - function vecSub(a, b) { return [a[0] - b[0], a[1] - b[1]]; } + var _delta = tryDelta; function setupCache(graph) { function canMove(nodeId) { @@ -118,11 +117,11 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Place a vertex where the moved vertex used to be, to preserve way shape.. - function replaceMovedVertex(nodeId, wayId, graph, delta) { - var way = graph.entity(wayId), - moved = graph.entity(nodeId), - movedIndex = way.nodes.indexOf(nodeId), - len, prevIndex, nextIndex; + function replaceMovedVertex(nodeId, wayId, graph, _delta) { + var way = graph.entity(wayId); + var moved = graph.entity(nodeId); + var movedIndex = way.nodes.indexOf(nodeId); + var len, prevIndex, nextIndex; if (way.isClosed()) { len = way.nodes.length - 1; @@ -134,14 +133,14 @@ export function actionMove(moveIds, tryDelta, projection, cache) { nextIndex = movedIndex + 1; } - var prev = graph.hasEntity(way.nodes[prevIndex]), - next = graph.hasEntity(way.nodes[nextIndex]); + var prev = graph.hasEntity(way.nodes[prevIndex]); + var next = graph.hasEntity(way.nodes[nextIndex]); // Don't add orig vertex at endpoint.. if (!prev || !next) return graph; - var key = wayId + '_' + nodeId, - orig = cache.replacedVertex[key]; + var key = wayId + '_' + nodeId; + var orig = cache.replacedVertex[key]; if (!orig) { orig = osmNode(); cache.replacedVertex[key] = orig; @@ -149,9 +148,9 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } var start, end; - if (delta) { + if (_delta) { start = projection(cache.startLoc[nodeId]); - end = projection.invert(vecAdd(start, delta)); + end = projection.invert(geoVecAdd(start, _delta)); } else { end = cache.startLoc[nodeId]; } @@ -184,24 +183,24 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Reorder nodes around intersections that have moved.. function unZorroIntersection(intersection, graph) { - var vertex = graph.entity(intersection.nodeId), - way1 = graph.entity(intersection.movedId), - way2 = graph.entity(intersection.unmovedId), - isEP1 = intersection.movedIsEP, - isEP2 = intersection.unmovedIsEP; + var vertex = graph.entity(intersection.nodeId); + var way1 = graph.entity(intersection.movedId); + var way2 = graph.entity(intersection.unmovedId); + var isEP1 = intersection.movedIsEP; + var isEP2 = intersection.unmovedIsEP; // don't move the vertex if it is the endpoint of both ways. if (isEP1 && isEP2) return graph; - var nodes1 = _without(graph.childNodes(way1), vertex), - nodes2 = _without(graph.childNodes(way2), vertex); + var nodes1 = _without(graph.childNodes(way1), vertex); + var nodes2 = _without(graph.childNodes(way2), vertex); if (way1.isClosed() && way1.first() === vertex.id) nodes1.push(nodes1[0]); if (way2.isClosed() && way2.first() === vertex.id) nodes2.push(nodes2[0]); - var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection), - edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection), - loc; + var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection); + var edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection); + var loc; // snap vertex to nearest edge (or some point between them).. if (!isEP1 && !isEP2) { @@ -236,7 +235,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { function cleanupIntersections(graph) { _each(cache.intersection, function(obj) { - graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, delta); + graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, _delta); graph = replaceMovedVertex(obj.nodeId, obj.unmovedId, graph, null); graph = unZorroIntersection(obj, graph); }); @@ -245,7 +244,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } - // check if moving way endpoint can cross an unmoved way, if so limit delta.. + // check if moving way endpoint can cross an unmoved way, if so limit _delta.. function limitDelta(graph) { _each(cache.intersection, function(obj) { // Don't limit movement if this is vertex joins 2 endpoints.. @@ -253,27 +252,28 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Don't limit movement if this vertex is not an endpoint anyway.. if (!obj.movedIsEP) return; - var node = graph.entity(obj.nodeId), - start = projection(node.loc), - end = vecAdd(start, delta), - movedNodes = graph.childNodes(graph.entity(obj.movedId)), - movedPath = _map(_map(movedNodes, 'loc'), - function(loc) { return vecAdd(projection(loc), delta); }), - unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)), - unmovedPath = _map(_map(unmovedNodes, 'loc'), projection), - hits = geoPathIntersections(movedPath, unmovedPath); + var node = graph.entity(obj.nodeId); + var start = projection(node.loc); + var end = geoVecAdd(start, _delta); + var movedNodes = graph.childNodes(graph.entity(obj.movedId)); + var movedPath = _map(_map(movedNodes, 'loc'), function(loc) { + return geoVecAdd(projection(loc), _delta); + }); + var unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)); + var unmovedPath = _map(_map(unmovedNodes, 'loc'), projection); + var hits = geoPathIntersections(movedPath, unmovedPath); for (var i = 0; i < hits.length; i++) { if (_isEqual(hits[i], end)) continue; var edge = geoChooseEdge(unmovedNodes, end, projection); - delta = vecSub(projection(edge.loc), start); + _delta = geoVecSubtract(projection(edge.loc), start); } }); } var action = function(graph) { - if (delta[0] === 0 && delta[1] === 0) return graph; + if (_delta[0] === 0 && _delta[1] === 0) return graph; setupCache(graph); @@ -282,9 +282,9 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } _each(cache.nodes, function(id) { - var node = graph.entity(id), - start = projection(node.loc), - end = vecAdd(start, delta); + var node = graph.entity(id); + var start = projection(node.loc); + var end = geoVecAdd(start, _delta); graph = graph.replace(node.move(projection.invert(end))); }); @@ -297,7 +297,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { action.delta = function() { - return delta; + return _delta; }; diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 2f74eb40a9..e96bc81b9c 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -2,94 +2,130 @@ import _every from 'lodash-es/every'; import _some from 'lodash-es/some'; -export function geoRoundCoords(c) { - return [Math.floor(c[0]), Math.floor(c[1])]; +// constants +var TAU = 2 * Math.PI; +var EQUATORIAL_RADIUS = 6356752.314245179; +var POLAR_RADIUS = 6378137.0; + + +// vector addition +export function geoVecAdd(a, b) { + return [ a[0] + b[0], a[1] + b[1] ]; +} + +// vector subtraction +export function geoVecSubtract(a, b) { + return [ a[0] - b[0], a[1] - b[1] ]; } +// vector multiplication +export function geoVecScale(a, b) { + return [ a[0] * b, a[1] * b ]; +} + +// vector rounding (was: geoRoundCoordinates) +export function geoVecFloor(a) { + return [ Math.floor(a[0]), Math.floor(a[1]) ]; +} +// linear interpolation export function geoInterp(p1, p2, t) { - return [p1[0] + (p2[0] - p1[0]) * t, - p1[1] + (p2[1] - p1[1]) * t]; + return [ + p1[0] + (p2[0] - p1[0]) * t, + p1[1] + (p2[1] - p1[1]) * t + ]; +} + + +// dot product +export function geoDot(a, b, origin) { + origin = origin || [0, 0]; + return (a[0] - origin[0]) * (b[0] - origin[0]) + + (a[1] - origin[1]) * (b[1] - origin[1]); } -// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. +// 2D cross product of OA and OB vectors, returns magnitude of Z vector // Returns a positive value, if OAB makes a counter-clockwise turn, // negative for clockwise turn, and zero if the points are collinear. -export function geoCross(o, a, b) { - return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); +export function geoCross(a, b, origin) { + origin = origin || [0, 0]; + return (a[0] - origin[0]) * (b[1] - origin[1]) - + (a[1] - origin[1]) * (b[0] - origin[0]); } // http://jsperf.com/id-dist-optimization export function geoEuclideanDistance(a, b) { - var x = a[0] - b[0], y = a[1] - b[1]; + var x = a[0] - b[0]; + var y = a[1] - b[1]; return Math.sqrt((x * x) + (y * y)); } -// using WGS84 polar radius (6356752.314245179 m) -// const = 2 * PI * r / 360 export function geoLatToMeters(dLat) { - return dLat * 110946.257617; + return dLat * (TAU * POLAR_RADIUS / 360); } -// using WGS84 equatorial radius (6378137.0 m) -// const = 2 * PI * r / 360 export function geoLonToMeters(dLon, atLat) { return Math.abs(atLat) >= 90 ? 0 : - dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180))); + dLon * (TAU * EQUATORIAL_RADIUS / 360) * Math.abs(Math.cos(atLat * (Math.PI / 180))); } -// using WGS84 polar radius (6356752.314245179 m) -// const = 2 * PI * r / 360 export function geoMetersToLat(m) { - return m / 110946.257617; + return m / (TAU * POLAR_RADIUS / 360); } -// using WGS84 equatorial radius (6378137.0 m) -// const = 2 * PI * r / 360 export function geoMetersToLon(m, atLat) { return Math.abs(atLat) >= 90 ? 0 : - m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180))); + m / (TAU * EQUATORIAL_RADIUS / 360) / Math.abs(Math.cos(atLat * (Math.PI / 180))); } -export function geoOffsetToMeters(offset) { - var equatRadius = 6356752.314245179, - polarRadius = 6378137.0, - tileSize = 256; - +export function geoOffsetToMeters(offset, tileSize) { + tileSize = tileSize || 256; return [ - offset[0] * 2 * Math.PI * equatRadius / tileSize, - -offset[1] * 2 * Math.PI * polarRadius / tileSize + offset[0] * TAU * EQUATORIAL_RADIUS / tileSize, + -offset[1] * TAU * POLAR_RADIUS / tileSize ]; } -export function geoMetersToOffset(meters) { - var equatRadius = 6356752.314245179, - polarRadius = 6378137.0, - tileSize = 256; - +export function geoMetersToOffset(meters, tileSize) { + tileSize = tileSize || 256; return [ - meters[0] * tileSize / (2 * Math.PI * equatRadius), - -meters[1] * tileSize / (2 * Math.PI * polarRadius) + meters[0] * tileSize / (TAU * EQUATORIAL_RADIUS), + -meters[1] * tileSize / (TAU * POLAR_RADIUS) ]; } // Equirectangular approximation of spherical distances on Earth export function geoSphericalDistance(a, b) { - var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2), - y = geoLatToMeters(a[1] - b[1]); + var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2); + var y = geoLatToMeters(a[1] - b[1]); return Math.sqrt((x * x) + (y * y)); } +// zoom to scale +export function geoZoomToScale(z, tileSize) { + tileSize = tileSize || 256; + return tileSize * Math.pow(2, z) / TAU; +} + + +// scale to zoom +export function geoScaleToZoom(k, tileSize) { + tileSize = tileSize || 256; + var log2ts = Math.log(tileSize) * Math.LOG2E; + return Math.log(k * TAU) / Math.LN2 - log2ts; +} + + export function geoEdgeEqual(a, b) { return (a[0] === b[0] && a[1] === b[1]) || (a[0] === b[1] && a[1] === b[0]); @@ -122,23 +158,18 @@ export function geoRotate(points, angle, around) { // the closest vertex on that edge. Returns an object with the `index` of the // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. export function geoChooseEdge(nodes, point, projection) { - var dist = geoEuclideanDistance, - points = nodes.map(function(n) { return projection(n.loc); }), - min = Infinity, - idx, loc; - - function dot(p, q) { - return p[0] * q[0] + p[1] * q[1]; - } + var dist = geoEuclideanDistance; + var points = nodes.map(function(n) { return projection(n.loc); }); + var min = Infinity; + var idx; + var loc; for (var i = 0; i < points.length - 1; i++) { - var o = points[i], - s = [points[i + 1][0] - o[0], - points[i + 1][1] - o[1]], - v = [point[0] - o[0], - point[1] - o[1]], - proj = dot(v, s) / dot(s, s), - p; + var o = points[i]; + var s = geoVecSubtract(points[i + 1], o); + var v = geoVecSubtract(point, o); + var proj = geoDot(v, s) / geoDot(s, s); + var p; if (proj < 0) { p = o; @@ -169,25 +200,18 @@ export function geoChooseEdge(nodes, point, projection) { // This uses the vector cross product approach described below: // http://stackoverflow.com/a/565282/786339 export function geoLineIntersection(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); + var p = [a[0][0], a[0][1]]; + var p2 = [a[1][0], a[1][1]]; + var q = [b[0][0], b[0][1]]; + var q2 = [b[1][0], b[1][1]]; + var r = geoVecSubtract(p2, p); + var s = geoVecSubtract(q2, q); + var uNumerator = geoCross(geoVecSubtract(q, p), r); + var denominator = geoCross(r, s); if (uNumerator && denominator) { - var u = uNumerator / denominator, - t = crossProduct(subtractPoints(q, p), s) / denominator; + var u = uNumerator / denominator; + var t = geoCross(geoVecSubtract(q, p), s) / denominator; if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { return geoInterp(p, p2, t); @@ -202,10 +226,12 @@ export function geoPathIntersections(path1, path2) { var intersections = []; for (var i = 0; i < path1.length - 1; i++) { for (var j = 0; j < path2.length - 1; j++) { - var a = [ path1[i], path1[i+1] ], - b = [ path2[j], path2[j+1] ], - hit = geoLineIntersection(a, b); - if (hit) intersections.push(hit); + var a = [ path1[i], path1[i+1] ]; + var b = [ path2[j], path2[j+1] ]; + var hit = geoLineIntersection(a, b); + if (hit) { + intersections.push(hit); + } } } return intersections; @@ -222,9 +248,9 @@ export function geoPathIntersections(path1, path2) { // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html // export function geoPointInPolygon(point, polygon) { - var x = point[0], - y = point[1], - inside = false; + var x = point[0]; + var y = point[1]; + var inside = false; for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { var xi = polygon[i][0], yi = polygon[i][1]; @@ -250,8 +276,8 @@ export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { function testSegments(outer, inner) { for (var i = 0; i < outer.length - 1; i++) { for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i+1] ], - b = [ inner[j], inner[j+1] ]; + var a = [ outer[i], outer[i +1 ] ]; + var b = [ inner[j], inner[j + 1] ]; if (geoLineIntersection(a, b)) return true; } } diff --git a/modules/geo/index.js b/modules/geo/index.js index 8966c218bc..2a1c8fa31d 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -1,12 +1,12 @@ export { geoAngle } from './geo.js'; export { geoChooseEdge } from './geo.js'; export { geoCross } from './geo.js'; +export { geoDot } from './geo.js'; export { geoEdgeEqual } from './geo.js'; export { geoEuclideanDistance } from './geo.js'; export { geoExtent } from './extent.js'; export { geoInterp } from './geo.js'; export { geoRawMercator } from './raw_mercator.js'; -export { geoRoundCoords } from './geo.js'; export { geoRotate } from './geo.js'; export { geoLatToMeters } from './geo.js'; export { geoLineIntersection } from './geo.js'; @@ -20,5 +20,11 @@ export { geoPathLength } from './geo.js'; export { geoPointInPolygon } from './geo.js'; export { geoPolygonContainsPolygon } from './geo.js'; export { geoPolygonIntersectsPolygon } from './geo.js'; +export { geoScaleToZoom } from './geo.js'; export { geoSphericalDistance } from './geo.js'; +export { geoVecAdd } from './geo.js'; +export { geoVecFloor } from './geo.js'; +export { geoVecSubtract } from './geo.js'; +export { geoVecScale } from './geo.js'; +export { geoZoomToScale } from './geo.js'; export { geoViewportEdge } from './geo.js'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index b52c2da976..915706bbd7 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -23,7 +23,12 @@ import { modeSelect } from './index'; -import { geoChooseEdge, geoViewportEdge } from '../geo'; +import { + geoChooseEdge, + geoVecSubtract, + geoViewportEdge +} from '../geo'; + import { osmNode } from '../osm'; import { utilEntitySelector } from '../util'; import { uiFlash } from '../ui'; @@ -46,11 +51,6 @@ export function modeDragNode(context) { var _lastLoc; - function vecSub(a, b) { - return [a[0] - b[0], a[1] - b[1]]; - } - - function startNudge(entity, nudge) { if (_nudgeInterval) window.clearInterval(_nudgeInterval); _nudgeInterval = window.setInterval(function() { @@ -135,7 +135,7 @@ export function modeDragNode(context) { nudge = nudge || [0, 0]; var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); - var currMouse = vecSub(currPoint, nudge); + var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); if (!_nudgeInterval) { diff --git a/modules/osm/way.js b/modules/osm/way.js index 76918a52ea..c932241461 100644 --- a/modules/osm/way.js +++ b/modules/osm/way.js @@ -133,15 +133,16 @@ _extend(osmWay.prototype, { isConvex: function(resolver) { if (!this.isClosed() || this.isDegenerate()) return null; - var nodes = _uniq(resolver.childNodes(this)), - coords = _map(nodes, 'loc'), - curr = 0, prev = 0; + var nodes = _uniq(resolver.childNodes(this)); + var coords = _map(nodes, 'loc'); + var curr = 0; + var prev = 0; for (var i = 0; i < coords.length; i++) { - var o = coords[(i+1) % coords.length], - a = coords[i], - b = coords[(i+2) % coords.length], - res = geoCross(o, a, b); + var o = coords[(i+1) % coords.length]; + var a = coords[i]; + var b = coords[(i+2) % coords.length]; + var res = geoCross(a, b, o); curr = (res > 0) ? 1 : (res < 0) ? -1 : 0; if (curr === 0) { diff --git a/modules/renderer/tile_layer.js b/modules/renderer/tile_layer.js index ceb8461175..9d143f0456 100644 --- a/modules/renderer/tile_layer.js +++ b/modules/renderer/tile_layer.js @@ -2,28 +2,29 @@ import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; -import { geoEuclideanDistance } from '../geo'; +import { geoEuclideanDistance, geoScaleToZoom } from '../geo'; import { utilPrefixCSSProperty } from '../util'; export function rendererTileLayer(context) { - var tileSize = 256, - geotile = d3_geoTile(), - projection, - cache = {}, - tileOrigin, - z, - transformProp = utilPrefixCSSProperty('Transform'), - source; + var tileSize = 256; + var transformProp = utilPrefixCSSProperty('Transform'); + var geotile = d3_geoTile(); + + var _projection; + var _cache = {}; + var _tileOrigin; + var _zoom; + var _source; // blacklist overlay tiles around Null Island.. function nearNullIsland(x, y, z) { if (z >= 7) { - var center = Math.pow(2, z - 1), - width = Math.pow(2, z - 6), - min = center - (width / 2), - max = center + (width / 2) - 1; + var center = Math.pow(2, z - 1); + var width = Math.pow(2, z - 6); + var min = center - (width / 2); + var max = center + (width / 2) - 1; return x >= min && x <= max && y >= min && y <= max; } return false; @@ -31,8 +32,8 @@ export function rendererTileLayer(context) { function tileSizeAtZoom(d, z) { - var epsilon = 0.002; - return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + epsilon; + var EPSILON = 0.002; + return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + EPSILON; } @@ -49,7 +50,7 @@ export function rendererTileLayer(context) { function lookUp(d) { for (var up = -1; up > -d[2]; up--) { var tile = atZoom(d, up); - if (cache[source.url(tile)] !== false) { + if (_cache[_source.url(tile)] !== false) { return tile; } } @@ -57,7 +58,8 @@ export function rendererTileLayer(context) { function uniqueBy(a, n) { - var o = [], seen = {}; + var o = []; + var seen = {}; for (var i = 0; i < a.length; i++) { if (seen[a[i][n]] === undefined) { o.push(a[i]); @@ -69,37 +71,37 @@ export function rendererTileLayer(context) { function addSource(d) { - d.push(source.url(d)); + d.push(_source.url(d)); return d; } // Update tiles based on current state of `projection`. function background(selection) { - z = Math.max(Math.log(projection.scale() * 2 * Math.PI) / Math.log(2) - 8, 0); + _zoom = geoScaleToZoom(_projection.scale(), tileSize); var pixelOffset; - if (source) { + if (_source) { pixelOffset = [ - source.offset()[0] * Math.pow(2, z), - source.offset()[1] * Math.pow(2, z) + _source.offset()[0] * Math.pow(2, _zoom), + _source.offset()[1] * Math.pow(2, _zoom) ]; } else { pixelOffset = [0, 0]; } var translate = [ - projection.translate()[0] + pixelOffset[0], - projection.translate()[1] + pixelOffset[1] + _projection.translate()[0] + pixelOffset[0], + _projection.translate()[1] + pixelOffset[1] ]; geotile - .scale(projection.scale() * 2 * Math.PI) + .scale(_projection.scale() * 2 * Math.PI) .translate(translate); - tileOrigin = [ - projection.scale() * Math.PI - translate[0], - projection.scale() * Math.PI - translate[1] + _tileOrigin = [ + _projection.scale() * Math.PI - translate[0], + _projection.scale() * Math.PI - translate[1] ]; render(selection); @@ -107,36 +109,36 @@ export function rendererTileLayer(context) { // Derive the tiles onscreen, remove those offscreen and position them. - // Important that this part not depend on `projection` because it's + // Important that this part not depend on `_projection` because it's // rentered when tiles load/error (see #644). function render(selection) { - if (!source) return; + if (!_source) return; var requests = []; - var showDebug = context.getDebug('tile') && !source.overlay; + var showDebug = context.getDebug('tile') && !_source.overlay; - if (source.validZoom(z)) { + if (_source.validZoom(_zoom)) { geotile().forEach(function(d) { addSource(d); if (d[3] === '') return; if (typeof d[3] !== 'string') return; // Workaround for #2295 requests.push(d); - if (cache[d[3]] === false && lookUp(d)) { + if (_cache[d[3]] === false && lookUp(d)) { requests.push(addSource(lookUp(d))); } }); requests = uniqueBy(requests, 3).filter(function(r) { - if (!!source.overlay && nearNullIsland(r[0], r[1], r[2])) { + if (!!_source.overlay && nearNullIsland(r[0], r[1], r[2])) { return false; } // don't re-request tiles which have failed in the past - return cache[r[3]] !== false; + return _cache[r[3]] !== false; }); } function load(d) { - cache[d[3]] = true; + _cache[d[3]] = true; d3_select(this) .on('error', null) .on('load', null) @@ -145,7 +147,7 @@ export function rendererTileLayer(context) { } function error(d) { - cache[d[3]] = false; + _cache[d[3]] = false; d3_select(this) .on('error', null) .on('load', null) @@ -154,19 +156,19 @@ export function rendererTileLayer(context) { } function imageTransform(d) { - var _ts = tileSize * Math.pow(2, z - d[2]); - var scale = tileSizeAtZoom(d, z); + var ts = tileSize * Math.pow(2, _zoom - d[2]); + var scale = tileSizeAtZoom(d, _zoom); return 'translate(' + - ((d[0] * _ts) - tileOrigin[0]) + 'px,' + - ((d[1] * _ts) - tileOrigin[1]) + 'px) ' + + ((d[0] * ts) - _tileOrigin[0]) + 'px,' + + ((d[1] * ts) - _tileOrigin[1]) + 'px) ' + 'scale(' + scale + ',' + scale + ')'; } function tileCenter(d) { - var _ts = tileSize * Math.pow(2, z - d[2]); + var ts = tileSize * Math.pow(2, _zoom - d[2]); return [ - ((d[0] * _ts) - tileOrigin[0] + (_ts / 2)), - ((d[1] * _ts) - tileOrigin[1] + (_ts / 2)) + ((d[0] * ts) - _tileOrigin[0] + (ts / 2)), + ((d[1] * ts) - _tileOrigin[1] + (ts / 2)) ]; } @@ -178,10 +180,10 @@ export function rendererTileLayer(context) { // Pick a representative tile near the center of the viewport // (This is useful for sampling the imagery vintage) - var dims = geotile.size(), - mapCenter = [dims[0] / 2, dims[1] / 2], - minDist = Math.max(dims[0], dims[1]), - nearCenter; + var dims = geotile.size(); + var mapCenter = [dims[0] / 2, dims[1] / 2]; + var minDist = Math.max(dims[0], dims[1]); + var nearCenter; requests.forEach(function(d) { var c = tileCenter(d); @@ -255,8 +257,8 @@ export function rendererTileLayer(context) { .selectAll('.tile-label-debug-vintage') .each(function(d) { var span = d3_select(this); - var center = context.projection.invert(tileCenter(d)); - source.getMetadata(center, d, function(err, result) { + var center = context._projection.invert(tileCenter(d)); + _source.getMetadata(center, d, function(err, result) { span.text((result && result.vintage && result.vintage.range) || t('info_panels.background.vintage') + ': ' + t('info_panels.background.unknown') ); @@ -268,8 +270,8 @@ export function rendererTileLayer(context) { background.projection = function(_) { - if (!arguments.length) return projection; - projection = _; + if (!arguments.length) return _projection; + _projection = _; return background; }; @@ -282,10 +284,10 @@ export function rendererTileLayer(context) { background.source = function(_) { - if (!arguments.length) return source; - source = _; - cache = {}; - geotile.scaleExtent(source.scaleExtent); + if (!arguments.length) return _source; + _source = _; + _cache = {}; + geotile.scaleExtent(_source.scaleExtent); return background; }; diff --git a/modules/services/osm.js b/modules/services/osm.js index cf33659a3f..af96685a33 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -500,8 +500,8 @@ export default { } // update blacklists - var elements = xml.getElementsByTagName('blacklist'), - regexes = []; + var elements = xml.getElementsByTagName('blacklist'); + var regexes = []; for (var i = 0; i < elements.length; i++) { var regex = elements[i].getAttribute('regex'); // needs unencode? if (regex) { @@ -516,8 +516,8 @@ export default { if (rateLimitError) { callback(rateLimitError, 'rateLimited'); } else { - var apiStatus = xml.getElementsByTagName('status'), - val = apiStatus[0].getAttribute('api'); + var apiStatus = xml.getElementsByTagName('status'); + var val = apiStatus[0].getAttribute('api'); callback(undefined, val); } @@ -544,14 +544,14 @@ export default { loadTiles: function(projection, dimensions, callback) { if (off) return; - var that = this, - s = projection.scale() * 2 * Math.PI, - z = Math.max(Math.log(s) / Math.log(2) - 8, 0), - ts = 256 * Math.pow(2, z - tileZoom), - origin = [ - s / 2 - projection.translate()[0], - s / 2 - projection.translate()[1] - ]; + var that = this; + var s = projection.scale() * 2 * Math.PI; + var z = Math.max(Math.log(s) / Math.log(2) - 8, 0); + var ts = 256 * Math.pow(2, z - tileZoom); + var origin = [ + s / 2 - projection.translate()[0], + s / 2 - projection.translate()[1] + ]; var tiles = d3_geoTile() .scaleExtent([tileZoom, tileZoom]) @@ -559,8 +559,8 @@ export default { .size(dimensions) .translate(projection.translate())() .map(function(tile) { - var x = tile[0] * ts - origin[0], - y = tile[1] * ts - origin[1]; + var x = tile[0] * ts - origin[0]; + var y = tile[1] * ts - origin[1]; return { id: tile.toString(), diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 2e65d593ab..15c3d2b08f 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -13,7 +13,8 @@ import { geoEuclideanDistance, geoInterp, geoPolygonIntersectsPolygon, - geoPathLength + geoPathLength, + geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; @@ -27,9 +28,6 @@ import { } from '../util'; -var TAU = 2 * Math.PI; -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } - export function svgLabels(projection, context) { var path = d3_geoPath(projection); @@ -261,7 +259,7 @@ export function svgLabels(projection, context) { function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); var labelable = []; var renderNodeAs = {}; diff --git a/modules/svg/points.js b/modules/svg/points.js index 124dba5bc2..8c41f7a1ae 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -1,12 +1,9 @@ import { dataFeatureIcons } from '../../data'; +import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform, svgTagClasses } from './index'; -var TAU = 2 * Math.PI; -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } - - export function svgPoints(projection, context) { function markerPath(selection, klass) { @@ -55,7 +52,7 @@ export function svgPoints(projection, context) { function drawPoints(selection, graph, entities, filter) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); // points with a direction will render as vertices at higher zooms function renderAsPoint(entity) { diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 06ab1e7b6f..df681a2d14 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -4,14 +4,11 @@ import _values from 'lodash-es/values'; import { select as d3_select } from 'd3-selection'; import { dataFeatureIcons } from '../../data'; +import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform } from './index'; -var TAU = 2 * Math.PI; -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } - - export function svgVertices(projection, context) { var radiuses = { // z16-, z17, z18+, w/icon @@ -46,7 +43,7 @@ export function svgVertices(projection, context) { var icons = {}; var directions = {}; var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); @@ -258,7 +255,7 @@ export function svgVertices(projection, context) { function drawVertices(selection, graph, entities, filter, extent, fullRedraw) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); var mode = context.mode(); var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); @@ -318,7 +315,7 @@ export function svgVertices(projection, context) { // partial redraw - only update the selected items.. drawVertices.drawSelected = function(selection, graph, target, extent) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); _prevSelected = _currSelected || {}; _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom); @@ -334,7 +331,7 @@ export function svgVertices(projection, context) { if (target === _currHoverTarget) return; // continue only if something changed var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); _prevHover = _currHover || {}; _currHoverTarget = target; diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index b0856c56fd..f29503bec5 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoRoundCoords } from '../geo'; +import { geoVecFloor } from '../geo'; import { textDirection } from '../util/locale'; import { uiTooltipHtml } from './tooltipHtml'; @@ -81,7 +81,7 @@ export function uiEditMenu(context, operations) { .attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; }) .classed('disabled', function (d) { return d.disabled(); }) .attr('transform', function (d, i) { - return 'translate(' + geoRoundCoords([ + return 'translate(' + geoVecFloor([ 0, m + i * buttonHeight ]).join(',') + ')'; diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index a89c7a9b50..f56bc868df 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -26,7 +26,8 @@ import { import { geoExtent, - geoRawMercator + geoRawMercator, + geoZoomToScale } from '../../geo'; import { @@ -84,7 +85,7 @@ export function uiFieldRestrictions(field, context) { var z = 24; projection - .scale(256 * Math.pow(2, z) / (2 * Math.PI)); + .scale(geoZoomToScale(z)); var s = projection(vertex.loc); diff --git a/modules/ui/map_in_map.js b/modules/ui/map_in_map.js index 4a6609c982..08ca8b44da 100644 --- a/modules/ui/map_in_map.js +++ b/modules/ui/map_in_map.js @@ -13,45 +13,44 @@ import { import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; -import { svgDebug, svgGpx } from '../svg'; -import { geoRawMercator } from '../geo'; +import { + geoRawMercator, + geoScaleToZoom, + geoVecSubtract, + geoVecScale, + geoZoomToScale, +} from '../geo'; + import { rendererTileLayer } from '../renderer'; +import { svgDebug, svgGpx } from '../svg'; import { utilSetTransform } from '../util'; import { utilGetDimensions } from '../util/dimensions'; -var TAU = 2 * Math.PI; -function ztok(z) { return 256 * Math.pow(2, z) / TAU; } -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } -function vecSub(a, b) { return [ a[0] - b[0], a[1] - b[1] ]; } -function vecScale(a, b) { return [ a[0] * b, a[1] * b ]; } - - export function uiMapInMap(context) { - function map_in_map(selection) { - var backgroundLayer = rendererTileLayer(context), - overlayLayers = {}, - projection = geoRawMercator(), - gpxLayer = svgGpx(projection, context).showLabels(false), - debugLayer = svgDebug(projection, context), - zoom = d3_zoom() - .scaleExtent([ztok(0.5), ztok(24)]) - .on('start', zoomStarted) - .on('zoom', zoomed) - .on('end', zoomEnded), - isTransformed = false, - isHidden = true, - skipEvents = false, - gesture = null, - zDiff = 6, // by default, minimap renders at (main zoom - 6) - wrap = d3_select(null), - tiles = d3_select(null), - viewport = d3_select(null), - tStart, // transform at start of gesture - tCurr, // transform at most recent event - timeoutId; + var backgroundLayer = rendererTileLayer(context); + var overlayLayers = {}; + var projection = geoRawMercator(); + var gpxLayer = svgGpx(projection, context).showLabels(false); + var debugLayer = svgDebug(projection, context); + var zoom = d3_zoom() + .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)]) + .on('start', zoomStarted) + .on('zoom', zoomed) + .on('end', zoomEnded); + var isTransformed = false; + var isHidden = true; + var skipEvents = false; + var gesture = null; + var zDiff = 6; // by default, minimap renders at (main zoom - 6) + var wrap = d3_select(null); + var tiles = d3_select(null); + var viewport = d3_select(null); + var tStart; // transform at start of gesture + var tCurr; // transform at most recent event + var timeoutId; function zoomStarted() { @@ -64,11 +63,11 @@ export function uiMapInMap(context) { function zoomed() { if (skipEvents) return; - var x = d3_event.transform.x, - y = d3_event.transform.y, - k = d3_event.transform.k, - isZooming = (k !== tStart.k), - isPanning = (x !== tStart.x || y !== tStart.y); + var x = d3_event.transform.x; + var y = d3_event.transform.y; + var k = d3_event.transform.k; + var isZooming = (k !== tStart.k); + var isPanning = (x !== tStart.x || y !== tStart.y); if (!isZooming && !isPanning) { return; // no change @@ -79,12 +78,12 @@ export function uiMapInMap(context) { gesture = isZooming ? 'zoom' : 'pan'; } - var tMini = projection.transform(), - tX, tY, scale; + var tMini = projection.transform(); + var tX, tY, scale; if (gesture === 'zoom') { - var dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); scale = k / tMini.k; tX = (cMini[0] / scale - cMini[0]) * scale; tY = (cMini[1] / scale - cMini[1]) * scale; @@ -100,8 +99,8 @@ export function uiMapInMap(context) { isTransformed = true; tCurr = d3_zoomIdentity.translate(x, y).scale(k); - var zMain = ktoz(context.projection.scale()), - zMini = ktoz(k); + var zMain = geoScaleToZoom(context.projection.scale()); + var zMini = geoScaleToZoom(k); zDiff = zMain - zMini; @@ -115,29 +114,29 @@ export function uiMapInMap(context) { updateProjection(); gesture = null; - var dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); context.map().center(projection.invert(cMini)); // recenter main map.. } function updateProjection() { - var loc = context.map().center(), - dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5), - tMain = context.projection.transform(), - zMain = ktoz(tMain.k), - zMini = Math.max(zMain - zDiff, 0.5), - kMini = ztok(zMini); + var loc = context.map().center(); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); + var tMain = context.projection.transform(); + var zMain = geoScaleToZoom(tMain.k); + var zMini = Math.max(zMain - zDiff, 0.5); + var kMini = geoZoomToScale(zMini); projection .translate([tMain.x, tMain.y]) .scale(kMini); - var point = projection(loc), - mouse = (gesture === 'pan') ? vecSub([tCurr.x, tCurr.y], [tStart.x, tStart.y]) : [0, 0], - xMini = cMini[0] - point[0] + tMain.x + mouse[0], - yMini = cMini[1] - point[1] + tMain.y + mouse[1]; + var point = projection(loc); + var mouse = (gesture === 'pan') ? geoVecSubtract([tCurr.x, tCurr.y], [tStart.x, tStart.y]) : [0, 0]; + var xMini = cMini[0] - point[0] + tMain.x + mouse[0]; + var yMini = cMini[1] - point[1] + tMain.y + mouse[1]; projection .translate([xMini, yMini]) @@ -152,7 +151,7 @@ export function uiMapInMap(context) { } zoom - .scaleExtent([ztok(0.5), ztok(zMain - 3)]); + .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(zMain - 3)]); skipEvents = true; wrap.call(zoom.transform, tCurr); @@ -166,8 +165,8 @@ export function uiMapInMap(context) { updateProjection(); - var dMini = utilGetDimensions(wrap), - zMini = ktoz(projection.scale()); + var dMini = utilGetDimensions(wrap); + var zMini = geoScaleToZoom(projection.scale()); // setup tile container tiles = wrap @@ -249,8 +248,8 @@ export function uiMapInMap(context) { // redraw viewport bounding box if (gesture !== 'pan') { - var getPath = d3_geoPath(projection), - bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] }; + var getPath = d3_geoPath(projection); + var bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] }; viewport = wrap.selectAll('.map-in-map-viewport') .data([0]); diff --git a/modules/ui/radial_menu.js b/modules/ui/radial_menu.js index 01b6d24586..3afd4f7380 100644 --- a/modules/ui/radial_menu.js +++ b/modules/ui/radial_menu.js @@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoRoundCoords } from '../geo'; +import { geoVecFloor } from '../geo'; import { uiTooltipHtml } from './tooltipHtml'; @@ -58,7 +58,7 @@ export function uiRadialMenu(context, operations) { .attr('class', function(d) { return 'radial-menu-item radial-menu-item-' + d.id; }) .classed('disabled', function(d) { return d.disabled(); }) .attr('transform', function(d, i) { - return 'translate(' + geoRoundCoords([ + return 'translate(' + geoVecFloor([ r * Math.sin(a0 + i * a), r * Math.cos(a0 + i * a)]).join(',') + ')'; }); diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index b4ac874c64..989c557146 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,60 +1,96 @@ describe('iD.geo', function() { - describe('geoRoundCoords', function() { - it('rounds coordinates', function() { - expect(iD.geoRoundCoords([0.1, 1])).to.eql([0, 1]); - expect(iD.geoRoundCoords([0, 1])).to.eql([0, 1]); - expect(iD.geoRoundCoords([0, 1.1])).to.eql([0, 1]); + + describe('geoVecAdd', function() { + it('adds vectors', function() { + expect(iD.geoVecAdd([1, 2], [3, 4])).to.eql([4, 6]); + expect(iD.geoVecAdd([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecAdd([1, 2], [-3, -4])).to.eql([-2, -2]); + }); + }); + + describe('geoVecSubtract', function() { + it('subtracts vectors', function() { + expect(iD.geoVecSubtract([1, 2], [3, 4])).to.eql([-2, -2]); + expect(iD.geoVecSubtract([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecSubtract([1, 2], [-3, -4])).to.eql([4, 6]); + }); + }); + + describe('geoVecScale', function() { + it('multiplies vectors', function() { + expect(iD.geoVecScale([1, 2], 0)).to.eql([0, 0]); + expect(iD.geoVecScale([1, 2], 1)).to.eql([1, 2]); + expect(iD.geoVecScale([1, 2], 2)).to.eql([2, 4]); + expect(iD.geoVecScale([1, 2], 0.5)).to.eql([0.5, 1]); + }); + }); + + describe('geoVecFloor (was: geoRoundCoordinates)', function() { + it('rounds vectors', function() { + expect(iD.geoVecFloor([0.1, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1.1])).to.eql([0, 1]); }); }); describe('geoInterp', function() { it('interpolates halfway', function() { - var a = [0, 0], - b = [10, 10]; + var a = [0, 0]; + var b = [10, 10]; expect(iD.geoInterp(a, b, 0.5)).to.eql([5, 5]); }); it('interpolates to one side', function() { - var a = [0, 0], - b = [10, 10]; + var a = [0, 0]; + var b = [10, 10]; expect(iD.geoInterp(a, b, 0)).to.eql([0, 0]); }); }); + describe('geoDot', function() { + it('dot product of right angle is zero', function() { + var a = [1, 0]; + var b = [0, 1]; + expect(iD.geoDot(a, b)).to.eql(0); + }); + it('dot product of same vector multiplies', function() { + var a = [2, 0]; + var b = [2, 0]; + expect(iD.geoDot(a, b)).to.eql(4); + }); + }); + describe('geoCross', function() { - it('cross product of right hand turn is positive', function() { - var o = [0, 0], - a = [2, 0], - b = [0, 2]; - expect(iD.geoCross(o, a, b)).to.eql(4); - }); - it('cross product of left hand turn is negative', function() { - var o = [0, 0], - a = [2, 0], - b = [0, -2]; - expect(iD.geoCross(o, a, b)).to.eql(-4); - }); - it('cross product of colinear points is zero', function() { - var o = [0, 0], - a = [-2, 0], - b = [2, 0]; - expect(iD.geoCross(o, a, b)).to.equal(0); + it('2D cross product of right hand turn is positive', function() { + var a = [2, 0]; + var b = [0, 2]; + expect(iD.geoCross(a, b)).to.eql(4); + }); + it('2D cross product of left hand turn is negative', function() { + var a = [2, 0]; + var b = [0, -2]; + expect(iD.geoCross(a, b)).to.eql(-4); + }); + it('2D cross product of colinear points is zero', function() { + var a = [-2, 0]; + var b = [2, 0]; + expect(iD.geoCross(a, b)).to.equal(0); }); }); describe('geoEuclideanDistance', function() { it('distance between two same points is zero', function() { - var a = [0, 0], - b = [0, 0]; + var a = [0, 0]; + var b = [0, 0]; expect(iD.geoEuclideanDistance(a, b)).to.eql(0); }); it('a straight 10 unit line is 10', function() { - var a = [0, 0], - b = [10, 0]; + var a = [0, 0]; + var b = [10, 0]; expect(iD.geoEuclideanDistance(a, b)).to.eql(10); }); it('a pythagorean triangle is right', function() { - var a = [0, 0], - b = [4, 3]; + var a = [0, 0]; + var b = [4, 3]; expect(iD.geoEuclideanDistance(a, b)).to.eql(5); }); }); @@ -64,10 +100,10 @@ describe('iD.geo', function() { expect(iD.geoLatToMeters(0)).to.eql(0); }); it('1 degree latitude is approx 111 km', function() { - expect(iD.geoLatToMeters(1)).to.be.within(110E3, 112E3); + expect(iD.geoLatToMeters(1)).to.be.closeTo(111319, 10); }); it('-1 degree latitude is approx -111 km', function() { - expect(iD.geoLatToMeters(-1)).to.be.within(-112E3, -110E3); + expect(iD.geoLatToMeters(-1)).to.be.closeTo(-111319, 10); }); }); @@ -76,21 +112,21 @@ describe('iD.geo', function() { expect(iD.geoLonToMeters(0, 0)).to.eql(0); }); it('distance of 1 degree longitude varies with latitude', function() { - expect(iD.geoLonToMeters(1, 0)).to.be.within(110E3, 112E3); - expect(iD.geoLonToMeters(1, 15)).to.be.within(107E3, 108E3); - expect(iD.geoLonToMeters(1, 30)).to.be.within(96E3, 97E3); - expect(iD.geoLonToMeters(1, 45)).to.be.within(78E3, 79E3); - expect(iD.geoLonToMeters(1, 60)).to.be.within(55E3, 56E3); - expect(iD.geoLonToMeters(1, 75)).to.be.within(28E3, 29E3); + expect(iD.geoLonToMeters(1, 0)).to.be.closeTo(110946, 10); + expect(iD.geoLonToMeters(1, 15)).to.be.closeTo(107165, 10); + expect(iD.geoLonToMeters(1, 30)).to.be.closeTo(96082, 10); + expect(iD.geoLonToMeters(1, 45)).to.be.closeTo(78450, 10); + expect(iD.geoLonToMeters(1, 60)).to.be.closeTo(55473, 10); + expect(iD.geoLonToMeters(1, 75)).to.be.closeTo(28715, 10); expect(iD.geoLonToMeters(1, 90)).to.eql(0); }); it('distance of -1 degree longitude varies with latitude', function() { - expect(iD.geoLonToMeters(-1, 0)).to.be.within(-112E3, -110E3); - expect(iD.geoLonToMeters(-1, -15)).to.be.within(-108E3, -107E3); - expect(iD.geoLonToMeters(-1, -30)).to.be.within(-97E3, -96E3); - expect(iD.geoLonToMeters(-1, -45)).to.be.within(-79E3, -78E3); - expect(iD.geoLonToMeters(-1, -60)).to.be.within(-56E3, -55E3); - expect(iD.geoLonToMeters(-1, -75)).to.be.within(-29E3, -28E3); + expect(iD.geoLonToMeters(-1, -0)).to.be.closeTo(-110946, 10); + expect(iD.geoLonToMeters(-1, -15)).to.be.closeTo(-107165, 10); + expect(iD.geoLonToMeters(-1, -30)).to.be.closeTo(-96082, 10); + expect(iD.geoLonToMeters(-1, -45)).to.be.closeTo(-78450, 10); + expect(iD.geoLonToMeters(-1, -60)).to.be.closeTo(-55473, 10); + expect(iD.geoLonToMeters(-1, -75)).to.be.closeTo(-28715, 10); expect(iD.geoLonToMeters(-1, -90)).to.eql(0); }); }); @@ -100,10 +136,10 @@ describe('iD.geo', function() { expect(iD.geoMetersToLat(0)).to.eql(0); }); it('111 km is approx 1 degree latitude', function() { - expect(iD.geoMetersToLat(111E3)).to.be.within(0.995, 1.005); + expect(iD.geoMetersToLat(111319)).to.be.closeTo(1, 0.0001); }); it('-111 km is approx -1 degree latitude', function() { - expect(iD.geoMetersToLat(-111E3)).to.be.within(-1.005, -0.995); + expect(iD.geoMetersToLat(-111319)).to.be.closeTo(-1, 0.0001); }); }); @@ -112,22 +148,22 @@ describe('iD.geo', function() { expect(iD.geoMetersToLon(0, 0)).to.eql(0); }); it('distance of 1 degree longitude varies with latitude', function() { - expect(iD.geoMetersToLon(111320, 0)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(107551, 15)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(96486, 30)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(78847, 45)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(55800, 60)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(28902, 75)).to.be.within(0.995, 1.005); + expect(iD.geoMetersToLon(110946, 0)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(107165, 15)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(96082, 30)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(78450, 45)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(55473, 60)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(28715, 75)).to.be.closeTo(1, 1e-4); expect(iD.geoMetersToLon(1, 90)).to.eql(0); }); it('distance of -1 degree longitude varies with latitude', function() { - expect(iD.geoMetersToLon(-111320, 0)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-107551, 15)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-96486, 30)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-78847, 45)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-55800, 60)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-28902, 75)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-1, 90)).to.eql(0); + expect(iD.geoMetersToLon(-110946, -0)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-107165, -15)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-96082, -30)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-78450, -45)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-55473, -60)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-28715, -75)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-1, -90)).to.eql(0); }); }); @@ -159,43 +195,61 @@ describe('iD.geo', function() { describe('geoSphericalDistance', function() { it('distance between two same points is zero', function() { - var a = [0, 0], - b = [0, 0]; + var a = [0, 0]; + var b = [0, 0]; expect(iD.geoSphericalDistance(a, b)).to.eql(0); }); it('a straight 1 degree line at the equator is aproximately 111 km', function() { - var a = [0, 0], - b = [1, 0]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(110E3, 112E3); + var a = [0, 0]; + var b = [1, 0]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(110946, 10); }); it('a pythagorean triangle is (nearly) right', function() { - var a = [0, 0], - b = [4, 3]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(555E3, 556E3); + var a = [0, 0]; + var b = [4, 3]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(555282, 10); }); it('east-west distances at high latitude are shorter', function() { - var a = [0, 60], - b = [1, 60]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(55E3, 56E3); + var a = [0, 60]; + var b = [1, 60]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(55473, 10); }); it('north-south distances at high latitude are not shorter', function() { - var a = [0, 60], - b = [0, 61]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(110E3, 112E3); + var a = [0, 60]; + var b = [0, 61]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(111319, 10); + }); + }); + + describe('geoZoomToScale', function() { + it('converts from zoom to projection scale (tileSize = 256)', function() { + expect(iD.geoZoomToScale(17)).to.be.closeTo(5340353.715440872, 1e-6); + }); + it('converts from zoom to projection scale (tileSize = 512)', function() { + expect(iD.geoZoomToScale(17, 512)).to.be.closeTo(10680707.430881744, 1e-6); + }); + }); + + describe('geoScaleToZoom', function() { + it('converts from projection scale to zoom (tileSize = 256)', function() { + expect(iD.geoScaleToZoom(5340353.715440872)).to.be.closeTo(17, 1e-6); + }); + it('converts from projection scale to zoom (tileSize = 512)', function() { + expect(iD.geoScaleToZoom(10680707.430881744, 512)).to.be.closeTo(17, 1e-6); }); }); describe('geoEdgeEqual', function() { it('returns false for inequal edges', function() { - expect(iD.geoEdgeEqual(['a','b'], ['a','c'])).to.be.false; + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'c'])).to.be.false; }); it('returns true for equal edges along same direction', function() { - expect(iD.geoEdgeEqual(['a','b'], ['a','b'])).to.be.true; + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'b'])).to.be.true; }); it('returns true for equal edges along opposite direction', function() { - expect(iD.geoEdgeEqual(['a','b'], ['b','a'])).to.be.true; + expect(iD.geoEdgeEqual(['a', 'b'], ['b', 'a'])).to.be.true; }); }); @@ -211,10 +265,10 @@ describe('iD.geo', function() { describe('geoRotate', function() { it('rotates points around [0, 0]', function() { - var points = [[5, 0], [5, 1]], - angle = Math.PI, - around = [0, 0], - result = iD.geoRotate(points, angle, around); + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [0, 0]; + var result = iD.geoRotate(points, angle, around); expect(result[0][0]).to.be.closeTo(-5, 1e-6); expect(result[0][1]).to.be.closeTo(0, 1e-6); expect(result[1][0]).to.be.closeTo(-5, 1e-6); @@ -222,10 +276,10 @@ describe('iD.geo', function() { }); it('rotates points around [3, 0]', function() { - var points = [[5, 0], [5, 1]], - angle = Math.PI, - around = [3, 0], - result = iD.geoRotate(points, angle, around); + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [3, 0]; + var result = iD.geoRotate(points, angle, around); expect(result[0][0]).to.be.closeTo(1, 1e-6); expect(result[0][1]).to.be.closeTo(0, 1e-6); expect(result[1][0]).to.be.closeTo(1, 1e-6); @@ -246,7 +300,7 @@ describe('iD.geo', function() { }); it('returns undefined properties for a degenerate way (single node)', function() { - expect(iD.geoChooseEdge([iD.Node({loc: [0, 0]})], [0, 0], projection)).to.eql({ + expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.eql({ index: undefined, distance: Infinity, loc: undefined @@ -259,14 +313,10 @@ describe('iD.geo', function() { // c // // * = [2, 0] - var a = [0, 0], - b = [5, 0], - c = [2, 1], - nodes = [ - iD.Node({loc: a}), - iD.Node({loc: b}) - ]; - + var a = [0, 0]; + var b = [5, 0]; + var c = [2, 1]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; var choice = iD.geoChooseEdge(nodes, c, projection); expect(choice.index).to.eql(1); expect(choice.distance).to.eql(1); @@ -274,14 +324,10 @@ describe('iD.geo', function() { }); it('returns the starting vertex when the orthogonal projection is < 0', function() { - var a = [0, 0], - b = [5, 0], - c = [-3, 4], - nodes = [ - iD.Node({loc: a}), - iD.Node({loc: b}) - ]; - + var a = [0, 0]; + var b = [5, 0]; + var c = [-3, 4]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; var choice = iD.geoChooseEdge(nodes, c, projection); expect(choice.index).to.eql(1); expect(choice.distance).to.eql(5); @@ -289,14 +335,10 @@ describe('iD.geo', function() { }); it('returns the ending vertex when the orthogonal projection is > 1', function() { - var a = [0, 0], - b = [5, 0], - c = [8, 4], - nodes = [ - iD.Node({loc: a}), - iD.Node({loc: b}) - ]; - + var a = [0, 0]; + var b = [5, 0]; + var c = [8, 4]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; var choice = iD.geoChooseEdge(nodes, c, projection); expect(choice.index).to.eql(1); expect(choice.distance).to.eql(5); @@ -306,28 +348,28 @@ describe('iD.geo', function() { describe('geoLineIntersection', function() { it('returns null if lines are colinear with overlap', function() { - var a = [[0, 0], [10, 0]], - b = [[-5, 0], [5, 0]]; + var a = [[0, 0], [10, 0]]; + var b = [[-5, 0], [5, 0]]; expect(iD.geoLineIntersection(a, b)).to.be.null; }); it('returns null if lines are colinear but disjoint', function() { - var a = [[5, 0], [10, 0]], - b = [[-10, 0], [-5, 0]]; + var a = [[5, 0], [10, 0]]; + var b = [[-10, 0], [-5, 0]]; expect(iD.geoLineIntersection(a, b)).to.be.null; }); it('returns null if lines are parallel', function() { - var a = [[0, 0], [10, 0]], - b = [[0, 5], [10, 5]]; + var a = [[0, 0], [10, 0]]; + var b = [[0, 5], [10, 5]]; expect(iD.geoLineIntersection(a, b)).to.be.null; }); it('returns the intersection point between 2 lines', function() { - var a = [[0, 0], [10, 0]], - b = [[5, 10], [5, -10]]; + var a = [[0, 0], [10, 0]]; + var b = [[5, 10], [5, -10]]; expect(iD.geoLineIntersection(a, b)).to.eql([5, 0]); }); it('returns null if lines are not parallel but not intersecting', function() { - var a = [[0, 0], [10, 0]], - b = [[-5, 10], [-5, -10]]; + var a = [[0, 0], [10, 0]]; + var b = [[-5, 10], [-5, -10]]; expect(iD.geoLineIntersection(a, b)).to.be.null; }); }); @@ -339,12 +381,7 @@ describe('iD.geo', function() { expect(iD.geoPointInPolygon(point, poly)).to.be.true; }); it('says a point outside of a polygon is outside', function() { - var poly = [ - [0, 0], - [0, 1], - [1, 1], - [1, 0], - [0, 0]]; + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; var point = [0.5, 1.5]; expect(iD.geoPointInPolygon(point, poly)).to.be.false; }); diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 140576398e..1b9e59e08f 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -1,13 +1,10 @@ describe('iD.svgAreas', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var all = function() { return true; }; var none = function() { return false; }; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index fa70b6565f..75c957daaf 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -1,11 +1,8 @@ describe('iD.svgLayers', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, container; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index 2a414afcda..d10fe09a60 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -1,13 +1,10 @@ describe('iD.svgLines', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var all = function() { return true; }; var none = function() { return false; }; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js index 230ad8446c..c30530fae4 100644 --- a/test/spec/svg/midpoints.js +++ b/test/spec/svg/midpoints.js @@ -1,13 +1,10 @@ describe('iD.svgMidpoints', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var _selectedIDs = []; var filter = function() { return true; }; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); diff --git a/test/spec/svg/points.js b/test/spec/svg/points.js index 8faaa54c22..c1012a7cdf 100644 --- a/test/spec/svg/points.js +++ b/test/spec/svg/points.js @@ -1,11 +1,8 @@ describe('iD.svgPoints', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index 3f7789643d..1c29ca70d2 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -1,12 +1,9 @@ describe('iD.svgVertices', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context; var surface; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); From 7155ef8bc6e46ef80cb9d191457877a543cd2842 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 16:09:07 -0500 Subject: [PATCH 054/206] Fix double clicking on a way to create a vertex --- modules/modes/select.js | 12 ++++++------ modules/svg/vertices.js | 20 ++++++++++++++++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/modules/modes/select.js b/modules/modes/select.js index fa5a3ddd92..941c417514 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -245,13 +245,13 @@ export function modeSelect(context, selectedIDs) { function dblclick() { - var target = d3_select(d3_event.target), - datum = target.datum(); + var target = d3_select(d3_event.target); + var datum = target.datum(); - if (datum instanceof osmWay && !target.classed('fill')) { - var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection), - prev = datum.nodes[choice.index - 1], - next = datum.nodes[choice.index]; + if (datum instanceof osmWay && target.classed('target')) { + var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection); + var prev = datum.nodes[choice.index - 1]; + var next = datum.nodes[choice.index]; context.perform( actionAddMidpoint({loc: choice.loc, edge: [prev, next]}, osmNode()), diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index df681a2d14..a6bc3ca7d6 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -268,15 +268,27 @@ export function svgVertices(projection, context) { for (var i = 0; i < entities.length; i++) { var entity = entities[i]; var geometry = entity.geometry(graph); + var keep = false; + // a point that looks like a vertex.. if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) { _currPersistent[entity.id] = entity; + keep = true; - } else if ((geometry === 'vertex') && - (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) ) { - _currPersistent[entity.id] = entity; + // a vertex of some importance.. + } else if (geometry === 'vertex') { + if (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) { + _currPersistent[entity.id] = entity; + keep = true; + } + // partial redraw in select mode - probably because the user double clicked a way. + if (!fullRedraw && mode.id === 'select') { + _currSelected[entity.id] = entity; + } + } - } else if (!fullRedraw) { + // whatever this is, it's not a persistent vertex.. + if (!keep && !fullRedraw) { delete _currPersistent[entity.id]; } } From eafc2b43009db57ef75e2883501e2ca5706b23c2 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 17:00:47 -0500 Subject: [PATCH 055/206] Adjust touch target radii --- modules/svg/midpoints.js | 4 ++-- modules/svg/vertices.js | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index 60ff03de82..ed629c6c6b 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -14,7 +14,7 @@ import { export function svgMidpoints(projection, context) { - + var targetRadius = 8; function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; @@ -29,7 +29,7 @@ export function svgMidpoints(projection, context) { // enter/update targets.enter() .append('circle') - .attr('r', 12) + .attr('r', targetRadius) .merge(targets) .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; }) .attr('transform', svgPointTransform(projection)); diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index a6bc3ca7d6..4b066cb59f 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -23,6 +23,7 @@ export function svgVertices(projection, context) { var _prevHover = {}; var _currSelected = {}; var _prevSelected = {}; + var _radii = {}; function sortY(a, b) { @@ -40,6 +41,7 @@ export function svgVertices(projection, context) { function draw(selection, graph, vertices, sets, filter) { sets = sets || { selected: {}, important: {}, hovered: {} }; + var icons = {}; var directions = {}; var wireframe = context.surface().classed('fill-wireframe'); @@ -80,9 +82,13 @@ export function svgVertices(projection, context) { r += 1.5; } + if (klass === 'shadow') { // remember this value, so we don't need to + _radii[entity.id] = r; // recompute it when we draw the touch targets + } + d3_select(this) .attr('r', r) - .attr('visibility', ((i && klass === 'fill') ? 'hidden' : null)); + .attr('visibility', (i && klass === 'fill') ? 'hidden' : null); }); }); @@ -188,7 +194,7 @@ export function svgVertices(projection, context) { // enter/update targets.enter() .append('circle') - .attr('r', radiuses.shadow[3]) // just use the biggest one for now + .attr('r', function(d) { return _radii[d.id] || radiuses.shadow[3]; }) .merge(targets) .attr('class', function(d) { return 'node vertex target ' + fillClass + d.id; }) .attr('transform', svgPointTransform(projection)); @@ -259,12 +265,13 @@ export function svgVertices(projection, context) { var mode = context.mode(); var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); - // Collect important vertices from the `entities` list.. - // (during a paritial redraw, it will not contain everything) if (fullRedraw) { _currPersistent = {}; + _radii = {}; } + // Collect important vertices from the `entities` list.. + // (during a paritial redraw, it will not contain everything) for (var i = 0; i < entities.length; i++) { var entity = entities[i]; var geometry = entity.geometry(graph); @@ -282,7 +289,7 @@ export function svgVertices(projection, context) { keep = true; } // partial redraw in select mode - probably because the user double clicked a way. - if (!fullRedraw && mode.id === 'select') { + if (!fullRedraw && mode && mode.id === 'select') { _currSelected[entity.id] = entity; } } From 7a8f50c74e54e6749fc0f4c64e0644657044a0ef Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 22:54:49 -0500 Subject: [PATCH 056/206] More fixes for drawing/snapping, don't draw touch targets for activeIDs --- css/20_map.css | 2 +- css/70_fills.css | 4 +++ modules/behavior/draw.js | 17 +++++++----- modules/behavior/draw_way.js | 51 ++++++++++++++++++------------------ modules/core/context.js | 7 +++++ modules/modes/drag_node.js | 17 +++++++----- modules/modes/draw_area.js | 8 ++++-- modules/modes/draw_line.js | 10 ++++--- modules/renderer/map.js | 6 ++++- modules/svg/areas.js | 5 +++- modules/svg/lines.js | 5 +++- modules/svg/points.js | 6 ++++- modules/svg/vertices.js | 6 ++++- 13 files changed, 95 insertions(+), 49 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 38106a2236..87fea93097 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -26,7 +26,7 @@ .way.target { pointer-events: stroke; fill: none; - stroke-width: 10; + stroke-width: 12; stroke-opacity: 0.8; stroke: currentColor; } diff --git a/css/70_fills.css b/css/70_fills.css index 3587bbee8c..1c536510c3 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -33,5 +33,9 @@ .fill-partial path.area.fill { fill-opacity: 0; stroke-width: 60px; + pointer-events: none; +} +.mode-browse .fill-partial path.area.fill, +.mode-select .fill-partial path.area.fill { pointer-events: visibleStroke; } diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 61ca00af55..4bce319ddd 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -48,11 +48,17 @@ export function behaviorDraw(context) { function datum() { if (d3_event.altKey) return {}; + var element; if (d3_event.type === 'keydown') { - return (_lastMouse && _lastMouse.target.__data__) || {}; + element = _lastMouse && _lastMouse.target; } else { - return d3_event.target.__data__ || {}; + element = d3_event.target; } + + // When drawing, connect only to things classed as targets.. + // (this excludes area fills and active drawing elements) + var selection = d3_select(element); + return (selection.classed('target') && element.__data__) || {}; } @@ -116,8 +122,7 @@ export function behaviorDraw(context) { function click() { - var trySnap = geoViewportEdge(context.mouse, context.map().dimensions()) !== null; - + var trySnap = geoViewportEdge(context.mouse(), context.map().dimensions()) === null; if (trySnap) { // If we're not at the edge of the viewport, try to snap.. // See also: `modes/drag_node.js doMove()` @@ -128,8 +133,8 @@ export function behaviorDraw(context) { dispatch.call('clickNode', this, d); return; - // Snap to a way (not an area fill) - } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { + // Snap to a way + } else if (d.type === 'way') { var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); var edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; dispatch.call('clickWay', this, choice.loc, edge); diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 57ecfb193c..045b8a8554 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -13,7 +13,8 @@ import { behaviorDraw } from './draw'; import { geoChooseEdge, - geoEdgeEqual + geoEdgeEqual, + geoViewportEdge } from '../geo'; import { @@ -31,17 +32,18 @@ import { utilEntitySelector } from '../util'; export function behaviorDrawWay(context, wayId, index, mode, startGraph) { - var origWay = context.entity(wayId), - isArea = context.geometry(wayId) === 'area', - tempEdits = 0, - annotation = t((origWay.isDegenerate() ? - 'operations.start.annotation.' : - 'operations.continue.annotation.') + context.geometry(wayId)), - draw = behaviorDraw(context), - startIndex, - start, - end, - segment; + var origWay = context.entity(wayId); + var isArea = context.geometry(wayId) === 'area'; + var tempEdits = 0; + var annotation = t((origWay.isDegenerate() ? + 'operations.start.annotation.' : + 'operations.continue.annotation.') + context.geometry(wayId)); + var draw = behaviorDraw(context); + var _activeIDs = []; + var startIndex; + var start; + var end; + var segment; // initialize the temporary drawing entities @@ -49,7 +51,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { startIndex = typeof index === 'undefined' ? origWay.nodes.length - 1 : 0; start = osmNode({ id: 'nStart', loc: context.entity(origWay.nodes[startIndex]).loc }); end = osmNode({ id: 'nEnd', loc: context.map().mouseCoordinates() }); - segment = osmWay({ id: 'wTemp', + segment = osmWay({ + id: 'wTemp', nodes: typeof index === 'undefined' ? [start.id, end.id] : [end.id, start.id], tags: _clone(origWay.tags) }); @@ -70,20 +73,11 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function move(datum) { var loc; - if (datum.type === 'node' && datum.id !== end.id) { loc = datum.loc; } else if (datum.type === 'way') { - var dims = context.map().dimensions(), - mouse = context.mouse(), - pad = 5, - trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad && - mouse[1] > pad && mouse[1] < dims[1] - pad; - - if (trySnap) { - loc = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc; - } + loc = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc; } if (!loc) { @@ -110,8 +104,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function setActiveElements() { - var active = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; - context.surface().selectAll(utilEntitySelector(active)) + _activeIDs = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; + context.surface().selectAll(utilEntitySelector(_activeIDs)) .classed('active', true); } @@ -326,6 +320,13 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { }; + drawWay.activeIDs = function() { + if (!arguments.length) return _activeIDs; + // no assign + return drawWay; + }; + + drawWay.tail = function(text) { draw.tail(text); return drawWay; diff --git a/modules/core/context.js b/modules/core/context.js index 95146e2480..e7f0deacf4 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -255,6 +255,13 @@ export function coreContext() { return []; } }; + context.activeIDs = function() { + if (mode && mode.activeIDs) { + return mode.activeIDs(); + } else { + return []; + } + }; /* Behaviors */ diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 915706bbd7..e39832b988 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -123,7 +123,7 @@ export function modeDragNode(context) { function datum() { var event = d3_event && d3_event.sourceEvent; - if (!event || event.altKey) { + if (!event || event.altKey || !d3_select(event.target).classed('target')) { return {}; } else { return event.target.__data__ || {}; @@ -147,11 +147,8 @@ export function modeDragNode(context) { if (d.type === 'node' && d.id !== entity.id) { loc = d.loc; - // Snap to a way (not an area fill) - } else if (d.type === 'way' && !d3_select(d3_event.sourceEvent.target).classed('fill')) { - - // var childNodes = context.childNodes(d); - // var childIDs = childNodes.map(function(node) { return node.id; }); + // Snap to a way + } else if (d.type === 'way') { var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); // (not along a segment adjacent to self) if (entity.id !== d.nodes[choice.index - 1] && entity.id !== d.nodes[choice.index]) { @@ -274,6 +271,7 @@ export function modeDragNode(context) { context.map() .on('drawn.drag-node', null); + _activeIDs = []; context.surface() .selectAll('.active') .classed('active', false); @@ -289,6 +287,13 @@ export function modeDragNode(context) { }; + mode.activeIDs = function() { + if (!arguments.length) return _activeIDs; + // no assign + return mode; + }; + + mode.restoreSelectedIDs = function(_) { if (!arguments.length) return _restoreSelectedIDs; _restoreSelectedIDs = _; diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index e5478d3390..23bd196b55 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -20,8 +20,8 @@ export function modeDrawArea(context, wayId, startGraph) { var addNode = behavior.addNode; behavior.addNode = function(node) { - var length = way.nodes.length, - penultimate = length > 2 ? way.nodes[length - 2] : null; + var length = way.nodes.length; + var penultimate = length > 2 ? way.nodes[length - 2] : null; if (node.id === way.first() || node.id === penultimate) { behavior.finish(); @@ -43,6 +43,10 @@ export function modeDrawArea(context, wayId, startGraph) { return [wayId]; }; + mode.activeIDs = function() { + return (behavior && behavior.activeIDs()) || []; + }; + return mode; } diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index 5198fd4447..e6099b9bdd 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -12,15 +12,14 @@ export function modeDrawLine(context, wayId, startGraph, affix) { mode.enter = function() { - var way = context.entity(wayId), - index = (affix === 'prefix') ? 0 : undefined, - headId = (affix === 'prefix') ? way.first() : way.last(); + var way = context.entity(wayId); + var index = (affix === 'prefix') ? 0 : undefined; + var headId = (affix === 'prefix') ? way.first() : way.last(); behavior = behaviorDrawWay(context, wayId, index, mode, startGraph) .tail(t('modes.draw_line.tail')); var addNode = behavior.addNode; - behavior.addNode = function(node) { if (node.id === headId) { behavior.finish(); @@ -42,6 +41,9 @@ export function modeDrawLine(context, wayId, startGraph, affix) { return [wayId]; }; + mode.activeIDs = function() { + return (behavior && behavior.activeIDs()) || []; + }; return mode; } diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 39996d04fc..5987d7f2bc 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -199,8 +199,9 @@ export function rendererMap(context) { supersurface .call(context.background()); - context.on('enter.map', function() { + context.on('enter.map', function() { if (map.editable() && !transformed) { + // redraw immediately the objects that are affected by a chnage in selectedIDs. var all = context.intersects(map.extent()); var filter = utilFunctor(true); var graph = context.graph(); @@ -210,6 +211,9 @@ export function rendererMap(context) { .call(drawVertices.drawSelected, graph, all, map.extent()) .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); dispatch.call('drawn', this, { full: false }); + + // redraw everything else later + scheduleRedraw(); } }); diff --git a/modules/svg/areas.js b/modules/svg/areas.js index d540a08a0c..b017d531c7 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -44,10 +44,13 @@ export function svgAreas(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var getPath = svgPath(projection, graph); + var passive = entities.filter(function(d) { + return context.activeIDs().indexOf(d.id) === -1; + }); var targets = selection.selectAll('.area.target') .filter(filter) - .data(entities, function key(d) { return d.id; }); + .data(passive, function key(d) { return d.id; }); // exit targets.exit() diff --git a/modules/svg/lines.js b/modules/svg/lines.js index caf11c294d..d5b1010bbf 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -39,10 +39,13 @@ export function svgLines(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var getPath = svgPath(projection, graph); + var passive = entities.filter(function(d) { + return context.activeIDs().indexOf(d.id) === -1; + }); var targets = selection.selectAll('.line.target') .filter(filter) - .data(entities, function key(d) { return d.id; }); + .data(passive, function key(d) { return d.id; }); // exit targets.exit() diff --git a/modules/svg/points.js b/modules/svg/points.js index 8c41f7a1ae..40898e2aa7 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -29,9 +29,13 @@ export function svgPoints(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var passive = entities.filter(function(d) { + return context.activeIDs().indexOf(d.id) === -1; + }); + var targets = selection.selectAll('.point.target') .filter(filter) - .data(entities, function key(d) { return d.id; }); + .data(passive, function key(d) { return d.id; }); // exit targets.exit() diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 4b066cb59f..4ed0ea17b9 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -183,9 +183,13 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var passive = entities.filter(function(d) { + return context.activeIDs().indexOf(d.id) === -1; + }); + var targets = selection.selectAll('.vertex.target') .filter(filter) - .data(entities, function key(d) { return d.id; }); + .data(passive, function key(d) { return d.id; }); // exit targets.exit() From 563c496a652018a3b487a3dd8908ac5e5bdd33a9 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 19 Dec 2017 09:08:59 -0500 Subject: [PATCH 057/206] Update selected vertices when drawing in select mode This is a better solution to catching and drawing new verteices that got added because a user double clicked on a line. --- modules/behavior/draw_way.js | 3 +-- modules/renderer/map.js | 10 +++++++++- modules/svg/vertices.js | 15 +++++---------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 045b8a8554..6dd7c87b36 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -13,8 +13,7 @@ import { behaviorDraw } from './draw'; import { geoChooseEdge, - geoEdgeEqual, - geoViewportEdge + geoEdgeEqual } from '../geo'; import { diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 5987d7f2bc..b2718ec25c 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -208,7 +208,7 @@ export function rendererMap(context) { all = context.features().filter(all, graph); surface.selectAll('.data-layer-osm') - .call(drawVertices.drawSelected, graph, all, map.extent()) + .call(drawVertices.drawSelected, graph, map.extent()) .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); dispatch.call('drawn', this, { full: false }); @@ -269,6 +269,7 @@ export function rendererMap(context) { function drawVector(difference, extent) { + var mode = context.mode(); var graph = context.graph(); var features = context.features(); var all = context.intersects(map.extent()); @@ -303,6 +304,13 @@ export function rendererMap(context) { data = features.filter(data, graph); + if (mode && mode.id === 'select') { + // update selected vertices - the user might have just double-clicked a way, + // creating a new vertex, triggering a partial redraw without a mode change + surface.selectAll('.data-layer-osm') + .call(drawVertices.drawSelected, graph, map.extent()); + } + surface.selectAll('.data-layer-osm') .call(drawVertices, graph, data, filter, map.extent(), fullRedraw) .call(drawLines, graph, data, filter) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 4ed0ea17b9..2c9bef2907 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -287,15 +287,10 @@ export function svgVertices(projection, context) { keep = true; // a vertex of some importance.. - } else if (geometry === 'vertex') { - if (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)) { - _currPersistent[entity.id] = entity; - keep = true; - } - // partial redraw in select mode - probably because the user double clicked a way. - if (!fullRedraw && mode && mode.id === 'select') { - _currSelected[entity.id] = entity; - } + } else if (geometry === 'vertex' && + (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph))) { + _currPersistent[entity.id] = entity; + keep = true; } // whatever this is, it's not a persistent vertex.. @@ -336,7 +331,7 @@ export function svgVertices(projection, context) { // partial redraw - only update the selected items.. - drawVertices.drawSelected = function(selection, graph, target, extent) { + drawVertices.drawSelected = function(selection, graph, extent) { var wireframe = context.surface().classed('fill-wireframe'); var zoom = geoScaleToZoom(projection.scale()); From 7994baae23823dce7db191d721a973bda20ee750 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 19 Dec 2017 11:23:35 -0500 Subject: [PATCH 058/206] WIP: trying singular activeID and smarter target drawing code The goal here is that the code that draws the targets should know better what parts of the lines/vertices are targetable, rather than just relying on CSS to ignore the pointer events on the whole line. e.g. when drawing a line, it's ok for it to loop back and connect to itself, just not on a segment or vertex adjacent to the active node. --- modules/behavior/draw_way.js | 19 ++++++++++++++----- modules/core/context.js | 15 +++++++++------ modules/modes/drag_node.js | 25 +++++++++++++++++-------- modules/modes/draw_area.js | 7 +++++-- modules/modes/draw_line.js | 7 +++++-- modules/svg/areas.js | 3 ++- modules/svg/lines.js | 3 ++- modules/svg/points.js | 3 ++- modules/svg/vertices.js | 11 +++++++---- 9 files changed, 63 insertions(+), 30 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 6dd7c87b36..163513b3c5 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -38,7 +38,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { 'operations.start.annotation.' : 'operations.continue.annotation.') + context.geometry(wayId)); var draw = behaviorDraw(context); - var _activeIDs = []; + // var _activeIDs = []; + var _activeID; var startIndex; var start; var end; @@ -103,8 +104,11 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function setActiveElements() { - _activeIDs = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; - context.surface().selectAll(utilEntitySelector(_activeIDs)) + // _activeIDs = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; + // context.surface().selectAll(utilEntitySelector(_activeIDs)) + // .classed('active', true); + _activeID = end.id; + context.surface().selectAll('.' + end.id) .classed('active', true); } @@ -319,8 +323,13 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { }; - drawWay.activeIDs = function() { - if (!arguments.length) return _activeIDs; + // drawWay.activeIDs = function() { + // if (!arguments.length) return _activeIDs; + // // no assign + // return drawWay; + // }; + drawWay.activeID = function() { + if (!arguments.length) return _activeID; // no assign return drawWay; }; diff --git a/modules/core/context.js b/modules/core/context.js index e7f0deacf4..dcf0b64120 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -255,12 +255,15 @@ export function coreContext() { return []; } }; - context.activeIDs = function() { - if (mode && mode.activeIDs) { - return mode.activeIDs(); - } else { - return []; - } + // context.activeIDs = function() { + // if (mode && mode.activeIDs) { + // return mode.activeIDs(); + // } else { + // return []; + // } + // }; + context.activeID = function() { + return mode && mode.activeID && mode.activeID(); }; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index e39832b988..956b78314a 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -44,7 +44,8 @@ export function modeDragNode(context) { var _nudgeInterval; var _restoreSelectedIDs = []; - var _activeIDs = []; + // var _activeIDs = []; + var _activeID; var _wasMidpoint = false; var _isCancelled = false; var _dragEntity; @@ -112,9 +113,9 @@ export function modeDragNode(context) { // `.active` elements have `pointer-events: none`. // This prevents the node or vertex being dragged from trying to connect to itself. - _activeIDs = context.graph().parentWays(entity) - .map(function(parent) { return parent.id; }); - _activeIDs.push(entity.id); + // _activeIDs = context.graph().parentWays(entity).map(function(parent) { return parent.id; }); + // _activeIDs.push(entity.id); + _activeID = entity.id; setActiveElements(); context.enter(mode); @@ -232,7 +233,9 @@ export function modeDragNode(context) { function setActiveElements() { - context.surface().selectAll(utilEntitySelector(_activeIDs)) + // context.surface().selectAll(utilEntitySelector(_activeIDs)) + // .classed('active', true); + context.surface().selectAll('.' + _activeID) .classed('active', true); } @@ -271,7 +274,8 @@ export function modeDragNode(context) { context.map() .on('drawn.drag-node', null); - _activeIDs = []; + // _activeIDs = []; + _activeID = null; context.surface() .selectAll('.active') .classed('active', false); @@ -287,8 +291,13 @@ export function modeDragNode(context) { }; - mode.activeIDs = function() { - if (!arguments.length) return _activeIDs; + // mode.activeIDs = function() { + // if (!arguments.length) return _activeIDs; + // // no assign + // return mode; + // }; + mode.activeID = function() { + if (!arguments.length) return _activeID; // no assign return mode; }; diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 23bd196b55..0dd5b1272d 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -43,8 +43,11 @@ export function modeDrawArea(context, wayId, startGraph) { return [wayId]; }; - mode.activeIDs = function() { - return (behavior && behavior.activeIDs()) || []; + // mode.activeIDs = function() { + // return (behavior && behavior.activeIDs()) || []; + // }; + mode.activeID = function() { + return (behavior && behavior.activeID()) || []; }; diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index e6099b9bdd..3e638cbd99 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -41,8 +41,11 @@ export function modeDrawLine(context, wayId, startGraph, affix) { return [wayId]; }; - mode.activeIDs = function() { - return (behavior && behavior.activeIDs()) || []; + // mode.activeIDs = function() { + // return (behavior && behavior.activeIDs()) || []; + // }; + mode.activeID = function() { + return (behavior && behavior.activeID()) || []; }; return mode; diff --git a/modules/svg/areas.js b/modules/svg/areas.js index b017d531c7..c6ff49cbb1 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -45,7 +45,8 @@ export function svgAreas(projection, context) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var getPath = svgPath(projection, graph); var passive = entities.filter(function(d) { - return context.activeIDs().indexOf(d.id) === -1; + return true; + // return context.activeIDs().indexOf(d.id) === -1; }); var targets = selection.selectAll('.area.target') diff --git a/modules/svg/lines.js b/modules/svg/lines.js index d5b1010bbf..4770bebd68 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -40,7 +40,8 @@ export function svgLines(projection, context) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var getPath = svgPath(projection, graph); var passive = entities.filter(function(d) { - return context.activeIDs().indexOf(d.id) === -1; + return true; + // return context.activeIDs().indexOf(d.id) === -1; }); var targets = selection.selectAll('.line.target') diff --git a/modules/svg/points.js b/modules/svg/points.js index 40898e2aa7..ef1d504d9e 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -30,7 +30,8 @@ export function svgPoints(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var passive = entities.filter(function(d) { - return context.activeIDs().indexOf(d.id) === -1; + return d.id !== context.activeID(); + // return context.activeIDs().indexOf(d.id) === -1; }); var targets = selection.selectAll('.point.target') diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 2c9bef2907..e880363f7f 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -183,13 +183,16 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; - var passive = entities.filter(function(d) { - return context.activeIDs().indexOf(d.id) === -1; - }); + // no targets for entities that are active, or adjacent to active. + function passive(d) { + return d.id !== context.activeID(); + } + + var data = entities.filter(passive); var targets = selection.selectAll('.vertex.target') .filter(filter) - .data(passive, function key(d) { return d.id; }); + .data(data, function key(d) { return d.id; }); // exit targets.exit() From 4b98bafec2e93c748a0c62069faa1ce6ff57ce44 Mon Sep 17 00:00:00 2001 From: Nicolas Decoster Date: Wed, 20 Dec 2017 18:20:12 +0100 Subject: [PATCH 059/206] Highlight the changeset editor when the comment is empty (closes #4613) --- css/80_app.css | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/css/80_app.css b/css/80_app.css index 83205fb0b5..141b84e39c 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -1834,6 +1834,28 @@ input[type=number] { text-align: center; } +/* Changeset editor while comment text is empty */ + +.form-field-comment:not(.present) #preset-input-comment { + border-color: rgb(230, 100, 100); +} + +.form-field-comment:not(.present) .form-label { + border-color: rgb(230, 100, 100); + background: rgba(230, 100, 100, 0.2); +} + +.form-field-comment:not(.present) .form-label { +} + +.form-field-comment:not(.present) .form-label-button-wrap { + border-color: rgb(230, 100, 100); +} + +.form-field-comment:not(.present) button { + border-color: rgb(230, 100, 100); +} + /* combobox dropdown */ div.combobox { From f58349864c37ae9fcd3ddb12b59d3be9b1be2744 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 20 Dec 2017 13:52:16 -0500 Subject: [PATCH 060/206] Add support for nope targets, line sub-segment targeting --- css/20_map.css | 6 ++ modules/behavior/draw.js | 5 +- modules/behavior/hover.js | 16 ++-- modules/behavior/select.js | 22 ++--- modules/modes/drag_node.js | 3 +- modules/modes/select.js | 3 + modules/svg/lines.js | 163 +++++++++++++++++++++++++++++++++++-- modules/svg/path.js | 28 ++++--- modules/svg/vertices.js | 98 +++++++++++++++++++--- 9 files changed, 294 insertions(+), 50 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 87fea93097..1004a3d1de 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -31,6 +31,12 @@ stroke: currentColor; } +/* `.target-nope` objects are explicitly forbidden to join to */ +.node.target.target-nope, +.way.target.target-nope { + cursor: not-allowed; +} + /* `.active` objects (currently being drawn or dragged) are not interactive */ /* This is important to allow the events to drop through to whatever is */ /* below them on the map, so you can still hover and connect to other things. */ diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 4bce319ddd..d2421ffc31 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -58,7 +58,10 @@ export function behaviorDraw(context) { // When drawing, connect only to things classed as targets.. // (this excludes area fills and active drawing elements) var selection = d3_select(element); - return (selection.classed('target') && element.__data__) || {}; + if (selection.classed('target')) return {}; + + var d = selection.datum(); + return (d && d.id && context.hasEntity(d.id)) || {}; } diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index b9c4bfa5cc..1a496ee2d3 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -6,7 +6,6 @@ import { } from 'd3-selection'; import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; -import { osmEntity } from '../osm/index'; import { utilRebind } from '../util/rebind'; @@ -107,19 +106,20 @@ export function behaviorHover(context) { _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - if (_target instanceof osmEntity && _target.id !== _newId) { + var entity = _target && _target.id && context.hasEntity(_target.id); + if (entity && entity.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 var mode = context.mode() && context.mode().id; - if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && _target.type === 'node') { - _newId = _target.id; + if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && entity.type === 'node') { + _newId = entity.id; return; } - var selector = '.' + _target.id; + var selector = '.' + entity.id; - if (_target.type === 'relation') { - _target.members.forEach(function(member) { + if (entity.type === 'relation') { + entity.members.forEach(function(member) { selector += ', .' + member.id; }); } @@ -129,7 +129,7 @@ export function behaviorHover(context) { _selection.selectAll(selector) .classed(suppressed ? 'hover-suppressed' : 'hover', true); - dispatch.call('hover', this, !suppressed && _target.id); + dispatch.call('hover', this, !suppressed && entity.id); } else { dispatch.call('hover', this, null); diff --git a/modules/behavior/select.js b/modules/behavior/select.js index f3daf0bc20..a77044133a 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -17,10 +17,10 @@ import { osmEntity } from '../osm'; export function behaviorSelect(context) { - var lastMouse = null, - suppressMenu = true, - tolerance = 4, - p1 = null; + var lastMouse = null; + var suppressMenu = true; + var tolerance = 4; + var p1 = null; function point() { @@ -102,19 +102,21 @@ export function behaviorSelect(context) { .on('mouseup.select', null, true); if (!p1) return; - var p2 = point(), - dist = geoEuclideanDistance(p1, p2); + var p2 = point(); + var dist = geoEuclideanDistance(p1, p2); p1 = null; if (dist > tolerance) { return; } - var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node(), - isShowAlways = +context.storage('edit-menu-show-always') === 1, - datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__), - mode = context.mode(); + var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node(); + var isShowAlways = +context.storage('edit-menu-show-always') === 1; + var datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__); + var mode = context.mode(); + var entity = datum && datum.id && context.hasEntity(datum.id); + if (entity) datum = entity; if (datum && datum.type === 'midpoint') { datum = datum.parents[0]; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 956b78314a..2446ffbfa7 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -127,7 +127,8 @@ export function modeDragNode(context) { if (!event || event.altKey || !d3_select(event.target).classed('target')) { return {}; } else { - return event.target.__data__ || {}; + var d = event.target.__data__; + return (d && d.id && context.hasEntity(d.id)) || {}; } } diff --git a/modules/modes/select.js b/modules/modes/select.js index 941c417514..1c87a96ed7 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -246,7 +246,10 @@ export function modeSelect(context, selectedIDs) { function dblclick() { var target = d3_select(d3_event.target); + var datum = target.datum(); + var entity = datum && datum.id && context.hasEntity(datum.id); + if (entity) datum = entity; if (datum instanceof osmWay && target.classed('target')) { var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection); diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 4770bebd68..b9012f0d9b 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -4,6 +4,7 @@ import _flatten from 'lodash-es/flatten'; import _forOwn from 'lodash-es/forOwn'; import _map from 'lodash-es/map'; +import { geoPath as d3_geoPath } from 'd3-geo'; import { range as d3_range } from 'd3-array'; import { @@ -37,16 +38,145 @@ export function svgLines(projection, context) { function drawTargets(selection, graph, entities, filter) { - var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; - var getPath = svgPath(projection, graph); - var passive = entities.filter(function(d) { - return true; - // return context.activeIDs().indexOf(d.id) === -1; + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var getPath = svgPath(projection, graph).geojson; + // var getPath = d3_geoPath(projection); + + var activeID = context.activeID(); + + // Rather than drawing lines directly, we'll cut out pieces + // depending on which parts are active. + var data = { targets: [], nopes: [] }; + + // Touch targets control which other vertices we can drag a vertex onto. + // - the activeID - nope + // - next to the activeID - yes (vertices will be merged) + // - 2 away from the activeID - nope (would create a self intersecting segment) + // - all others on a closed way - nope (would create a self intersecting polygon) + // + // 0 = active vertex - no touch/connect + // 1 = passive vertex - yes touch/connect + // 2 = adjacent vertex - special rules + function passive(d) { + if (!activeID) return 1; + if (activeID === d.id) return 0; + + var parents = graph.parentWays(d); + var i, j; + + for (i = 0; i < parents.length; i++) { + var nodes = parents[i].nodes; + var isClosed = parents[i].isClosed(); + for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby + if (nodes[j] === d.id) { + var ix1 = j - 2; + var ix2 = j - 1; + var ix3 = j + 1; + var ix4 = j + 2; + + if (isClosed) { // wraparound if needed + var max = nodes.length - 1; + if (ix1 < 0) ix1 = max + ix1; + if (ix2 < 0) ix2 = max + ix2; + if (ix3 > max) ix3 = ix3 - max; + if (ix4 > max) ix4 = ix4 - max; + } + + if (nodes[ix1] === activeID) return 0; // prevent self intersect + else if (nodes[ix2] === activeID) return 2; // adjacent - ok! + else if (nodes[ix3] === activeID) return 2; // adjacent - ok! + else if (nodes[ix4] === activeID) return 0; // prevent self intersect + else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // prevent self intersect + } + } + } + + return 1; + } + + entities.forEach(function(way) { + var coordGroups = { passive: [], active: [] }; + var segment = []; + var startType = null; // 0 = active, 1 = passive, 2 = adjacent + var currType = null; + var node; + + for (var i = 0; i < way.nodes.length; i++) { + + if (way.nodes[i] === activeID) { // vertex is the activeID + segment = []; // draw no segment here + startType = null; + continue; + } + + node = graph.entity(way.nodes[i]); + currType = passive(node); + + if (startType === null) { + startType = currType; + } + + if (currType !== startType) { // line changes here - try to save a segment + + if (segment.length > 0) { // finish previous segment + segment.push(node.loc); + + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(segment); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(segment); + } else { + coordGroups.passive.push(segment); + } + } + + segment = []; + startType = currType; + } + + segment.push(node.loc); + } + + // complete whatever segment we ended on + if (segment.length > 1) { + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(segment); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(segment); + } else { + coordGroups.passive.push(segment); + } + } + + if (coordGroups.passive.length) { + data.targets.push({ + 'type': 'Feature', + 'id': way.id, + 'geometry': { + 'type': 'MultiLineString', + 'coordinates': coordGroups.passive + } + }); + } + + if (coordGroups.active.length) { + data.nopes.push({ + 'type': 'Feature', + 'id': way.id + '-nope', // break the ids on purpose + 'geometry': { + 'type': 'MultiLineString', + 'coordinates': coordGroups.active + } + }); + } }); - var targets = selection.selectAll('.line.target') + + // Places to hover and connect + var targets = selection.selectAll('.line.target-allowed') .filter(filter) - .data(passive, function key(d) { return d.id; }); + .data(data.targets, function key(d) { return d.id; }); // exit targets.exit() @@ -57,7 +187,24 @@ export function svgLines(projection, context) { .append('path') .merge(targets) .attr('d', getPath) - .attr('class', function(d) { return 'way line target ' + fillClass + d.id; }); + .attr('class', function(d) { return 'way line target target-allowed ' + targetClass + d.id; }); + + + // NOPE + var nopes = selection.selectAll('.line.target-nope') + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('path') + .merge(nopes) + .attr('d', getPath) + .attr('class', function(d) { return 'way line target target-nope ' + nopeClass + d.id; }); + } diff --git a/modules/svg/path.js b/modules/svg/path.js index d2e5229956..cf9f09b440 100644 --- a/modules/svg/path.js +++ b/modules/svg/path.js @@ -15,23 +15,27 @@ export function svgPath(projection, graph, isArea) { // When drawing areas, pad viewport by 65px in each direction to allow // for 60px area fill stroke (see ".fill-partial path.fill" css rule) - var cache = {}, - padding = isArea ? 65 : 5, - viewport = projection.clipExtent(), - paddedExtent = [ - [viewport[0][0] - padding, viewport[0][1] - padding], - [viewport[1][0] + padding, viewport[1][1] + padding] - ], - clip = d3_geoIdentity().clipExtent(paddedExtent).stream, - project = projection.stream, - path = d3_geoPath() - .projection({stream: function(output) { return project(clip(output)); }}); + var cache = {}; + var padding = isArea ? 65 : 5; + var viewport = projection.clipExtent(); + var paddedExtent = [ + [viewport[0][0] - padding, viewport[0][1] - padding], + [viewport[1][0] + padding, viewport[1][1] + padding] + ]; + var clip = d3_geoIdentity().clipExtent(paddedExtent).stream; + var project = projection.stream; + var path = d3_geoPath() + .projection({stream: function(output) { return project(clip(output)); }}); - return function(entity) { + var svgpath = function(entity) { if (entity.id in cache) { return cache[entity.id]; } else { return cache[entity.id] = path(entity.asGeoJSON(graph)); } }; + + svgpath.geojson = path; + + return svgpath; } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index e880363f7f..14b0ab3696 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -182,17 +182,75 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { - var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; - - // no targets for entities that are active, or adjacent to active. + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var activeID = context.activeID(); + var data = { targets: [], nopes: [] }; + + // Touch targets control which other vertices we can drag a vertex onto. + // - the activeID - nope + // - next to the activeID - yes (vertices will be merged) + // - 2 away from the activeID - nope (would create a self intersecting segment) + // - all others on a closed way - nope (would create a self intersecting polygon) + // + // 0 = active vertex - no touch/connect + // 1 = passive vertex - yes touch/connect + // 2 = adjacent vertex - special rules function passive(d) { - return d.id !== context.activeID(); + if (!activeID) return 1; + if (activeID === d.id) return 0; + + var parents = graph.parentWays(d); + var i, j; + + for (i = 0; i < parents.length; i++) { + var nodes = parents[i].nodes; + var isClosed = parents[i].isClosed(); + for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby + if (nodes[j] === d.id) { + var ix1 = j - 2; + var ix2 = j - 1; + var ix3 = j + 1; + var ix4 = j + 2; + + if (isClosed) { // wraparound if needed + var max = nodes.length - 1; + if (ix1 < 0) ix1 = max + ix1; + if (ix2 < 0) ix2 = max + ix2; + if (ix3 > max) ix3 = ix3 - max; + if (ix4 > max) ix4 = ix4 - max; + } + + if (nodes[ix1] === activeID) return 0; // prevent self intersect + else if (nodes[ix2] === activeID) return 2; // adjacent - ok! + else if (nodes[ix3] === activeID) return 2; // adjacent - ok! + else if (nodes[ix4] === activeID) return 0; // prevent self intersect + else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // prevent self intersect + } + } + } + + return 1; } - var data = entities.filter(passive); - var targets = selection.selectAll('.vertex.target') + + entities.forEach(function(node) { + if (activeID === node.id) return; // draw no vertex on the activeID + + var currType = passive(node); + if (currType !== 0) { + data.targets.push(node); // passive or adjacent - allow to connect + } else { + data.nopes.push({ + id: node.id + '-nope', // not a real osmNode, break the id on purpose + loc: node.loc + }); + } + }); + + var targets = selection.selectAll('.vertex.target-allowed') .filter(filter) - .data(data, function key(d) { return d.id; }); + .data(data.targets, function key(d) { return d.id; }); // exit targets.exit() @@ -201,9 +259,26 @@ export function svgVertices(projection, context) { // enter/update targets.enter() .append('circle') - .attr('r', function(d) { return _radii[d.id] || radiuses.shadow[3]; }) + .attr('r', function(d) { return (_radii[d.id] || radiuses.shadow[3]); }) .merge(targets) - .attr('class', function(d) { return 'node vertex target ' + fillClass + d.id; }) + .attr('class', function(d) { return 'node vertex target target-allowed ' + targetClass + d.id; }) + .attr('transform', svgPointTransform(projection)); + + + // NOPE + var nopes = selection.selectAll('.vertex.target-nope') + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('circle') + .attr('r', function(d) { return (_radii[d.id.replace('-nope','')] || radiuses.shadow[3]); }) + .merge(nopes) + .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; }) .attr('transform', svgPointTransform(projection)); } @@ -321,8 +396,11 @@ export function svgVertices(projection, context) { .call(draw, graph, currentVisible(all), sets, filterRendered); // Draw touch targets.. + var filterTargets = function(d) { + return isMoving ? true : filterRendered(d); + }; selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, currentVisible(all), filterRendered); + .call(drawTargets, graph, currentVisible(all), filterTargets); function currentVisible(which) { From 6d7659b3bbffe506a54a668c20e5b099fe86adc9 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 21 Dec 2017 10:36:02 -0500 Subject: [PATCH 061/206] Refactor common helper code to svg/helpers.js, add area nopes --- modules/svg/areas.js | 43 ++++- modules/svg/debug.js | 12 +- modules/svg/helpers.js | 256 ++++++++++++++++++++++++++++ modules/svg/index.js | 10 +- modules/svg/lines.js | 135 +-------------- modules/svg/mapillary_images.js | 35 ++-- modules/svg/mapillary_signs.js | 8 +- modules/svg/one_way_segments.js | 67 -------- modules/svg/openstreetcam_images.js | 35 ++-- modules/svg/path.js | 41 ----- modules/svg/point_transform.js | 7 - modules/svg/relation_member_tags.js | 15 -- modules/svg/vertices.js | 59 +------ 13 files changed, 339 insertions(+), 384 deletions(-) create mode 100644 modules/svg/helpers.js delete mode 100644 modules/svg/one_way_segments.js delete mode 100644 modules/svg/path.js delete mode 100644 modules/svg/point_transform.js delete mode 100644 modules/svg/relation_member_tags.js diff --git a/modules/svg/areas.js b/modules/svg/areas.js index c6ff49cbb1..3fb7c37235 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -4,7 +4,7 @@ import _values from 'lodash-es/values'; import { bisector as d3_bisector } from 'd3-array'; import { osmEntity, osmIsSimpleMultipolygonOuterMember } from '../osm'; -import { svgPath, svgTagClasses } from './index'; +import { svgPath, svgSegmentWay, svgTagClasses } from './index'; export function svgAreas(projection, context) { @@ -42,16 +42,25 @@ export function svgAreas(projection, context) { function drawTargets(selection, graph, entities, filter) { - var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; - var getPath = svgPath(projection, graph); - var passive = entities.filter(function(d) { - return true; - // return context.activeIDs().indexOf(d.id) === -1; + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var getPath = svgPath(projection).geojson; + var activeID = context.activeID(); + + // The targets and nopes will be MultiLineString sub-segments of the ways + var data = { targets: [], nopes: [] }; + + entities.forEach(function(way) { + var features = svgSegmentWay(way, graph, activeID); + data.targets.push.apply(data.targets, features.passive); + data.nopes.push.apply(data.nopes, features.active); }); - var targets = selection.selectAll('.area.target') + + // Targets allow hover and vertex snapping + var targets = selection.selectAll('.area.target-allowed') .filter(filter) - .data(passive, function key(d) { return d.id; }); + .data(data.targets, function key(d) { return d.id; }); // exit targets.exit() @@ -62,7 +71,23 @@ export function svgAreas(projection, context) { .append('path') .merge(targets) .attr('d', getPath) - .attr('class', function(d) { return 'way area target ' + fillClass + d.id; }); + .attr('class', function(d) { return 'way area target target-allowed ' + targetClass + d.id; }); + + + // NOPE + var nopes = selection.selectAll('.area.target-nope') + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('path') + .merge(nopes) + .attr('d', getPath) + .attr('class', function(d) { return 'way area target target-nope ' + nopeClass + d.id; }); } diff --git a/modules/svg/debug.js b/modules/svg/debug.js index 8ab5617d42..71f0074173 100644 --- a/modules/svg/debug.js +++ b/modules/svg/debug.js @@ -1,12 +1,8 @@ -import { geoPath as d3_geoPath } from 'd3-geo'; import { select as d3_select } from 'd3-selection'; import { geoPolygonIntersectsPolygon } from '../geo'; -import { - data, - dataImperial, - dataDriveLeft -} from '../../data'; +import { data, dataImperial, dataDriveLeft } from '../../data'; +import { svgPath } from './index'; export function svgDebug(projection, context) { @@ -27,8 +23,6 @@ export function svgDebug(projection, context) { var showsImperial = context.getDebug('imperial'); var showsDriveLeft = context.getDebug('driveLeft'); var showsTouchTargets = context.getDebug('target'); - var path = d3_geoPath(projection); - var debugData = []; if (showsTile) { @@ -134,7 +128,7 @@ export function svgDebug(projection, context) { // update layer.selectAll('path') - .attr('d', path); + .attr('d', svgPath(projection).geojson); } diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js new file mode 100644 index 0000000000..88f04f578e --- /dev/null +++ b/modules/svg/helpers.js @@ -0,0 +1,256 @@ +import _extend from 'lodash-es/extend'; + +import { + geoIdentity as d3_geoIdentity, + geoPath as d3_geoPath, + geoStream as d3_geoStream +} from 'd3-geo'; + +import { geoEuclideanDistance } from '../geo'; + + +// Touch targets control which other vertices we can drag a vertex onto. +// +// - the activeID - nope +// - 1 away (adjacent) to the activeID - yes (vertices will be merged) +// - 2 away from the activeID - nope (would create a self intersecting segment) +// - all others on a linear way - yes +// - all others on a closed way - nope (would create a self intersecting polygon) +// +// returns +// 0 = active vertex - no touch/connect +// 1 = passive vertex - yes touch/connect +// 2 = adjacent vertex - yes but pay attention segmenting a line here +// +export function svgPassiveVertex(node, graph, activeID) { + if (!activeID) return 1; + if (activeID === node.id) return 0; + + var parents = graph.parentWays(node); + + for (var i = 0; i < parents.length; i++) { + var nodes = parents[i].nodes; + var isClosed = parents[i].isClosed(); + for (var j = 0; j < nodes.length; j++) { // find this vertex, look nearby + if (nodes[j] === node.id) { + var ix1 = j - 2; + var ix2 = j - 1; + var ix3 = j + 1; + var ix4 = j + 2; + + if (isClosed) { // wraparound if needed + var max = nodes.length - 1; + if (ix1 < 0) ix1 = max + ix1; + if (ix2 < 0) ix2 = max + ix2; + if (ix3 > max) ix3 = ix3 - max; + if (ix4 > max) ix4 = ix4 - max; + } + + if (nodes[ix1] === activeID) return 0; // no - prevent self intersect + else if (nodes[ix2] === activeID) return 2; // ok - adjacent + else if (nodes[ix3] === activeID) return 2; // ok - adjacent + else if (nodes[ix4] === activeID) return 0; // no - prevent self intersect + else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // no - prevent self intersect + } + } + } + + return 1; // ok +} + + +export function svgOneWaySegments(projection, graph, dt) { + return function(entity) { + var i = 0; + var offset = dt; + var segments = []; + var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; + var coordinates = graph.childNodes(entity).map(function(n) { return n.loc; }); + var a, b; + + if (entity.tags.oneway === '-1') { + coordinates.reverse(); + } + + d3_geoStream({ + type: 'LineString', + coordinates: coordinates + }, projection.stream(clip({ + lineStart: function() {}, + lineEnd: function() { a = null; }, + point: function(x, y) { + b = [x, y]; + + if (a) { + var span = geoEuclideanDistance(a, b) - offset; + + if (span >= 0) { + var angle = Math.atan2(b[1] - a[1], b[0] - a[0]); + var dx = dt * Math.cos(angle); + var dy = dt * Math.sin(angle); + var p = [ + a[0] + offset * Math.cos(angle), + a[1] + offset * Math.sin(angle) + ]; + var segment = 'M' + a[0] + ',' + a[1] + 'L' + p[0] + ',' + p[1]; + + for (span -= dt; span >= 0; span -= dt) { + p[0] += dx; + p[1] += dy; + segment += 'L' + p[0] + ',' + p[1]; + } + + segment += 'L' + b[0] + ',' + b[1]; + segments.push({id: entity.id, index: i, d: segment}); + } + + offset = -span; + i++; + } + + a = b; + } + }))); + + return segments; + }; +} + + +export function svgPath(projection, graph, isArea) { + + // Explanation of magic numbers: + // "padding" here allows space for strokes to extend beyond the viewport, + // so that the stroke isn't drawn along the edge of the viewport when + // the shape is clipped. + // + // When drawing lines, pad viewport by 5px. + // When drawing areas, pad viewport by 65px in each direction to allow + // for 60px area fill stroke (see ".fill-partial path.fill" css rule) + + var cache = {}; + var padding = isArea ? 65 : 5; + var viewport = projection.clipExtent(); + var paddedExtent = [ + [viewport[0][0] - padding, viewport[0][1] - padding], + [viewport[1][0] + padding, viewport[1][1] + padding] + ]; + var clip = d3_geoIdentity().clipExtent(paddedExtent).stream; + var project = projection.stream; + var path = d3_geoPath() + .projection({stream: function(output) { return project(clip(output)); }}); + + var svgpath = function(entity) { + if (entity.id in cache) { + return cache[entity.id]; + } else { + return cache[entity.id] = path(entity.asGeoJSON(graph)); + } + }; + + svgpath.geojson = path; + + return svgpath; +} + + +export function svgPointTransform(projection) { + return function(entity) { + // http://jsperf.com/short-array-join + var pt = projection(entity.loc); + return 'translate(' + pt[0] + ',' + pt[1] + ')'; + }; +} + + +export function svgRelationMemberTags(graph) { + return function(entity) { + var tags = entity.tags; + graph.parentRelations(entity).forEach(function(relation) { + var type = relation.tags.type; + if (type === 'multipolygon' || type === 'boundary') { + tags = _extend({}, relation.tags, tags); + } + }); + return tags; + }; +} + + +export function svgSegmentWay(way, graph, activeID) { + var features = { passive: [], active: [] }; + var coordGroups = { passive: [], active: [] }; + var segment = []; + var startType = null; // 0 = active, 1 = passive, 2 = adjacent + var currType = null; + var node; + + for (var i = 0; i < way.nodes.length; i++) { + if (way.nodes[i] === activeID) { // vertex is the activeID + segment = []; // draw no segment here + startType = null; + continue; + } + + node = graph.entity(way.nodes[i]); + currType = svgPassiveVertex(node, graph, activeID); + + if (startType === null) { + startType = currType; + } + + if (currType !== startType) { // line changes here - try to save a segment + + if (segment.length > 0) { // finish previous segment + segment.push(node.loc); + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(segment); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(segment); + } else { + coordGroups.passive.push(segment); + } + } + + segment = []; + startType = currType; + } + + segment.push(node.loc); + } + + // complete whatever segment we ended on + if (segment.length > 1) { + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(segment); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(segment); + } else { + coordGroups.passive.push(segment); + } + } + + if (coordGroups.passive.length) { + features.passive.push({ + 'type': 'Feature', + 'id': way.id, + 'geometry': { + 'type': 'MultiLineString', + 'coordinates': coordGroups.passive + } + }); + } + + if (coordGroups.active.length) { + features.active.push({ + 'type': 'Feature', + 'id': way.id + '-nope', // break the ids on purpose + 'geometry': { + 'type': 'MultiLineString', + 'coordinates': coordGroups.active + } + }); + } + + return features; +} diff --git a/modules/svg/index.js b/modules/svg/index.js index 8f54f5a5e8..68e2f2f289 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -9,13 +9,15 @@ export { svgLines } from './lines.js'; export { svgMapillaryImages } from './mapillary_images.js'; export { svgMapillarySigns } from './mapillary_signs.js'; export { svgMidpoints } from './midpoints.js'; -export { svgOneWaySegments } from './one_way_segments.js'; +export { svgOneWaySegments } from './helpers.js'; export { svgOpenstreetcamImages } from './openstreetcam_images.js'; export { svgOsm } from './osm.js'; -export { svgPath } from './path.js'; -export { svgPointTransform } from './point_transform.js'; +export { svgPassiveVertex } from './helpers.js'; +export { svgPath } from './helpers.js'; +export { svgPointTransform } from './helpers.js'; export { svgPoints } from './points.js'; -export { svgRelationMemberTags } from './relation_member_tags.js'; +export { svgRelationMemberTags } from './helpers.js'; +export { svgSegmentWay } from './helpers.js'; export { svgTagClasses } from './tag_classes.js'; export { svgTurns } from './turns.js'; export { svgVertices } from './vertices.js'; diff --git a/modules/svg/lines.js b/modules/svg/lines.js index b9012f0d9b..fbecbed7be 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -4,13 +4,13 @@ import _flatten from 'lodash-es/flatten'; import _forOwn from 'lodash-es/forOwn'; import _map from 'lodash-es/map'; -import { geoPath as d3_geoPath } from 'd3-geo'; import { range as d3_range } from 'd3-array'; import { svgOneWaySegments, svgPath, svgRelationMemberTags, + svgSegmentWay, svgTagClasses } from './index'; @@ -40,140 +40,20 @@ export function svgLines(projection, context) { function drawTargets(selection, graph, entities, filter) { var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; - var getPath = svgPath(projection, graph).geojson; - // var getPath = d3_geoPath(projection); - + var getPath = svgPath(projection).geojson; var activeID = context.activeID(); - // Rather than drawing lines directly, we'll cut out pieces - // depending on which parts are active. + // The targets and nopes will be MultiLineString sub-segments of the ways var data = { targets: [], nopes: [] }; - // Touch targets control which other vertices we can drag a vertex onto. - // - the activeID - nope - // - next to the activeID - yes (vertices will be merged) - // - 2 away from the activeID - nope (would create a self intersecting segment) - // - all others on a closed way - nope (would create a self intersecting polygon) - // - // 0 = active vertex - no touch/connect - // 1 = passive vertex - yes touch/connect - // 2 = adjacent vertex - special rules - function passive(d) { - if (!activeID) return 1; - if (activeID === d.id) return 0; - - var parents = graph.parentWays(d); - var i, j; - - for (i = 0; i < parents.length; i++) { - var nodes = parents[i].nodes; - var isClosed = parents[i].isClosed(); - for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby - if (nodes[j] === d.id) { - var ix1 = j - 2; - var ix2 = j - 1; - var ix3 = j + 1; - var ix4 = j + 2; - - if (isClosed) { // wraparound if needed - var max = nodes.length - 1; - if (ix1 < 0) ix1 = max + ix1; - if (ix2 < 0) ix2 = max + ix2; - if (ix3 > max) ix3 = ix3 - max; - if (ix4 > max) ix4 = ix4 - max; - } - - if (nodes[ix1] === activeID) return 0; // prevent self intersect - else if (nodes[ix2] === activeID) return 2; // adjacent - ok! - else if (nodes[ix3] === activeID) return 2; // adjacent - ok! - else if (nodes[ix4] === activeID) return 0; // prevent self intersect - else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // prevent self intersect - } - } - } - - return 1; - } - entities.forEach(function(way) { - var coordGroups = { passive: [], active: [] }; - var segment = []; - var startType = null; // 0 = active, 1 = passive, 2 = adjacent - var currType = null; - var node; - - for (var i = 0; i < way.nodes.length; i++) { - - if (way.nodes[i] === activeID) { // vertex is the activeID - segment = []; // draw no segment here - startType = null; - continue; - } - - node = graph.entity(way.nodes[i]); - currType = passive(node); - - if (startType === null) { - startType = currType; - } - - if (currType !== startType) { // line changes here - try to save a segment - - if (segment.length > 0) { // finish previous segment - segment.push(node.loc); - - if (startType === 2 || currType === 2) { // one adjacent vertex - coordGroups.active.push(segment); - } else if (startType === 0 && currType === 0) { // both active vertices - coordGroups.active.push(segment); - } else { - coordGroups.passive.push(segment); - } - } - - segment = []; - startType = currType; - } - - segment.push(node.loc); - } - - // complete whatever segment we ended on - if (segment.length > 1) { - if (startType === 2 || currType === 2) { // one adjacent vertex - coordGroups.active.push(segment); - } else if (startType === 0 && currType === 0) { // both active vertices - coordGroups.active.push(segment); - } else { - coordGroups.passive.push(segment); - } - } - - if (coordGroups.passive.length) { - data.targets.push({ - 'type': 'Feature', - 'id': way.id, - 'geometry': { - 'type': 'MultiLineString', - 'coordinates': coordGroups.passive - } - }); - } - - if (coordGroups.active.length) { - data.nopes.push({ - 'type': 'Feature', - 'id': way.id + '-nope', // break the ids on purpose - 'geometry': { - 'type': 'MultiLineString', - 'coordinates': coordGroups.active - } - }); - } + var features = svgSegmentWay(way, graph, activeID); + data.targets.push.apply(data.targets, features.passive); + data.nopes.push.apply(data.nopes, features.active); }); - // Places to hover and connect + // Targets allow hover and vertex snapping var targets = selection.selectAll('.line.target-allowed') .filter(filter) .data(data.targets, function key(d) { return d.id; }); @@ -204,7 +84,6 @@ export function svgLines(projection, context) { .merge(nopes) .attr('d', getPath) .attr('class', function(d) { return 'way line target target-nope ' + nopeClass + d.id; }); - } diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 1bae3436ca..fb8a4b4b84 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -1,23 +1,16 @@ import _throttle from 'lodash-es/throttle'; - -import { - geoIdentity as d3_geoIdentity, - geoPath as d3_geoPath -} from 'd3-geo'; - import { select as d3_select } from 'd3-selection'; - -import { svgPointTransform } from './point_transform'; +import { svgPath, svgPointTransform } from './index'; import { services } from '../services'; export function svgMapillaryImages(projection, context, dispatch) { - var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), - minZoom = 12, - minMarkerZoom = 16, - minViewfieldZoom = 18, - layer = d3_select(null), - _mapillary; + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var minMarkerZoom = 16; + var minViewfieldZoom = 18; + var layer = d3_select(null); + var _mapillary; function init() { @@ -128,25 +121,19 @@ export function svgMapillaryImages(projection, context, dispatch) { var sequences = (service ? service.sequences(projection) : []); var images = (service && showMarkers ? service.images(projection) : []); - var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; - var project = projection.stream; - var makePath = d3_geoPath().projection({ stream: function(output) { - return project(clip(output)); - }}); - var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); + // exit traces.exit() .remove(); + // enter/update traces = traces.enter() .append('path') .attr('class', 'sequence') - .merge(traces); - - traces - .attr('d', makePath); + .merge(traces) + .attr('d', svgPath(projection).geojson); var groups = layer.selectAll('.markers').selectAll('.viewfield-group') diff --git a/modules/svg/mapillary_signs.js b/modules/svg/mapillary_signs.js index a327c108d0..4b6095cd29 100644 --- a/modules/svg/mapillary_signs.js +++ b/modules/svg/mapillary_signs.js @@ -5,10 +5,10 @@ import { services } from '../services'; export function svgMapillarySigns(projection, context, dispatch) { - var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), - minZoom = 12, - layer = d3_select(null), - _mapillary; + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var layer = d3_select(null); + var _mapillary; function init() { diff --git a/modules/svg/one_way_segments.js b/modules/svg/one_way_segments.js deleted file mode 100644 index 66759d66d1..0000000000 --- a/modules/svg/one_way_segments.js +++ /dev/null @@ -1,67 +0,0 @@ -import { - geoIdentity as d3_geoIdentity, - geoStream as d3_geoStream -} from 'd3-geo'; - -import { geoEuclideanDistance } from '../geo'; - - -export function svgOneWaySegments(projection, graph, dt) { - return function(entity) { - var a, - b, - i = 0, - offset = dt, - segments = [], - clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream, - coordinates = graph.childNodes(entity).map(function(n) { - return n.loc; - }); - - if (entity.tags.oneway === '-1') coordinates.reverse(); - - d3_geoStream({ - type: 'LineString', - coordinates: coordinates - }, projection.stream(clip({ - lineStart: function() {}, - lineEnd: function() { - a = null; - }, - point: function(x, y) { - b = [x, y]; - - if (a) { - var span = geoEuclideanDistance(a, b) - offset; - - if (span >= 0) { - var angle = Math.atan2(b[1] - a[1], b[0] - a[0]), - dx = dt * Math.cos(angle), - dy = dt * Math.sin(angle), - p = [a[0] + offset * Math.cos(angle), - a[1] + offset * Math.sin(angle)]; - - var segment = 'M' + a[0] + ',' + a[1] + - 'L' + p[0] + ',' + p[1]; - - for (span -= dt; span >= 0; span -= dt) { - p[0] += dx; - p[1] += dy; - segment += 'L' + p[0] + ',' + p[1]; - } - - segment += 'L' + b[0] + ',' + b[1]; - segments.push({id: entity.id, index: i, d: segment}); - } - - offset = -span; - i++; - } - - a = b; - } - }))); - - return segments; - }; -} diff --git a/modules/svg/openstreetcam_images.js b/modules/svg/openstreetcam_images.js index 3184a7bc2b..35fc76cc0b 100644 --- a/modules/svg/openstreetcam_images.js +++ b/modules/svg/openstreetcam_images.js @@ -1,23 +1,16 @@ import _throttle from 'lodash-es/throttle'; - -import { - geoIdentity as d3_geoIdentity, - geoPath as d3_geoPath -} from 'd3-geo'; - import { select as d3_select } from 'd3-selection'; - -import { svgPointTransform } from './point_transform'; +import { svgPath, svgPointTransform } from './index'; import { services } from '../services'; export function svgOpenstreetcamImages(projection, context, dispatch) { - var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000), - minZoom = 12, - minMarkerZoom = 16, - minViewfieldZoom = 18, - layer = d3_select(null), - _openstreetcam; + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var minMarkerZoom = 16; + var minViewfieldZoom = 18; + var layer = d3_select(null); + var _openstreetcam; function init() { @@ -128,25 +121,19 @@ export function svgOpenstreetcamImages(projection, context, dispatch) { var sequences = (service ? service.sequences(projection) : []); var images = (service && showMarkers ? service.images(projection) : []); - var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; - var project = projection.stream; - var makePath = d3_geoPath().projection({ stream: function(output) { - return project(clip(output)); - }}); - var traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.key; }); + // exit traces.exit() .remove(); + // enter/update traces = traces.enter() .append('path') .attr('class', 'sequence') - .merge(traces); - - traces - .attr('d', makePath); + .merge(traces) + .attr('d', svgPath(projection).geojson); var groups = layer.selectAll('.markers').selectAll('.viewfield-group') diff --git a/modules/svg/path.js b/modules/svg/path.js deleted file mode 100644 index cf9f09b440..0000000000 --- a/modules/svg/path.js +++ /dev/null @@ -1,41 +0,0 @@ -import { - geoIdentity as d3_geoIdentity, - geoPath as d3_geoPath -} from 'd3-geo'; - - -export function svgPath(projection, graph, isArea) { - - // Explanation of magic numbers: - // "padding" here allows space for strokes to extend beyond the viewport, - // so that the stroke isn't drawn along the edge of the viewport when - // the shape is clipped. - // - // When drawing lines, pad viewport by 5px. - // When drawing areas, pad viewport by 65px in each direction to allow - // for 60px area fill stroke (see ".fill-partial path.fill" css rule) - - var cache = {}; - var padding = isArea ? 65 : 5; - var viewport = projection.clipExtent(); - var paddedExtent = [ - [viewport[0][0] - padding, viewport[0][1] - padding], - [viewport[1][0] + padding, viewport[1][1] + padding] - ]; - var clip = d3_geoIdentity().clipExtent(paddedExtent).stream; - var project = projection.stream; - var path = d3_geoPath() - .projection({stream: function(output) { return project(clip(output)); }}); - - var svgpath = function(entity) { - if (entity.id in cache) { - return cache[entity.id]; - } else { - return cache[entity.id] = path(entity.asGeoJSON(graph)); - } - }; - - svgpath.geojson = path; - - return svgpath; -} diff --git a/modules/svg/point_transform.js b/modules/svg/point_transform.js deleted file mode 100644 index cf80f845dd..0000000000 --- a/modules/svg/point_transform.js +++ /dev/null @@ -1,7 +0,0 @@ -export function svgPointTransform(projection) { - return function(entity) { - // http://jsperf.com/short-array-join - var pt = projection(entity.loc); - return 'translate(' + pt[0] + ',' + pt[1] + ')'; - }; -} diff --git a/modules/svg/relation_member_tags.js b/modules/svg/relation_member_tags.js deleted file mode 100644 index 824d56fe0b..0000000000 --- a/modules/svg/relation_member_tags.js +++ /dev/null @@ -1,15 +0,0 @@ -import _extend from 'lodash-es/extend'; - - -export function svgRelationMemberTags(graph) { - return function(entity) { - var tags = entity.tags; - graph.parentRelations(entity).forEach(function(relation) { - var type = relation.tags.type; - if (type === 'multipolygon' || type === 'boundary') { - tags = _extend({}, relation.tags, tags); - } - }); - return tags; - }; -} diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 14b0ab3696..77aad0a1d6 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -6,7 +6,7 @@ import { select as d3_select } from 'd3-selection'; import { dataFeatureIcons } from '../../data'; import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; -import { svgPointTransform } from './index'; +import { svgPassiveVertex, svgPointTransform } from './index'; export function svgVertices(projection, context) { @@ -187,67 +187,22 @@ export function svgVertices(projection, context) { var activeID = context.activeID(); var data = { targets: [], nopes: [] }; - // Touch targets control which other vertices we can drag a vertex onto. - // - the activeID - nope - // - next to the activeID - yes (vertices will be merged) - // - 2 away from the activeID - nope (would create a self intersecting segment) - // - all others on a closed way - nope (would create a self intersecting polygon) - // - // 0 = active vertex - no touch/connect - // 1 = passive vertex - yes touch/connect - // 2 = adjacent vertex - special rules - function passive(d) { - if (!activeID) return 1; - if (activeID === d.id) return 0; - - var parents = graph.parentWays(d); - var i, j; - - for (i = 0; i < parents.length; i++) { - var nodes = parents[i].nodes; - var isClosed = parents[i].isClosed(); - for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby - if (nodes[j] === d.id) { - var ix1 = j - 2; - var ix2 = j - 1; - var ix3 = j + 1; - var ix4 = j + 2; - - if (isClosed) { // wraparound if needed - var max = nodes.length - 1; - if (ix1 < 0) ix1 = max + ix1; - if (ix2 < 0) ix2 = max + ix2; - if (ix3 > max) ix3 = ix3 - max; - if (ix4 > max) ix4 = ix4 - max; - } - - if (nodes[ix1] === activeID) return 0; // prevent self intersect - else if (nodes[ix2] === activeID) return 2; // adjacent - ok! - else if (nodes[ix3] === activeID) return 2; // adjacent - ok! - else if (nodes[ix4] === activeID) return 0; // prevent self intersect - else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // prevent self intersect - } - } - } - - return 1; - } - - entities.forEach(function(node) { - if (activeID === node.id) return; // draw no vertex on the activeID + if (activeID === node.id) return; // draw no target on the activeID - var currType = passive(node); + var currType = svgPassiveVertex(node, graph, activeID); if (currType !== 0) { - data.targets.push(node); // passive or adjacent - allow to connect + data.targets.push(node); // passive or adjacent - allow to connect } else { data.nopes.push({ - id: node.id + '-nope', // not a real osmNode, break the id on purpose + id: node.id + '-nope', // not a real osmNode, break the id on purpose loc: node.loc }); } }); + + // Targets allow hover and vertex snapping var targets = selection.selectAll('.vertex.target-allowed') .filter(filter) .data(data.targets, function key(d) { return d.id; }); From fcacdb440049a23bef2efb956d181e2370da146f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 21 Dec 2017 11:18:06 -0500 Subject: [PATCH 062/206] Fix hover test (requires context.hasEntity stub) --- test/spec/behavior/hover.js | 105 +++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/test/spec/behavior/hover.js b/test/spec/behavior/hover.js index 8ad0cf3d8e..4dbc5ac9a1 100644 --- a/test/spec/behavior/hover.js +++ b/test/spec/behavior/hover.js @@ -1,108 +1,117 @@ describe('iD.behaviorHover', function() { - var container, context; + var _container; + var _context; + var _graph; beforeEach(function() { - container = d3.select('body').append('div'); - context = { + _container = d3.select('body').append('div'); + _context = { hover: function() {}, - mode: function() { return { id: 'browse' }; } + mode: function() { return { id: 'browse' }; }, + hasEntity: function(d) { return _graph && _graph.hasEntity(d); } }; }); afterEach(function() { - container.remove(); + _container.remove(); + _graph = null; }); describe('#off', function () { it('removes the .hover class from all elements', function () { - container.append('span').attr('class', 'hover'); - container.call(iD.behaviorHover(context).off); - expect(container.select('span').classed('hover')).to.be.false; + _container.append('span').attr('class', 'hover'); + _container.call(iD.behaviorHover(_context).off); + expect(_container.select('span').classed('hover')).to.be.false; }); it('removes the .hover-disabled class from the surface element', function () { - container.attr('class', 'hover-disabled'); - container.call(iD.behaviorHover(context).off); - expect(container.classed('hover-disabled')).to.be.false; + _container.attr('class', 'hover-disabled'); + _container.call(iD.behaviorHover(_context).off); + expect(_container.classed('hover-disabled')).to.be.false; }); }); describe('mouseover', function () { it('adds the .hover class to all elements to which the same datum is bound', function () { - var a = iD.Node({id: 'a'}), - b = iD.Node({id: 'b'}); + var a = iD.osmNode({id: 'a'}); + var b = iD.osmNode({id: 'b'}); + _graph = iD.coreGraph([a, b]); - container.selectAll('span') + _container.selectAll('span') .data([a, b, a, b]) .enter().append('span').attr('class', function(d) { return d.id; }); - container.call(iD.behaviorHover(context)); - iD.utilTriggerEvent(container.selectAll('.a'), 'mouseover'); + _container.call(iD.behaviorHover(_context)); + iD.utilTriggerEvent(_container.selectAll('.a'), 'mouseover'); - expect(container.selectAll('.a.hover').nodes()).to.have.length(2); - expect(container.selectAll('.b.hover').nodes()).to.have.length(0); + expect(_container.selectAll('.a.hover').nodes()).to.have.length(2); + expect(_container.selectAll('.b.hover').nodes()).to.have.length(0); }); it('adds the .hover class to all members of a relation', function() { - container.selectAll('span') - .data([iD.Relation({id: 'a', members: [{id: 'b'}]}), iD.Node({id: 'b'})]) + var a = iD.osmRelation({id: 'a', members: [{id: 'b'}]}); + var b = iD.osmNode({id: 'b'}); + _graph = iD.coreGraph([a, b]); + + _container.selectAll('span') + .data([a, b]) .enter().append('span').attr('class', function(d) { return d.id; }); - container.call(iD.behaviorHover(context)); - iD.utilTriggerEvent(container.selectAll('.a'), 'mouseover'); + _container.call(iD.behaviorHover(_context)); + iD.utilTriggerEvent(_container.selectAll('.a'), 'mouseover'); - expect(container.selectAll('.a.hover').nodes()).to.have.length(1); - expect(container.selectAll('.b.hover').nodes()).to.have.length(1); + expect(_container.selectAll('.a.hover').nodes()).to.have.length(1); + expect(_container.selectAll('.b.hover').nodes()).to.have.length(1); }); }); describe('mouseout', function () { it('removes the .hover class from all elements', function () { - container.append('span').attr('class', 'hover'); + _container.append('span').attr('class', 'hover'); - container.call(iD.behaviorHover(context)); - iD.utilTriggerEvent(container.selectAll('.hover'), 'mouseout'); + _container.call(iD.behaviorHover(_context)); + iD.utilTriggerEvent(_container.selectAll('.hover'), 'mouseout'); - expect(container.selectAll('.hover').nodes()).to.have.length(0); + expect(_container.selectAll('.hover').nodes()).to.have.length(0); }); }); describe('alt keydown', function () { it('replaces the .hover class with .hover-suppressed', function () { - container.append('span').attr('class', 'hover'); - container.call(iD.behaviorHover(context).altDisables(true)); + _container.append('span').attr('class', 'hover'); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - expect(container.selectAll('.hover').nodes()).to.have.length(0); - expect(container.selectAll('.hover-suppressed').nodes()).to.have.length(1); - happen.keyup(window, {keyCode: 18}); + happen.keydown(window, { keyCode: 18 }); + expect(_container.selectAll('.hover').nodes()).to.have.length(0); + expect(_container.selectAll('.hover-suppressed').nodes()).to.have.length(1); + happen.keyup(window, { keyCode: 18 }); }); it('adds the .hover-disabled class to the surface', function () { - container.call(iD.behaviorHover(context).altDisables(true)); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - expect(container.classed('hover-disabled')).to.be.true; - happen.keyup(window, {keyCode: 18}); + happen.keydown(window, { keyCode: 18 }); + expect(_container.classed('hover-disabled')).to.be.true; + happen.keyup(window, { keyCode: 18 }); }); }); describe('alt keyup', function () { it('replaces the .hover-suppressed class with .hover', function () { - container.append('span').attr('class', 'hover-suppressed'); - container.call(iD.behaviorHover(context).altDisables(true)); + _container.append('span').attr('class', 'hover-suppressed'); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - happen.keyup(window, {keyCode: 18}); - expect(container.selectAll('.hover').nodes()).to.have.length(1); - expect(container.selectAll('.hover-suppressed').nodes()).to.have.length(0); + happen.keydown(window, { keyCode: 18 }); + happen.keyup(window, { keyCode: 18 }); + expect(_container.selectAll('.hover').nodes()).to.have.length(1); + expect(_container.selectAll('.hover-suppressed').nodes()).to.have.length(0); }); it('removes the .hover-disabled class from the surface', function () { - container.call(iD.behaviorHover(context).altDisables(true)); + _container.call(iD.behaviorHover(_context).altDisables(true)); - happen.keydown(window, {keyCode: 18}); - happen.keyup(window, {keyCode: 18}); - expect(container.classed('hover-disabled')).to.be.false; + happen.keydown(window, { keyCode: 18 }); + happen.keyup(window, { keyCode: 18 }); + expect(_container.classed('hover-disabled')).to.be.false; }); }); }); From d82d5dc3d04b5f407118444a22afc38437291add Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 21 Dec 2017 14:54:15 -0500 Subject: [PATCH 063/206] Add skipID to geoChooseEdge, to ignore dragging node --- modules/geo/geo.js | 15 +++++---- test/spec/geo/geo.js | 77 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/modules/geo/geo.js b/modules/geo/geo.js index e96bc81b9c..3182839315 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -157,14 +157,17 @@ export function geoRotate(points, angle, around) { // projection onto that edge, if such a projection exists, or the distance to // the closest vertex on that edge. Returns an object with the `index` of the // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. -export function geoChooseEdge(nodes, point, projection) { +export function geoChooseEdge(nodes, point, projection, skipID) { var dist = geoEuclideanDistance; var points = nodes.map(function(n) { return projection(n.loc); }); + var ids = nodes.map(function(n) { return n.id; }); var min = Infinity; var idx; var loc; for (var i = 0; i < points.length - 1; i++) { + if (ids[i] === skipID || ids[i + 1] === skipID) continue; + var o = points[i]; var s = geoVecSubtract(points[i + 1], o); var v = geoVecSubtract(point, o); @@ -187,11 +190,11 @@ export function geoChooseEdge(nodes, point, projection) { } } - return { - index: idx, - distance: min, - loc: loc - }; + if (idx !== undefined) { + return { index: idx, distance: min, loc: loc }; + } else { + return null; + } } diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 989c557146..c9762e051a 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -291,20 +291,12 @@ describe('iD.geo', function() { var projection = function (l) { return l; }; projection.invert = projection; - it('returns undefined properties for a degenerate way (no nodes)', function() { - expect(iD.geoChooseEdge([], [0, 0], projection)).to.eql({ - index: undefined, - distance: Infinity, - loc: undefined - }); + it('returns null for a degenerate way (no nodes)', function() { + expect(iD.geoChooseEdge([], [0, 0], projection)).to.be.null; }); - it('returns undefined properties for a degenerate way (single node)', function() { - expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.eql({ - index: undefined, - distance: Infinity, - loc: undefined - }); + it('returns null for a degenerate way (single node)', function() { + expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.be.null; }); it('calculates the orthogonal projection of a point onto a segment', function() { @@ -344,6 +336,67 @@ describe('iD.geo', function() { expect(choice.distance).to.eql(5); expect(choice.loc).to.eql([5, 0]); }); + + it('skips the given nodeID at end of way', function() { + // + // a --*-- b + // e | + // | | + // d - c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 5]; + var e = [2, 0.1]; // e.g. user is dragging e onto ab + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, e, projection, 'e'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('skips the given nodeID in middle of way', function() { + // + // a --*-- b + // d | + // / \ | + // e c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 0.1]; // e.g. user is dragging d onto ab + var e = [0, 5]; + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, d, projection, 'd'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('returns null if all nodes are skipped', function() { + var nodes = [ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [5, 0]}), + ]; + var choice = iD.geoChooseEdge(nodes, [2, 2], projection, 'a'); + expect(choice).to.be.null; + }); }); describe('geoLineIntersection', function() { From 5d9b051f84c9c45f2a4235d063717e34965bd445 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 21 Dec 2017 20:31:20 -0500 Subject: [PATCH 064/206] Fix drag_node for touch targets and line snapping --- css/20_map.css | 2 + modules/behavior/draw.js | 42 +++++----- modules/behavior/draw_way.js | 152 +++++++++++++++++------------------ modules/core/context.js | 7 -- modules/modes/drag_node.js | 123 ++++++++++------------------ modules/modes/draw_area.js | 4 +- modules/modes/draw_line.js | 4 +- modules/svg/points.js | 1 - 8 files changed, 145 insertions(+), 190 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 1004a3d1de..6eec272614 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -29,6 +29,8 @@ stroke-width: 12; stroke-opacity: 0.8; stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; } /* `.target-nope` objects are explicitly forbidden to join to */ diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index d2421ffc31..513c0b30ce 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -33,8 +33,7 @@ export function behaviorDraw(context) { var keybinding = d3_keybinding('draw'); - var hover = behaviorHover(context) - .altDisables(true) + var hover = behaviorHover(context).altDisables(true) .on('hover', context.ui().sidebar.hover); var tail = behaviorTail(); var edit = behaviorEdit(context); @@ -58,10 +57,8 @@ export function behaviorDraw(context) { // When drawing, connect only to things classed as targets.. // (this excludes area fills and active drawing elements) var selection = d3_select(element); - if (selection.classed('target')) return {}; - - var d = selection.datum(); - return (d && d.id && context.hasEntity(d.id)) || {}; + if (!selection.classed('target')) return {}; + return selection.datum(); } @@ -124,28 +121,33 @@ export function behaviorDraw(context) { } + // related code + // - `mode/drag_node.js` `doMode()` + // - `behavior/draw.js` `click()` + // - `behavior/draw_way.js` `move()` function click() { + var d = datum(); + var target = d && d.id && context.hasEntity(d.id); + var trySnap = geoViewportEdge(context.mouse(), context.map().dimensions()) === null; if (trySnap) { - // If we're not at the edge of the viewport, try to snap.. - // See also: `modes/drag_node.js doMove()` - var d = datum(); - - // Snap to a node - if (d.type === 'node') { - dispatch.call('clickNode', this, d); + if (target && target.type === 'node') { // Snap to a node + dispatch.call('clickNode', this, target); return; - // Snap to a way - } else if (d.type === 'way') { - var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); - var edge = [d.nodes[choice.index - 1], d.nodes[choice.index]]; - dispatch.call('clickWay', this, choice.loc, edge); - return; + } else if (target && target.type === 'way') { // Snap to a way + var choice = geoChooseEdge( + context.childNodes(target), context.mouse(), context.projection, context.activeID() + ); + if (choice) { + var edge = [target.nodes[choice.index - 1], target.nodes[choice.index]]; + dispatch.call('clickWay', this, choice.loc, edge); + return; + } } } - dispatch.call('click', this, context.map().mouseCoordinates()); + dispatch.call('click', this, context.map().mouseCoordinates(), d); } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 163513b3c5..c9a34c8503 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -10,37 +10,22 @@ import { } from '../actions'; import { behaviorDraw } from './draw'; - -import { - geoChooseEdge, - geoEdgeEqual -} from '../geo'; - -import { - modeBrowse, - modeSelect -} from '../modes'; - -import { - osmNode, - osmWay -} from '../osm'; - -import { utilEntitySelector } from '../util'; +import { geoChooseEdge, geoEdgeEqual } from '../geo'; +import { modeBrowse, modeSelect } from '../modes'; +import { osmNode, osmWay } from '../osm'; export function behaviorDrawWay(context, wayId, index, mode, startGraph) { - var origWay = context.entity(wayId); var isArea = context.geometry(wayId) === 'area'; - var tempEdits = 0; var annotation = t((origWay.isDegenerate() ? 'operations.start.annotation.' : - 'operations.continue.annotation.') + context.geometry(wayId)); - var draw = behaviorDraw(context); - // var _activeIDs = []; - var _activeID; - var startIndex; + 'operations.continue.annotation.') + context.geometry(wayId) + ); + var behavior = behaviorDraw(context); + var _tempEdits = 0; + var _startIndex; + var start; var end; var segment; @@ -48,9 +33,15 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // initialize the temporary drawing entities if (!isArea) { - startIndex = typeof index === 'undefined' ? origWay.nodes.length - 1 : 0; - start = osmNode({ id: 'nStart', loc: context.entity(origWay.nodes[startIndex]).loc }); - end = osmNode({ id: 'nEnd', loc: context.map().mouseCoordinates() }); + _startIndex = (typeof index === 'undefined' ? origWay.nodes.length - 1 : 0); + start = osmNode({ + id: 'nStart', + loc: context.entity(origWay.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], @@ -63,21 +54,30 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Push an annotated state for undo to return back to. // We must make sure to remove this edit later. context.perform(actionNoop(), annotation); - tempEdits++; + _tempEdits++; // Add the temporary drawing entities to the graph. // We must make sure to remove this edit later. context.perform(AddDrawEntities()); - tempEdits++; + _tempEdits++; + // related code + // - `mode/drag_node.js` `doMode()` + // - `behavior/draw.js` `click()` + // - `behavior/draw_way.js` `move()` function move(datum) { var loc; - if (datum.type === 'node' && datum.id !== end.id) { + var target = datum && datum.id && context.hasEntity(datum.id); + if (target && target.type === 'node') { // snap to node loc = datum.loc; - - } else if (datum.type === 'way') { - loc = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc; + } else if (target && target.type === 'way') { // snap to way + var choice = geoChooseEdge( + context.childNodes(target), context.mouse(), context.projection, end.id + ); + if (choice) { + loc = choice.loc; + } } if (!loc) { @@ -93,7 +93,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Undo popped the history back to the initial annotated no-op edit. // Remove initial no-op edit and whatever edit happened immediately before it. context.pop(2); - tempEdits = 0; + _tempEdits = 0; if (context.hasEntity(wayId)) { context.enter(mode); @@ -104,17 +104,14 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function setActiveElements() { - // _activeIDs = isArea ? [wayId, end.id] : [segment.id, start.id, end.id]; - // context.surface().selectAll(utilEntitySelector(_activeIDs)) - // .classed('active', true); - _activeID = end.id; context.surface().selectAll('.' + end.id) .classed('active', true); } var drawWay = function(surface) { - draw.on('move', move) + behavior + .on('move', move) .on('click', drawWay.add) .on('clickWay', drawWay.addWay) .on('clickNode', drawWay.addNode) @@ -128,7 +125,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { setActiveElements(); - surface.call(draw); + surface.call(behavior); context.history() .on('undone.draw', undone); @@ -139,8 +136,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Drawing was interrupted unexpectedly. // This can happen if the user changes modes, // clicks geolocate button, a hashchange event occurs, etc. - if (tempEdits) { - context.pop(tempEdits); + if (_tempEdits) { + context.pop(_tempEdits); while (context.graph() !== startGraph) { context.pop(); } @@ -149,7 +146,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.map() .on('drawn.draw', null); - surface.call(draw.off) + surface.call(behavior.off) .selectAll('.active') .classed('active', false); @@ -201,12 +198,18 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Accept the current position of the temporary node and continue drawing. - drawWay.add = function(loc) { + drawWay.add = function(loc, datum) { +// shouldn't happen now? // prevent duplicate nodes - var last = context.hasEntity(origWay.nodes[origWay.nodes.length - (isArea ? 2 : 1)]); - if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return; + // var last = context.hasEntity(origWay.nodes[origWay.nodes.length - (isArea ? 2 : 1)]); + // if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return; + + if (datum && datum.id && /-nope/.test(datum.id)) { // can't click here + return; + } - context.pop(tempEdits); + context.pop(_tempEdits); + _tempEdits = 0; if (isArea) { context.perform( @@ -222,31 +225,30 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { ); } - tempEdits = 0; context.enter(mode); }; // Connect the way to an existing way. drawWay.addWay = function(loc, edge) { - if (isArea) { - context.pop(tempEdits); + context.pop(_tempEdits); + _tempEdits = 0; + if (isArea) { context.perform( AddDrawEntities(), actionAddMidpoint({ loc: loc, edge: edge}, end), annotation ); } else { - var previousEdge = startIndex ? - [origWay.nodes[startIndex], origWay.nodes[startIndex - 1]] : - [origWay.nodes[0], origWay.nodes[1]]; - - // Avoid creating duplicate segments - if (geoEdgeEqual(edge, previousEdge)) - return; +// shouldn't happen now? + // var previousEdge = _startIndex ? + // [origWay.nodes[_startIndex], origWay.nodes[_startIndex - 1]] : + // [origWay.nodes[0], origWay.nodes[1]]; - context.pop(tempEdits); + // // Avoid creating duplicate segments + // if (geoEdgeEqual(edge, previousEdge)) + // return; var newNode = osmNode({ loc: loc }); context.perform( @@ -256,7 +258,6 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { ); } - tempEdits = 0; context.enter(mode); }; @@ -264,23 +265,25 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing node and continue drawing. drawWay.addNode = function(node) { // Avoid creating duplicate segments - if (origWay.areAdjacent(node.id, origWay.nodes[origWay.nodes.length - 1])) return; +// shouldn't happen now? + // if (origWay.areAdjacent(node.id, origWay.nodes[origWay.nodes.length - 1])) return; // Clicks should not occur on the drawing node, however a space keypress can // sometimes grab that node's datum (before it gets classed as `active`?) #4016 - if (node.id === end.id) { - drawWay.add(node.loc); - return; - } +// shouldn't happen now? + // if (node.id === end.id) { + // drawWay.add(node.loc); + // return; + // } - context.pop(tempEdits); + context.pop(_tempEdits); + _tempEdits = 0; context.perform( ReplaceDrawEntities(node), annotation ); - tempEdits = 0; context.enter(mode); }; @@ -289,8 +292,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // If the way has enough nodes to be valid, it's selected. // Otherwise, delete everything and return to browse mode. drawWay.finish = function() { - context.pop(tempEdits); - tempEdits = 0; + context.pop(_tempEdits); + _tempEdits = 0; var way = context.hasEntity(wayId); if (!way || way.isDegenerate()) { @@ -308,8 +311,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Cancel the draw operation, delete everything, and return to browse mode. drawWay.cancel = function() { - context.pop(tempEdits); - tempEdits = 0; + context.pop(_tempEdits); + _tempEdits = 0; while (context.graph() !== startGraph) { context.pop(); @@ -323,20 +326,15 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { }; - // drawWay.activeIDs = function() { - // if (!arguments.length) return _activeIDs; - // // no assign - // return drawWay; - // }; drawWay.activeID = function() { - if (!arguments.length) return _activeID; + if (!arguments.length) return end.id; // no assign return drawWay; }; drawWay.tail = function(text) { - draw.tail(text); + behavior.tail(text); return drawWay; }; diff --git a/modules/core/context.js b/modules/core/context.js index dcf0b64120..1f606d5feb 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -255,13 +255,6 @@ export function coreContext() { return []; } }; - // context.activeIDs = function() { - // if (mode && mode.activeIDs) { - // return mode.activeIDs(); - // } else { - // return []; - // } - // }; context.activeID = function() { return mode && mode.activeID && mode.activeID(); }; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 2446ffbfa7..4e905f98db 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -12,25 +12,10 @@ import { actionNoop } from '../actions'; -import { - behaviorEdit, - behaviorHover, - behaviorDrag -} from '../behavior'; - -import { - modeBrowse, - modeSelect -} from './index'; - -import { - geoChooseEdge, - geoVecSubtract, - geoViewportEdge -} from '../geo'; - +import { behaviorEdit, behaviorHover, behaviorDrag } from '../behavior'; +import { geoChooseEdge, geoVecSubtract, geoViewportEdge } from '../geo'; +import { modeBrowse, modeSelect } from './index'; import { osmNode } from '../osm'; -import { utilEntitySelector } from '../util'; import { uiFlash } from '../ui'; @@ -39,16 +24,15 @@ export function modeDragNode(context) { id: 'drag-node', button: 'browse' }; - var hover = behaviorHover(context).altDisables(true).on('hover', context.ui().sidebar.hover); + var hover = behaviorHover(context).altDisables(true) + .on('hover', context.ui().sidebar.hover); var edit = behaviorEdit(context); var _nudgeInterval; var _restoreSelectedIDs = []; - // var _activeIDs = []; - var _activeID; var _wasMidpoint = false; var _isCancelled = false; - var _dragEntity; + var _activeEntity; var _lastLoc; @@ -94,7 +78,7 @@ export function modeDragNode(context) { if (hasHidden) { uiFlash().text(t('modes.drag_node.connected_to_hidden'))(); } - return behavior.cancel(); + return drag.cancel(); } if (_wasMidpoint) { @@ -103,20 +87,15 @@ export function modeDragNode(context) { context.perform(actionAddMidpoint(midpoint, entity)); var vertex = context.surface().selectAll('.' + entity.id); - behavior.target(vertex.node(), entity); + drag.target(vertex.node(), entity); } else { context.perform(actionNoop()); } - _dragEntity = entity; - - // `.active` elements have `pointer-events: none`. - // This prevents the node or vertex being dragged from trying to connect to itself. - // _activeIDs = context.graph().parentWays(entity).map(function(parent) { return parent.id; }); - // _activeIDs.push(entity.id); - _activeID = entity.id; - setActiveElements(); + _activeEntity = entity; + context.surface().selectAll('.' + _activeEntity.id) + .classed('active', true); context.enter(mode); } @@ -127,8 +106,7 @@ export function modeDragNode(context) { if (!event || event.altKey || !d3_select(event.target).classed('target')) { return {}; } else { - var d = event.target.__data__; - return (d && d.id && context.hasEntity(d.id)) || {}; + return event.target.__data__ || {}; } } @@ -140,20 +118,21 @@ export function modeDragNode(context) { var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); - if (!_nudgeInterval) { - // If we're not nudging at the edge of the viewport, try to snap.. - // See also `behavior/draw.js click()` + if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap.. + // related code + // - `mode/drag_node.js` `doMode()` + // - `behavior/draw.js` `click()` + // - `behavior/draw_way.js` `move()` var d = datum(); - - // Snap to a node (not self) - if (d.type === 'node' && d.id !== entity.id) { - loc = d.loc; - - // Snap to a way - } else if (d.type === 'way') { - var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); - // (not along a segment adjacent to self) - if (entity.id !== d.nodes[choice.index - 1] && entity.id !== d.nodes[choice.index]) { + var target = d && d.id && context.hasEntity(d.id); + + if (target && target.type === 'node') { + loc = target.loc; + } else if (target && target.type === 'way') { + var choice = geoChooseEdge( + context.childNodes(target), context.mouse(), context.projection, entity.id + ); + if (choice) { loc = choice.loc; } } @@ -188,18 +167,22 @@ export function modeDragNode(context) { if (_isCancelled) return; var d = datum(); + var target = d && d.id && context.hasEntity(d.id); - if (d.type === 'way') { - var choice = geoChooseEdge(context.childNodes(d), context.mouse(), context.projection); + if (target && target.type === 'way') { + var choice = geoChooseEdge(context.childNodes(target), context.mouse(), context.projection, entity.id); context.replace( - actionAddMidpoint({ loc: choice.loc, edge: [d.nodes[choice.index - 1], d.nodes[choice.index]] }, entity), - connectAnnotation(d) + actionAddMidpoint({ + loc: choice.loc, + edge: [target.nodes[choice.index - 1], target.nodes[choice.index]] + }, entity), + connectAnnotation(target) ); - } else if (d.type === 'node' && d.id !== entity.id) { + } else if (target && target.type === 'node') { context.replace( - actionConnect([d.id, entity.id]), - connectAnnotation(d) + actionConnect([target.id, entity.id]), + connectAnnotation(target) ); } else if (_wasMidpoint) { @@ -228,20 +211,12 @@ export function modeDragNode(context) { function cancel() { - behavior.cancel(); + drag.cancel(); context.enter(modeBrowse(context)); } - function setActiveElements() { - // context.surface().selectAll(utilEntitySelector(_activeIDs)) - // .classed('active', true); - context.surface().selectAll('.' + _activeID) - .classed('active', true); - } - - - var behavior = behaviorDrag() + var drag = behaviorDrag() .selector('.layer-points-targets .target') .surface(d3_select('#map').node()) .origin(origin) @@ -256,11 +231,6 @@ export function modeDragNode(context) { context.history() .on('undone.drag-node', cancel); - - context.map() - .on('drawn.drag-node', setActiveElements); - - setActiveElements(); }; @@ -275,8 +245,8 @@ export function modeDragNode(context) { context.map() .on('drawn.drag-node', null); - // _activeIDs = []; - _activeID = null; + _activeEntity = null; + context.surface() .selectAll('.active') .classed('active', false); @@ -286,19 +256,14 @@ export function modeDragNode(context) { mode.selectedIDs = function() { - if (!arguments.length) return _dragEntity ? [_dragEntity.id] : []; + if (!arguments.length) return _activeEntity ? [_activeEntity.id] : []; // no assign return mode; }; - // mode.activeIDs = function() { - // if (!arguments.length) return _activeIDs; - // // no assign - // return mode; - // }; mode.activeID = function() { - if (!arguments.length) return _activeID; + if (!arguments.length) return _activeEntity && _activeEntity.id; // no assign return mode; }; @@ -311,7 +276,7 @@ export function modeDragNode(context) { }; - mode.behavior = behavior; + mode.behavior = drag; return mode; diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 0dd5b1272d..0890e3592b 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -43,9 +43,7 @@ export function modeDrawArea(context, wayId, startGraph) { return [wayId]; }; - // mode.activeIDs = function() { - // return (behavior && behavior.activeIDs()) || []; - // }; + mode.activeID = function() { return (behavior && behavior.activeID()) || []; }; diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index 3e638cbd99..ca567601ee 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -41,9 +41,7 @@ export function modeDrawLine(context, wayId, startGraph, affix) { return [wayId]; }; - // mode.activeIDs = function() { - // return (behavior && behavior.activeIDs()) || []; - // }; + mode.activeID = function() { return (behavior && behavior.activeID()) || []; }; diff --git a/modules/svg/points.js b/modules/svg/points.js index ef1d504d9e..0b3366ab39 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -31,7 +31,6 @@ export function svgPoints(projection, context) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var passive = entities.filter(function(d) { return d.id !== context.activeID(); - // return context.activeIDs().indexOf(d.id) === -1; }); var targets = selection.selectAll('.point.target') From 2be62fffe5a105b55709e13969ed1f34b463ca1a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 21 Dec 2017 23:48:52 -0500 Subject: [PATCH 065/206] All the complicated code in drawWay can be removed Now that we can target specific segments along a line, we don't need temporary drawing objects or extra code to check for self intersections --- modules/behavior/draw_way.js | 150 ++++++----------------------------- 1 file changed, 26 insertions(+), 124 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index c9a34c8503..8f138c45a2 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -1,64 +1,36 @@ -import _clone from 'lodash-es/clone'; - import { t } from '../util/locale'; import { - actionAddEntity, actionAddMidpoint, actionMoveNode, actionNoop } from '../actions'; import { behaviorDraw } from './draw'; -import { geoChooseEdge, geoEdgeEqual } from '../geo'; +import { geoChooseEdge } from '../geo'; import { modeBrowse, modeSelect } from '../modes'; -import { osmNode, osmWay } from '../osm'; +import { osmNode } from '../osm'; export function behaviorDrawWay(context, wayId, index, mode, startGraph) { var origWay = context.entity(wayId); - var isArea = context.geometry(wayId) === 'area'; var annotation = t((origWay.isDegenerate() ? 'operations.start.annotation.' : 'operations.continue.annotation.') + context.geometry(wayId) ); var behavior = behaviorDraw(context); var _tempEdits = 0; - var _startIndex; - - var start; - var end; - var segment; - - - // initialize the temporary drawing entities - if (!isArea) { - _startIndex = (typeof index === 'undefined' ? origWay.nodes.length - 1 : 0); - start = osmNode({ - id: 'nStart', - loc: context.entity(origWay.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(origWay.tags) - }); - } else { - end = osmNode({ loc: context.map().mouseCoordinates() }); - } + + var end = osmNode({ loc: context.map().mouseCoordinates() }); // Push an annotated state for undo to return back to. // We must make sure to remove this edit later. context.perform(actionNoop(), annotation); _tempEdits++; - // Add the temporary drawing entities to the graph. + // Add the drawing node to the graph. // We must make sure to remove this edit later. - context.perform(AddDrawEntities()); + context.perform(_actionAddDrawNode()); _tempEdits++; @@ -155,75 +127,35 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { }; - function AddDrawEntities() { + function _actionAddDrawNode() { return function(graph) { - if (isArea) { - // For area drawing, there is no need for a temporary node. - // `end` gets inserted into the way as the penultimate node. - return graph - .replace(end) - .replace(origWay.addNode(end.id)); - } else { - // For line drawing, add a temporary start, end, and segment to the graph. - // This allows us to class the new segment as `active`, but still - // connect it back to parts of the way that have already been drawn. - return graph - .replace(start) - .replace(end) - .replace(segment); - } + return graph + .replace(end) + .replace(origWay.addNode(end.id)); }; } - function ReplaceDrawEntities(newNode) { + function _actionReplaceDrawNode(newNode) { return function(graph) { - if (isArea) { - // For area drawing, we didn't create a temporary node. - // `newNode` gets inserted into the _original_ way as the penultimate node. - return graph - .replace(origWay.addNode(newNode.id)) - .remove(end); - } else { - // For line drawing, add the `newNode` to the way at specified index, - // and remove the temporary start, end, and segment. - return graph - .replace(origWay.addNode(newNode.id, index)) - .remove(end) - .remove(segment) - .remove(start); - } + return graph + .replace(origWay.addNode(newNode.id)) + .remove(end); }; } - // Accept the current position of the temporary node and continue drawing. + // Accept the current position of the drawing node and continue drawing. drawWay.add = function(loc, datum) { -// shouldn't happen now? - // prevent duplicate nodes - // var last = context.hasEntity(origWay.nodes[origWay.nodes.length - (isArea ? 2 : 1)]); - // if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return; - - if (datum && datum.id && /-nope/.test(datum.id)) { // can't click here - return; - } + if (datum && datum.id && /-nope/.test(datum.id)) return; // can't click here context.pop(_tempEdits); _tempEdits = 0; - if (isArea) { - context.perform( - AddDrawEntities(), - annotation - ); - } else { - var newNode = osmNode({loc: loc}); - context.perform( - actionAddEntity(newNode), - ReplaceDrawEntities(newNode), - annotation - ); - } + context.perform( + _actionAddDrawNode(), + annotation + ); context.enter(mode); }; @@ -234,29 +166,11 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.pop(_tempEdits); _tempEdits = 0; - if (isArea) { - context.perform( - AddDrawEntities(), - actionAddMidpoint({ loc: loc, edge: edge}, end), - annotation - ); - } else { -// shouldn't happen now? - // var previousEdge = _startIndex ? - // [origWay.nodes[_startIndex], origWay.nodes[_startIndex - 1]] : - // [origWay.nodes[0], origWay.nodes[1]]; - - // // Avoid creating duplicate segments - // if (geoEdgeEqual(edge, previousEdge)) - // return; - - var newNode = osmNode({ loc: loc }); - context.perform( - actionAddMidpoint({ loc: loc, edge: edge}, newNode), - ReplaceDrawEntities(newNode), - annotation - ); - } + context.perform( + _actionAddDrawNode(), + actionAddMidpoint({ loc: loc, edge: edge }, end), + annotation + ); context.enter(mode); }; @@ -264,23 +178,11 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing node and continue drawing. drawWay.addNode = function(node) { - // Avoid creating duplicate segments -// shouldn't happen now? - // if (origWay.areAdjacent(node.id, origWay.nodes[origWay.nodes.length - 1])) return; - - // Clicks should not occur on the drawing node, however a space keypress can - // sometimes grab that node's datum (before it gets classed as `active`?) #4016 -// shouldn't happen now? - // if (node.id === end.id) { - // drawWay.add(node.loc); - // return; - // } - context.pop(_tempEdits); _tempEdits = 0; context.perform( - ReplaceDrawEntities(node), + _actionReplaceDrawNode(node), annotation ); From 7851c49e80b59c9c8b8da0975d774488ee0566fc Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 00:26:39 -0500 Subject: [PATCH 066/206] Fix nopefilters by testing the original id for the filter --- modules/svg/areas.js | 1 + modules/svg/helpers.js | 3 +++ modules/svg/lines.js | 12 +++++++++--- modules/svg/vertices.js | 7 +++---- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/modules/svg/areas.js b/modules/svg/areas.js index 3fb7c37235..90f35eb57a 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -76,6 +76,7 @@ export function svgAreas(projection, context) { // NOPE var nopes = selection.selectAll('.area.target-nope') + .filter(function(d) { return filter({ id: d.properties.originalID }); }) .data(data.nopes, function key(d) { return d.id; }); // exit diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index 88f04f578e..bbd22f11d4 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -245,6 +245,9 @@ export function svgSegmentWay(way, graph, activeID) { features.active.push({ 'type': 'Feature', 'id': way.id + '-nope', // break the ids on purpose + 'properties': { + 'originalID': way.id + }, 'geometry': { 'type': 'MultiLineString', 'coordinates': coordGroups.active diff --git a/modules/svg/lines.js b/modules/svg/lines.js index fbecbed7be..67e433ab8a 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -72,6 +72,7 @@ export function svgLines(projection, context) { // NOPE var nopes = selection.selectAll('.line.target-nope') + .filter(function(d) { return filter({ id: d.properties.originalID }); }) .data(data.nopes, function key(d) { return d.id; }); // exit @@ -101,6 +102,11 @@ export function svgLines(projection, context) { function drawLineGroup(selection, klass, isSelected) { + // Note: Don't add `.selected` class in draw modes + var mode = context.mode(); + var isDrawing = mode && /^draw/.test(mode.id); + var selectedClass = (!isDrawing && isSelected) ? 'selected ' : ''; + var lines = selection .selectAll('path') .filter(filter) @@ -109,13 +115,13 @@ export function svgLines(projection, context) { lines.exit() .remove(); - // Optimization: call simple TagClasses only on enter selection. This + // Optimization: Call expensive TagClasses only on enter selection. This // works because osmEntity.key is defined to include the entity v attribute. lines.enter() .append('path') .attr('class', function(d) { - return 'way line ' + klass + ' ' + d.id + (isSelected ? ' selected' : '') + - (oldMultiPolygonOuters[d.id] ? ' old-multipolygon' : ''); + var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : ''; + return 'way line ' + klass + ' ' + selectedClass + oldMPClass + d.id; }) .call(svgTagClasses()) .merge(lines) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 77aad0a1d6..a0584b501e 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -196,6 +196,7 @@ export function svgVertices(projection, context) { } else { data.nopes.push({ id: node.id + '-nope', // not a real osmNode, break the id on purpose + originalID: node.id, loc: node.loc }); } @@ -222,6 +223,7 @@ export function svgVertices(projection, context) { // NOPE var nopes = selection.selectAll('.vertex.target-nope') + .filter(function(d) { return filter({ id: d.originalID }); }) .data(data.nopes, function key(d) { return d.id; }); // exit @@ -351,11 +353,8 @@ export function svgVertices(projection, context) { .call(draw, graph, currentVisible(all), sets, filterRendered); // Draw touch targets.. - var filterTargets = function(d) { - return isMoving ? true : filterRendered(d); - }; selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, currentVisible(all), filterTargets); + .call(drawTargets, graph, currentVisible(all), filterRendered); function currentVisible(which) { From be00a526b6c8caf611e18b864dcee8cee8d6a88c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 11:42:21 -0500 Subject: [PATCH 067/206] Make sure all targets are redrawn during a mode change There was an issue where the lines did not redraw their targets right away when entering drag node, which could make it possible for a quick drag node to try to connect to its parent line. With the chooseEdge exclusion it would not connect to the parent nearby, but in another weird part of the line. --- modules/renderer/map.js | 28 +++++++++++++++++++++++----- modules/svg/vertices.js | 8 ++++++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/modules/renderer/map.js b/modules/renderer/map.js index b2718ec25c..a4ebac8a43 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -201,17 +201,35 @@ export function rendererMap(context) { context.on('enter.map', function() { if (map.editable() && !transformed) { - // redraw immediately the objects that are affected by a chnage in selectedIDs. - var all = context.intersects(map.extent()); - var filter = utilFunctor(true); + + // redraw immediately any objects affected by a change in selectedIDs. var graph = context.graph(); + var selectedAndParents = {}; + context.selectedIDs().forEach(function(id) { + var entity = graph.hasEntity(id); + if (entity) { + selectedAndParents[entity.id] = entity; + if (entity.type === 'node') { + graph.parentWays(entity).forEach(function(parent) { + selectedAndParents[parent.id] = parent; + }); + } + } + }); + var data = _values(selectedAndParents); + var filter = function(d) { return d.id in selectedAndParents; }; + + data = context.features().filter(data, graph); - all = context.features().filter(all, graph); surface.selectAll('.data-layer-osm') .call(drawVertices.drawSelected, graph, map.extent()) - .call(drawMidpoints, graph, all, filter, map.trimmedExtent()); + .call(drawLines, graph, data, filter) + .call(drawAreas, graph, data, filter) + .call(drawMidpoints, graph, data, filter, map.trimmedExtent()); + dispatch.call('drawn', this, { full: false }); + // redraw everything else later scheduleRedraw(); } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index a0584b501e..1a5ed0a88e 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -345,7 +345,7 @@ export function svgVertices(projection, context) { // Draw the vertices.. // The filter function controls the scope of what objects d3 will touch (exit/enter/update) - // It's important to adjust the filter function to expand the scope beyond whatever entities were passed in. + // Adjust the filter function to expand the scope beyond whatever entities were passed in. var filterRendered = function(d) { return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d); }; @@ -353,8 +353,12 @@ export function svgVertices(projection, context) { .call(draw, graph, currentVisible(all), sets, filterRendered); // Draw touch targets.. + // When drawing, render all targets (not just those affected by a partial redraw) + var filterTouch = function(d) { + return isMoving ? true : filterRendered(d); + }; selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, currentVisible(all), filterRendered); + .call(drawTargets, graph, currentVisible(all), filterTouch); function currentVisible(which) { From fc680545ad976fc3c94326d5e1e9fce29bee9cc5 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 13:58:27 -0500 Subject: [PATCH 068/206] Don't dispatch drag start and move together Drag start is responsible for switching into drag mode, classing stuff as `active` and kicking off a bunch of other things. If the drag move happens immediately after this, and includes the target from the initial active drag, it can cause weird snapping from the dragnode to its own parent way. (Happened if the user did a very fast drag from the node along the parent way just next to it) --- modules/behavior/drag.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index 52e9fb7ffd..b9ea9c0af3 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -110,19 +110,19 @@ export function behaviorDrag() { if (dx === 0 && dy === 0) return; + startOrigin = p; + d3_eventCancel(); + if (!started) { started = true; _event({ type: 'start' }); + } else { + _event({ + type: 'move', + point: [p[0] + offset[0], p[1] + offset[1]], + delta: [dx, dy] + }); } - - startOrigin = p; - d3_eventCancel(); - - _event({ - type: 'move', - point: [p[0] + offset[0], p[1] + offset[1]], - delta: [dx, dy] - }); } From 64a11f4cbfa30d291a1e7ced620880d84060e82b Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 15:08:05 -0500 Subject: [PATCH 069/206] Restore ability to extend line at beginning or end --- modules/behavior/draw_way.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 8f138c45a2..68e56c1823 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -131,7 +131,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { return function(graph) { return graph .replace(end) - .replace(origWay.addNode(end.id)); + .replace(origWay.addNode(end.id, index)); }; } @@ -139,7 +139,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function _actionReplaceDrawNode(newNode) { return function(graph) { return graph - .replace(origWay.addNode(newNode.id)) + .replace(origWay.addNode(newNode.id, index)) .remove(end); }; } From 117ad7d6b65acf4f3211f26b62a1653c269a45fa Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 16:57:27 -0500 Subject: [PATCH 070/206] Transitionable actionMoveNode --- modules/actions/move_node.js | 21 ++++++++++++---- test/spec/actions/move_node.js | 46 +++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/modules/actions/move_node.js b/modules/actions/move_node.js index 6e2593b458..8de29a16a7 100644 --- a/modules/actions/move_node.js +++ b/modules/actions/move_node.js @@ -1,7 +1,18 @@ -// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java -// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as -export function actionMoveNode(nodeId, loc) { - return function(graph) { - return graph.replace(graph.entity(nodeId).move(loc)); +import { geoInterp } from '../geo'; + +export function actionMoveNode(nodeID, toLoc) { + + var action = function(graph, t) { + if (t === null || !isFinite(t)) t = 1; + t = Math.min(Math.max(+t, 0), 1); + + var node = graph.entity(nodeID); + return graph.replace( + node.move(geoInterp(node.loc, toLoc, t)) + ); }; + + action.transitionable = true; + + return action; } diff --git a/test/spec/actions/move_node.js b/test/spec/actions/move_node.js index bc28f748de..771f66b4a0 100644 --- a/test/spec/actions/move_node.js +++ b/test/spec/actions/move_node.js @@ -1,8 +1,46 @@ describe('iD.actionMoveNode', function () { it('changes a node\'s location', function () { - var node = iD.Node(), - loc = [2, 3], - graph = iD.actionMoveNode(node.id, loc)(iD.Graph([node])); - expect(graph.entity(node.id).loc).to.eql(loc); + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph); + expect(graph.entity('a').loc).to.eql(toLoc); + }); + + describe('transitions', function () { + it('is transitionable', function() { + expect(iD.actionMoveNode().transitionable).to.be.true; + }); + + it('move node at t = 0', function() { + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph, 0); + expect(graph.entity('a').loc[0]).to.be.closeTo(0, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(0, 1e-6); + }); + + it('move node at t = 0.5', function() { + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph, 0.5); + expect(graph.entity('a').loc[0]).to.be.closeTo(1, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(1.5, 1e-6); + }); + + it('move node at t = 1', function() { + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var toLoc = [2, 3]; + var graph = iD.coreGraph([node]); + + graph = iD.actionMoveNode('a', toLoc)(graph, 1); + expect(graph.entity('a').loc[0]).to.be.closeTo(2, 1e-6); + expect(graph.entity('a').loc[1]).to.be.closeTo(3, 1e-6); + }); }); }); From d6e8ca2a1c87f2cb2eecf93a3604de7403bb066a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 22 Dec 2017 21:06:44 -0500 Subject: [PATCH 071/206] Transitioned bouncebacks when user drags node onto a nope --- modules/modes/drag_node.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 4e905f98db..9218527f18 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -33,6 +33,7 @@ export function modeDragNode(context) { var _wasMidpoint = false; var _isCancelled = false; var _activeEntity; + var _startLoc; var _lastLoc; @@ -85,6 +86,7 @@ export function modeDragNode(context) { var midpoint = entity; entity = osmNode(); context.perform(actionAddMidpoint(midpoint, entity)); + entity = context.entity(entity.id); // get post-action entity var vertex = context.surface().selectAll('.' + entity.id); drag.target(vertex.node(), entity); @@ -94,6 +96,8 @@ export function modeDragNode(context) { } _activeEntity = entity; + _startLoc = entity.loc; + context.surface().selectAll('.' + _activeEntity.id) .classed('active', true); @@ -167,9 +171,15 @@ export function modeDragNode(context) { if (_isCancelled) return; var d = datum(); - var target = d && d.id && context.hasEntity(d.id); + var nope = d && d.id && /-nope$/.test(d.id); // can't drag here + var target = d && d.id && context.hasEntity(d.id); // entity to snap to + + if (nope) { // bounce back + context.perform( + _actionBounceBack(entity.id, _startLoc) + ); - if (target && target.type === 'way') { + } else if (target && target.type === 'way') { var choice = geoChooseEdge(context.childNodes(target), context.mouse(), context.projection, entity.id); context.replace( actionAddMidpoint({ @@ -210,6 +220,19 @@ export function modeDragNode(context) { } + function _actionBounceBack(nodeID, toLoc) { + var moveNode = actionMoveNode(nodeID, toLoc); + var action = function(graph, t) { + // last time through, pop off the bounceback perform. + // it will then overwrite the initial perform with a moveNode that does nothing + if (t === 1) context.pop(); + return moveNode(graph, t); + }; + action.transitionable = true; + return action; + } + + function cancel() { drag.cancel(); context.enter(modeBrowse(context)); From 9fbb4d350f8b7d0ffc57dd6577245570dc82951c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 23 Dec 2017 22:33:35 -0500 Subject: [PATCH 072/206] Add geoVecEquals for strict comparisons --- modules/geo/geo.js | 5 +++++ modules/geo/index.js | 1 + test/spec/geo/geo.js | 8 ++++++++ 3 files changed, 14 insertions(+) diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 3182839315..7a4fe121a3 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -8,6 +8,11 @@ var EQUATORIAL_RADIUS = 6356752.314245179; var POLAR_RADIUS = 6378137.0; +// vector addition +export function geoVecEquals(a, b) { + return (a[0] === b[0]) && (a[1] === b[1]); +} + // vector addition export function geoVecAdd(a, b) { return [ a[0] + b[0], a[1] + b[1] ]; diff --git a/modules/geo/index.js b/modules/geo/index.js index 2a1c8fa31d..95bba82c51 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -23,6 +23,7 @@ export { geoPolygonIntersectsPolygon } from './geo.js'; export { geoScaleToZoom } from './geo.js'; export { geoSphericalDistance } from './geo.js'; export { geoVecAdd } from './geo.js'; +export { geoVecEquals } from './geo.js'; export { geoVecFloor } from './geo.js'; export { geoVecSubtract } from './geo.js'; export { geoVecScale } from './geo.js'; diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index c9762e051a..585e7a2e76 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,5 +1,13 @@ describe('iD.geo', function() { + describe('geoVecEquals', function() { + it('tests vectors for equality', function() { + expect(iD.geoVecEquals([1, 2], [1, 2])).to.be.true; + expect(iD.geoVecEquals([1, 2], [1, 0])).to.be.false; + expect(iD.geoVecEquals([1, 2], [2, 1])).to.be.false; + }); + }); + describe('geoVecAdd', function() { it('adds vectors', function() { expect(iD.geoVecAdd([1, 2], [3, 4])).to.eql([4, 6]); From 009d7b0d65343195ccf2525d1361308fb9eaf1d0 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 24 Dec 2017 09:21:32 -0500 Subject: [PATCH 073/206] Add layer blocker and polygon self-intersection geometry check --- css/20_map.css | 2 ++ modules/modes/drag_node.js | 74 ++++++++++++++++++++++++++++++++++++-- modules/svg/blocker.js | 26 ++++++++++++++ modules/svg/index.js | 1 + 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 modules/svg/blocker.js diff --git a/css/20_map.css b/css/20_map.css index 6eec272614..3b7225dd27 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -16,6 +16,7 @@ /* `.target` objects are interactive */ /* They can be picked up, clicked, hovered, or things can connect to them */ +.layer-blocker.target, .node.target { pointer-events: fill; fill-opacity: 0.8; @@ -34,6 +35,7 @@ } /* `.target-nope` objects are explicitly forbidden to join to */ +.layer-blocker.target.target-nope, .node.target.target-nope, .way.target.target-nope { cursor: not-allowed; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 9218527f18..e76180f476 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -12,10 +12,23 @@ import { actionNoop } from '../actions'; -import { behaviorEdit, behaviorHover, behaviorDrag } from '../behavior'; -import { geoChooseEdge, geoVecSubtract, geoViewportEdge } from '../geo'; +import { + behaviorEdit, + behaviorHover, + behaviorDrag +} from '../behavior'; + +import { + geoChooseEdge, + geoLineIntersection, + geoVecEquals, + geoVecSubtract, + geoViewportEdge +} from '../geo'; + import { modeBrowse, modeSelect } from './index'; import { osmNode } from '../osm'; +import { svgBlocker } from '../svg'; import { uiFlash } from '../ui'; @@ -27,6 +40,7 @@ export function modeDragNode(context) { var hover = behaviorHover(context).altDisables(true) .on('hover', context.ui().sidebar.hover); var edit = behaviorEdit(context); + var blocker = svgBlocker(context.projection, context); var _nudgeInterval; var _restoreSelectedIDs = []; @@ -147,10 +161,63 @@ export function modeDragNode(context) { moveAnnotation(entity) ); + + checkGeometry(entity); _lastLoc = loc; } + function checkGeometry(entity) { + var doBlock = false; + var graph = context.graph(); + var parents = graph.parentWays(entity); + + function checkSelfIntersections(way, activeID) { + // check active (dragged) segments against inactive segments + var actives = []; + var inactives = []; + var j, k; + for (j = 0; j < way.nodes.length - 1; j++) { + var n1 = graph.entity(way.nodes[j]); + var n2 = graph.entity(way.nodes[j+1]); + var segment = [n1.loc, n2.loc]; + if (n1.id === activeID || n2.id === activeID) { + actives.push(segment); + } else { + inactives.push(segment); + } + } + for (j = 0; j < actives.length; j++) { + for (k = 0; k < inactives.length; k++) { + var p = actives[j]; + var q = inactives[k]; + // skip if segments share an endpoint + if (geoVecEquals(p[1], q[0]) || geoVecEquals(p[0], q[1]) || + geoVecEquals(p[0], q[0]) || geoVecEquals(p[1], q[1]) ) { + continue; + } else if (geoLineIntersection(p, q)) { + return true; + } + } + } + return false; + } + + for (var i = 0; i < parents.length; i++) { + var parent = parents[i]; + if (parent.isClosed()) { // check for self intersections + if (checkSelfIntersections(parent, entity.id)) { + doBlock = true; + break; + } + } + } + + d3_select('.data-layer-osm') + .call(doBlock ? blocker : blocker.off); + } + + function move(entity) { if (_isCancelled) return; @@ -270,6 +337,9 @@ export function modeDragNode(context) { _activeEntity = null; + d3_select('.data-layer-osm') + .call(blocker.off); + context.surface() .selectAll('.active') .classed('active', false); diff --git a/modules/svg/blocker.js b/modules/svg/blocker.js new file mode 100644 index 0000000000..93a3a5f1cd --- /dev/null +++ b/modules/svg/blocker.js @@ -0,0 +1,26 @@ +export function svgBlocker(projection, context) { + + function blocker(selection) { + var dimensions = projection.clipExtent()[1]; + var fillClass = context.getDebug('target') ? 'red ' : 'nocolor '; + + var blocker = selection.selectAll('.layer-blocker') + .data([{id: 'target-nope'}]); + + blocker.enter() + .append('rect') + .attr('class', 'layer-blocker target target-nope ' + fillClass) + .attr('x', 0) + .attr('y', 0) + .merge(blocker) + .attr('width', dimensions[0]) + .attr('height', dimensions[1]); + } + + blocker.off = function(selection) { + selection.selectAll('.layer-blocker') + .remove(); + }; + + return blocker; +} diff --git a/modules/svg/index.js b/modules/svg/index.js index 68e2f2f289..798893dbb7 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -1,4 +1,5 @@ export { svgAreas } from './areas.js'; +export { svgBlocker } from './blocker.js'; export { svgDebug } from './debug.js'; export { svgDefs } from './defs.js'; export { svgGpx } from './gpx.js'; From ee617779a4efa590a27807b155fcc4096ab92725 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 25 Dec 2017 23:11:00 -0500 Subject: [PATCH 074/206] Snap to nope targets too (snapping is useful feedback) Also remove "acting" cursor, which was overriding the no-action cursor in some situations. --- css/55_cursors.css | 10 ---------- modules/behavior/draw_way.js | 2 +- modules/modes/drag_node.js | 6 +++--- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/css/55_cursors.css b/css/55_cursors.css index db5834e6de..9e72d7ddfa 100644 --- a/css/55_cursors.css +++ b/css/55_cursors.css @@ -49,16 +49,6 @@ cursor: url(img/cursor-select-remove.png), pointer; /* FF */ } -#map .point:active, -#map .vertex:active, -#map .line:active, -#map .area:active, -#map .midpoint:active, -#map .mode-select .selected { - cursor: pointer; /* Opera */ - cursor: url(img/cursor-select-acting.png), pointer; /* FF */ -} - .mode-draw-line #map, .mode-draw-area #map, .mode-add-line #map, diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 68e56c1823..18e270c7e6 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -41,7 +41,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function move(datum) { var loc; var target = datum && datum.id && context.hasEntity(datum.id); - if (target && target.type === 'node') { // snap to node + if (datum.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` loc = datum.loc; } else if (target && target.type === 'way') { // snap to way var choice = geoChooseEdge( diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index e76180f476..3759a1c3dc 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -144,9 +144,9 @@ export function modeDragNode(context) { var d = datum(); var target = d && d.id && context.hasEntity(d.id); - if (target && target.type === 'node') { - loc = target.loc; - } else if (target && target.type === 'way') { + if (d.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` + loc = d.loc; + } else if (target && target.type === 'way') { // snap to way var choice = geoChooseEdge( context.childNodes(target), context.mouse(), context.projection, entity.id ); From 81910d4f276a4b4132d626ec5e29f10dfdda77c5 Mon Sep 17 00:00:00 2001 From: ajlomagno Date: Tue, 26 Dec 2017 15:12:11 -0500 Subject: [PATCH 075/206] Fixed click counter persisting --- modules/ui/curtain.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/ui/curtain.js b/modules/ui/curtain.js index ba671bfdd9..7e08341d20 100644 --- a/modules/ui/curtain.js +++ b/modules/ui/curtain.js @@ -221,6 +221,9 @@ export function uiCurtain() { } curtain.cut(box, options.duration); + + //remove any leftover .counter elements + tooltip.selectAll('.counter').remove(); return tooltip; }; From 5d3df2596066fbfe60897589aa5ba24af0b30033 Mon Sep 17 00:00:00 2001 From: ajlomagno Date: Wed, 27 Dec 2017 13:19:22 -0500 Subject: [PATCH 076/206] Moved cleanuo code to exit function in welcome.js --- modules/ui/curtain.js | 3 --- modules/ui/intro/welcome.js | 5 ++++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ui/curtain.js b/modules/ui/curtain.js index 7e08341d20..ba671bfdd9 100644 --- a/modules/ui/curtain.js +++ b/modules/ui/curtain.js @@ -221,9 +221,6 @@ export function uiCurtain() { } curtain.cut(box, options.duration); - - //remove any leftover .counter elements - tooltip.selectAll('.counter').remove(); return tooltip; }; diff --git a/modules/ui/intro/welcome.js b/modules/ui/intro/welcome.js index f54c81bc42..8187998e72 100644 --- a/modules/ui/intro/welcome.js +++ b/modules/ui/intro/welcome.js @@ -137,7 +137,7 @@ export function uiIntroWelcome(context, reveal) { ); } - + chapter.enter = function() { welcome(); }; @@ -145,6 +145,9 @@ export function uiIntroWelcome(context, reveal) { chapter.exit = function() { listener.off(); + var tooltip = d3_select('.curtain-tooltip.intro-mouse') + .selectAll('.counter') + .remove(); }; From fa7a6ebb128801966cd4149991e702019ad34390 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 27 Dec 2017 22:51:36 -0500 Subject: [PATCH 077/206] Snap to nope line targets too, also remove svgBlocker, not needed (it's easier to just class the surface, and won't interfere with snapping) --- css/20_map.css | 3 +-- css/55_cursors.css | 4 +++ modules/modes/drag_node.js | 53 +++++++++++++++++++++----------------- modules/svg/blocker.js | 26 ------------------- modules/svg/helpers.js | 44 +++++++++++++++++++++---------- modules/svg/index.js | 1 - 6 files changed, 65 insertions(+), 66 deletions(-) delete mode 100644 modules/svg/blocker.js diff --git a/css/20_map.css b/css/20_map.css index 3b7225dd27..0c0905799a 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -16,7 +16,6 @@ /* `.target` objects are interactive */ /* They can be picked up, clicked, hovered, or things can connect to them */ -.layer-blocker.target, .node.target { pointer-events: fill; fill-opacity: 0.8; @@ -35,12 +34,12 @@ } /* `.target-nope` objects are explicitly forbidden to join to */ -.layer-blocker.target.target-nope, .node.target.target-nope, .way.target.target-nope { cursor: not-allowed; } + /* `.active` objects (currently being drawn or dragged) are not interactive */ /* This is important to allow the events to drop through to whatever is */ /* below them on the map, so you can still hover and connect to other things. */ diff --git a/css/55_cursors.css b/css/55_cursors.css index 9e72d7ddfa..0e4732b586 100644 --- a/css/55_cursors.css +++ b/css/55_cursors.css @@ -1,5 +1,9 @@ /* Cursors */ +.nope { + cursor: not-allowed !important; +} + .map-in-map, #map { cursor: auto; /* Opera */ diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 3759a1c3dc..d165949d5f 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -28,7 +28,6 @@ import { import { modeBrowse, modeSelect } from './index'; import { osmNode } from '../osm'; -import { svgBlocker } from '../svg'; import { uiFlash } from '../ui'; @@ -40,7 +39,6 @@ export function modeDragNode(context) { var hover = behaviorHover(context).altDisables(true) .on('hover', context.ui().sidebar.hover); var edit = behaviorEdit(context); - var blocker = svgBlocker(context.projection, context); var _nudgeInterval; var _restoreSelectedIDs = []; @@ -135,6 +133,7 @@ export function modeDragNode(context) { var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); + var didSnap = false; if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap.. // related code @@ -142,16 +141,22 @@ export function modeDragNode(context) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` var d = datum(); - var target = d && d.id && context.hasEntity(d.id); + var nodegroups = d && d.properties && d.properties.nodes; if (d.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` loc = d.loc; - } else if (target && target.type === 'way') { // snap to way - var choice = geoChooseEdge( - context.childNodes(target), context.mouse(), context.projection, entity.id - ); - if (choice) { - loc = choice.loc; + didSnap = true; + + } else if (nodegroups) { // snap to way - a line touch target or nope target with nodes + var best = Infinity; + for (var i = 0; i < nodegroups.length; i++) { + var childNodes = nodegroups[i].map(function(id) { return context.entity(id); }); + var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, entity.id); + if (choice && choice.distance < best) { + best = choice.distance; + loc = choice.loc; + didSnap = true; + } } } } @@ -162,17 +167,23 @@ export function modeDragNode(context) { ); - checkGeometry(entity); + // check if this movement causes the geometry to break + var doBlock = false; + if (!didSnap) { + doBlock = invalidGeometry(entity, context.graph()); + } + + context.surface() + .classed('nope', doBlock); + _lastLoc = loc; } - function checkGeometry(entity) { - var doBlock = false; - var graph = context.graph(); + function invalidGeometry(entity, graph) { var parents = graph.parentWays(entity); - function checkSelfIntersections(way, activeID) { + function hasSelfIntersections(way, activeID) { // check active (dragged) segments against inactive segments var actives = []; var inactives = []; @@ -206,15 +217,13 @@ export function modeDragNode(context) { for (var i = 0; i < parents.length; i++) { var parent = parents[i]; if (parent.isClosed()) { // check for self intersections - if (checkSelfIntersections(parent, entity.id)) { - doBlock = true; - break; + if (hasSelfIntersections(parent, entity.id)) { + return true; } } } - d3_select('.data-layer-osm') - .call(doBlock ? blocker : blocker.off); + return false; } @@ -238,7 +247,7 @@ export function modeDragNode(context) { if (_isCancelled) return; var d = datum(); - var nope = d && d.id && /-nope$/.test(d.id); // can't drag here + var nope = (d && d.id && /-nope$/.test(d.id)) || context.surface().classed('nope'); var target = d && d.id && context.hasEntity(d.id); // entity to snap to if (nope) { // bounce back @@ -337,10 +346,8 @@ export function modeDragNode(context) { _activeEntity = null; - d3_select('.data-layer-osm') - .call(blocker.off); - context.surface() + .classed('nope', false) .selectAll('.active') .classed('active', false); diff --git a/modules/svg/blocker.js b/modules/svg/blocker.js deleted file mode 100644 index 93a3a5f1cd..0000000000 --- a/modules/svg/blocker.js +++ /dev/null @@ -1,26 +0,0 @@ -export function svgBlocker(projection, context) { - - function blocker(selection) { - var dimensions = projection.clipExtent()[1]; - var fillClass = context.getDebug('target') ? 'red ' : 'nocolor '; - - var blocker = selection.selectAll('.layer-blocker') - .data([{id: 'target-nope'}]); - - blocker.enter() - .append('rect') - .attr('class', 'layer-blocker target target-nope ' + fillClass) - .attr('x', 0) - .attr('y', 0) - .merge(blocker) - .attr('width', dimensions[0]) - .attr('height', dimensions[1]); - } - - blocker.off = function(selection) { - selection.selectAll('.layer-blocker') - .remove(); - }; - - return blocker; -} diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index bbd22f11d4..b4d00264e0 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -180,14 +180,17 @@ export function svgRelationMemberTags(graph) { export function svgSegmentWay(way, graph, activeID) { var features = { passive: [], active: [] }; var coordGroups = { passive: [], active: [] }; - var segment = []; + var nodeGroups = { passive: [], active: [] }; + var coords = []; + var nodes = []; var startType = null; // 0 = active, 1 = passive, 2 = adjacent var currType = null; var node; for (var i = 0; i < way.nodes.length; i++) { if (way.nodes[i] === activeID) { // vertex is the activeID - segment = []; // draw no segment here + coords = []; // draw no segment here + nodes = []; startType = null; continue; } @@ -201,32 +204,41 @@ export function svgSegmentWay(way, graph, activeID) { if (currType !== startType) { // line changes here - try to save a segment - if (segment.length > 0) { // finish previous segment - segment.push(node.loc); + if (coords.length > 0) { // finish previous segment + coords.push(node.loc); + nodes.push(node.id); if (startType === 2 || currType === 2) { // one adjacent vertex - coordGroups.active.push(segment); + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); } else if (startType === 0 && currType === 0) { // both active vertices - coordGroups.active.push(segment); + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); } else { - coordGroups.passive.push(segment); + coordGroups.passive.push(coords); + nodeGroups.passive.push(nodes); } } - segment = []; + coords = []; + nodes = []; startType = currType; } - segment.push(node.loc); + coords.push(node.loc); + nodes.push(node.id); } // complete whatever segment we ended on - if (segment.length > 1) { + if (coords.length > 1) { if (startType === 2 || currType === 2) { // one adjacent vertex - coordGroups.active.push(segment); + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); } else if (startType === 0 && currType === 0) { // both active vertices - coordGroups.active.push(segment); + coordGroups.active.push(coords); + nodeGroups.active.push(nodes); } else { - coordGroups.passive.push(segment); + coordGroups.passive.push(coords); + nodeGroups.passive.push(nodes); } } @@ -234,6 +246,9 @@ export function svgSegmentWay(way, graph, activeID) { features.passive.push({ 'type': 'Feature', 'id': way.id, + 'properties': { + 'nodes': nodeGroups.passive + }, 'geometry': { 'type': 'MultiLineString', 'coordinates': coordGroups.passive @@ -246,7 +261,8 @@ export function svgSegmentWay(way, graph, activeID) { 'type': 'Feature', 'id': way.id + '-nope', // break the ids on purpose 'properties': { - 'originalID': way.id + 'originalID': way.id, + 'nodes': nodeGroups.active }, 'geometry': { 'type': 'MultiLineString', diff --git a/modules/svg/index.js b/modules/svg/index.js index 798893dbb7..68e2f2f289 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -1,5 +1,4 @@ export { svgAreas } from './areas.js'; -export { svgBlocker } from './blocker.js'; export { svgDebug } from './debug.js'; export { svgDefs } from './defs.js'; export { svgGpx } from './gpx.js'; From 96afbbd7852de0c584b744774ce7c4b539d47ccc Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 28 Dec 2017 01:08:11 -0500 Subject: [PATCH 078/206] Refactor vector math functions from geo.js to vector.js --- modules/actions/circularize.js | 20 +++--- modules/actions/move.js | 4 +- modules/actions/move_node.js | 4 +- modules/actions/orthogonalize.js | 11 ++- modules/actions/reflect.js | 10 +-- modules/actions/straighten.js | 8 +-- modules/behavior/draw.js | 6 +- modules/behavior/select.js | 4 +- modules/geo/geo.js | 89 +++++------------------- modules/geo/index.js | 19 ++--- modules/geo/vector.js | 62 +++++++++++++++++ modules/index.js | 4 ++ modules/modes/drag_node.js | 6 +- modules/modes/rotate.js | 4 +- modules/osm/way.js | 4 +- modules/renderer/tile_layer.js | 4 +- modules/svg/helpers.js | 4 +- modules/svg/labels.js | 12 ++-- modules/svg/midpoints.js | 14 ++-- test/index.html | 1 + test/spec/actions/circularize.js | 8 +-- test/spec/geo/geo.js | 103 --------------------------- test/spec/geo/vector.js | 115 +++++++++++++++++++++++++++++++ 23 files changed, 268 insertions(+), 248 deletions(-) create mode 100644 modules/geo/vector.js create mode 100644 test/spec/geo/vector.js diff --git a/modules/actions/circularize.js b/modules/actions/circularize.js index dc66825acf..908edb3497 100644 --- a/modules/actions/circularize.js +++ b/modules/actions/circularize.js @@ -10,11 +10,7 @@ import { polygonCentroid as d3_polygonCentroid } from 'd3-polygon'; -import { - geoEuclideanDistance, - geoInterp -} from '../geo'; - +import { geoVecInterp, geoVecLength } from '../geo'; import { osmNode } from '../osm'; @@ -41,8 +37,8 @@ export function actionCircularize(wayId, projection, maxAngle) { keyNodes = nodes.filter(function(n) { return graph.parentWays(n).length !== 1; }), points = nodes.map(function(n) { return projection(n.loc); }), keyPoints = keyNodes.map(function(n) { return projection(n.loc); }), - centroid = (points.length === 2) ? geoInterp(points[0], points[1], 0.5) : d3_polygonCentroid(points), - radius = d3_median(points, function(p) { return geoEuclideanDistance(centroid, p); }), + centroid = (points.length === 2) ? geoVecInterp(points[0], points[1], 0.5) : d3_polygonCentroid(points), + radius = d3_median(points, function(p) { return geoVecLength(centroid, p); }), sign = d3_polygonArea(points) > 0 ? 1 : -1, ids; @@ -82,7 +78,7 @@ export function actionCircularize(wayId, projection, maxAngle) { } // position this key node - var distance = geoEuclideanDistance(centroid, keyPoints[i]); + var distance = geoVecLength(centroid, keyPoints[i]); if (distance === 0) { distance = 1e-4; } keyPoints[i] = [ centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius, @@ -91,7 +87,7 @@ export function actionCircularize(wayId, projection, maxAngle) { loc = projection.invert(keyPoints[i]); node = keyNodes[i]; origNode = origNodes[node.id]; - node = node.move(geoInterp(origNode.loc, loc, t)); + node = node.move(geoVecInterp(origNode.loc, loc, t)); graph = graph.replace(node); // figure out the between delta angle we want to match to @@ -122,7 +118,7 @@ export function actionCircularize(wayId, projection, maxAngle) { origNode = origNodes[node.id]; nearNodes[node.id] = angle; - node = node.move(geoInterp(origNode.loc, loc, t)); + node = node.move(geoVecInterp(origNode.loc, loc, t)); graph = graph.replace(node); } @@ -145,7 +141,7 @@ export function actionCircularize(wayId, projection, maxAngle) { } } - node = osmNode({ loc: geoInterp(origNode.loc, loc, t) }); + node = osmNode({ loc: geoVecInterp(origNode.loc, loc, t) }); graph = graph.replace(node); nodes.splice(endNodeIndex + j, 0, node); @@ -220,7 +216,7 @@ export function actionCircularize(wayId, projection, maxAngle) { // move interior nodes to the surface of the convex hull.. for (var j = 1; j < indexRange; j++) { - var point = geoInterp(hull[i], hull[i+1], j / indexRange), + var point = geoVecInterp(hull[i], hull[i+1], j / indexRange), node = nodes[(j + startIndex) % nodes.length].move(projection.invert(point)); graph = graph.replace(node); } diff --git a/modules/actions/move.js b/modules/actions/move.js index baa75fb328..1f400085c6 100644 --- a/modules/actions/move.js +++ b/modules/actions/move.js @@ -13,11 +13,11 @@ import { osmNode } from '../osm'; import { geoAngle, geoChooseEdge, - geoInterp, geoPathIntersections, geoPathLength, geoSphericalDistance, geoVecAdd, + geoVecInterp, geoVecSubtract } from '../geo'; @@ -206,7 +206,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { if (!isEP1 && !isEP2) { var epsilon = 1e-4, maxIter = 10; for (var i = 0; i < maxIter; i++) { - loc = geoInterp(edge1.loc, edge2.loc, 0.5); + loc = geoVecInterp(edge1.loc, edge2.loc, 0.5); edge1 = geoChooseEdge(nodes1, projection(loc), projection); edge2 = geoChooseEdge(nodes2, projection(loc), projection); if (Math.abs(edge1.distance - edge2.distance) < epsilon) break; diff --git a/modules/actions/move_node.js b/modules/actions/move_node.js index 8de29a16a7..4288dcbc59 100644 --- a/modules/actions/move_node.js +++ b/modules/actions/move_node.js @@ -1,4 +1,4 @@ -import { geoInterp } from '../geo'; +import { geoVecInterp } from '../geo'; export function actionMoveNode(nodeID, toLoc) { @@ -8,7 +8,7 @@ export function actionMoveNode(nodeID, toLoc) { var node = graph.entity(nodeID); return graph.replace( - node.move(geoInterp(node.loc, toLoc, t)) + node.move(geoVecInterp(node.loc, toLoc, t)) ); }; diff --git a/modules/actions/orthogonalize.js b/modules/actions/orthogonalize.js index 33b4a4c616..b9d64b205f 100644 --- a/modules/actions/orthogonalize.js +++ b/modules/actions/orthogonalize.js @@ -2,10 +2,7 @@ import _clone from 'lodash-es/clone'; import _uniq from 'lodash-es/uniq'; import { actionDeleteNode } from './delete_node'; -import { - geoEuclideanDistance, - geoInterp -} from '../geo'; +import { geoVecInterp, geoVecLength } from '../geo'; /* @@ -40,7 +37,7 @@ export function actionOrthogonalize(wayId, projection) { node = graph.entity(nodes[corner.i].id); loc = projection.invert(points[corner.i]); - graph = graph.replace(node.move(geoInterp(node.loc, loc, t))); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t))); } else { var best, @@ -69,7 +66,7 @@ export function actionOrthogonalize(wayId, projection) { if (originalPoints[i][0] !== points[i][0] || originalPoints[i][1] !== points[i][1]) { loc = projection.invert(points[i]); node = graph.entity(nodes[i].id); - graph = graph.replace(node.move(geoInterp(node.loc, loc, t))); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t))); } } @@ -100,7 +97,7 @@ export function actionOrthogonalize(wayId, projection) { q = subtractPoints(c, b), scale, dotp; - scale = 2 * Math.min(geoEuclideanDistance(p, [0, 0]), geoEuclideanDistance(q, [0, 0])); + scale = 2 * Math.min(geoVecLength(p, [0, 0]), geoVecLength(q, [0, 0])); p = normalizePoint(p, 1.0); q = normalizePoint(q, 1.0); diff --git a/modules/actions/reflect.js b/modules/actions/reflect.js index b27c9eadf0..be435c5f90 100644 --- a/modules/actions/reflect.js +++ b/modules/actions/reflect.js @@ -4,10 +4,10 @@ import { } from 'd3-polygon'; import { - geoEuclideanDistance, geoExtent, - geoInterp, - geoRotate + geoRotate, + geoVecInterp, + geoVecLength } from '../geo'; import { utilGetAllNodes } from '../util'; @@ -69,7 +69,7 @@ export function actionReflect(reflectIds, projection) { q2 = [(ssr.poly[1][0] + ssr.poly[2][0]) / 2, (ssr.poly[1][1] + ssr.poly[2][1]) / 2 ], p, q; - var isLong = (geoEuclideanDistance(p1, q1) > geoEuclideanDistance(p2, q2)); + var isLong = (geoVecLength(p1, q1) > geoVecLength(p2, q2)); if ((useLongAxis && isLong) || (!useLongAxis && !isLong)) { p = p1; q = q1; @@ -92,7 +92,7 @@ export function actionReflect(reflectIds, projection) { b * (c[0] - p[0]) - a * (c[1] - p[1]) + p[1] ]; var loc2 = projection.invert(c2); - node = node.move(geoInterp(node.loc, loc2, t)); + node = node.move(geoVecInterp(node.loc, loc2, t)); graph = graph.replace(node); } diff --git a/modules/actions/straighten.js b/modules/actions/straighten.js index e16bdd6136..e9456d220a 100644 --- a/modules/actions/straighten.js +++ b/modules/actions/straighten.js @@ -1,8 +1,8 @@ import { actionDeleteNode } from './delete_node'; import { - geoEuclideanDistance, - geoInterp + geoVecInterp, + geoVecLength } from '../geo'; @@ -44,7 +44,7 @@ export function actionStraighten(wayId, projection) { ], loc2 = projection.invert(p); - graph = graph.replace(node.move(geoInterp(node.loc, loc2, t))); + graph = graph.replace(node.move(geoVecInterp(node.loc, loc2, t))); } else { // safe to delete @@ -69,7 +69,7 @@ export function actionStraighten(wayId, projection) { points = nodes.map(function(n) { return projection(n.loc); }), startPoint = points[0], endPoint = points[points.length-1], - threshold = 0.2 * geoEuclideanDistance(startPoint, endPoint), + threshold = 0.2 * geoVecLength(startPoint, endPoint), i; if (threshold === 0) { diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 513c0b30ce..77315e03b2 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -14,7 +14,7 @@ import { behaviorTail } from './tail'; import { geoChooseEdge, - geoEuclideanDistance, + geoVecLength, geoViewportEdge } from '../geo'; @@ -81,7 +81,7 @@ export function behaviorDraw(context) { d3_select(window).on('mouseup.draw', function() { var t2 = +new Date(); var p2 = point(); - var dist = geoEuclideanDistance(p1, p2); + var dist = geoVecLength(p1, p2); element.on('mousemove.draw', mousemove); d3_select(window).on('mouseup.draw', null); @@ -157,7 +157,7 @@ export function behaviorDraw(context) { var currSpace = context.mouse(); if (_disableSpace && _lastSpace) { - var dist = geoEuclideanDistance(_lastSpace, currSpace); + var dist = geoVecLength(_lastSpace, currSpace); if (dist > tolerance) { _disableSpace = false; } diff --git a/modules/behavior/select.js b/modules/behavior/select.js index a77044133a..98799e6258 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -6,7 +6,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoEuclideanDistance } from '../geo'; +import { geoVecLength } from '../geo'; import { modeBrowse, @@ -103,7 +103,7 @@ export function behaviorSelect(context) { if (!p1) return; var p2 = point(); - var dist = geoEuclideanDistance(p1, p2); + var dist = geoVecLength(p1, p2); p1 = null; if (dist > tolerance) { diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 7a4fe121a3..0c5a277789 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -1,6 +1,15 @@ import _every from 'lodash-es/every'; import _some from 'lodash-es/some'; +import { + geoVecAngle, + geoVecCross, + geoVecDot, + geoVecInterp, + geoVecLength, + geoVecSubtract +} from './vector.js'; + // constants var TAU = 2 * Math.PI; @@ -8,66 +17,6 @@ var EQUATORIAL_RADIUS = 6356752.314245179; var POLAR_RADIUS = 6378137.0; -// vector addition -export function geoVecEquals(a, b) { - return (a[0] === b[0]) && (a[1] === b[1]); -} - -// vector addition -export function geoVecAdd(a, b) { - return [ a[0] + b[0], a[1] + b[1] ]; -} - -// vector subtraction -export function geoVecSubtract(a, b) { - return [ a[0] - b[0], a[1] - b[1] ]; -} - -// vector multiplication -export function geoVecScale(a, b) { - return [ a[0] * b, a[1] * b ]; -} - -// vector rounding (was: geoRoundCoordinates) -export function geoVecFloor(a) { - return [ Math.floor(a[0]), Math.floor(a[1]) ]; -} - -// linear interpolation -export function geoInterp(p1, p2, t) { - return [ - p1[0] + (p2[0] - p1[0]) * t, - p1[1] + (p2[1] - p1[1]) * t - ]; -} - - -// dot product -export function geoDot(a, b, origin) { - origin = origin || [0, 0]; - return (a[0] - origin[0]) * (b[0] - origin[0]) + - (a[1] - origin[1]) * (b[1] - origin[1]); -} - - -// 2D cross product of OA and OB vectors, returns magnitude of Z vector -// Returns a positive value, if OAB makes a counter-clockwise turn, -// negative for clockwise turn, and zero if the points are collinear. -export function geoCross(a, b, origin) { - origin = origin || [0, 0]; - return (a[0] - origin[0]) * (b[1] - origin[1]) - - (a[1] - origin[1]) * (b[0] - origin[0]); -} - - -// http://jsperf.com/id-dist-optimization -export function geoEuclideanDistance(a, b) { - var x = a[0] - b[0]; - var y = a[1] - b[1]; - return Math.sqrt((x * x) + (y * y)); -} - - export function geoLatToMeters(dLat) { return dLat * (TAU * POLAR_RADIUS / 360); } @@ -140,9 +89,7 @@ export function geoEdgeEqual(a, b) { // Return the counterclockwise angle in the range (-pi, pi) // between the positive X axis and the line intersecting a and b. export function geoAngle(a, b, projection) { - a = projection(a.loc); - b = projection(b.loc); - return Math.atan2(b[1] - a[1], b[0] - a[0]); + return geoVecAngle(projection(a.loc), projection(b.loc)); } @@ -163,7 +110,7 @@ export function geoRotate(points, angle, around) { // the closest vertex on that edge. Returns an object with the `index` of the // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. export function geoChooseEdge(nodes, point, projection, skipID) { - var dist = geoEuclideanDistance; + var dist = geoVecLength; var points = nodes.map(function(n) { return projection(n.loc); }); var ids = nodes.map(function(n) { return n.id; }); var min = Infinity; @@ -176,7 +123,7 @@ export function geoChooseEdge(nodes, point, projection, skipID) { var o = points[i]; var s = geoVecSubtract(points[i + 1], o); var v = geoVecSubtract(point, o); - var proj = geoDot(v, s) / geoDot(s, s); + var proj = geoVecDot(v, s) / geoVecDot(s, s); var p; if (proj < 0) { @@ -214,15 +161,15 @@ export function geoLineIntersection(a, b) { var q2 = [b[1][0], b[1][1]]; var r = geoVecSubtract(p2, p); var s = geoVecSubtract(q2, q); - var uNumerator = geoCross(geoVecSubtract(q, p), r); - var denominator = geoCross(r, s); + var uNumerator = geoVecCross(geoVecSubtract(q, p), r); + var denominator = geoVecCross(r, s); if (uNumerator && denominator) { var u = uNumerator / denominator; - var t = geoCross(geoVecSubtract(q, p), s) / denominator; + var t = geoVecCross(geoVecSubtract(q, p), s) / denominator; if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { - return geoInterp(p, p2, t); + return geoVecInterp(p, p2, t); } } @@ -284,7 +231,7 @@ export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { function testSegments(outer, inner) { for (var i = 0; i < outer.length - 1; i++) { for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i +1 ] ]; + var a = [ outer[i], outer[i + 1] ]; var b = [ inner[j], inner[j + 1] ]; if (geoLineIntersection(a, b)) return true; } @@ -305,7 +252,7 @@ export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { export function geoPathLength(path) { var length = 0; for (var i = 0; i < path.length - 1; i++) { - length += geoEuclideanDistance(path[i], path[i + 1]); + length += geoVecLength(path[i], path[i + 1]); } return length; } diff --git a/modules/geo/index.js b/modules/geo/index.js index 95bba82c51..dca2f31dd7 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -1,11 +1,7 @@ export { geoAngle } from './geo.js'; export { geoChooseEdge } from './geo.js'; -export { geoCross } from './geo.js'; -export { geoDot } from './geo.js'; export { geoEdgeEqual } from './geo.js'; -export { geoEuclideanDistance } from './geo.js'; export { geoExtent } from './extent.js'; -export { geoInterp } from './geo.js'; export { geoRawMercator } from './raw_mercator.js'; export { geoRotate } from './geo.js'; export { geoLatToMeters } from './geo.js'; @@ -22,10 +18,15 @@ export { geoPolygonContainsPolygon } from './geo.js'; export { geoPolygonIntersectsPolygon } from './geo.js'; export { geoScaleToZoom } from './geo.js'; export { geoSphericalDistance } from './geo.js'; -export { geoVecAdd } from './geo.js'; -export { geoVecEquals } from './geo.js'; -export { geoVecFloor } from './geo.js'; -export { geoVecSubtract } from './geo.js'; -export { geoVecScale } from './geo.js'; +export { geoVecAdd } from './vector.js'; +export { geoVecAngle } from './vector.js'; +export { geoVecCross } from './vector.js'; +export { geoVecDot } from './vector.js'; +export { geoVecEqual } from './vector.js'; +export { geoVecFloor } from './vector.js'; +export { geoVecInterp } from './vector.js'; +export { geoVecLength } from './vector.js'; +export { geoVecSubtract } from './vector.js'; +export { geoVecScale } from './vector.js'; export { geoZoomToScale } from './geo.js'; export { geoViewportEdge } from './geo.js'; diff --git a/modules/geo/vector.js b/modules/geo/vector.js new file mode 100644 index 0000000000..0e7929b3ba --- /dev/null +++ b/modules/geo/vector.js @@ -0,0 +1,62 @@ +// vector equals +export function geoVecEqual(a, b) { + return (a[0] === b[0]) && (a[1] === b[1]); +} + +// vector addition +export function geoVecAdd(a, b) { + return [ a[0] + b[0], a[1] + b[1] ]; +} + +// vector subtraction +export function geoVecSubtract(a, b) { + return [ a[0] - b[0], a[1] - b[1] ]; +} + +// vector multiplication +export function geoVecScale(a, b) { + return [ a[0] * b, a[1] * b ]; +} + +// vector rounding (was: geoRoundCoordinates) +export function geoVecFloor(a) { + return [ Math.floor(a[0]), Math.floor(a[1]) ]; +} + +// linear interpolation +export function geoVecInterp(a, b, t) { + return [ + a[0] + (b[0] - a[0]) * t, + a[1] + (b[1] - a[1]) * t + ]; +} + +// http://jsperf.com/id-dist-optimization +export function geoVecLength(a, b) { + var x = a[0] - b[0]; + var y = a[1] - b[1]; + return Math.sqrt((x * x) + (y * y)); +} + +// Return the counterclockwise angle in the range (-pi, pi) +// between the positive X axis and the line intersecting a and b. +export function geoVecAngle(a, b) { + return Math.atan2(b[1] - a[1], b[0] - a[0]); +} + +// dot product +export function geoVecDot(a, b, origin) { + origin = origin || [0, 0]; + return (a[0] - origin[0]) * (b[0] - origin[0]) + + (a[1] - origin[1]) * (b[1] - origin[1]); +} + +// 2D cross product of OA and OB vectors, returns magnitude of Z vector +// Returns a positive value, if OAB makes a counter-clockwise turn, +// negative for clockwise turn, and zero if the points are collinear. +export function geoVecCross(a, b, origin) { + origin = origin || [0, 0]; + return (a[0] - origin[0]) * (b[1] - origin[1]) - + (a[1] - origin[1]) * (b[0] - origin[0]); +} + diff --git a/modules/index.js b/modules/index.js index 981a047dbe..dcd7f276e8 100644 --- a/modules/index.js +++ b/modules/index.js @@ -29,6 +29,10 @@ export { coreDifference as Difference } from './core/difference'; export { coreGraph as Graph } from './core/graph'; export { coreHistory as History } from './core/history'; export { coreTree as Tree } from './core/tree'; +export { geoVecCross as geoCross } from './geo/vector'; +export { geoVecInterp as geoInterp } from './geo/vector'; +export { geoVecFloor as geoRoundCoordinates } from './geo/vector'; +export { geoVecLength as geoEuclideanDistance } from './geo/vector'; export { osmEntity as Entity } from './osm/entity'; export { osmNode as Node } from './osm/node'; export { osmRelation as Relation } from './osm/relation'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index d165949d5f..3809a352f6 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -21,7 +21,7 @@ import { import { geoChooseEdge, geoLineIntersection, - geoVecEquals, + geoVecEqual, geoVecSubtract, geoViewportEdge } from '../geo'; @@ -203,8 +203,8 @@ export function modeDragNode(context) { var p = actives[j]; var q = inactives[k]; // skip if segments share an endpoint - if (geoVecEquals(p[1], q[0]) || geoVecEquals(p[0], q[1]) || - geoVecEquals(p[0], q[0]) || geoVecEquals(p[1], q[1]) ) { + if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) || + geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) { continue; } else if (geoLineIntersection(p, q)) { return true; diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index d889a174b1..4addaed887 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -13,7 +13,7 @@ import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; import { actionRotate } from '../actions'; import { behaviorEdit } from '../behavior'; -import { geoInterp } from '../geo'; +import { geoVecInterp } from '../geo'; import { modeBrowse, @@ -80,7 +80,7 @@ export function modeRotate(context, entityIDs) { if (points.length === 1) { // degenerate case _pivot = points[0]; } else if (points.length === 2) { - _pivot = geoInterp(points[0], points[1], 0.5); + _pivot = geoVecInterp(points[0], points[1], 0.5); } else { _pivot = d3_polygonCentroid(d3_polygonHull(points)); } diff --git a/modules/osm/way.js b/modules/osm/way.js index c932241461..14e4e410ce 100644 --- a/modules/osm/way.js +++ b/modules/osm/way.js @@ -4,7 +4,7 @@ import _uniq from 'lodash-es/uniq'; import { geoArea as d3_geoArea } from 'd3-geo'; -import { geoExtent, geoCross } from '../geo'; +import { geoExtent, geoVecCross } from '../geo'; import { osmEntity } from './entity'; import { osmLanes } from './lanes'; import { osmOneWayTags } from './tags'; @@ -142,7 +142,7 @@ _extend(osmWay.prototype, { var o = coords[(i+1) % coords.length]; var a = coords[i]; var b = coords[(i+2) % coords.length]; - var res = geoCross(a, b, o); + var res = geoVecCross(a, b, o); curr = (res > 0) ? 1 : (res < 0) ? -1 : 0; if (curr === 0) { diff --git a/modules/renderer/tile_layer.js b/modules/renderer/tile_layer.js index 9d143f0456..b0fb42aa32 100644 --- a/modules/renderer/tile_layer.js +++ b/modules/renderer/tile_layer.js @@ -2,7 +2,7 @@ import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; -import { geoEuclideanDistance, geoScaleToZoom } from '../geo'; +import { geoScaleToZoom, geoVecLength } from '../geo'; import { utilPrefixCSSProperty } from '../util'; @@ -187,7 +187,7 @@ export function rendererTileLayer(context) { requests.forEach(function(d) { var c = tileCenter(d); - var dist = geoEuclideanDistance(c, mapCenter); + var dist = geoVecLength(c, mapCenter); if (dist < minDist) { minDist = dist; nearCenter = d; diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index b4d00264e0..40f28d0a6c 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -6,7 +6,7 @@ import { geoStream as d3_geoStream } from 'd3-geo'; -import { geoEuclideanDistance } from '../geo'; +import { geoVecLength } from '../geo'; // Touch targets control which other vertices we can drag a vertex onto. @@ -82,7 +82,7 @@ export function svgOneWaySegments(projection, graph, dt) { b = [x, y]; if (a) { - var span = geoEuclideanDistance(a, b) - offset; + var span = geoVecLength(a, b) - offset; if (span >= 0) { var angle = Math.atan2(b[1] - a[1], b[0] - a[0]); diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 15c3d2b08f..c79448d75d 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -10,11 +10,11 @@ import { textDirection } from '../util/locale'; import { geoExtent, - geoEuclideanDistance, - geoInterp, geoPolygonIntersectsPolygon, geoPathLength, - geoScaleToZoom + geoScaleToZoom, + geoVecInterp, + geoVecLength } from '../geo'; import { osmEntity } from '../osm'; @@ -489,10 +489,10 @@ export function svgLabels(projection, context) { var b = sub[j + 1]; // split up the text into small collision boxes - var num = Math.max(1, Math.floor(geoEuclideanDistance(a, b) / boxsize / 2)); + var num = Math.max(1, Math.floor(geoVecLength(a, b) / boxsize / 2)); for (var box = 0; box < num; box++) { - var p = geoInterp(a, b, box / num); + var p = geoVecInterp(a, b, box / num); var x0 = p[0] - boxsize - padding; var y0 = p[1] - boxsize - padding; var x1 = p[0] + boxsize + padding; @@ -532,7 +532,7 @@ export function svgLabels(projection, context) { for (var i = 0; i < points.length - 1; i++) { var a = points[i]; var b = points[i + 1]; - var current = geoEuclideanDistance(a, b); + var current = geoVecLength(a, b); var portion; if (!start && sofar + current >= from) { portion = (from - sofar) / current; diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index ed629c6c6b..bcf9495b22 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -7,9 +7,9 @@ import { import { geoAngle, - geoEuclideanDistance, - geoInterp, - geoLineIntersection + geoLineIntersection, + geoVecInterp, + geoVecLength } from '../geo'; @@ -73,8 +73,8 @@ export function svgMidpoints(projection, context) { if (midpoints[id]) { midpoints[id].parents.push(entity); } else { - if (geoEuclideanDistance(projection(a.loc), projection(b.loc)) > 40) { - var point = geoInterp(a.loc, b.loc, 0.5); + if (geoVecLength(projection(a.loc), projection(b.loc)) > 40) { + var point = geoVecInterp(a.loc, b.loc, 0.5); var loc = null; if (extent.intersects(point)) { @@ -83,8 +83,8 @@ export function svgMidpoints(projection, context) { for (var k = 0; k < 4; k++) { point = geoLineIntersection([a.loc, b.loc], [poly[k], poly[k + 1]]); if (point && - geoEuclideanDistance(projection(a.loc), projection(point)) > 20 && - geoEuclideanDistance(projection(b.loc), projection(point)) > 20) + geoVecLength(projection(a.loc), projection(point)) > 20 && + geoVecLength(projection(b.loc), projection(point)) > 20) { loc = point; break; diff --git a/test/index.html b/test/index.html index 18092851b3..ed2cb0c548 100644 --- a/test/index.html +++ b/test/index.html @@ -74,6 +74,7 @@ + diff --git a/test/spec/actions/circularize.js b/test/spec/actions/circularize.js index fb54bbd055..5d7e4c73ca 100644 --- a/test/spec/actions/circularize.js +++ b/test/spec/actions/circularize.js @@ -5,7 +5,7 @@ describe('iD.actionCircularize', function () { var points = graph.childNodes(graph.entity(id)) .map(function (n) { return projection(n.loc); }), centroid = d3.polygonCentroid(points), - radius = iD.geoEuclideanDistance(centroid, points[0]), + radius = iD.geoVecLength(centroid, points[0]), estArea = Math.PI * radius * radius, trueArea = Math.abs(d3.polygonArea(points)), pctDiff = (estArea - trueArea) / estArea; @@ -31,10 +31,10 @@ describe('iD.actionCircularize', function () { vector2 = [point2[0] - center[0], point2[1] - center[1]], distance; - distance = iD.geoEuclideanDistance(vector1, [0, 0]); + distance = iD.geoVecLength(vector1, [0, 0]); vector1 = [vector1[0] / distance, vector1[1] / distance]; - distance = iD.geoEuclideanDistance(vector2, [0, 0]); + distance = iD.geoVecLength(vector2, [0, 0]); vector2 = [vector2[0] / distance, vector2[1] / distance]; return 180 / Math.PI * Math.acos(vector1[0] * vector2[0] + vector1[1] * vector2[1]); @@ -106,7 +106,7 @@ describe('iD.actionCircularize', function () { graph = iD.actionCircularize('-', projection)(graph); expect(isCircular('-', graph)).to.be.ok; - expect(iD.geoEuclideanDistance(graph.entity('d').loc, [2, -2])).to.be.lt(0.5); + expect(iD.geoVecLength(graph.entity('d').loc, [2, -2])).to.be.lt(0.5); }); it('creates circle respecting min-angle limit', function() { diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 585e7a2e76..9f31f1d272 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,108 +1,5 @@ describe('iD.geo', function() { - describe('geoVecEquals', function() { - it('tests vectors for equality', function() { - expect(iD.geoVecEquals([1, 2], [1, 2])).to.be.true; - expect(iD.geoVecEquals([1, 2], [1, 0])).to.be.false; - expect(iD.geoVecEquals([1, 2], [2, 1])).to.be.false; - }); - }); - - describe('geoVecAdd', function() { - it('adds vectors', function() { - expect(iD.geoVecAdd([1, 2], [3, 4])).to.eql([4, 6]); - expect(iD.geoVecAdd([1, 2], [0, 0])).to.eql([1, 2]); - expect(iD.geoVecAdd([1, 2], [-3, -4])).to.eql([-2, -2]); - }); - }); - - describe('geoVecSubtract', function() { - it('subtracts vectors', function() { - expect(iD.geoVecSubtract([1, 2], [3, 4])).to.eql([-2, -2]); - expect(iD.geoVecSubtract([1, 2], [0, 0])).to.eql([1, 2]); - expect(iD.geoVecSubtract([1, 2], [-3, -4])).to.eql([4, 6]); - }); - }); - - describe('geoVecScale', function() { - it('multiplies vectors', function() { - expect(iD.geoVecScale([1, 2], 0)).to.eql([0, 0]); - expect(iD.geoVecScale([1, 2], 1)).to.eql([1, 2]); - expect(iD.geoVecScale([1, 2], 2)).to.eql([2, 4]); - expect(iD.geoVecScale([1, 2], 0.5)).to.eql([0.5, 1]); - }); - }); - - describe('geoVecFloor (was: geoRoundCoordinates)', function() { - it('rounds vectors', function() { - expect(iD.geoVecFloor([0.1, 1])).to.eql([0, 1]); - expect(iD.geoVecFloor([0, 1])).to.eql([0, 1]); - expect(iD.geoVecFloor([0, 1.1])).to.eql([0, 1]); - }); - }); - - describe('geoInterp', function() { - it('interpolates halfway', function() { - var a = [0, 0]; - var b = [10, 10]; - expect(iD.geoInterp(a, b, 0.5)).to.eql([5, 5]); - }); - it('interpolates to one side', function() { - var a = [0, 0]; - var b = [10, 10]; - expect(iD.geoInterp(a, b, 0)).to.eql([0, 0]); - }); - }); - - describe('geoDot', function() { - it('dot product of right angle is zero', function() { - var a = [1, 0]; - var b = [0, 1]; - expect(iD.geoDot(a, b)).to.eql(0); - }); - it('dot product of same vector multiplies', function() { - var a = [2, 0]; - var b = [2, 0]; - expect(iD.geoDot(a, b)).to.eql(4); - }); - }); - - describe('geoCross', function() { - it('2D cross product of right hand turn is positive', function() { - var a = [2, 0]; - var b = [0, 2]; - expect(iD.geoCross(a, b)).to.eql(4); - }); - it('2D cross product of left hand turn is negative', function() { - var a = [2, 0]; - var b = [0, -2]; - expect(iD.geoCross(a, b)).to.eql(-4); - }); - it('2D cross product of colinear points is zero', function() { - var a = [-2, 0]; - var b = [2, 0]; - expect(iD.geoCross(a, b)).to.equal(0); - }); - }); - - describe('geoEuclideanDistance', function() { - it('distance between two same points is zero', function() { - var a = [0, 0]; - var b = [0, 0]; - expect(iD.geoEuclideanDistance(a, b)).to.eql(0); - }); - it('a straight 10 unit line is 10', function() { - var a = [0, 0]; - var b = [10, 0]; - expect(iD.geoEuclideanDistance(a, b)).to.eql(10); - }); - it('a pythagorean triangle is right', function() { - var a = [0, 0]; - var b = [4, 3]; - expect(iD.geoEuclideanDistance(a, b)).to.eql(5); - }); - }); - describe('geoLatToMeters', function() { it('0 degrees latitude is 0 meters', function() { expect(iD.geoLatToMeters(0)).to.eql(0); diff --git a/test/spec/geo/vector.js b/test/spec/geo/vector.js new file mode 100644 index 0000000000..21968bb5a8 --- /dev/null +++ b/test/spec/geo/vector.js @@ -0,0 +1,115 @@ +describe('iD.geo vector', function() { + + describe('geoVecEqual', function() { + it('tests vectors for equality', function() { + expect(iD.geoVecEqual([1, 2], [1, 2])).to.be.true; + expect(iD.geoVecEqual([1, 2], [1, 0])).to.be.false; + expect(iD.geoVecEqual([1, 2], [2, 1])).to.be.false; + }); + }); + + describe('geoVecAdd', function() { + it('adds vectors', function() { + expect(iD.geoVecAdd([1, 2], [3, 4])).to.eql([4, 6]); + expect(iD.geoVecAdd([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecAdd([1, 2], [-3, -4])).to.eql([-2, -2]); + }); + }); + + describe('geoVecSubtract', function() { + it('subtracts vectors', function() { + expect(iD.geoVecSubtract([1, 2], [3, 4])).to.eql([-2, -2]); + expect(iD.geoVecSubtract([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecSubtract([1, 2], [-3, -4])).to.eql([4, 6]); + }); + }); + + describe('geoVecScale', function() { + it('multiplies vectors', function() { + expect(iD.geoVecScale([1, 2], 0)).to.eql([0, 0]); + expect(iD.geoVecScale([1, 2], 1)).to.eql([1, 2]); + expect(iD.geoVecScale([1, 2], 2)).to.eql([2, 4]); + expect(iD.geoVecScale([1, 2], 0.5)).to.eql([0.5, 1]); + }); + }); + + describe('geoVecFloor (was: geoRoundCoordinates)', function() { + it('rounds vectors', function() { + expect(iD.geoVecFloor([0.1, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1.1])).to.eql([0, 1]); + }); + }); + + describe('geoVecInterp', function() { + it('interpolates halfway', function() { + var a = [0, 0]; + var b = [10, 10]; + expect(iD.geoVecInterp(a, b, 0.5)).to.eql([5, 5]); + }); + it('interpolates to one side', function() { + var a = [0, 0]; + var b = [10, 10]; + expect(iD.geoVecInterp(a, b, 0)).to.eql([0, 0]); + }); + }); + + describe('geoVecLength (was: geoEuclideanDistance)', function() { + it('distance between two same points is zero', function() { + var a = [0, 0]; + var b = [0, 0]; + expect(iD.geoVecLength(a, b)).to.eql(0); + }); + it('a straight 10 unit line is 10', function() { + var a = [0, 0]; + var b = [10, 0]; + expect(iD.geoVecLength(a, b)).to.eql(10); + }); + it('a pythagorean triangle is right', function() { + var a = [0, 0]; + var b = [4, 3]; + expect(iD.geoVecLength(a, b)).to.eql(5); + }); + }); + + describe('geoVecAngle', function() { + it('returns angle between a and b', function() { + expect(iD.geoVecAngle([0, 0], [1, 0])).to.be.closeTo(0, 1e-6); + expect(iD.geoVecAngle([0, 0], [0, 1])).to.be.closeTo(Math.PI / 2, 1e-6); + expect(iD.geoVecAngle([0, 0], [-1, 0])).to.be.closeTo(Math.PI, 1e-6); + expect(iD.geoVecAngle([0, 0], [0, -1])).to.be.closeTo(-Math.PI / 2, 1e-6); + }); + }); + + describe('geoVecDot', function() { + it('dot product of right angle is zero', function() { + var a = [1, 0]; + var b = [0, 1]; + expect(iD.geoVecDot(a, b)).to.eql(0); + }); + it('dot product of same vector multiplies', function() { + var a = [2, 0]; + var b = [2, 0]; + expect(iD.geoVecDot(a, b)).to.eql(4); + }); + }); + + describe('geoVecCross', function() { + it('2D cross product of right hand turn is positive', function() { + var a = [2, 0]; + var b = [0, 2]; + expect(iD.geoVecCross(a, b)).to.eql(4); + }); + it('2D cross product of left hand turn is negative', function() { + var a = [2, 0]; + var b = [0, -2]; + expect(iD.geoVecCross(a, b)).to.eql(-4); + }); + it('2D cross product of colinear points is zero', function() { + var a = [-2, 0]; + var b = [2, 0]; + expect(iD.geoVecCross(a, b)).to.equal(0); + }); + }); + +}); From 7af73c10ef70c7f69fd38632bbac06f7ccc7cd81 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 28 Dec 2017 01:28:38 -0500 Subject: [PATCH 079/206] Refactor geometric math functions from geo.js to geom.js --- modules/geo/geo.js | 219 ++--------------------------- modules/geo/geom.js | 213 ++++++++++++++++++++++++++++ modules/geo/index.js | 31 +++-- test/index.html | 1 + test/spec/geo/geo.js | 296 +-------------------------------------- test/spec/geo/geom.js | 297 ++++++++++++++++++++++++++++++++++++++++ test/spec/geo/vector.js | 2 +- 7 files changed, 541 insertions(+), 518 deletions(-) create mode 100644 modules/geo/geom.js create mode 100644 test/spec/geo/geom.js diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 0c5a277789..e0f7aa4368 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -39,20 +39,20 @@ export function geoMetersToLon(m, atLat) { } -export function geoOffsetToMeters(offset, tileSize) { +export function geoMetersToOffset(meters, tileSize) { tileSize = tileSize || 256; return [ - offset[0] * TAU * EQUATORIAL_RADIUS / tileSize, - -offset[1] * TAU * POLAR_RADIUS / tileSize + meters[0] * tileSize / (TAU * EQUATORIAL_RADIUS), + -meters[1] * tileSize / (TAU * POLAR_RADIUS) ]; } -export function geoMetersToOffset(meters, tileSize) { +export function geoOffsetToMeters(offset, tileSize) { tileSize = tileSize || 256; return [ - meters[0] * tileSize / (TAU * EQUATORIAL_RADIUS), - -meters[1] * tileSize / (TAU * POLAR_RADIUS) + offset[0] * TAU * EQUATORIAL_RADIUS / tileSize, + -offset[1] * TAU * POLAR_RADIUS / tileSize ]; } @@ -65,13 +65,6 @@ export function geoSphericalDistance(a, b) { } -// zoom to scale -export function geoZoomToScale(z, tileSize) { - tileSize = tileSize || 256; - return tileSize * Math.pow(2, z) / TAU; -} - - // scale to zoom export function geoScaleToZoom(k, tileSize) { tileSize = tileSize || 256; @@ -80,203 +73,11 @@ export function geoScaleToZoom(k, tileSize) { } -export function geoEdgeEqual(a, b) { - return (a[0] === b[0] && a[1] === b[1]) || - (a[0] === b[1] && a[1] === b[0]); -} - - -// Return the counterclockwise angle in the range (-pi, pi) -// between the positive X axis and the line intersecting a and b. -export function geoAngle(a, b, projection) { - return geoVecAngle(projection(a.loc), projection(b.loc)); -} - - -// Rotate all points counterclockwise around a pivot point by given angle -export function geoRotate(points, angle, around) { - return points.map(function(point) { - var radial = [point[0] - around[0], point[1] - around[1]]; - return [ - radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], - radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] - ]; - }); -} - - -// Choose the edge with the minimal distance from `point` to its orthogonal -// projection onto that edge, if such a projection exists, or the distance to -// the closest vertex on that edge. Returns an object with the `index` of the -// chosen edge, the chosen `loc` on that edge, and the `distance` to to it. -export function geoChooseEdge(nodes, point, projection, skipID) { - var dist = geoVecLength; - var points = nodes.map(function(n) { return projection(n.loc); }); - var ids = nodes.map(function(n) { return n.id; }); - var min = Infinity; - var idx; - var loc; - - for (var i = 0; i < points.length - 1; i++) { - if (ids[i] === skipID || ids[i + 1] === skipID) continue; - - var o = points[i]; - var s = geoVecSubtract(points[i + 1], o); - var v = geoVecSubtract(point, o); - var proj = geoVecDot(v, s) / geoVecDot(s, s); - var p; - - if (proj < 0) { - p = o; - } else if (proj > 1) { - p = points[i + 1]; - } else { - p = [o[0] + proj * s[0], o[1] + proj * s[1]]; - } - - var d = dist(p, point); - if (d < min) { - min = d; - idx = i + 1; - loc = projection.invert(p); - } - } - - if (idx !== undefined) { - return { index: idx, distance: min, loc: loc }; - } else { - return null; - } -} - - -// 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 -export function geoLineIntersection(a, b) { - var p = [a[0][0], a[0][1]]; - var p2 = [a[1][0], a[1][1]]; - var q = [b[0][0], b[0][1]]; - var q2 = [b[1][0], b[1][1]]; - var r = geoVecSubtract(p2, p); - var s = geoVecSubtract(q2, q); - var uNumerator = geoVecCross(geoVecSubtract(q, p), r); - var denominator = geoVecCross(r, s); - - if (uNumerator && denominator) { - var u = uNumerator / denominator; - var t = geoVecCross(geoVecSubtract(q, p), s) / denominator; - - if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { - return geoVecInterp(p, p2, t); - } - } - - return null; -} - - -export function geoPathIntersections(path1, path2) { - var intersections = []; - for (var i = 0; i < path1.length - 1; i++) { - for (var j = 0; j < path2.length - 1; j++) { - var a = [ path1[i], path1[i+1] ]; - var b = [ path2[j], path2[j+1] ]; - var hit = geoLineIntersection(a, b); - if (hit) { - intersections.push(hit); - } - } - } - return intersections; -} - - -// Return whether point is contained in polygon. -// -// `point` should be a 2-item array of coordinates. -// `polygon` should be an array of 2-item arrays of coordinates. -// -// From https://github.com/substack/point-in-polygon. -// ray-casting algorithm based on -// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html -// -export function geoPointInPolygon(point, polygon) { - var x = point[0]; - var y = point[1]; - var inside = false; - - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - - var intersect = ((yi > y) !== (yj > y)) && - (x < (xj - xi) * (y - yi) / (yj - yi) + xi); - if (intersect) inside = !inside; - } - - return inside; -} - - -export function geoPolygonContainsPolygon(outer, inner) { - return _every(inner, function(point) { - return geoPointInPolygon(point, outer); - }); -} - - -export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { - function testSegments(outer, inner) { - for (var i = 0; i < outer.length - 1; i++) { - for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i + 1] ]; - var b = [ inner[j], inner[j + 1] ]; - if (geoLineIntersection(a, b)) return true; - } - } - return false; - } - - function testPoints(outer, inner) { - return _some(inner, function(point) { - return geoPointInPolygon(point, outer); - }); - } - - return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); -} - - -export function geoPathLength(path) { - var length = 0; - for (var i = 0; i < path.length - 1; i++) { - length += geoVecLength(path[i], path[i + 1]); - } - return length; +// zoom to scale +export function geoZoomToScale(z, tileSize) { + tileSize = tileSize || 256; + return tileSize * Math.pow(2, z) / TAU; } -// If the given point is at the edge of the padded viewport, -// return a vector that will nudge the viewport in that direction -export function geoViewportEdge(point, dimensions) { - var pad = [80, 20, 50, 20]; // top, right, bottom, left - var x = 0; - var y = 0; - if (point[0] > dimensions[0] - pad[1]) - x = -10; - if (point[0] < pad[3]) - x = 10; - if (point[1] > dimensions[1] - pad[2]) - y = -10; - if (point[1] < pad[0]) - y = 10; - - if (x || y) { - return [x, y]; - } else { - return null; - } -} diff --git a/modules/geo/geom.js b/modules/geo/geom.js new file mode 100644 index 0000000000..fef500eb6c --- /dev/null +++ b/modules/geo/geom.js @@ -0,0 +1,213 @@ +import _every from 'lodash-es/every'; +import _some from 'lodash-es/some'; + +import { + geoVecAngle, + geoVecCross, + geoVecDot, + geoVecInterp, + geoVecLength, + geoVecSubtract +} from './vector.js'; + + +// Return the counterclockwise angle in the range (-pi, pi) +// between the positive X axis and the line intersecting a and b. +export function geoAngle(a, b, projection) { + return geoVecAngle(projection(a.loc), projection(b.loc)); +} + +export function geoEdgeEqual(a, b) { + return (a[0] === b[0] && a[1] === b[1]) || + (a[0] === b[1] && a[1] === b[0]); +} + +// Rotate all points counterclockwise around a pivot point by given angle +export function geoRotate(points, angle, around) { + return points.map(function(point) { + var radial = [point[0] - around[0], point[1] - around[1]]; + return [ + radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], + radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] + ]; + }); +} + + +// Choose the edge with the minimal distance from `point` to its orthogonal +// projection onto that edge, if such a projection exists, or the distance to +// the closest vertex on that edge. Returns an object with the `index` of the +// chosen edge, the chosen `loc` on that edge, and the `distance` to to it. +export function geoChooseEdge(nodes, point, projection, skipID) { + var dist = geoVecLength; + var points = nodes.map(function(n) { return projection(n.loc); }); + var ids = nodes.map(function(n) { return n.id; }); + var min = Infinity; + var idx; + var loc; + + for (var i = 0; i < points.length - 1; i++) { + if (ids[i] === skipID || ids[i + 1] === skipID) continue; + + var o = points[i]; + var s = geoVecSubtract(points[i + 1], o); + var v = geoVecSubtract(point, o); + var proj = geoVecDot(v, s) / geoVecDot(s, s); + var p; + + if (proj < 0) { + p = o; + } else if (proj > 1) { + p = points[i + 1]; + } else { + p = [o[0] + proj * s[0], o[1] + proj * s[1]]; + } + + var d = dist(p, point); + if (d < min) { + min = d; + idx = i + 1; + loc = projection.invert(p); + } + } + + if (idx !== undefined) { + return { index: idx, distance: min, loc: loc }; + } else { + return null; + } +} + + +// 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 +export function geoLineIntersection(a, b) { + var p = [a[0][0], a[0][1]]; + var p2 = [a[1][0], a[1][1]]; + var q = [b[0][0], b[0][1]]; + var q2 = [b[1][0], b[1][1]]; + var r = geoVecSubtract(p2, p); + var s = geoVecSubtract(q2, q); + var uNumerator = geoVecCross(geoVecSubtract(q, p), r); + var denominator = geoVecCross(r, s); + + if (uNumerator && denominator) { + var u = uNumerator / denominator; + var t = geoVecCross(geoVecSubtract(q, p), s) / denominator; + + if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { + return geoVecInterp(p, p2, t); + } + } + + return null; +} + + +export function geoPathIntersections(path1, path2) { + var intersections = []; + for (var i = 0; i < path1.length - 1; i++) { + for (var j = 0; j < path2.length - 1; j++) { + var a = [ path1[i], path1[i+1] ]; + var b = [ path2[j], path2[j+1] ]; + var hit = geoLineIntersection(a, b); + if (hit) { + intersections.push(hit); + } + } + } + return intersections; +} + + +// Return whether point is contained in polygon. +// +// `point` should be a 2-item array of coordinates. +// `polygon` should be an array of 2-item arrays of coordinates. +// +// From https://github.com/substack/point-in-polygon. +// ray-casting algorithm based on +// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html +// +export function geoPointInPolygon(point, polygon) { + var x = point[0]; + var y = point[1]; + var inside = false; + + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0]; + var yi = polygon[i][1]; + var xj = polygon[j][0]; + var yj = polygon[j][1]; + + var intersect = ((yi > y) !== (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + if (intersect) inside = !inside; + } + + return inside; +} + + +export function geoPolygonContainsPolygon(outer, inner) { + return _every(inner, function(point) { + return geoPointInPolygon(point, outer); + }); +} + + +export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { + function testSegments(outer, inner) { + for (var i = 0; i < outer.length - 1; i++) { + for (var j = 0; j < inner.length - 1; j++) { + var a = [ outer[i], outer[i + 1] ]; + var b = [ inner[j], inner[j + 1] ]; + if (geoLineIntersection(a, b)) return true; + } + } + return false; + } + + function testPoints(outer, inner) { + return _some(inner, function(point) { + return geoPointInPolygon(point, outer); + }); + } + + return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); +} + + +export function geoPathLength(path) { + var length = 0; + for (var i = 0; i < path.length - 1; i++) { + length += geoVecLength(path[i], path[i + 1]); + } + return length; +} + + +// If the given point is at the edge of the padded viewport, +// return a vector that will nudge the viewport in that direction +export function geoViewportEdge(point, dimensions) { + var pad = [80, 20, 50, 20]; // top, right, bottom, left + var x = 0; + var y = 0; + + if (point[0] > dimensions[0] - pad[1]) + x = -10; + if (point[0] < pad[3]) + x = 10; + if (point[1] > dimensions[1] - pad[2]) + y = -10; + if (point[1] < pad[0]) + y = 10; + + if (x || y) { + return [x, y]; + } else { + return null; + } +} diff --git a/modules/geo/index.js b/modules/geo/index.js index dca2f31dd7..fe801aff6b 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -1,23 +1,29 @@ -export { geoAngle } from './geo.js'; -export { geoChooseEdge } from './geo.js'; -export { geoEdgeEqual } from './geo.js'; export { geoExtent } from './extent.js'; -export { geoRawMercator } from './raw_mercator.js'; -export { geoRotate } from './geo.js'; + export { geoLatToMeters } from './geo.js'; -export { geoLineIntersection } from './geo.js'; export { geoLonToMeters } from './geo.js'; export { geoMetersToLat } from './geo.js'; export { geoMetersToLon } from './geo.js'; export { geoMetersToOffset } from './geo.js'; export { geoOffsetToMeters } from './geo.js'; -export { geoPathIntersections } from './geo.js'; -export { geoPathLength } from './geo.js'; -export { geoPointInPolygon } from './geo.js'; -export { geoPolygonContainsPolygon } from './geo.js'; -export { geoPolygonIntersectsPolygon } from './geo.js'; export { geoScaleToZoom } from './geo.js'; export { geoSphericalDistance } from './geo.js'; +export { geoZoomToScale } from './geo.js'; + +export { geoAngle } from './geom.js'; +export { geoChooseEdge } from './geom.js'; +export { geoEdgeEqual } from './geom.js'; +export { geoRotate } from './geom.js'; +export { geoLineIntersection } from './geom.js'; +export { geoPathIntersections } from './geom.js'; +export { geoPathLength } from './geom.js'; +export { geoPointInPolygon } from './geom.js'; +export { geoPolygonContainsPolygon } from './geom.js'; +export { geoPolygonIntersectsPolygon } from './geom.js'; +export { geoViewportEdge } from './geom.js'; + +export { geoRawMercator } from './raw_mercator.js'; + export { geoVecAdd } from './vector.js'; export { geoVecAngle } from './vector.js'; export { geoVecCross } from './vector.js'; @@ -28,5 +34,4 @@ export { geoVecInterp } from './vector.js'; export { geoVecLength } from './vector.js'; export { geoVecSubtract } from './vector.js'; export { geoVecScale } from './vector.js'; -export { geoZoomToScale } from './geo.js'; -export { geoViewportEdge } from './geo.js'; + diff --git a/test/index.html b/test/index.html index ed2cb0c548..2357d5ba1f 100644 --- a/test/index.html +++ b/test/index.html @@ -74,6 +74,7 @@ + diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 9f31f1d272..76c77ca09f 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,4 +1,4 @@ -describe('iD.geo', function() { +describe('iD.geo - geography', function() { describe('geoLatToMeters', function() { it('0 degrees latitude is 0 meters', function() { @@ -144,298 +144,4 @@ describe('iD.geo', function() { }); }); - describe('geoEdgeEqual', function() { - it('returns false for inequal edges', function() { - expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'c'])).to.be.false; - }); - - it('returns true for equal edges along same direction', function() { - expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'b'])).to.be.true; - }); - - it('returns true for equal edges along opposite direction', function() { - expect(iD.geoEdgeEqual(['a', 'b'], ['b', 'a'])).to.be.true; - }); - }); - - describe('geoAngle', function() { - it('returns angle between a and b', function() { - var projection = function (_) { return _; }; - expect(iD.geoAngle({loc:[0, 0]}, {loc:[1, 0]}, projection)).to.be.closeTo(0, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, 1]}, projection)).to.be.closeTo(Math.PI / 2, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[-1, 0]}, projection)).to.be.closeTo(Math.PI, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, -1]}, projection)).to.be.closeTo(-Math.PI / 2, 1e-6); - }); - }); - - describe('geoRotate', function() { - it('rotates points around [0, 0]', function() { - var points = [[5, 0], [5, 1]]; - var angle = Math.PI; - var around = [0, 0]; - var result = iD.geoRotate(points, angle, around); - expect(result[0][0]).to.be.closeTo(-5, 1e-6); - expect(result[0][1]).to.be.closeTo(0, 1e-6); - expect(result[1][0]).to.be.closeTo(-5, 1e-6); - expect(result[1][1]).to.be.closeTo(-1, 1e-6); - }); - - it('rotates points around [3, 0]', function() { - var points = [[5, 0], [5, 1]]; - var angle = Math.PI; - var around = [3, 0]; - var result = iD.geoRotate(points, angle, around); - expect(result[0][0]).to.be.closeTo(1, 1e-6); - expect(result[0][1]).to.be.closeTo(0, 1e-6); - expect(result[1][0]).to.be.closeTo(1, 1e-6); - expect(result[1][1]).to.be.closeTo(-1, 1e-6); - }); - }); - - describe('geoChooseEdge', function() { - var projection = function (l) { return l; }; - projection.invert = projection; - - it('returns null for a degenerate way (no nodes)', function() { - expect(iD.geoChooseEdge([], [0, 0], projection)).to.be.null; - }); - - it('returns null for a degenerate way (single node)', function() { - expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.be.null; - }); - - it('calculates the orthogonal projection of a point onto a segment', function() { - // a --*--- b - // | - // c - // - // * = [2, 0] - var a = [0, 0]; - var b = [5, 0]; - var c = [2, 1]; - var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; - var choice = iD.geoChooseEdge(nodes, c, projection); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(1); - expect(choice.loc).to.eql([2, 0]); - }); - - it('returns the starting vertex when the orthogonal projection is < 0', function() { - var a = [0, 0]; - var b = [5, 0]; - var c = [-3, 4]; - var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; - var choice = iD.geoChooseEdge(nodes, c, projection); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(5); - expect(choice.loc).to.eql([0, 0]); - }); - - it('returns the ending vertex when the orthogonal projection is > 1', function() { - var a = [0, 0]; - var b = [5, 0]; - var c = [8, 4]; - var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; - var choice = iD.geoChooseEdge(nodes, c, projection); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(5); - expect(choice.loc).to.eql([5, 0]); - }); - - it('skips the given nodeID at end of way', function() { - // - // a --*-- b - // e | - // | | - // d - c - // - // * = [2, 0] - var a = [0, 0]; - var b = [5, 0]; - var c = [5, 5]; - var d = [2, 5]; - var e = [2, 0.1]; // e.g. user is dragging e onto ab - var nodes = [ - iD.osmNode({id: 'a', loc: a}), - iD.osmNode({id: 'b', loc: b}), - iD.osmNode({id: 'c', loc: c}), - iD.osmNode({id: 'd', loc: d}), - iD.osmNode({id: 'e', loc: e}) - ]; - var choice = iD.geoChooseEdge(nodes, e, projection, 'e'); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(0.1); - expect(choice.loc).to.eql([2, 0]); - }); - - it('skips the given nodeID in middle of way', function() { - // - // a --*-- b - // d | - // / \ | - // e c - // - // * = [2, 0] - var a = [0, 0]; - var b = [5, 0]; - var c = [5, 5]; - var d = [2, 0.1]; // e.g. user is dragging d onto ab - var e = [0, 5]; - var nodes = [ - iD.osmNode({id: 'a', loc: a}), - iD.osmNode({id: 'b', loc: b}), - iD.osmNode({id: 'c', loc: c}), - iD.osmNode({id: 'd', loc: d}), - iD.osmNode({id: 'e', loc: e}) - ]; - var choice = iD.geoChooseEdge(nodes, d, projection, 'd'); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(0.1); - expect(choice.loc).to.eql([2, 0]); - }); - - it('returns null if all nodes are skipped', function() { - var nodes = [ - iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [5, 0]}), - ]; - var choice = iD.geoChooseEdge(nodes, [2, 2], projection, 'a'); - expect(choice).to.be.null; - }); - }); - - describe('geoLineIntersection', function() { - it('returns null if lines are colinear with overlap', function() { - var a = [[0, 0], [10, 0]]; - var b = [[-5, 0], [5, 0]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns null if lines are colinear but disjoint', function() { - var a = [[5, 0], [10, 0]]; - var b = [[-10, 0], [-5, 0]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns null if lines are parallel', function() { - var a = [[0, 0], [10, 0]]; - var b = [[0, 5], [10, 5]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns the intersection point between 2 lines', function() { - var a = [[0, 0], [10, 0]]; - var b = [[5, 10], [5, -10]]; - expect(iD.geoLineIntersection(a, b)).to.eql([5, 0]); - }); - it('returns null if lines are not parallel but not intersecting', function() { - var a = [[0, 0], [10, 0]]; - var b = [[-5, 10], [-5, -10]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - }); - - describe('geoPointInPolygon', function() { - it('says a point in a polygon is on a polygon', function() { - var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; - var point = [0.5, 0.5]; - expect(iD.geoPointInPolygon(point, poly)).to.be.true; - }); - it('says a point outside of a polygon is outside', function() { - var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; - var point = [0.5, 1.5]; - expect(iD.geoPointInPolygon(point, poly)).to.be.false; - }); - }); - - describe('geoPolygonContainsPolygon', function() { - it('says a polygon in a polygon is in', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.true; - }); - it('says a polygon outside of a polygon is out', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('geoPolygonIntersectsPolygon', function() { - it('returns true when outer polygon fully contains inner', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; - }); - - it('returns true when outer polygon partially contains inner (some vertices contained)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; - }); - - it('returns false when outer polygon partially contains inner (no vertices contained - lax test)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; - }); - - it('returns true when outer polygon partially contains inner (no vertices contained - strict test)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner, true)).to.be.true; - }); - - it('returns false when outer and inner are fully disjoint', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('geoPathLength', function() { - it('calculates a simple path length', function() { - var path = [[0, 0], [0, 1], [3, 5]]; - expect(iD.geoPathLength(path)).to.eql(6); - }); - - it('does not fail on single-point path', function() { - var path = [[0, 0]]; - expect(iD.geoPathLength(path)).to.eql(0); - }); - - it('estimates zero-length edges', function() { - var path = [[0, 0], [0, 0]]; - expect(iD.geoPathLength(path)).to.eql(0); - }); - }); - - describe('geoViewportEdge', function() { - var dimensions = [1000, 1000]; - it('returns null if the point is not at the edge', function() { - expect(iD.geoViewportEdge([500, 500], dimensions)).to.be.null; - }); - it('nudges top edge', function() { - expect(iD.geoViewportEdge([500, 5], dimensions)).to.eql([0, 10]); - }); - it('nudges top-right corner', function() { - expect(iD.geoViewportEdge([995, 5], dimensions)).to.eql([-10, 10]); - }); - it('nudges right edge', function() { - expect(iD.geoViewportEdge([995, 500], dimensions)).to.eql([-10, 0]); - }); - it('nudges bottom-right corner', function() { - expect(iD.geoViewportEdge([995, 995], dimensions)).to.eql([-10, -10]); - }); - it('nudges bottom edge', function() { - expect(iD.geoViewportEdge([500, 995], dimensions)).to.eql([0, -10]); - }); - it('nudges bottom-left corner', function() { - expect(iD.geoViewportEdge([5, 995], dimensions)).to.eql([10, -10]); - }); - it('nudges left edge', function() { - expect(iD.geoViewportEdge([5, 500], dimensions)).to.eql([10, 0]); - }); - it('nudges top-left corner', function() { - expect(iD.geoViewportEdge([5, 5], dimensions)).to.eql([10, 10]); - }); - }); - }); diff --git a/test/spec/geo/geom.js b/test/spec/geo/geom.js new file mode 100644 index 0000000000..11b37c35ae --- /dev/null +++ b/test/spec/geo/geom.js @@ -0,0 +1,297 @@ +describe('iD.geo - geometry', function() { + + describe('geoAngle', function() { + it('returns angle between a and b', function() { + var projection = function (_) { return _; }; + expect(iD.geoAngle({loc:[0, 0]}, {loc:[1, 0]}, projection)).to.be.closeTo(0, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, 1]}, projection)).to.be.closeTo(Math.PI / 2, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[-1, 0]}, projection)).to.be.closeTo(Math.PI, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, -1]}, projection)).to.be.closeTo(-Math.PI / 2, 1e-6); + }); + }); + + describe('geoEdgeEqual', function() { + it('returns false for inequal edges', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'c'])).to.be.false; + }); + + it('returns true for equal edges along same direction', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'b'])).to.be.true; + }); + + it('returns true for equal edges along opposite direction', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['b', 'a'])).to.be.true; + }); + }); + + describe('geoRotate', function() { + it('rotates points around [0, 0]', function() { + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [0, 0]; + var result = iD.geoRotate(points, angle, around); + expect(result[0][0]).to.be.closeTo(-5, 1e-6); + expect(result[0][1]).to.be.closeTo(0, 1e-6); + expect(result[1][0]).to.be.closeTo(-5, 1e-6); + expect(result[1][1]).to.be.closeTo(-1, 1e-6); + }); + + it('rotates points around [3, 0]', function() { + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [3, 0]; + var result = iD.geoRotate(points, angle, around); + expect(result[0][0]).to.be.closeTo(1, 1e-6); + expect(result[0][1]).to.be.closeTo(0, 1e-6); + expect(result[1][0]).to.be.closeTo(1, 1e-6); + expect(result[1][1]).to.be.closeTo(-1, 1e-6); + }); + }); + + describe('geoChooseEdge', function() { + var projection = function (l) { return l; }; + projection.invert = projection; + + it('returns null for a degenerate way (no nodes)', function() { + expect(iD.geoChooseEdge([], [0, 0], projection)).to.be.null; + }); + + it('returns null for a degenerate way (single node)', function() { + expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.be.null; + }); + + it('calculates the orthogonal projection of a point onto a segment', function() { + // a --*--- b + // | + // c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [2, 1]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(nodes, c, projection); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('returns the starting vertex when the orthogonal projection is < 0', function() { + var a = [0, 0]; + var b = [5, 0]; + var c = [-3, 4]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(nodes, c, projection); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(5); + expect(choice.loc).to.eql([0, 0]); + }); + + it('returns the ending vertex when the orthogonal projection is > 1', function() { + var a = [0, 0]; + var b = [5, 0]; + var c = [8, 4]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(nodes, c, projection); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(5); + expect(choice.loc).to.eql([5, 0]); + }); + + it('skips the given nodeID at end of way', function() { + // + // a --*-- b + // e | + // | | + // d - c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 5]; + var e = [2, 0.1]; // e.g. user is dragging e onto ab + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, e, projection, 'e'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('skips the given nodeID in middle of way', function() { + // + // a --*-- b + // d | + // / \ | + // e c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 0.1]; // e.g. user is dragging d onto ab + var e = [0, 5]; + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, d, projection, 'd'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('returns null if all nodes are skipped', function() { + var nodes = [ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [5, 0]}), + ]; + var choice = iD.geoChooseEdge(nodes, [2, 2], projection, 'a'); + expect(choice).to.be.null; + }); + }); + + describe('geoLineIntersection', function() { + it('returns null if lines are colinear with overlap', function() { + var a = [[0, 0], [10, 0]]; + var b = [[-5, 0], [5, 0]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns null if lines are colinear but disjoint', function() { + var a = [[5, 0], [10, 0]]; + var b = [[-10, 0], [-5, 0]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns null if lines are parallel', function() { + var a = [[0, 0], [10, 0]]; + var b = [[0, 5], [10, 5]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns the intersection point between 2 lines', function() { + var a = [[0, 0], [10, 0]]; + var b = [[5, 10], [5, -10]]; + expect(iD.geoLineIntersection(a, b)).to.eql([5, 0]); + }); + it('returns null if lines are not parallel but not intersecting', function() { + var a = [[0, 0], [10, 0]]; + var b = [[-5, 10], [-5, -10]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + }); + + describe('geoPointInPolygon', function() { + it('says a point in a polygon is on a polygon', function() { + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + var point = [0.5, 0.5]; + expect(iD.geoPointInPolygon(point, poly)).to.be.true; + }); + it('says a point outside of a polygon is outside', function() { + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + var point = [0.5, 1.5]; + expect(iD.geoPointInPolygon(point, poly)).to.be.false; + }); + }); + + describe('geoPolygonContainsPolygon', function() { + it('says a polygon in a polygon is in', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.true; + }); + it('says a polygon outside of a polygon is out', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonContainsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('geoPolygonIntersectsPolygon', function() { + it('returns true when outer polygon fully contains inner', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('returns true when outer polygon partially contains inner (some vertices contained)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('returns false when outer polygon partially contains inner (no vertices contained - lax test)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + + it('returns true when outer polygon partially contains inner (no vertices contained - strict test)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner, true)).to.be.true; + }); + + it('returns false when outer and inner are fully disjoint', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('geoPathLength', function() { + it('calculates a simple path length', function() { + var path = [[0, 0], [0, 1], [3, 5]]; + expect(iD.geoPathLength(path)).to.eql(6); + }); + + it('does not fail on single-point path', function() { + var path = [[0, 0]]; + expect(iD.geoPathLength(path)).to.eql(0); + }); + + it('estimates zero-length edges', function() { + var path = [[0, 0], [0, 0]]; + expect(iD.geoPathLength(path)).to.eql(0); + }); + }); + + describe('geoViewportEdge', function() { + var dimensions = [1000, 1000]; + it('returns null if the point is not at the edge', function() { + expect(iD.geoViewportEdge([500, 500], dimensions)).to.be.null; + }); + it('nudges top edge', function() { + expect(iD.geoViewportEdge([500, 5], dimensions)).to.eql([0, 10]); + }); + it('nudges top-right corner', function() { + expect(iD.geoViewportEdge([995, 5], dimensions)).to.eql([-10, 10]); + }); + it('nudges right edge', function() { + expect(iD.geoViewportEdge([995, 500], dimensions)).to.eql([-10, 0]); + }); + it('nudges bottom-right corner', function() { + expect(iD.geoViewportEdge([995, 995], dimensions)).to.eql([-10, -10]); + }); + it('nudges bottom edge', function() { + expect(iD.geoViewportEdge([500, 995], dimensions)).to.eql([0, -10]); + }); + it('nudges bottom-left corner', function() { + expect(iD.geoViewportEdge([5, 995], dimensions)).to.eql([10, -10]); + }); + it('nudges left edge', function() { + expect(iD.geoViewportEdge([5, 500], dimensions)).to.eql([10, 0]); + }); + it('nudges top-left corner', function() { + expect(iD.geoViewportEdge([5, 5], dimensions)).to.eql([10, 10]); + }); + }); + +}); diff --git a/test/spec/geo/vector.js b/test/spec/geo/vector.js index 21968bb5a8..7b69aab003 100644 --- a/test/spec/geo/vector.js +++ b/test/spec/geo/vector.js @@ -1,4 +1,4 @@ -describe('iD.geo vector', function() { +describe('iD.geo - vector', function() { describe('geoVecEqual', function() { it('tests vectors for equality', function() { From 462fef148dcf65fdb334a3a0dafad817f71b7e15 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 28 Dec 2017 21:20:14 -0500 Subject: [PATCH 080/206] Remove unused imports (eslint warning) --- modules/geo/geo.js | 13 ------------- modules/geo/geom.js | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/modules/geo/geo.js b/modules/geo/geo.js index e0f7aa4368..639887bac8 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -1,16 +1,3 @@ -import _every from 'lodash-es/every'; -import _some from 'lodash-es/some'; - -import { - geoVecAngle, - geoVecCross, - geoVecDot, - geoVecInterp, - geoVecLength, - geoVecSubtract -} from './vector.js'; - - // constants var TAU = 2 * Math.PI; var EQUATORIAL_RADIUS = 6356752.314245179; diff --git a/modules/geo/geom.js b/modules/geo/geom.js index fef500eb6c..e8b37c6fe5 100644 --- a/modules/geo/geom.js +++ b/modules/geo/geom.js @@ -25,7 +25,7 @@ export function geoEdgeEqual(a, b) { // Rotate all points counterclockwise around a pivot point by given angle export function geoRotate(points, angle, around) { return points.map(function(point) { - var radial = [point[0] - around[0], point[1] - around[1]]; + var radial = geoVecSubtract(point, around); return [ radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] From 4f02340374e2154dd54156fa756ed24b3650e2d8 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 28 Dec 2017 23:10:52 -0500 Subject: [PATCH 081/206] Extract self-intersection code to geoHasSelfIntersections Test for self-intersecting areas in both drag_node and draw_way --- css/55_cursors.css | 3 +- modules/behavior/draw_way.js | 69 ++++++++++++++++++++++++++++-------- modules/geo/geom.js | 40 +++++++++++++++++++-- modules/geo/index.js | 1 + modules/modes/drag_node.js | 56 +++++------------------------ 5 files changed, 105 insertions(+), 64 deletions(-) diff --git a/css/55_cursors.css b/css/55_cursors.css index 0e4732b586..854e2f88bf 100644 --- a/css/55_cursors.css +++ b/css/55_cursors.css @@ -1,6 +1,7 @@ /* Cursors */ -.nope { +.nope, +.nope * { cursor: not-allowed !important; } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 18e270c7e6..4a49856e95 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -7,7 +7,7 @@ import { } from '../actions'; import { behaviorDraw } from './draw'; -import { geoChooseEdge } from '../geo'; +import { geoChooseEdge, geoHasSelfIntersections } from '../geo'; import { modeBrowse, modeSelect } from '../modes'; import { osmNode } from '../osm'; @@ -39,25 +39,48 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` function move(datum) { - var loc; - var target = datum && datum.id && context.hasEntity(datum.id); + var nodeGroups = datum && datum.properties && datum.properties.nodes; + var loc = context.map().mouseCoordinates(); + if (datum.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` loc = datum.loc; - } else if (target && target.type === 'way') { // snap to way - var choice = geoChooseEdge( - context.childNodes(target), context.mouse(), context.projection, end.id - ); - if (choice) { - loc = choice.loc; - } - } - if (!loc) { - loc = context.map().mouseCoordinates(); + } else if (nodeGroups) { // snap to way - a line touch target or nope target with nodes + var best = Infinity; + for (var i = 0; i < nodeGroups.length; i++) { + var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); + var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, end.id); + if (choice && choice.distance < best) { + best = choice.distance; + loc = choice.loc; + } + } } context.replace(actionMoveNode(end.id, loc)); end = context.entity(end.id); + + // check if this movement causes the geometry to break + var doBlock = invalidGeometry(end, context.graph()); + context.surface() + .classed('nope', doBlock); + } + + + function invalidGeometry(entity, graph) { + var parents = graph.parentWays(entity); + + for (var i = 0; i < parents.length; i++) { + var parent = parents[i]; + var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); + if (parent.isClosed()) { + if (geoHasSelfIntersections(nodes, entity.id)) { + return true; + } + } + } + + return false; } @@ -147,7 +170,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Accept the current position of the drawing node and continue drawing. drawWay.add = function(loc, datum) { - if (datum && datum.id && /-nope/.test(datum.id)) return; // can't click here + if ((datum && datum.id && /-nope$/.test(datum.id)) || + context.surface().classed('nope')) { + return; // can't click here + } context.pop(_tempEdits); _tempEdits = 0; @@ -163,6 +189,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing way. drawWay.addWay = function(loc, edge) { + if (context.surface().classed('nope')) { + return; // can't click here + } + context.pop(_tempEdits); _tempEdits = 0; @@ -178,6 +208,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing node and continue drawing. drawWay.addNode = function(node) { + if (context.surface().classed('nope')) { + return; // can't click here + } + context.pop(_tempEdits); _tempEdits = 0; @@ -194,6 +228,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // If the way has enough nodes to be valid, it's selected. // Otherwise, delete everything and return to browse mode. drawWay.finish = function() { + if (context.surface().classed('nope')) { + return; // can't click here + } + context.pop(_tempEdits); _tempEdits = 0; @@ -224,6 +262,9 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.map().dblclickEnable(true); }, 1000); + context.surface() + .classed('nope', false); + context.enter(modeBrowse(context)); }; diff --git a/modules/geo/geom.js b/modules/geo/geom.js index e8b37c6fe5..dbef358004 100644 --- a/modules/geo/geom.js +++ b/modules/geo/geom.js @@ -5,6 +5,7 @@ import { geoVecAngle, geoVecCross, geoVecDot, + geoVecEqual, geoVecInterp, geoVecLength, geoVecSubtract @@ -38,7 +39,7 @@ export function geoRotate(points, angle, around) { // projection onto that edge, if such a projection exists, or the distance to // the closest vertex on that edge. Returns an object with the `index` of the // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. -export function geoChooseEdge(nodes, point, projection, skipID) { +export function geoChooseEdge(nodes, point, projection, activeID) { var dist = geoVecLength; var points = nodes.map(function(n) { return projection(n.loc); }); var ids = nodes.map(function(n) { return n.id; }); @@ -47,7 +48,7 @@ export function geoChooseEdge(nodes, point, projection, skipID) { var loc; for (var i = 0; i < points.length - 1; i++) { - if (ids[i] === skipID || ids[i + 1] === skipID) continue; + if (ids[i] === activeID || ids[i + 1] === activeID) continue; var o = points[i]; var s = geoVecSubtract(points[i + 1], o); @@ -79,6 +80,41 @@ export function geoChooseEdge(nodes, point, projection, skipID) { } +// check active (dragged or drawing) segments against inactive segments +export function geoHasSelfIntersections(nodes, activeID) { + var actives = []; + var inactives = []; + var j, k; + + for (j = 0; j < nodes.length - 1; j++) { + var n1 = nodes[j]; + var n2 = nodes[j+1]; + var segment = [n1.loc, n2.loc]; + if (n1.id === activeID || n2.id === activeID) { + actives.push(segment); + } else { + inactives.push(segment); + } + } + + for (j = 0; j < actives.length; j++) { + for (k = 0; k < inactives.length; k++) { + var p = actives[j]; + var q = inactives[k]; + // skip if segments share an endpoint + if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) || + geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) { + continue; + } else if (geoLineIntersection(p, q)) { + return true; + } + } + } + + return false; +} + + // 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: diff --git a/modules/geo/index.js b/modules/geo/index.js index fe801aff6b..982596f04f 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -13,6 +13,7 @@ export { geoZoomToScale } from './geo.js'; export { geoAngle } from './geom.js'; export { geoChooseEdge } from './geom.js'; export { geoEdgeEqual } from './geom.js'; +export { geoHasSelfIntersections } from './geom.js'; export { geoRotate } from './geom.js'; export { geoLineIntersection } from './geom.js'; export { geoPathIntersections } from './geom.js'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 3809a352f6..4251fb7a81 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -20,8 +20,7 @@ import { import { geoChooseEdge, - geoLineIntersection, - geoVecEqual, + geoHasSelfIntersections, geoVecSubtract, geoViewportEdge } from '../geo'; @@ -133,7 +132,6 @@ export function modeDragNode(context) { var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); - var didSnap = false; if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap.. // related code @@ -141,21 +139,19 @@ export function modeDragNode(context) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` var d = datum(); - var nodegroups = d && d.properties && d.properties.nodes; + var nodeGroups = d && d.properties && d.properties.nodes; if (d.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` loc = d.loc; - didSnap = true; - } else if (nodegroups) { // snap to way - a line touch target or nope target with nodes + } else if (nodeGroups) { // snap to way - a line touch target or nope target with nodes var best = Infinity; - for (var i = 0; i < nodegroups.length; i++) { - var childNodes = nodegroups[i].map(function(id) { return context.entity(id); }); + for (var i = 0; i < nodeGroups.length; i++) { + var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, entity.id); if (choice && choice.distance < best) { best = choice.distance; loc = choice.loc; - didSnap = true; } } } @@ -168,11 +164,7 @@ export function modeDragNode(context) { // check if this movement causes the geometry to break - var doBlock = false; - if (!didSnap) { - doBlock = invalidGeometry(entity, context.graph()); - } - + var doBlock = invalidGeometry(entity, context.graph()); context.surface() .classed('nope', doBlock); @@ -183,41 +175,11 @@ export function modeDragNode(context) { function invalidGeometry(entity, graph) { var parents = graph.parentWays(entity); - function hasSelfIntersections(way, activeID) { - // check active (dragged) segments against inactive segments - var actives = []; - var inactives = []; - var j, k; - for (j = 0; j < way.nodes.length - 1; j++) { - var n1 = graph.entity(way.nodes[j]); - var n2 = graph.entity(way.nodes[j+1]); - var segment = [n1.loc, n2.loc]; - if (n1.id === activeID || n2.id === activeID) { - actives.push(segment); - } else { - inactives.push(segment); - } - } - for (j = 0; j < actives.length; j++) { - for (k = 0; k < inactives.length; k++) { - var p = actives[j]; - var q = inactives[k]; - // skip if segments share an endpoint - if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) || - geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) { - continue; - } else if (geoLineIntersection(p, q)) { - return true; - } - } - } - return false; - } - for (var i = 0; i < parents.length; i++) { var parent = parents[i]; - if (parent.isClosed()) { // check for self intersections - if (hasSelfIntersections(parent, entity.id)) { + var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); + if (parent.isClosed()) { + if (geoHasSelfIntersections(nodes, entity.id)) { return true; } } From 08cd2c7325a55e0eb9e5ccb7ab0bed9e7855901c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 28 Dec 2017 23:46:08 -0500 Subject: [PATCH 082/206] Tests for geoHasSelfIntersections --- test/spec/geo/geom.js | 121 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/test/spec/geo/geom.js b/test/spec/geo/geom.js index 11b37c35ae..3acf7f940f 100644 --- a/test/spec/geo/geom.js +++ b/test/spec/geo/geom.js @@ -160,6 +160,127 @@ describe('iD.geo - geometry', function() { }); }); + describe('geoHasSelfIntersections', function() { + it('returns false for a degenerate way (no nodes)', function() { + expect(iD.geoHasSelfIntersections([], '')).to.be.false; + }); + + it('returns false if no activeID', function() { + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 2]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d, a]; + expect(iD.geoHasSelfIntersections(nodes, '')).to.be.false; + }); + + it('returns false if there are no self intersections (closed way)', function() { + // a --- b + // | | + // | | + // d --- c + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 2]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d, a]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + }); + + it('returns true if there are self intersections without a junction (closed way)', function() { + // a c + // | \ / | + // | / | + // | / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d, a]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.true; + }); + + it('returns false if there are self intersections with a junction (closed way)', function() { + // a c + // | \ / | + // | x | + // | / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var x = iD.osmNode({id: 'x', loc: [1, 1]}); + var nodes = [a, x, b, c, x, d, a]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'x')).to.be.false; + }); + + it('returns false if there are no self intersections (open way)', function() { + // a --- b + // | + // | + // d --- c + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 2]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + }); + + it('returns true if there are self intersections without a junction (open way)', function() { + // a c + // \ / | + // / | + // / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var nodes = [a, b, c, d]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.true; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.true; + }); + + it('returns false if there are self intersections with a junction (open way)', function() { + // a c + // \ / | + // x | + // / \ | + // d b + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [2, 2]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [0, 2]}); + var x = iD.osmNode({id: 'x', loc: [1, 1]}); + var nodes = [a, x, b, c, x, d]; + expect(iD.geoHasSelfIntersections(nodes, 'a')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'b')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'c')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'd')).to.be.false; + expect(iD.geoHasSelfIntersections(nodes, 'x')).to.be.false; + }); + + }); + + describe('geoLineIntersection', function() { it('returns null if lines are colinear with overlap', function() { var a = [[0, 0], [10, 0]]; From 35f8d562493a1ed3223d890c3c3056dabfbdf6c4 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 29 Dec 2017 01:25:45 -0500 Subject: [PATCH 083/206] Update direction fields and add to several presets --- data/presets.yaml | 141 +++++++++++------- data/presets/fields.json | 137 ++++++++++------- data/presets/fields/direction.json | 6 + ...direction.json => direction_cardinal.json} | 0 ...ck_direction.json => direction_clock.json} | 0 ...l_direction.json => direction_vertex.json} | 3 +- .../position.json} | 0 .../fields/railway/signal/direction.json | 12 ++ .../fields/traffic_signals/direction.json | 12 ++ data/presets/presets.json | 57 ++++--- .../presets/advertising/billboard.json | 2 +- data/presets/presets/highway/give_way.json | 2 +- .../presets/highway/mini_roundabout.json | 2 +- .../presets/presets/highway/speed_camera.json | 1 + data/presets/presets/highway/stop.json | 2 +- data/presets/presets/highway/street_lamp.json | 1 + .../presets/highway/traffic_mirror.json | 3 + .../presets/highway/traffic_signals.json | 3 +- data/presets/presets/man_made/adit.json | 3 +- .../presets/man_made/surveillance.json | 3 +- .../presets/natural/cave_entrance.json | 3 +- data/presets/presets/railway/milestone.json | 2 +- data/presets/presets/railway/signal.json | 5 + .../presets/tourism/information/board.json | 3 +- .../presets/tourism/information/map.json | 3 +- data/presets/presets/tourism/viewpoint.json | 3 + data/presets/presets/traffic_calming.json | 2 +- .../presets/presets/traffic_calming/bump.json | 2 +- .../presets/traffic_calming/chicane.json | 2 +- .../presets/traffic_calming/choker.json | 2 +- .../presets/traffic_calming/cushion.json | 2 +- data/presets/presets/traffic_calming/dip.json | 2 +- .../presets/presets/traffic_calming/hump.json | 2 +- .../presets/traffic_calming/rumble_strip.json | 2 +- dist/locales/en.json | 99 +++++++----- 35 files changed, 336 insertions(+), 188 deletions(-) create mode 100644 data/presets/fields/direction.json rename data/presets/fields/{cardinal_direction.json => direction_cardinal.json} (100%) rename data/presets/fields/{clock_direction.json => direction_clock.json} (100%) rename data/presets/fields/{parallel_direction.json => direction_vertex.json} (71%) rename data/presets/fields/{milestone_position.json => railway/position.json} (100%) create mode 100644 data/presets/fields/railway/signal/direction.json create mode 100644 data/presets/fields/traffic_signals/direction.json diff --git a/data/presets.yaml b/data/presets.yaml index c7ee602ec1..b386ab42a1 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -292,53 +292,9 @@ en: label: Capacity # capacity field placeholder placeholder: '50, 100, 200...' - cardinal_direction: - # direction=* - label: Direction - options: - # direction=E - E: East - # direction=ENE - ENE: East-northeast - # direction=ESE - ESE: East-southeast - # direction=N - 'N': North - # direction=NE - NE: Northeast - # direction=NNE - NNE: North-northeast - # direction=NNW - NNW: North-northwest - # direction=NW - NW: Northwest - # direction=S - S: South - # direction=SE - SE: Southeast - # direction=SSE - SSE: South-southeast - # direction=SSW - SSW: South-southwest - # direction=SW - SW: Southwest - # direction=W - W: West - # direction=WNW - WNW: West-northwest - # direction=WSW - WSW: West-southwest castle_type: # castle_type=* label: Type - clock_direction: - # direction=* - label: Direction - options: - # direction=anticlockwise - anticlockwise: Counterclockwise - # direction=clockwise - clockwise: Clockwise clothes: # clothes=* label: Clothes @@ -466,6 +422,65 @@ en: diaper: # diaper=* label: Diaper Changing Available + direction: + # direction=* + label: Direction (Degrees Clockwise) + # direction field placeholder + placeholder: '45, 90, 180, 270' + direction_cardinal: + # direction=* + label: Direction + options: + # direction=E + E: East + # direction=ENE + ENE: East-northeast + # direction=ESE + ESE: East-southeast + # direction=N + 'N': North + # direction=NE + NE: Northeast + # direction=NNE + NNE: North-northeast + # direction=NNW + NNW: North-northwest + # direction=NW + NW: Northwest + # direction=S + S: South + # direction=SE + SE: Southeast + # direction=SSE + SSE: South-southeast + # direction=SSW + SSW: South-southwest + # direction=SW + SW: Southwest + # direction=W + W: West + # direction=WNW + WNW: West-northwest + # direction=WSW + WSW: West-southwest + direction_clock: + # direction=* + label: Direction + options: + # direction=anticlockwise + anticlockwise: Counterclockwise + # direction=clockwise + clockwise: Clockwise + direction_vertex: + # direction=* + label: Direction + options: + # direction=backward + backward: Backward + # direction=both + both: Both / All + # direction=forward + forward: Forward display: # display=* label: Display @@ -812,11 +827,6 @@ en: memorial: # memorial=* label: Type - milestone_position: - # 'railway:position=*' - label: Milestone Position - # milestone_position field placeholder - placeholder: Distance to one decimal (123.4) monitoring_multi: # 'monitoring:=*' label: Monitoring @@ -970,14 +980,6 @@ en: label: Par # par field placeholder placeholder: '3, 4, 5...' - parallel_direction: - # direction=* - label: Direction - options: - # direction=backward - backward: Backward - # direction=forward - forward: Forward park_ride: # park_ride=* label: Park and Ride @@ -1108,6 +1110,21 @@ en: railway: # railway=* label: Type + railway/position: + # 'railway:position=*' + label: Milestone Position + # railway/position field placeholder + placeholder: Distance to one decimal (123.4) + railway/signal/direction: + # 'railway:signal:direction=*' + label: Direction + options: + # 'railway:signal:direction=backward' + backward: Backward + # 'railway:signal:direction=both' + both: Both / All + # 'railway:signal:direction=forward' + forward: Forward rating: # rating=* label: Power Rating @@ -1462,6 +1479,16 @@ en: traffic_signals: # traffic_signals=* label: Type + traffic_signals/direction: + # 'traffic_signals:direction=*' + label: Direction + options: + # 'traffic_signals:direction=backward' + backward: Backward + # 'traffic_signals:direction=both' + both: Both / All + # 'traffic_signals:direction=forward' + forward: Forward trail_visibility: # trail_visibility=* label: Trail Visibility diff --git a/data/presets/fields.json b/data/presets/fields.json index d3c833ac91..4f20de15ad 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -389,47 +389,11 @@ "label": "Capacity", "placeholder": "50, 100, 200..." }, - "cardinal_direction": { - "key": "direction", - "type": "combo", - "label": "Direction", - "strings": { - "options": { - "N": "North", - "E": "East", - "S": "South", - "W": "West", - "NE": "Northeast", - "SE": "Southeast", - "SW": "Southwest", - "NW": "Northwest", - "NNE": "North-northeast", - "ENE": "East-northeast", - "ESE": "East-southeast", - "SSE": "South-southeast", - "SSW": "South-southwest", - "WSW": "West-southwest", - "WNW": "West-northwest", - "NNW": "North-northwest" - } - } - }, "castle_type": { "key": "castle_type", "type": "combo", "label": "Type" }, - "clock_direction": { - "key": "direction", - "type": "combo", - "label": "Direction", - "strings": { - "options": { - "clockwise": "Clockwise", - "anticlockwise": "Counterclockwise" - } - } - }, "clothes": { "key": "clothes", "type": "semiCombo", @@ -626,6 +590,60 @@ "5" ] }, + "direction_cardinal": { + "key": "direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "N": "North", + "E": "East", + "S": "South", + "W": "West", + "NE": "Northeast", + "SE": "Southeast", + "SW": "Southwest", + "NW": "Northwest", + "NNE": "North-northeast", + "ENE": "East-northeast", + "ESE": "East-southeast", + "SSE": "South-southeast", + "SSW": "South-southwest", + "WSW": "West-southwest", + "WNW": "West-northwest", + "NNW": "North-northwest" + } + } + }, + "direction_clock": { + "key": "direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "clockwise": "Clockwise", + "anticlockwise": "Counterclockwise" + } + } + }, + "direction_vertex": { + "key": "direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } + }, + "direction": { + "key": "direction", + "type": "number", + "label": "Direction (Degrees Clockwise)", + "placeholder": "45, 90, 180, 270" + }, "display": { "key": "display", "type": "combo", @@ -1135,12 +1153,6 @@ "type": "typeCombo", "label": "Type" }, - "milestone_position": { - "key": "railway:position", - "type": "text", - "placeholder": "Distance to one decimal (123.4)", - "label": "Milestone Position" - }, "monitoring_multi": { "key": "monitoring:", "type": "multiCombo", @@ -1316,17 +1328,6 @@ "label": "Par", "placeholder": "3, 4, 5..." }, - "parallel_direction": { - "key": "direction", - "type": "combo", - "label": "Direction", - "strings": { - "options": { - "forward": "Forward", - "backward": "Backward" - } - } - }, "park_ride": { "key": "park_ride", "type": "check", @@ -1478,6 +1479,24 @@ "type": "typeCombo", "label": "Type" }, + "railway/position": { + "key": "railway:position", + "type": "text", + "placeholder": "Distance to one decimal (123.4)", + "label": "Milestone Position" + }, + "railway/signal/direction": { + "key": "railway:signal:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } + }, "rating": { "key": "rating", "type": "combo", @@ -2000,6 +2019,18 @@ "label": "Type", "default": "signal" }, + "traffic_signals/direction": { + "key": "traffic_signals:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } + }, "trail_visibility": { "key": "trail_visibility", "type": "combo", diff --git a/data/presets/fields/direction.json b/data/presets/fields/direction.json new file mode 100644 index 0000000000..c4324ce8e6 --- /dev/null +++ b/data/presets/fields/direction.json @@ -0,0 +1,6 @@ +{ + "key": "direction", + "type": "number", + "label": "Direction (Degrees Clockwise)", + "placeholder": "45, 90, 180, 270" +} diff --git a/data/presets/fields/cardinal_direction.json b/data/presets/fields/direction_cardinal.json similarity index 100% rename from data/presets/fields/cardinal_direction.json rename to data/presets/fields/direction_cardinal.json diff --git a/data/presets/fields/clock_direction.json b/data/presets/fields/direction_clock.json similarity index 100% rename from data/presets/fields/clock_direction.json rename to data/presets/fields/direction_clock.json diff --git a/data/presets/fields/parallel_direction.json b/data/presets/fields/direction_vertex.json similarity index 71% rename from data/presets/fields/parallel_direction.json rename to data/presets/fields/direction_vertex.json index 03801f0f38..9b0d7ebc00 100644 --- a/data/presets/fields/parallel_direction.json +++ b/data/presets/fields/direction_vertex.json @@ -5,7 +5,8 @@ "strings": { "options": { "forward": "Forward", - "backward": "Backward" + "backward": "Backward", + "both": "Both / All" } } } diff --git a/data/presets/fields/milestone_position.json b/data/presets/fields/railway/position.json similarity index 100% rename from data/presets/fields/milestone_position.json rename to data/presets/fields/railway/position.json diff --git a/data/presets/fields/railway/signal/direction.json b/data/presets/fields/railway/signal/direction.json new file mode 100644 index 0000000000..3034345b8a --- /dev/null +++ b/data/presets/fields/railway/signal/direction.json @@ -0,0 +1,12 @@ +{ + "key": "railway:signal:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } +} diff --git a/data/presets/fields/traffic_signals/direction.json b/data/presets/fields/traffic_signals/direction.json new file mode 100644 index 0000000000..079c2a133f --- /dev/null +++ b/data/presets/fields/traffic_signals/direction.json @@ -0,0 +1,12 @@ +{ + "key": "traffic_signals:direction", + "type": "combo", + "label": "Direction", + "strings": { + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + } +} diff --git a/data/presets/presets.json b/data/presets/presets.json index 8cc93e1d77..fa822573ea 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -170,7 +170,7 @@ }, "advertising/billboard": { "fields": [ - "parallel_direction", + "direction", "lit" ], "geometry": [ @@ -6896,7 +6896,7 @@ "highway/give_way": { "icon": "poi-yield", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" @@ -6941,7 +6941,7 @@ "highway": "mini_roundabout" }, "fields": [ - "clock_direction" + "direction_clock" ], "name": "Mini-Roundabout" }, @@ -7474,6 +7474,7 @@ "vertex" ], "fields": [ + "direction", "ref" ], "tags": { @@ -7508,7 +7509,7 @@ "icon": "poi-stop", "fields": [ "stop", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" @@ -7534,6 +7535,7 @@ }, "fields": [ "lamp_type", + "direction", "ref" ], "terms": [ @@ -7645,6 +7647,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "highway": "traffic_mirror" }, @@ -7670,7 +7675,8 @@ "highway": "traffic_signals" }, "fields": [ - "traffic_signals" + "traffic_signals", + "traffic_signals/direction" ], "terms": [ "light", @@ -10468,7 +10474,8 @@ "area" ], "fields": [ - "operator" + "operator", + "direction" ], "terms": [ "entrance", @@ -10801,7 +10808,8 @@ "fields": [ "surveillance", "surveillance/type", - "surveillance/zone" + "surveillance/zone", + "direction" ], "terms": [ "anpr", @@ -11128,7 +11136,8 @@ ], "fields": [ "fee", - "access_simple" + "access_simple", + "direction" ], "tags": { "natural": "cave_entrance" @@ -15038,7 +15047,7 @@ "vertex" ], "fields": [ - "milestone_position" + "railway/position" ], "tags": { "railway": "milestone" @@ -15134,6 +15143,11 @@ "point", "vertex" ], + "fields": [ + "railway/position", + "railway/signal/direction", + "ref" + ], "tags": { "railway": "signal" }, @@ -18134,7 +18148,8 @@ "fields": [ "name", "operator", - "board_type" + "board_type", + "direction" ], "geometry": [ "point", @@ -18178,7 +18193,8 @@ "fields": [ "operator", "map_type", - "map_size" + "map_size", + "direction" ], "geometry": [ "point", @@ -18312,6 +18328,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "tourism": "viewpoint" }, @@ -18363,7 +18382,7 @@ "icon": "poi-warning", "fields": [ "traffic_calming", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18384,7 +18403,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18403,7 +18422,7 @@ "traffic_calming/chicane": { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18422,7 +18441,7 @@ "traffic_calming/choker": { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18441,7 +18460,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18462,7 +18481,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18481,7 +18500,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", @@ -18515,7 +18534,7 @@ "traffic_calming/rumble_strip": { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/advertising/billboard.json b/data/presets/presets/advertising/billboard.json index e3cc660ba8..1259e88888 100644 --- a/data/presets/presets/advertising/billboard.json +++ b/data/presets/presets/advertising/billboard.json @@ -1,6 +1,6 @@ { "fields": [ - "parallel_direction", + "direction", "lit" ], "geometry": [ diff --git a/data/presets/presets/highway/give_way.json b/data/presets/presets/highway/give_way.json index 232c27d259..3477820fc1 100644 --- a/data/presets/presets/highway/give_way.json +++ b/data/presets/presets/highway/give_way.json @@ -1,7 +1,7 @@ { "icon": "poi-yield", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" diff --git a/data/presets/presets/highway/mini_roundabout.json b/data/presets/presets/highway/mini_roundabout.json index f6ec98f34c..b290cbb18a 100644 --- a/data/presets/presets/highway/mini_roundabout.json +++ b/data/presets/presets/highway/mini_roundabout.json @@ -7,7 +7,7 @@ "highway": "mini_roundabout" }, "fields": [ - "clock_direction" + "direction_clock" ], "name": "Mini-Roundabout" } diff --git a/data/presets/presets/highway/speed_camera.json b/data/presets/presets/highway/speed_camera.json index f0bea39da0..c3f9afc32a 100644 --- a/data/presets/presets/highway/speed_camera.json +++ b/data/presets/presets/highway/speed_camera.json @@ -5,6 +5,7 @@ "vertex" ], "fields": [ + "direction", "ref" ], "tags": { diff --git a/data/presets/presets/highway/stop.json b/data/presets/presets/highway/stop.json index 47b5e35ede..ec4576d126 100644 --- a/data/presets/presets/highway/stop.json +++ b/data/presets/presets/highway/stop.json @@ -2,7 +2,7 @@ "icon": "poi-stop", "fields": [ "stop", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex" diff --git a/data/presets/presets/highway/street_lamp.json b/data/presets/presets/highway/street_lamp.json index d72d74bc88..8c48d005f4 100644 --- a/data/presets/presets/highway/street_lamp.json +++ b/data/presets/presets/highway/street_lamp.json @@ -9,6 +9,7 @@ }, "fields": [ "lamp_type", + "direction", "ref" ], "terms": [ diff --git a/data/presets/presets/highway/traffic_mirror.json b/data/presets/presets/highway/traffic_mirror.json index 29ca4d78fe..f2d3f066d9 100644 --- a/data/presets/presets/highway/traffic_mirror.json +++ b/data/presets/presets/highway/traffic_mirror.json @@ -3,6 +3,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "highway": "traffic_mirror" }, diff --git a/data/presets/presets/highway/traffic_signals.json b/data/presets/presets/highway/traffic_signals.json index 3dee656404..2350052fd4 100644 --- a/data/presets/presets/highway/traffic_signals.json +++ b/data/presets/presets/highway/traffic_signals.json @@ -7,7 +7,8 @@ "highway": "traffic_signals" }, "fields": [ - "traffic_signals" + "traffic_signals", + "traffic_signals/direction" ], "terms": [ "light", diff --git a/data/presets/presets/man_made/adit.json b/data/presets/presets/man_made/adit.json index fdc5352088..b71decf248 100644 --- a/data/presets/presets/man_made/adit.json +++ b/data/presets/presets/man_made/adit.json @@ -5,7 +5,8 @@ "area" ], "fields": [ - "operator" + "operator", + "direction" ], "terms": [ "entrance", diff --git a/data/presets/presets/man_made/surveillance.json b/data/presets/presets/man_made/surveillance.json index 98ec693796..2bb824dc41 100644 --- a/data/presets/presets/man_made/surveillance.json +++ b/data/presets/presets/man_made/surveillance.json @@ -7,7 +7,8 @@ "fields": [ "surveillance", "surveillance/type", - "surveillance/zone" + "surveillance/zone", + "direction" ], "terms": [ "anpr", diff --git a/data/presets/presets/natural/cave_entrance.json b/data/presets/presets/natural/cave_entrance.json index 6dba4ed022..b8fa4023aa 100644 --- a/data/presets/presets/natural/cave_entrance.json +++ b/data/presets/presets/natural/cave_entrance.json @@ -6,7 +6,8 @@ ], "fields": [ "fee", - "access_simple" + "access_simple", + "direction" ], "tags": { "natural": "cave_entrance" diff --git a/data/presets/presets/railway/milestone.json b/data/presets/presets/railway/milestone.json index f3bd6de5ed..328eef5627 100644 --- a/data/presets/presets/railway/milestone.json +++ b/data/presets/presets/railway/milestone.json @@ -5,7 +5,7 @@ "vertex" ], "fields": [ - "milestone_position" + "railway/position" ], "tags": { "railway": "milestone" diff --git a/data/presets/presets/railway/signal.json b/data/presets/presets/railway/signal.json index d4fe389fdd..9c62f5276f 100644 --- a/data/presets/presets/railway/signal.json +++ b/data/presets/presets/railway/signal.json @@ -4,6 +4,11 @@ "point", "vertex" ], + "fields": [ + "railway/position", + "railway/signal/direction", + "ref" + ], "tags": { "railway": "signal" }, diff --git a/data/presets/presets/tourism/information/board.json b/data/presets/presets/tourism/information/board.json index 40b2e504c0..0acc5a5456 100644 --- a/data/presets/presets/tourism/information/board.json +++ b/data/presets/presets/tourism/information/board.json @@ -3,7 +3,8 @@ "fields": [ "name", "operator", - "board_type" + "board_type", + "direction" ], "geometry": [ "point", diff --git a/data/presets/presets/tourism/information/map.json b/data/presets/presets/tourism/information/map.json index 0527009f9e..fce5d3418c 100644 --- a/data/presets/presets/tourism/information/map.json +++ b/data/presets/presets/tourism/information/map.json @@ -3,7 +3,8 @@ "fields": [ "operator", "map_type", - "map_size" + "map_size", + "direction" ], "geometry": [ "point", diff --git a/data/presets/presets/tourism/viewpoint.json b/data/presets/presets/tourism/viewpoint.json index 8f6fc1baa7..1d7ebb4eb2 100644 --- a/data/presets/presets/tourism/viewpoint.json +++ b/data/presets/presets/tourism/viewpoint.json @@ -4,6 +4,9 @@ "point", "vertex" ], + "fields": [ + "direction" + ], "tags": { "tourism": "viewpoint" }, diff --git a/data/presets/presets/traffic_calming.json b/data/presets/presets/traffic_calming.json index 0cb5d8fcaa..5048a9e730 100644 --- a/data/presets/presets/traffic_calming.json +++ b/data/presets/presets/traffic_calming.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "traffic_calming", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/bump.json b/data/presets/presets/traffic_calming/bump.json index 071ed8e2ca..2d281beab8 100644 --- a/data/presets/presets/traffic_calming/bump.json +++ b/data/presets/presets/traffic_calming/bump.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/chicane.json b/data/presets/presets/traffic_calming/chicane.json index bbdbfa0681..3fbd53cb08 100644 --- a/data/presets/presets/traffic_calming/chicane.json +++ b/data/presets/presets/traffic_calming/chicane.json @@ -1,7 +1,7 @@ { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/choker.json b/data/presets/presets/traffic_calming/choker.json index bee59649a2..78688ef90f 100644 --- a/data/presets/presets/traffic_calming/choker.json +++ b/data/presets/presets/traffic_calming/choker.json @@ -1,7 +1,7 @@ { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/cushion.json b/data/presets/presets/traffic_calming/cushion.json index 788b2c4ba6..47a3d5930d 100644 --- a/data/presets/presets/traffic_calming/cushion.json +++ b/data/presets/presets/traffic_calming/cushion.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/dip.json b/data/presets/presets/traffic_calming/dip.json index 6098025ba4..aecc88ab45 100644 --- a/data/presets/presets/traffic_calming/dip.json +++ b/data/presets/presets/traffic_calming/dip.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/hump.json b/data/presets/presets/traffic_calming/hump.json index fd724701f5..3c17b96be5 100644 --- a/data/presets/presets/traffic_calming/hump.json +++ b/data/presets/presets/traffic_calming/hump.json @@ -2,7 +2,7 @@ "icon": "poi-warning", "fields": [ "surface", - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/data/presets/presets/traffic_calming/rumble_strip.json b/data/presets/presets/traffic_calming/rumble_strip.json index f706db4687..37a3ca57f9 100644 --- a/data/presets/presets/traffic_calming/rumble_strip.json +++ b/data/presets/presets/traffic_calming/rumble_strip.json @@ -1,7 +1,7 @@ { "icon": "poi-warning", "fields": [ - "parallel_direction" + "direction_vertex" ], "geometry": [ "vertex", diff --git a/dist/locales/en.json b/dist/locales/en.json index cf8eb15ba3..034b67fa56 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -1498,37 +1498,9 @@ "label": "Capacity", "placeholder": "50, 100, 200..." }, - "cardinal_direction": { - "label": "Direction", - "options": { - "N": "North", - "E": "East", - "S": "South", - "W": "West", - "NE": "Northeast", - "SE": "Southeast", - "SW": "Southwest", - "NW": "Northwest", - "NNE": "North-northeast", - "ENE": "East-northeast", - "ESE": "East-southeast", - "SSE": "South-southeast", - "SSW": "South-southwest", - "WSW": "West-southwest", - "WNW": "West-northwest", - "NNW": "North-northwest" - } - }, "castle_type": { "label": "Type" }, - "clock_direction": { - "label": "Direction", - "options": { - "clockwise": "Clockwise", - "anticlockwise": "Counterclockwise" - } - }, "clothes": { "label": "Clothes" }, @@ -1651,6 +1623,46 @@ "diaper": { "label": "Diaper Changing Available" }, + "direction_cardinal": { + "label": "Direction", + "options": { + "N": "North", + "E": "East", + "S": "South", + "W": "West", + "NE": "Northeast", + "SE": "Southeast", + "SW": "Southwest", + "NW": "Northwest", + "NNE": "North-northeast", + "ENE": "East-northeast", + "ESE": "East-southeast", + "SSE": "South-southeast", + "SSW": "South-southwest", + "WSW": "West-southwest", + "WNW": "West-northwest", + "NNW": "North-northwest" + } + }, + "direction_clock": { + "label": "Direction", + "options": { + "clockwise": "Clockwise", + "anticlockwise": "Counterclockwise" + } + }, + "direction_vertex": { + "label": "Direction", + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + }, + "direction": { + "label": "Direction (Degrees Clockwise)", + "placeholder": "45, 90, 180, 270" + }, "display": { "label": "Display" }, @@ -1953,10 +1965,6 @@ "memorial": { "label": "Type" }, - "milestone_position": { - "label": "Milestone Position", - "placeholder": "Distance to one decimal (123.4)" - }, "monitoring_multi": { "label": "Monitoring" }, @@ -2074,13 +2082,6 @@ "label": "Par", "placeholder": "3, 4, 5..." }, - "parallel_direction": { - "label": "Direction", - "options": { - "forward": "Forward", - "backward": "Backward" - } - }, "park_ride": { "label": "Park and Ride" }, @@ -2182,6 +2183,18 @@ "railway": { "label": "Type" }, + "railway/position": { + "label": "Milestone Position", + "placeholder": "Distance to one decimal (123.4)" + }, + "railway/signal/direction": { + "label": "Direction", + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + }, "rating": { "label": "Power Rating" }, @@ -2484,6 +2497,14 @@ "traffic_signals": { "label": "Type" }, + "traffic_signals/direction": { + "label": "Direction", + "options": { + "forward": "Forward", + "backward": "Backward", + "both": "Both / All" + } + }, "trail_visibility": { "label": "Trail Visibility", "placeholder": "Excellent, Good, Bad...", From 1bd41b894c1be70474ecad9de171e69a58fc4b10 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 29 Dec 2017 16:04:03 -0500 Subject: [PATCH 084/206] Add direction tagging to intro graph for walkthrough --- data/intro_graph.json | 374 ++++++++++++++++++++++++++++++++---------- 1 file changed, 288 insertions(+), 86 deletions(-) diff --git a/data/intro_graph.json b/data/intro_graph.json index 1647a777ea..fcf2b191e0 100644 --- a/data/intro_graph.json +++ b/data/intro_graph.json @@ -2984,7 +2984,8 @@ "loc": [-85.643097, 41.942575], "tags": { "highway": "traffic_signals", - "traffic_signals": "signal" + "traffic_signals": "signal", + "traffic_signals:direction": "both" } }, "n1643": { @@ -3840,7 +3841,8 @@ "loc": [-85.63582, 41.942771], "tags": { "highway": "traffic_signals", - "traffic_signals": "emergency" + "traffic_signals": "emergency", + "traffic_signals:direction": "both" } }, "n1835": { @@ -8001,7 +8003,8 @@ "loc": [-85.632793, 41.94405], "tags": { "highway": "traffic_signals", - "traffic_signals": "signal" + "traffic_signals": "signal", + "traffic_signals:direction": "both" } }, "n2749": { @@ -12872,7 +12875,7 @@ }, "n3858": { "id": "n3858", - "loc": [-85.616755, 41.952231] + "loc": [-85.616762, 41.952222] }, "n3859": { "id": "n3859", @@ -12908,11 +12911,11 @@ }, "n3866": { "id": "n3866", - "loc": [-85.616572, 41.951992] + "loc": [-85.616557, 41.951997] }, "n3867": { "id": "n3867", - "loc": [-85.616583, 41.952076] + "loc": [-85.61658, 41.952093] }, "n3868": { "id": "n3868", @@ -12920,7 +12923,7 @@ }, "n3869": { "id": "n3869", - "loc": [-85.616916, 41.952279] + "loc": [-85.616918, 41.952276] }, "n387": { "id": "n387", @@ -12928,7 +12931,7 @@ }, "n3870": { "id": "n3870", - "loc": [-85.617088, 41.952254] + "loc": [-85.617098, 41.952235] }, "n3871": { "id": "n3871", @@ -13284,7 +13287,7 @@ }, "n3950": { "id": "n3950", - "loc": [-85.616494, 41.951959] + "loc": [-85.616502, 41.951946] }, "n3951": { "id": "n3951", @@ -13517,7 +13520,9 @@ "id": "n4", "loc": [-85.622764, 41.950892], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n40": { @@ -15769,7 +15774,8 @@ "loc": [-85.628201, 41.954694], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "forward" } }, "n4501": { @@ -15777,7 +15783,8 @@ "loc": [-85.627921, 41.954783], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "backward" } }, "n4502": { @@ -15785,7 +15792,8 @@ "loc": [-85.62775, 41.954696], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "backward" } }, "n4503": { @@ -15793,35 +15801,44 @@ "loc": [-85.628046, 41.954591], "tags": { "highway": "stop", - "stop": "all" + "stop": "all", + "direction": "forward" } }, "n4504": { "id": "n4504", "loc": [-85.631074, 41.957428], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4505": { "id": "n4505", "loc": [-85.630768, 41.957429], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4506": { "id": "n4506", "loc": [-85.629888, 41.957432], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4507": { "id": "n4507", "loc": [-85.629565, 41.957433], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4508": { @@ -15931,7 +15948,9 @@ "id": "n4528", "loc": [-85.631073, 41.955913], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4529": { @@ -15969,21 +15988,27 @@ "id": "n4535", "loc": [-85.629675, 41.954564], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4536": { "id": "n4536", "loc": [-85.630881, 41.954806], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4537": { "id": "n4537", "loc": [-85.630879, 41.954564], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4538": { @@ -16023,49 +16048,61 @@ "id": "n4543", "loc": [-85.631045, 41.959036], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4544": { "id": "n4544", "loc": [-85.632071, 41.959029], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4545": { "id": "n4545", "loc": [-85.632257, 41.959027], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4546": { "id": "n4546", "loc": [-85.631966, 41.957427], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4547": { "id": "n4547", "loc": [-85.632297, 41.957426], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4548": { "id": "n4548", "loc": [-85.631976, 41.955911], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4549": { "id": "n4549", "loc": [-85.632272, 41.955911], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n455": { @@ -16076,14 +16113,18 @@ "id": "n4550", "loc": [-85.632097, 41.954805], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4551": { "id": "n4551", "loc": [-85.632094, 41.954566], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4552": { @@ -16126,7 +16167,9 @@ "id": "n4560", "loc": [-85.622763, 41.95109], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4561": { @@ -16148,7 +16191,9 @@ "id": "n4564", "loc": [-85.624599, 41.950984], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4565": { @@ -16250,7 +16295,9 @@ "id": "n4583", "loc": [-85.617856, 41.954642], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4584": { @@ -16271,7 +16318,9 @@ "id": "n4586", "loc": [-85.620352, 41.951894], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4587": { @@ -16285,14 +16334,18 @@ "id": "n4588", "loc": [-85.620316, 41.950999], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4589": { "id": "n4589", "loc": [-85.620311, 41.950131], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n459": { @@ -16310,21 +16363,27 @@ "id": "n4591", "loc": [-85.620301, 41.949239], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4592": { "id": "n4592", "loc": [-85.620278, 41.947443], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4593": { "id": "n4593", "loc": [-85.619844, 41.947444], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4594": { @@ -16345,14 +16404,18 @@ "id": "n4596", "loc": [-85.622744, 41.947541], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4597": { "id": "n4597", "loc": [-85.622739, 41.947316], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4598": { @@ -16388,14 +16451,18 @@ "id": "n4601", "loc": [-85.622768, 41.949125], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4602": { "id": "n4602", "loc": [-85.622769, 41.949325], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4603": { @@ -16409,14 +16476,17 @@ "id": "n4604", "loc": [-85.622614, 41.950113], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4605": { "id": "n4605", "loc": [-85.624777, 41.949219], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4606": { @@ -16462,7 +16532,9 @@ "id": "n4611", "loc": [-85.62476, 41.947428], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4612": { @@ -16483,7 +16555,7 @@ }, "n4616": { "id": "n4616", - "loc": [-85.61823, 41.9499] + "loc": [-85.618232, 41.949913] }, "n4617": { "id": "n4617", @@ -16628,7 +16700,9 @@ "id": "n4645", "loc": [-85.635815, 41.942638], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4646": { @@ -16863,14 +16937,18 @@ "id": "n4684", "loc": [-85.635566, 41.940102], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4685": { "id": "n4685", "loc": [-85.635961, 41.940125], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4686": { @@ -16934,14 +17012,18 @@ "id": "n4694", "loc": [-85.637038, 41.942513], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4695": { "id": "n4695", "loc": [-85.637174, 41.941354], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4696": { @@ -16955,14 +17037,16 @@ "id": "n4697", "loc": [-85.638058, 41.941346], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4698": { "id": "n4698", "loc": [-85.638359, 41.941344], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n4699": { @@ -16991,14 +17075,16 @@ "id": "n4701", "loc": [-85.639277, 41.941337], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4702": { "id": "n4702", "loc": [-85.639548, 41.941334], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n4703": { @@ -17016,28 +17102,36 @@ "id": "n4705", "loc": [-85.64049, 41.941327], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4706": { "id": "n4706", "loc": [-85.640803, 41.941324], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4707": { "id": "n4707", "loc": [-85.641717, 41.941317], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "all" } }, "n4708": { "id": "n4708", "loc": [-85.641846, 41.941415], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "all" } }, "n4709": { @@ -17058,21 +17152,27 @@ "id": "n4710", "loc": [-85.642014, 41.941313], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "all" } }, "n4711": { "id": "n4711", "loc": [-85.641854, 41.942455], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4712": { "id": "n4712", "loc": [-85.641859, 41.942739], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4713": { @@ -17086,14 +17186,18 @@ "id": "n4714", "loc": [-85.640669, 41.942716], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4715": { "id": "n4715", "loc": [-85.640664, 41.942478], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4716": { @@ -17107,14 +17211,18 @@ "id": "n4717", "loc": [-85.639455, 41.942731], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4718": { "id": "n4718", "loc": [-85.63945, 41.942492], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4719": { @@ -17132,14 +17240,18 @@ "id": "n4720", "loc": [-85.638238, 41.942745], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "minor" } }, "n4721": { "id": "n4721", "loc": [-85.638233, 41.942511], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4722": { @@ -17167,21 +17279,27 @@ "id": "n4725", "loc": [-85.63704, 41.942741], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4726": { "id": "n4726", "loc": [-85.633467, 41.943818], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4727": { "id": "n4727", "loc": [-85.633987, 41.943531], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4728": { @@ -17288,7 +17406,9 @@ "id": "n4741", "loc": [-85.63481, 41.946056], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4742": { @@ -17313,14 +17433,18 @@ "id": "n4745", "loc": [-85.639487, 41.945042], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4746": { "id": "n4746", "loc": [-85.639635, 41.94387], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n4747": { @@ -17334,14 +17458,18 @@ "id": "n4748", "loc": [-85.64055, 41.943862], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4749": { "id": "n4749", "loc": [-85.640864, 41.943859], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "backward" } }, "n475": { @@ -17352,7 +17480,9 @@ "id": "n4750", "loc": [-85.640718, 41.945022], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4751": { @@ -17366,7 +17496,9 @@ "id": "n4752", "loc": [-85.641913, 41.94502], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "forward", + "stop": "minor" } }, "n4753": { @@ -17380,21 +17512,25 @@ "id": "n4754", "loc": [-85.642045, 41.94385], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "backward" } }, "n4755": { "id": "n4755", "loc": [-85.641738, 41.943852], "tags": { - "highway": "give_way" + "highway": "give_way", + "direction": "forward" } }, "n4756": { "id": "n4756", "loc": [-85.642928, 41.943843], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "minor", + "direction": "forward" } }, "n4757": { @@ -17408,14 +17544,18 @@ "id": "n4758", "loc": [-85.642986, 41.945105], "tags": { - "highway": "stop" + "highway": "stop", + "direction": "backward", + "stop": "all" } }, "n4759": { "id": "n4759", "loc": [-85.643136, 41.94502], "tags": { - "highway": "stop" + "highway": "stop", + "stop": "all", + "direction": "forward" } }, "n476": { @@ -27090,7 +27230,25 @@ }, "w660": { "id": "w660", - "nodes": ["n3982", "n3842", "n3864", "n3865", "n3866", "n3867", "n3868", "n3858", "n3869", "n3870", "n3862"], + "nodes": [ + "n3982", + "n3842", + "n3864", + "n3865", + "n2938", + "n3866", + "n2939", + "n3867", + "n3868", + "n3858", + "n2937", + "n3869", + "n2935", + "n2934", + "n3870", + "n3348", + "n3862" + ], "tags": { "highway": "service" } @@ -27299,8 +27457,12 @@ "n4002", "n4003", "n3949", + "n3351", "n3950", + "n3354", + "n3350", "n3951", + "n3349", "n3952", "n3953", "n3954", @@ -29256,6 +29418,46 @@ "tags": { "amenity": "parking" } + }, + "n2934": { + "id": "n2934", + "loc": [-85.617051, 41.952263] + }, + "n2935": { + "id": "n2935", + "loc": [-85.61699, 41.952276] + }, + "n2937": { + "id": "n2937", + "loc": [-85.616847, 41.952262] + }, + "n2938": { + "id": "n2938", + "loc": [-85.616577, 41.951956] + }, + "n2939": { + "id": "n2939", + "loc": [-85.61656, 41.952044] + }, + "n3348": { + "id": "n3348", + "loc": [-85.61714, 41.9522] + }, + "n3349": { + "id": "n3349", + "loc": [-85.616517, 41.95212] + }, + "n3350": { + "id": "n3350", + "loc": [-85.616489, 41.952033] + }, + "n3351": { + "id": "n3351", + "loc": [-85.616529, 41.951907] + }, + "n3354": { + "id": "n3354", + "loc": [-85.616488, 41.951994] } } } From d02a880d8ad929eeefd9d8a14a48b4e25efbdef7 Mon Sep 17 00:00:00 2001 From: Brian Davidson Date: Fri, 29 Dec 2017 18:58:49 -0500 Subject: [PATCH 085/206] Lower the z-index --- css/60_photos.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/60_photos.css b/css/60_photos.css index a71255e892..2bfc92709e 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -14,7 +14,7 @@ position: absolute; right: 0; top: 0; - z-index: 500; + z-index: 10; } .photo-wrapper, From 012f0330957074eecbbac6da466681b1e8537472 Mon Sep 17 00:00:00 2001 From: Brian Davidson Date: Fri, 29 Dec 2017 19:02:29 -0500 Subject: [PATCH 086/206] Make z-index one smaller than .shaded class --- css/60_photos.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/60_photos.css b/css/60_photos.css index 2bfc92709e..3d2d8670fe 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -14,7 +14,7 @@ position: absolute; right: 0; top: 0; - z-index: 10; + z-index: 48; } .photo-wrapper, From 9c27893748efb7dafaaa08a7c8dc1fe88dff50c8 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 31 Dec 2017 02:26:19 -0500 Subject: [PATCH 087/206] Check for valid multipolygon geometry when dragging nodes (this can get a bit expensive for large/complex multipolygons) --- modules/geo/geom.js | 27 +++++++++++--------- modules/geo/index.js | 1 + modules/modes/drag_node.js | 51 ++++++++++++++++++++++++++++++++++---- test/spec/geo/geom.js | 6 +++++ 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/modules/geo/geom.js b/modules/geo/geom.js index dbef358004..da76cb6075 100644 --- a/modules/geo/geom.js +++ b/modules/geo/geom.js @@ -157,6 +157,20 @@ export function geoPathIntersections(path1, path2) { return intersections; } +export function geoPathHasIntersections(path1, path2) { + for (var i = 0; i < path1.length - 1; i++) { + for (var j = 0; j < path2.length - 1; j++) { + var a = [ path1[i], path1[i+1] ]; + var b = [ path2[j], path2[j+1] ]; + var hit = geoLineIntersection(a, b); + if (hit) { + return true; + } + } + } + return false; +} + // Return whether point is contained in polygon. // @@ -195,24 +209,13 @@ export function geoPolygonContainsPolygon(outer, inner) { export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { - function testSegments(outer, inner) { - for (var i = 0; i < outer.length - 1; i++) { - for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i + 1] ]; - var b = [ inner[j], inner[j + 1] ]; - if (geoLineIntersection(a, b)) return true; - } - } - return false; - } - function testPoints(outer, inner) { return _some(inner, function(point) { return geoPointInPolygon(point, outer); }); } - return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); + return testPoints(outer, inner) || (!!checkSegments && geoPathHasIntersections(outer, inner)); } diff --git a/modules/geo/index.js b/modules/geo/index.js index 982596f04f..0c80b12d67 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -16,6 +16,7 @@ export { geoEdgeEqual } from './geom.js'; export { geoHasSelfIntersections } from './geom.js'; export { geoRotate } from './geom.js'; export { geoLineIntersection } from './geom.js'; +export { geoPathHasIntersections } from './geom.js'; export { geoPathIntersections } from './geom.js'; export { geoPathLength } from './geom.js'; export { geoPointInPolygon } from './geom.js'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 4251fb7a81..fff0f08a07 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -1,3 +1,5 @@ +import _find from 'lodash-es/find'; + import { event as d3_event, select as d3_select @@ -21,12 +23,13 @@ import { import { geoChooseEdge, geoHasSelfIntersections, + geoPathHasIntersections, geoVecSubtract, geoViewportEdge } from '../geo'; import { modeBrowse, modeSelect } from './index'; -import { osmNode } from '../osm'; +import { osmJoinWays, osmNode } from '../osm'; import { uiFlash } from '../ui'; @@ -174,15 +177,53 @@ export function modeDragNode(context) { function invalidGeometry(entity, graph) { var parents = graph.parentWays(entity); + var i, j, k; - for (var i = 0; i < parents.length; i++) { + for (i = 0; i < parents.length; i++) { var parent = parents[i]; - var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); - if (parent.isClosed()) { - if (geoHasSelfIntersections(nodes, entity.id)) { + var nodes = []; + var activeIndex = null; // which multipolygon ring contains node being dragged + + // test any parent multipolygons for valid geometry + var relations = graph.parentRelations(parent); + for (j = 0; j < relations.length; j++) { + if (!relations[j].isMultipolygon()) continue; + + var rings = osmJoinWays(relations[j].members, graph); + + // find active ring and test it for self intersections + for (k = 0; k < rings.length; k++) { + nodes = rings[k].nodes; + if (_find(nodes, function(n) { return n.id === entity.id; })) { + activeIndex = k; + if (geoHasSelfIntersections(nodes, entity.id)) { + return true; + } + } + rings[k].coords = nodes.map(function(n) { return n.loc; }); + } + + // test active ring for intersections with other rings in the multipolygon + for (k = 0; k < rings.length; k++) { + if (k === activeIndex) continue; + + // make sure active ring doesnt cross passive rings + if (geoPathHasIntersections(rings[activeIndex].coords, rings[k].coords)) { + return true; + } + } + } + + + // If we still haven't tested this node's parent way for self-intersections. + // (because it's not a member of a multipolygon), test it now. + if (activeIndex !== null && parent.isClosed()) { + nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); + if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) { return true; } } + } return false; diff --git a/test/spec/geo/geom.js b/test/spec/geo/geom.js index 3acf7f940f..188c738fd3 100644 --- a/test/spec/geo/geom.js +++ b/test/spec/geo/geom.js @@ -342,6 +342,12 @@ describe('iD.geo - geometry', function() { expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; }); + it('returns false when inner polygon fully contains outer', function() { + var inner = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var outer = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + it('returns true when outer polygon partially contains inner (some vertices contained)', function() { var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; From 6881205d43a56a6446e55182191a75c4ae23a78a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 1 Jan 2018 22:37:10 -0500 Subject: [PATCH 088/206] All touch targets are GeoJSON now This makes the code a bit more consistent and lets us avoid some hacky and probably non-performant things: - abusing CSS classes in the draw/drag datum functions (classed `.target`) (is this thing target? just check d.properties) - regexing the id for `-nope$` (is this thing a nope target? just check d.properties) - using context.hasEntity to get a the real entity (is this thing a real osmEntity? just check d.properties) - fixes code like the restriction editor which uses fake ids for split ways --- css/20_map.css | 1 + modules/behavior/drag.js | 15 +++-------- modules/behavior/draw.js | 9 ++++--- modules/behavior/draw_way.js | 12 ++++----- modules/behavior/hover.js | 16 +++++------ modules/behavior/select.js | 2 +- modules/modes/drag_node.js | 22 +++++++++------ modules/svg/areas.js | 4 +-- modules/svg/helpers.js | 45 +++++++++++++++++++------------ modules/svg/lines.js | 4 +-- modules/svg/midpoints.js | 23 +++++++++++++--- modules/svg/points.js | 24 +++++++++++++---- modules/svg/vertices.js | 37 +++++++++++++++++-------- modules/ui/fields/restrictions.js | 4 +++ 14 files changed, 140 insertions(+), 78 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 0c0905799a..d162eef120 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -240,6 +240,7 @@ g.turn circle { .form-field-restrictions .vertex { cursor: auto !important; + pointer-events: none; } .lasso #map { diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index b9ea9c0af3..f32c952ce4 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -36,7 +36,6 @@ export function behaviorDrag() { var dispatch = d3_dispatch('start', 'move', 'end'); var _origin = null; var _selector = ''; - var _filter = null; var _event; var _target; var _surface; @@ -162,9 +161,10 @@ export function behaviorDrag() { var root = this; var target = d3_event.target; for (; target && target !== root; target = target.parentNode) { - if (target[matchesSelector](_selector) && - (!_filter || _filter(target.__data__))) { - return dragstart.call(target, target.__data__); + var datum = target.__data__; + var entity = datum && datum.properties && datum.properties.entity; + if (entity && target[matchesSelector](_selector)) { + return dragstart.call(target, entity); } } }; @@ -190,13 +190,6 @@ export function behaviorDrag() { }; - drag.filter = function(_) { - if (!arguments.length) return _filter; - _filter = _; - return drag; - }; - - drag.origin = function (_) { if (!arguments.length) return _origin; _origin = _; diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 77315e03b2..5866d3c263 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -44,6 +44,8 @@ export function behaviorDraw(context) { var _lastMouse = null; + // related code + // - `mode/drag_node.js` `datum()` function datum() { if (d3_event.altKey) return {}; @@ -54,11 +56,10 @@ export function behaviorDraw(context) { element = d3_event.target; } - // When drawing, connect only to things classed as targets.. + // When drawing, snap only to touch targets.. // (this excludes area fills and active drawing elements) - var selection = d3_select(element); - if (!selection.classed('target')) return {}; - return selection.datum(); + var d = element.__data__; + return (d && d.properties && d.properties.target) ? d : {}; } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 4a49856e95..6c43c6ea9f 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -39,13 +39,14 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` function move(datum) { + var nodeLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc; var nodeGroups = datum && datum.properties && datum.properties.nodes; var loc = context.map().mouseCoordinates(); - if (datum.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` - loc = datum.loc; + if (nodeLoc) { // snap to node/vertex - a point target with `.loc` + loc = nodeLoc; - } else if (nodeGroups) { // snap to way - a line touch target or nope target with nodes + } else if (nodeGroups) { // snap to way - a line target with `.nodes` var best = Infinity; for (var i = 0; i < nodeGroups.length; i++) { var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); @@ -169,9 +170,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Accept the current position of the drawing node and continue drawing. - drawWay.add = function(loc, datum) { - if ((datum && datum.id && /-nope$/.test(datum.id)) || - context.surface().classed('nope')) { + drawWay.add = function(loc, d) { + if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) { return; // can't click here } diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index 1a496ee2d3..1dbe142d3e 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -71,15 +71,15 @@ export function behaviorHover(context) { function mouseover() { if (_buttonDown) return; - var _target = d3_event.target; - enter(_target ? _target.__data__ : null); + var target = d3_event.target; + enter(target ? target.__data__ : null); } function mouseout() { if (_buttonDown) return; - var _target = d3_event.relatedTarget; - enter(_target ? _target.__data__ : null); + var target = d3_event.relatedTarget; + enter(target ? target.__data__ : null); } @@ -97,16 +97,16 @@ export function behaviorHover(context) { } - function enter(d) { - if (d === _target) return; - _target = d; + function enter(datum) { + if (datum === _target) return; + _target = datum; _selection.selectAll('.hover') .classed('hover', false); _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - var entity = _target && _target.id && context.hasEntity(_target.id); + var entity = datum && datum.properties && datum.properties.entity; if (entity && entity.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 diff --git a/modules/behavior/select.js b/modules/behavior/select.js index 98799e6258..9a9c0b34d2 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -115,7 +115,7 @@ export function behaviorSelect(context) { var datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__); var mode = context.mode(); - var entity = datum && datum.id && context.hasEntity(datum.id); + var entity = datum && datum.properties && datum.properties.entity; if (entity) datum = entity; if (datum && datum.type === 'midpoint') { diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index fff0f08a07..faf603f66e 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -100,7 +100,7 @@ export function modeDragNode(context) { var midpoint = entity; entity = osmNode(); context.perform(actionAddMidpoint(midpoint, entity)); - entity = context.entity(entity.id); // get post-action entity + entity = context.entity(entity.id); // get post-action entity var vertex = context.surface().selectAll('.' + entity.id); drag.target(vertex.node(), entity); @@ -119,12 +119,17 @@ export function modeDragNode(context) { } + // related code + // - `behavior/draw.js` `datum()` function datum() { var event = d3_event && d3_event.sourceEvent; - if (!event || event.altKey || !d3_select(event.target).classed('target')) { + if (!event || event.altKey) { return {}; } else { - return event.target.__data__ || {}; + // When dragging, snap only to touch targets.. + // (this excludes area fills and active drawing elements) + var d = event.target.__data__; + return (d && d.properties && d.properties.target) ? d : {}; } } @@ -142,12 +147,13 @@ export function modeDragNode(context) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` var d = datum(); + var nodeLoc = d && d.properties && d.properties.entity && d.properties.entity.loc; var nodeGroups = d && d.properties && d.properties.nodes; - if (d.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` - loc = d.loc; + if (nodeLoc) { // snap to node/vertex - a point target with `.loc` + loc = nodeLoc; - } else if (nodeGroups) { // snap to way - a line touch target or nope target with nodes + } else if (nodeGroups) { // snap to way - a line target with `.nodes` var best = Infinity; for (var i = 0; i < nodeGroups.length; i++) { var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); @@ -250,8 +256,8 @@ export function modeDragNode(context) { if (_isCancelled) return; var d = datum(); - var nope = (d && d.id && /-nope$/.test(d.id)) || context.surface().classed('nope'); - var target = d && d.id && context.hasEntity(d.id); // entity to snap to + var nope = (d && d.properties && d.properties.nope) || context.surface().classed('nope'); + var target = d && d.properties && d.properties.entity; // entity to snap to if (nope) { // bounce back context.perform( diff --git a/modules/svg/areas.js b/modules/svg/areas.js index 90f35eb57a..8826ddcad0 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -59,7 +59,7 @@ export function svgAreas(projection, context) { // Targets allow hover and vertex snapping var targets = selection.selectAll('.area.target-allowed') - .filter(filter) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.targets, function key(d) { return d.id; }); // exit @@ -76,7 +76,7 @@ export function svgAreas(projection, context) { // NOPE var nopes = selection.selectAll('.area.target-nope') - .filter(function(d) { return filter({ id: d.properties.originalID }); }) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.nopes, function key(d) { return d.id; }); // exit diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index 40f28d0a6c..3ca51ea8dc 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -155,11 +155,17 @@ export function svgPath(projection, graph, isArea) { export function svgPointTransform(projection) { - return function(entity) { + var svgpoint = function(entity) { // http://jsperf.com/short-array-join var pt = projection(entity.loc); return 'translate(' + pt[0] + ',' + pt[1] + ')'; }; + + svgpoint.geojson = function(d) { + return svgpoint(d.properties.entity); + }; + + return svgpoint; } @@ -184,7 +190,7 @@ export function svgSegmentWay(way, graph, activeID) { var coords = []; var nodes = []; var startType = null; // 0 = active, 1 = passive, 2 = adjacent - var currType = null; + var currType = null; // 0 = active, 1 = passive, 2 = adjacent var node; for (var i = 0; i < way.nodes.length; i++) { @@ -244,29 +250,34 @@ export function svgSegmentWay(way, graph, activeID) { if (coordGroups.passive.length) { features.passive.push({ - 'type': 'Feature', - 'id': way.id, - 'properties': { - 'nodes': nodeGroups.passive + type: 'Feature', + id: way.id, + properties: { + target: true, + entity: way, + nodes: nodeGroups.passive }, - 'geometry': { - 'type': 'MultiLineString', - 'coordinates': coordGroups.passive + geometry: { + type: 'MultiLineString', + coordinates: coordGroups.passive } }); } if (coordGroups.active.length) { features.active.push({ - 'type': 'Feature', - 'id': way.id + '-nope', // break the ids on purpose - 'properties': { - 'originalID': way.id, - 'nodes': nodeGroups.active + type: 'Feature', + id: way.id + '-nope', // break the ids on purpose + properties: { + target: true, + entity: way, + nodes: nodeGroups.active, + nope: true, + originalID: way.id }, - 'geometry': { - 'type': 'MultiLineString', - 'coordinates': coordGroups.active + geometry: { + type: 'MultiLineString', + coordinates: coordGroups.active } }); } diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 67e433ab8a..727d0178b1 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -55,7 +55,7 @@ export function svgLines(projection, context) { // Targets allow hover and vertex snapping var targets = selection.selectAll('.line.target-allowed') - .filter(filter) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.targets, function key(d) { return d.id; }); // exit @@ -72,7 +72,7 @@ export function svgLines(projection, context) { // NOPE var nopes = selection.selectAll('.line.target-nope') - .filter(function(d) { return filter({ id: d.properties.originalID }); }) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.nopes, function key(d) { return d.id; }); // exit diff --git a/modules/svg/midpoints.js b/modules/svg/midpoints.js index bcf9495b22..0f68d949c1 100644 --- a/modules/svg/midpoints.js +++ b/modules/svg/midpoints.js @@ -18,9 +18,26 @@ export function svgMidpoints(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var getTransform = svgPointTransform(projection).geojson; + + var data = entities.map(function(midpoint) { + return { + type: 'Feature', + id: midpoint.id, + properties: { + target: true, + entity: midpoint + }, + geometry: { + type: 'Point', + coordinates: midpoint.loc + } + }; + }); + var targets = selection.selectAll('.midpoint.target') - .filter(filter) - .data(entities, function key(d) { return d.id; }); + .filter(function(d) { return filter(d.properties.entity); }) + .data(data, function key(d) { return d.id; }); // exit targets.exit() @@ -32,7 +49,7 @@ export function svgMidpoints(projection, context) { .attr('r', targetRadius) .merge(targets) .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; }) - .attr('transform', svgPointTransform(projection)); + .attr('transform', getTransform); } diff --git a/modules/svg/points.js b/modules/svg/points.js index 0b3366ab39..c2f1fc0eb7 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -29,13 +29,27 @@ export function svgPoints(projection, context) { function drawTargets(selection, graph, entities, filter) { var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; - var passive = entities.filter(function(d) { - return d.id !== context.activeID(); + var getTransform = svgPointTransform(projection).geojson; + var activeID = context.activeID(); + var data = []; + + entities.forEach(function(node) { + if (activeID === node.id) return; // draw no target on the activeID + + data.push({ + type: 'Feature', + id: node.id, + properties: { + target: true, + entity: node + }, + geometry: node.asGeoJSON() + }); }); var targets = selection.selectAll('.point.target') - .filter(filter) - .data(passive, function key(d) { return d.id; }); + .filter(function(d) { return filter(d.properties.entity); }) + .data(data, function key(d) { return d.id; }); // exit targets.exit() @@ -50,7 +64,7 @@ export function svgPoints(projection, context) { .attr('height', 30) .merge(targets) .attr('class', function(d) { return 'node point target ' + fillClass + d.id; }) - .attr('transform', svgPointTransform(projection)); + .attr('transform', getTransform); } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 1a5ed0a88e..c6843a3af6 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -184,20 +184,35 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var getTransform = svgPointTransform(projection).geojson; var activeID = context.activeID(); var data = { targets: [], nopes: [] }; entities.forEach(function(node) { if (activeID === node.id) return; // draw no target on the activeID - var currType = svgPassiveVertex(node, graph, activeID); - if (currType !== 0) { - data.targets.push(node); // passive or adjacent - allow to connect + var vertexType = svgPassiveVertex(node, graph, activeID); + if (vertexType !== 0) { // passive or adjacent - allow to connect + data.targets.push({ + type: 'Feature', + id: node.id, + properties: { + target: true, + entity: node + }, + geometry: node.asGeoJSON() + }); } else { data.nopes.push({ - id: node.id + '-nope', // not a real osmNode, break the id on purpose - originalID: node.id, - loc: node.loc + type: 'Feature', + id: node.id + '-nope', // break the ids on purpose + properties: { + target: true, + entity: node, + nope: true, + originalID: node.id + }, + geometry: node.asGeoJSON() }); } }); @@ -205,7 +220,7 @@ export function svgVertices(projection, context) { // Targets allow hover and vertex snapping var targets = selection.selectAll('.vertex.target-allowed') - .filter(filter) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.targets, function key(d) { return d.id; }); // exit @@ -218,12 +233,12 @@ export function svgVertices(projection, context) { .attr('r', function(d) { return (_radii[d.id] || radiuses.shadow[3]); }) .merge(targets) .attr('class', function(d) { return 'node vertex target target-allowed ' + targetClass + d.id; }) - .attr('transform', svgPointTransform(projection)); + .attr('transform', getTransform); // NOPE var nopes = selection.selectAll('.vertex.target-nope') - .filter(function(d) { return filter({ id: d.originalID }); }) + .filter(function(d) { return filter(d.properties.entity); }) .data(data.nopes, function key(d) { return d.id; }); // exit @@ -233,10 +248,10 @@ export function svgVertices(projection, context) { // enter/update nopes.enter() .append('circle') - .attr('r', function(d) { return (_radii[d.id.replace('-nope','')] || radiuses.shadow[3]); }) + .attr('r', function(d) { return (_radii[d.properties.originalID] || radiuses.shadow[3]); }) .merge(nopes) .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; }) - .attr('transform', svgPointTransform(projection)); + .attr('transform', getTransform); } diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index f56bc868df..dde654d45a 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -154,9 +154,13 @@ export function uiFieldRestrictions(field, context) { .call(breathe); var datum = d3_event.target.__data__; + var entity = datum && datum.properties && datum.properties.entity; + if (entity) datum = entity; + if (datum instanceof osmEntity) { fromNodeID = intersection.adjacentNodeId(datum.id); render(); + } else if (datum instanceof osmTurn) { if (datum.restriction) { context.perform( From ddaa828ec99f79af4a510811f2b1b84acd5c8fdc Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 1 Jan 2018 22:53:40 -0500 Subject: [PATCH 089/206] Handle both target GeoJSON and osmEntity in hover (fixes test and allows hover on the area fills again) --- modules/behavior/hover.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index 1dbe142d3e..e57687369d 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -6,6 +6,7 @@ import { } from 'd3-selection'; import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; +import { osmEntity } from '../osm'; import { utilRebind } from '../util/rebind'; @@ -106,9 +107,14 @@ export function behaviorHover(context) { _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - var entity = datum && datum.properties && datum.properties.entity; - if (entity && entity.id !== _newId) { + var entity; + if (datum instanceof osmEntity) { + entity = datum; + } else { + entity = datum && datum.properties && datum.properties.entity; + } + if (entity && entity.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 var mode = context.mode() && context.mode().id; if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && entity.type === 'node') { From 88c71c182ceda08f91643fbce66a000c20b826b7 Mon Sep 17 00:00:00 2001 From: Wille Marcel Date: Tue, 2 Jan 2018 10:07:00 -0300 Subject: [PATCH 090/206] add preset for amenity=love_hotel --- data/presets.yaml | 4 ++++ data/presets/presets.json | 22 ++++++++++++++++++++ data/presets/presets/amenity/love_hotel.json | 22 ++++++++++++++++++++ data/taginfo.json | 4 ++++ dist/locales/en.json | 4 ++++ 5 files changed, 56 insertions(+) create mode 100644 data/presets/presets/amenity/love_hotel.json diff --git a/data/presets.yaml b/data/presets.yaml index 5ed920b012..7ef922e9d1 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -1991,6 +1991,10 @@ en: name: Library # 'terms: book' terms: '' + amenity/love_hotel: + # amenity=love_hotel + name: Love Hotel + terms: '' amenity/marketplace: # amenity=marketplace name: Marketplace diff --git a/data/presets/presets.json b/data/presets/presets.json index 9489fab675..4b06a254d4 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -1854,6 +1854,28 @@ }, "name": "Library" }, + "amenity/love_hotel": { + "icon": "heart", + "fields": [ + "name", + "operator", + "address", + "building_area", + "smoking", + "rooms", + "internet_access", + "internet_access/fee", + "internet_access/ssid" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "amenity": "love_hotel" + }, + "name": "Love Hotel" + }, "amenity/marketplace": { "icon": "shop", "fields": [ diff --git a/data/presets/presets/amenity/love_hotel.json b/data/presets/presets/amenity/love_hotel.json new file mode 100644 index 0000000000..5612483aea --- /dev/null +++ b/data/presets/presets/amenity/love_hotel.json @@ -0,0 +1,22 @@ +{ + "icon": "heart", + "fields": [ + "name", + "operator", + "address", + "building_area", + "smoking", + "rooms", + "internet_access", + "internet_access/fee", + "internet_access/ssid" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "amenity": "love_hotel" + }, + "name": "Love Hotel" +} diff --git a/data/taginfo.json b/data/taginfo.json index 64c92eeb2a..1553e1fc56 100644 --- a/data/taginfo.json +++ b/data/taginfo.json @@ -359,6 +359,10 @@ "key": "amenity", "value": "library" }, + { + "key": "amenity", + "value": "love_hotel" + }, { "key": "amenity", "value": "marketplace" diff --git a/dist/locales/en.json b/dist/locales/en.json index 283774bf32..cec324d3b8 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -2981,6 +2981,10 @@ "name": "Library", "terms": "book" }, + "amenity/love_hotel": { + "name": "Love Hotel", + "terms": "" + }, "amenity/marketplace": { "name": "Marketplace", "terms": "" From bf1f429f9cd4722291cb76455b0d014c69eb1dc6 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 08:30:47 -0500 Subject: [PATCH 091/206] Update to rollup v0.53.2 --- dist/locales/en.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/locales/en.json b/dist/locales/en.json index cec324d3b8..3fca35c483 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -6452,4 +6452,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 7fbecbb297..15524b40ae 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "npm-run-all": "^4.0.0", "phantomjs-prebuilt": "~2.1.11", "request": "^2.81.0", - "rollup": "0.53.0", + "rollup": "0.53.2", "rollup-plugin-commonjs": "8.2.6", "rollup-plugin-includepaths": "0.2.2", "rollup-plugin-json": "2.2.0", From 9f1b928f89bc35122fae7ee0b9b24f9c2e725936 Mon Sep 17 00:00:00 2001 From: Wille Marcel Date: Tue, 2 Jan 2018 10:45:44 -0300 Subject: [PATCH 092/206] add number of nodes information to measurement panel --- data/core.yaml | 1 + dist/locales/en.json | 3 ++- modules/ui/panels/measurement.js | 19 ++++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 9999cdc983..d087073a19 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -318,6 +318,7 @@ en: location: Location metric: Metric imperial: Imperial + node_count: Number of nodes geometry: point: point vertex: vertex diff --git a/dist/locales/en.json b/dist/locales/en.json index 283774bf32..ace6c355a3 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -397,7 +397,8 @@ "centroid": "Centroid", "location": "Location", "metric": "Metric", - "imperial": "Imperial" + "imperial": "Imperial", + "node_count": "Number of nodes" } }, "geometry": { diff --git a/modules/ui/panels/measurement.js b/modules/ui/panels/measurement.js index 7de2a601d6..48a6546bd7 100644 --- a/modules/ui/panels/measurement.js +++ b/modules/ui/panels/measurement.js @@ -42,6 +42,14 @@ export function uiPanelMeasurement(context) { return result; } + function nodeCount(feature) { + if (feature.type === 'LineString') return feature.coordinates.length; + + if (feature.type === 'Polygon') { + return feature.coordinates[0].length - 1; + } + } + function displayLength(m) { var d = m * (isImperial ? 3.28084 : 1), @@ -170,6 +178,15 @@ export function uiPanelMeasurement(context) { (closed ? t('info_panels.measurement.closed') + ' ' : '') + t('geometry.' + geometry) ); + if (entity.type !== 'relation') { + list + .append('li') + .text(t('info_panels.measurement.node_count') + ':') + .append('span') + .text(nodeCount(feature) + ); + } + if (closed) { var area = steradiansToSqmeters(entity.area(resolver)); list @@ -179,6 +196,7 @@ export function uiPanelMeasurement(context) { .text(displayArea(area)); } + list .append('li') .text(lengthLabel + ':') @@ -193,7 +211,6 @@ export function uiPanelMeasurement(context) { centroid[1].toFixed(OSM_PRECISION) + ', ' + centroid[0].toFixed(OSM_PRECISION) ); - var toggle = isImperial ? 'imperial' : 'metric'; selection From 74b6c1208fac4b77c00f90b9728a196f7daef792 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 11:48:24 -0500 Subject: [PATCH 093/206] Add light bulb icon for `highway=street_lamp` preset (closes #4609) --- data/presets/presets.json | 4 ++-- data/presets/presets/amenity/shower.json | 2 +- data/presets/presets/highway/street_lamp.json | 2 +- svg/iD-sprite.json | 6 ++++-- svg/iD-sprite.src.idraw | Bin 361145 -> 361221 bytes svg/iD-sprite.src.svg | 7 +++++-- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/data/presets/presets.json b/data/presets/presets.json index 4b06a254d4..24d47a41b4 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -2748,7 +2748,7 @@ "name": "Shelter" }, "amenity/shower": { - "icon": "water", + "icon": "poi-shower", "fields": [ "operator", "opening_hours", @@ -7550,7 +7550,7 @@ "name": "Stop Sign" }, "highway/street_lamp": { - "icon": "poi-street-lamp", + "icon": "poi-bulb", "geometry": [ "point", "vertex" diff --git a/data/presets/presets/amenity/shower.json b/data/presets/presets/amenity/shower.json index 1d5fcbe892..44eeee07b9 100644 --- a/data/presets/presets/amenity/shower.json +++ b/data/presets/presets/amenity/shower.json @@ -1,5 +1,5 @@ { - "icon": "water", + "icon": "poi-shower", "fields": [ "operator", "opening_hours", diff --git a/data/presets/presets/highway/street_lamp.json b/data/presets/presets/highway/street_lamp.json index 8c48d005f4..e5531b076a 100644 --- a/data/presets/presets/highway/street_lamp.json +++ b/data/presets/presets/highway/street_lamp.json @@ -1,5 +1,5 @@ { - "icon": "poi-street-lamp", + "icon": "poi-bulb", "geometry": [ "point", "vertex" diff --git a/svg/iD-sprite.json b/svg/iD-sprite.json index e7a182af0a..d0e9e8cd3e 100644 --- a/svg/iD-sprite.json +++ b/svg/iD-sprite.json @@ -326,7 +326,7 @@ "poi-fountain": { "viewBox": "445 335 15 15" }, "poi-storage-tank": { "viewBox": "460 335 15 15" }, "poi-silo": { "viewBox": "475 335 15 15" }, - "poi-street-lamp": { "viewBox": "490 335 15 15" }, + "poi-shower": { "viewBox": "490 335 15 15" }, "poi-tool": { "viewBox": "505 335 15 15" }, "poi-tower": { "viewBox": "520 335 15 15" }, "poi-military": { "viewBox": "535 335 15 15" }, @@ -339,6 +339,7 @@ "poi-taoist": { "viewBox": "640 335 15 15" }, "poi-milestone": { "viewBox": "655 335 15 15" }, "poi-crane": { "viewBox": "670 335 15 15" }, + "poi-bulb": { "viewBox": "685 335 15 15" }, "poi-vending-machine-shape": { "fill": "currentColor" }, "poi-dice-shape": { "fill": "currentColor" }, @@ -346,7 +347,7 @@ "poi-fountain-shape": { "fill": "currentColor" }, "poi-storage-tank-shape": { "fill": "currentColor" }, "poi-silo-shape": { "fill": "currentColor" }, - "poi-street-lamp-shape": { "fill": "currentColor" }, + "poi-shower-shape": { "fill": "currentColor" }, "poi-tool-shape": { "fill": "currentColor" }, "poi-tower-shape": { "fill": "currentColor" }, "poi-military-shape": { "fill": "currentColor" }, @@ -359,6 +360,7 @@ "poi-taoist-shape": { "fill": "currentColor" }, "poi-milestone-shape": { "fill": "currentColor" }, "poi-crane-shape": { "fill": "currentColor" }, + "poi-bulb-shape": { "fill": "currentColor" }, "poi-vertex": { "viewBox": "400 350 15 15" }, diff --git a/svg/iD-sprite.src.idraw b/svg/iD-sprite.src.idraw index d51f02054c492b2801844fafc5c398ffdc7c9da8..f05167f87187c9f592c80b7df0c6c6a0d849ff77 100644 GIT binary patch delta 231640 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 pv#7@<^38S3$z~ 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 uY2S{pulzz)_PIBOQN)+aZHb>yiBT zsB?Dj+;je=`+WwV*R?KdJ+bwi))lQcwBFwOQ0o(|FSfqb`ceD1)Qb1$))ir?Jl1WminYE?n$jD1M#y>#(#0!U@F1J z)Y1{!$q?mEsSOBZ;MRZM0XO!}Cq%1|#)<3?I%zOs1s}1EArz6*f9x5t){n=8ewxaN znlc~+V=^FW({c-Nyo}5Ww8U{=rTk#qMJg2>2Ri+W;U1Nfup=&v;g~x|aX1d7qveSRbL1M`l~u>qtrlqvOgNu9Ife+<`*l43EYc8;9}d+=~wAQi(Zo z#~FGQLXE{@yb8n9=oC`J!N%U0JSJ|3&G0zyG$cC+2UIyaYNO)-HU?(&w~>yPX?Ps2 z=kgqm=h6cjN@ag^?Bu9F*SUUJ^hC zqGh(84p070>lxz7lTJHCa)llNHe@C04>;OQ94HwJHjYFAFfeCi(-F*&R!*MVdLE4X zGB?%SiaYRY@67+8#66m6=+AGxC=UI1ZRjubLq9q?nc;up#jTgDF+a~dI5giZjGdgI ztF4za&ev<4cR>e}4^!MXFh#@u#hDnXLHeCNu-~q@@CG<|MeEfO2fuGPc-7T_gFk4! zW(^#ib8w=d)Q!w>;GNJC5&uY1;t3o%5+gW=Yv%3Zfon#tR83b_oMq zLvF9e?!|xCnhcpH*!IXS5HbPjA-+bYntRxX^Q>LAh-4ZF?dq(Rf;7hii(ciaQepUB z*_kkH@j`_ov{FuNml;8`N&)WjL)s^4C}}%F`jvDOOmjbU!q+5QJP_z-xrZd6$>T-v z7oHB0H{n&%Fc3dH<7janX+)bV%ZG0t_({pcVNHL-4@e#%n8QQb*Ojltw&t7Q5ch0C zPLWMK3~(z4*Bn%4Bx7-i4-vL`u=|2LKBa%1(e!U>>lO!P7Rrcj7A7$2@m+an8$%CL z;wfC@pO#PwOj~nLQGF(Z$O;BA0-N>1#W^$5nD>dXipo6}%53yVe3VArlIBZ>MvcXt z>j-~=Jm`TWV^%Jr z={9qd6d6Itkdv>4iV-IT2^3UoZe7X@z(Ri*fSzfk$R36!*vFVv6*k+rl~*%DxU&4K zTdKW-s5#acj{?X*Dp|7ipg;u(Q5|SHj-PR`P_30L%^Bwb))IVWrVYEi=@DE`oRJ`yI*JSQtV#?;8!cV2yE|g3sG0B$XT#$bg zs4+SWzCgtARGx{DnN6=&q*QKD%#@OxD?WrMaa3@6{%J+J4lJ;q69j>j2@$U>_+CDj z_*(6cQD#vXwQ!Z6ZsMWzTK}-PzQ6-ckhnKXOA}8q{-JG0p@P)(s~Q2|%D0z?+jBV9 zuLy5tDaB%(6LeMn$#Z7v=`b*mxmbT9s!fE3simDjo)A1mRe5vktx;96ocXOv&isPq z6P(EMkhhO=yo@3x`ze}8(*{yg*WT9p<26(jml6zal|12Cx3($7ucNv+a+0==fG84h zO_FNNJ6i8VjrpLoxl_>QU<1@PRw7f~+z%_TM@$NZm$u&3x-uf--KMj>x2%7&kryi% zg&8ykQ`C_|7cH<98zSD<`tu4R%({$y`M8&%w+uYhGU$Jri%hfQ>uebhn&2g?x(SL9K2jl!9 z<2=58-Zry7&T*~8)1u@ROO>Kru)!cFi2o?gk#r1f__GgcFj7LH1&BVj@4aVFhmG7g`}?gQtU>V|8+1f~a2~S37)nRO-b`-%VKPjx_2br0YNPhj z&p2QKs~&j{_-VTtIG|k<2ecc9#R2VByS*j{oNy2mAjT!Q-~6O&F+lqo?Q6mSH~bj` zTwM&%zE*ofWPocM16+Ui@EG9u_Vw0e0EqzO0FJsBIUwf(Xy2IsPS}3Q-d$M&|JSx} z(!Onb*gmbjt-ZZ{Vf)hdZ?u1>{o;i2zkOo+rZCNI-1^9l25C3yq1)wusK6faWBY3R zX6=n})Hk&lwRnIU?@Za6~NX@&dP)+jigff_obv z^pwCBoEk|&kJ^7+rQN>s&j{@`u{l1g6i2T~=t(=?;oY7Zp~D^CM~=`x-@em&W^|g+ zcWK`hLT}9ojlIu*q=ycM%xDF39_1OG?w0Af=z-4Xdq7Fm5+FT_GdkU2Hlw$+<89_` zyUn~EZZkh}guYk%-fIy0p$CU+Oz8Wz?+2Y9`m+fg*yew#Oz8W!PmhxOmu*5n@W@N< z2er>wgU(2998ViDDkdfOnQcD3$gvrAyHdRI;3j zfCO*y#E^er!6LvnDGe=IvPj(T^jst+`hRJo0X7({;qi$Tuo zvIEDDDTdZ=KHY&j%*Nxi*c>QwhAC!L95wkL`i%H{49l;5_+gpwcD`1t)ql_F?YRXZpQQ5q7zqGTZ$R)WRa|Ea& zgVdrjHn}am;%ds(Yp?|YWfKEs|Cg(;uud&q7j*H z)rFaGO8_Jqv0=plF*-*|8(o6h8l8f#%VSfSx|QX+Jg#apIz=X1_Qow$=G8H^6n0eV zfBJC0$`Djrva!YCFg8JtuT1m?R{Pq-c^4x&hI9_p&;`%jUv1GJ){ZHn+m<4_ou!Cg zhk?1hm+ki@Z3{j<=@)k1^9>93;Ab7=umt_G3s0Jr+mC2Jk~RKuTH~*AccoadwJ0n| z%xK19;wAMUld}XA=0pG-oLzX4FNHBpezUe#0Sxph zGfSlr5rP-WZEH*tCoZXb5qdSx1A;Vj!$M}+9L^4MC(Xf6S|nakf3FvyF@M5Xa&~9KoebOXflKZkYFRqe}!_K zOl4x|337`--5dkhI+LVb6^sOdt;Gu2tHV!pE8HcZG+c`|sGfmMvvm}CULI7nt3)NB zOqDu%dX}<^+@(mZb{nlP3U69Lz%4MVYZ}xy(*E7GE&F%E7_g6|RsTkSHe3 zSD5l2cignnHI&mLT7lK(xb%fGe`DJV7MW2TT%x-~Ey0>9t{^4$WB&X#(J`rt=pBCt7u4dEhMt0Wgo7IjNm%N>!pH3 zfl7w??2usO5_?dgbgG(gpDeE;Kh?dkzX0Zp0K}se@WB#d$QKutB#Rnje?C{MmrC1h!M96GL|&tswyJ{f2C0eu9HfUEj9^MOX{;BEDK?2Ioga#Hsz2iEk_!uptL{w zF<=wKcr4bgT%3SiJyu5~_!6kraL{ZY)&8|;!o18(me#9TikWE9|kGT0}m3T(6 z)^K~S2DK+*AEE6J|52bqeOoR4AS9HJAA#&@Dz&VhI?<|sY*s!}zO*^w3mEL1C|>oq znQs9>N^w&e*KH7R_ zhwQ!?g(@gAJ34KN7_?%W!S+k-2?4hM(hp57vA9Erevqfb{l_khAA-pJ<(D>vR@I6H ziT({W^rd0&*Sci;NLp@SiMBdIkByGmU-9`9+P@X~{E5cre@{Lf!Wkt{ z;Ok^KWBVEHXTsGlGOnI$GXr8EH;jp9C_04?d~JMSq|)JpXuX~5G?|ryJ$T!G@`s;7 zr~Yl6UTM%OX028lbX;ex=qI3M(?yV#Iu|@eV6AEO?%>cr<1c~f44%n>l-u%sfy~Uf7+@_4LJc24;!C2N0G9{q5wX>Slugc5&2pq+1BNO>9FL2mj8v+@0m6JdV(#$GI;Z#MYd9|M5esG7DWCl-D zZ~$0ie;{l~1LaFi5j%u-D1d=XtLG+92-X8t;7iph)yt+R{9gOzk?~$;jJJAtjQ5JR z&I>MLyfblc(UUSC-#pZEM;JQ)$bRS~Bo~Wh|FnPc z+&%7XviewJ_1#0O*qZ&OEfr-o`O=HliW=8H^sQEWuQDCGRyLcjDEp^}ii!T__FE&q z?es#sa(aQ20-SY;rxRFWxyCJ*J)4u%>p#7qoNDd2wbwqqutAjAyl-Xy%7M-MFWPy@ zf68{~W+`!cmv4{H%5-@bbor|*+V4)$Dq)N*^AY|Hn=HxrA)e`Zlf;@mgKnmFkf-KK(;E_4n&e00K87jy7T zXLj_9wQz_xgge=(Kh*&*b008je2w8nu9yOUxdK?rBD>XFmlxBt#^6vV# ziGAaE93^3toGNoj&=;u7o(Cjx$D*FY-igLk>M$-yIM~O{oZ)^S-7C)#V;rE7e~eI3 zge8AmfET*K0-PT*Y&bUHP!iZs95}*}>EwSX7?0ysuE_C^XC&s*mF*A2xuk^OC%514 zWY-aju(PT2!S;vNkZ)X}<)_Diz57<&l$*d|X8S$z3f`RC;c^w&(F|9*MNKs(Pk8^=rVRG~=)i5(B7tR=+gsxbt zbBv7i97CXHx*OPU(F5D8Bp@pUYUMf*Q1qUL%_8t}0}=l(>e>Rc7-pQK3Y6+f?U1w$TdJAm87wwv@K#@iwTo|RYf|o1r_V}#e^N~%k1Ily zJtO-_#@O_xO<#sLSfak%iG)C@mJpgMt`08IFL+77s2$~M`|sMHinHwFHp@PFC9;RZH~E?C)l;8Ip8H_e8emOIEr&B6r>QoCedXiJj!3yY@e| zKi~d(`(HJ;T06JtdMAVulMy!_e{qi4OFOBcIu#R`D@zIj#L6tS1%NVWVg-Q;i5uyH<=DWkOe_i5N#Anuc z9-&oyQb{PW{R&d7f=_$yhFC?Y#|`^mVb)_W%y?GpYJ*laa41^7ev`Nb5lYe>F0{@u zE5QTGSe88r>%LMVPzqk{n*-1~Pwn4RjC{2vj&-UZb{;w~P5scC6_b~Tr`48Js($qC zS`F_3MWQ7jcH`0hg`eute>WUnRk48aE#zRgQgMu)R(edeWT*fmdZ#jQLl7_}S`wss zhwsX`Q!N=PG#&)$7usKrg7k|fNWXOC1?gAXUp;I=I*>Vcq$mY1lJsqNu1kYSDJI`& zf3y9A_J2uuE`0!^p&Cer7;cm)iAzVb50o8%2K^uHwD8Cm^D-}5e+}-sVXNcc#Z+#I zo=AR@n;_D_390kEi?>2e+q_1B-m2eBaobg(;}453C*GxF&QzECRcQb$gFFvJc`%Z8 zz#&99JXn+A4ek=vbL&1v?Quy#6+{Ur76Z6*YxqUD1!F$Mi*6L9E(1hKfHg`-mGBxH z3O{uHs9>fFa22P4e*{}2VJxXtkr)X|3Q5&It<zuJA&tO~=w=2FLrJy>s(0V`<}> zM4O(Q)UW+cz~)}dwlef!93X#d&%?qO?aedQ`(TUpxIDrfoV^0Nzj z=1u&;QDh_8GEuVYX8Gp-m&_ z01mme6fS_xr~;w$M$xvhr{_^@I!Mi5?86df1quePC#NFz62y9RRR=8+7v8eh13g+ zhPr@VCAf&Fb*6UoLM}DTNB#ev* zy}+~zJ5iP?>4)6wo1a1?kljiP8Aup3@(|fW896wO@0WF&XG)MMFk}Nn_&Ue|WRROc ze|kHP78{;N;o5Vx6co=vT`>-b&V zs4@NL9|M|Q?yV$qxq)%^iKDz>PD_I!72SwPkD$RIM7UNR7=wm703-CC(Ug3GB%Sl~ ze69;caC->zeMO9n!fQS4N3jKxelYA7e|2q5kDF|IgWwYx5V<6l&}P74zMo5Ks0@kd zgy1K32@7$SX-xh;kL&(a(MZ(8@}ASrkYpSv$93cD+0_9gLEw}aW704oynm?Fg=^u{ z@Xg*wJcgr$ouYKy=L98v`Z3%mNnk)vC&GYVJ;IXre_ zXOe^y$x9r^J}{#Rx%zbfqVGr!NwPWvnWv&ME3=uR1idf&=$^p=S?~hX%EHL3VFih& z+jO&AJ!S$0a_8=cAG5|kWdaNFHOs5UEm$?~`SO!CLL~vnlUzb>0p^p-LMZ{TKjg zpkN;C#{o&kpo}2!h%&^8A=p)g*QkV|i#s_m9=>YNHSCMFb$&VSi|%XtqWd2yyV9m$ z?|VS!fi%7N$lvj1hrMv`-V1kU=W2D!9)_e!e-NbhP;#(_&*btmb1#?oPTSc#?Yg3K zW@_(rR_E-_g`JDc`*&$!?-Uw`TH@Qv#>H(g^3}94$u=iB=$PYS`;xC6_g7&(g<>X{ zs(UR+ScO14oWi?Q@_Vg9Iq_)O7OYZi?kW!!Y*B7fmL9#ls2r}%32nlxFp{eN!g92u ze;jgD-!kaR=Rv?lm?MZ7>9z3&SKCuy#2^?f&KiWDgm$GxrU`qSFin*C98rd!kid)V zqqrl&gV-GS0xe%mR6+m(fLRtNDJ`Ll>M(@ne+-;J<>FCb=yfuJ zDQ=hR&wMQem5#^|ALm7f-A#`NdXg8+sx?4V1{ieohRx9!cY~hU_(BWXRKO0dIFw?8 zQ*;IPK;OAE`RdEO)+esfzXu&D$%RW`55fk?mJ*wD_L0PX|q5XfdUKtU`_9`I-7j-DPsXIwO4Ua1aXQaC*Xd=D@#sF4N5 zX2Hl+CzK}4?|d`jmWp_)yo~+EnQ(Hi;-t(>JjHIjz$%QGiR#5$uABwD(zl7mTpe~w z(ehF#m%Hhl&be{<*)c%l; zbhIvz5z_|NANGu)BiU!o`+;jNn;29=;lSn_K-OLm$<6HvVVlj`C$43i6r!-S3t%!6 za1~%C{tU?>*`9})EgF%)Q4KQBKT(YiWRyCt&_?C}LO{L006eC15T}74N;rEeGXYxx z%vV;*P-z;^BFZNb$O0`#c+r_m9BdVRa};6DVnY^WHiU+%Xn((Gz`0sb zb==Q!B3;5q9Cjn(&-Y;M6Dba>djU&hil3YlWR%s?$}5&+Bbt?HTa~KC`mC3vwkIC5 z$3Z0zi=pmV0d95-g~`E2az=QHhj1jqYHcGp+yg}KihAd|4o{z%#hqHXKFcOVK>H`0 zIy{$Rz~E7FSbgKL*r81)aeq&2X#*KNsFW4PKF}S{=P>S1S@*Kx5krW|pW#i_kScNc zQ>J7gp;~J+3!6fGREui$F!hAtNkL9D21l~T@e4Ci@e*d1A^~qt2AOs%eQ4?FtMCO) z6--bAUH^`yW1fqc>1lKd&H(|$O1rmaQsrQM-v;|>+CTykpc4*0D1Y)8r`*(AuUsXf z*wAe-!Mm|&UskH5{$zJYltQ`6V;d3sIPH908>)nfVj7^c^XhXMMmP$Q>j*C#Hri|m zBb(@oMD7z$D#t39y|CBIp4)|Oa1GNVOkLY?!$Sn@W|V+hXj{5DEezXKGwcJopd*b4 ztp!jJFIoE*{fc}Kc7GH##S}$i#eqnz)ZqezGTaJT2qZyxW`&LiKvRCc_=mhvS=e}n z9ExeQAUv^tqyQX*0AHY|WFm`LHbi4JRwE7{km@TyFYhKseSlVdoE`oJX*iic&su;t zWL99c@O20;FV~TFiY<@DGwTCaNTrhbuNCXyD~a?IBIg6yq<``uSBiB^g;jua29UzU z6SV*U%Y>gj90GE#addd!9kPd!kTo$_lnC^pC9DszlCeZ@h>gr+OFJ$E+YAjkD>DwI zh)CpM3?a)bD>id1bf-y05i6$WT5MECRz)6Vt-FOz7Lhm|S37$<`=g3?iK%$|j=YL@ zpmXV(DxOQ-b${TIyAB*!+2=L9TOGuOJO8g?mpk-kN0@y|UMGNew%{#($S@ z-+Rf*zMThl?cKBgjT^4l`Ss4LJMZs&s`I_>4ZEZ6gSwY?pVNIy_kGgCqDTt5j{6^;~ou^7ctbhO5LScV7=N?!iF{w@vxUTVBe;$O`-_~j^R|$I!3J^`{GSq5+sqV^v`=� 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≪rsGDm}eS1MmO&Mi_~}V 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


nxEs()<{% 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 diff --git a/svg/iD-sprite.src.svg b/svg/iD-sprite.src.svg index 1fffde6587..59a6a71a71 100644 --- a/svg/iD-sprite.src.svg +++ b/svg/iD-sprite.src.svg @@ -315,6 +315,9 @@ + + + @@ -351,8 +354,8 @@ - - + + From a20d26fc9b2473c1d5089bf3730dda80449576dc Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 12:56:19 -0500 Subject: [PATCH 094/206] Add search terms "kennel" "cattery" "pet" to Animal Boarding preset (closes #4647) --- data/presets.yaml | 2 +- data/presets/presets.json | 3 +++ data/presets/presets/amenity/animal_boarding.json | 3 +++ dist/locales/en.json | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/data/presets.yaml b/data/presets.yaml index 7ef922e9d1..7db3bd310f 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -1744,7 +1744,7 @@ en: amenity/animal_boarding: # amenity=animal_boarding name: Animal Boarding Facility - # 'terms: boarding,cat,dog,horse,kitten,pet boarding,pet care,pet hotel,puppy,reptile' + # 'terms: boarding,cat,cattery,dog,horse,kennel,kitten,pet,pet boarding,pet care,pet hotel,puppy,reptile' terms: '' amenity/animal_breeding: # amenity=animal_breeding diff --git a/data/presets/presets.json b/data/presets/presets.json index 24d47a41b4..f1f502404b 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -688,9 +688,12 @@ "terms": [ "boarding", "cat", + "cattery", "dog", "horse", + "kennel", "kitten", + "pet", "pet boarding", "pet care", "pet hotel", diff --git a/data/presets/presets/amenity/animal_boarding.json b/data/presets/presets/amenity/animal_boarding.json index 1c1139ddc5..b5fa6d876a 100644 --- a/data/presets/presets/amenity/animal_boarding.json +++ b/data/presets/presets/amenity/animal_boarding.json @@ -15,9 +15,12 @@ "terms": [ "boarding", "cat", + "cattery", "dog", "horse", + "kennel", "kitten", + "pet", "pet boarding", "pet care", "pet hotel", diff --git a/dist/locales/en.json b/dist/locales/en.json index 5e2b92dc3f..0ad8b8474d 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -2780,7 +2780,7 @@ }, "amenity/animal_boarding": { "name": "Animal Boarding Facility", - "terms": "boarding,cat,dog,horse,kitten,pet boarding,pet care,pet hotel,puppy,reptile" + "terms": "boarding,cat,cattery,dog,horse,kennel,kitten,pet,pet boarding,pet care,pet hotel,puppy,reptile" }, "amenity/animal_breeding": { "name": "Animal Breeding Facility", From 9f1b71bb7ee95add66e71469ae52b00c73bc75e5 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 13:27:11 -0500 Subject: [PATCH 095/206] Add support for `junction=circular` (same as `junction=roundabout`) (closes #4637) --- data/presets.yaml | 3 +++ data/presets/presets.json | 14 ++++++++++++++ data/presets/presets/_circular.json | 14 ++++++++++++++ data/taginfo.json | 4 ++++ dist/locales/en.json | 4 ++++ modules/osm/tags.js | 1 + test/spec/osm/way.js | 2 ++ 7 files changed, 42 insertions(+) create mode 100644 data/presets/presets/_circular.json diff --git a/data/presets.yaml b/data/presets.yaml index 7db3bd310f..cd0897488c 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -2625,6 +2625,9 @@ en: name: Camp Pitch # 'terms: tent,rv' terms: '' + circular: + # junction=circular + name: Traffic Circle club: # club=* name: Club diff --git a/data/presets/presets.json b/data/presets/presets.json index f1f502404b..dcebda3708 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -52,6 +52,20 @@ "searchable": false, "name": "Amenity" }, + "circular": { + "geometry": [ + "vertex", + "line" + ], + "fields": [ + "name" + ], + "tags": { + "junction": "circular" + }, + "name": "Traffic Circle", + "searchable": false + }, "highway": { "fields": [ "name", diff --git a/data/presets/presets/_circular.json b/data/presets/presets/_circular.json new file mode 100644 index 0000000000..19c6d911ca --- /dev/null +++ b/data/presets/presets/_circular.json @@ -0,0 +1,14 @@ +{ + "geometry": [ + "vertex", + "line" + ], + "fields": [ + "name" + ], + "tags": { + "junction": "circular" + }, + "name": "Traffic Circle", + "searchable": false +} diff --git a/data/taginfo.json b/data/taginfo.json index 1553e1fc56..9d34fec620 100644 --- a/data/taginfo.json +++ b/data/taginfo.json @@ -21,6 +21,10 @@ { "key": "amenity" }, + { + "key": "junction", + "value": "circular" + }, { "key": "highway" }, diff --git a/dist/locales/en.json b/dist/locales/en.json index 0ad8b8474d..cc3227962d 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -2638,6 +2638,10 @@ "name": "Amenity", "terms": "" }, + "circular": { + "name": "Traffic Circle", + "terms": "" + }, "highway": { "name": "Highway", "terms": "" diff --git a/modules/osm/tags.js b/modules/osm/tags.js index 77905a201b..1a66bdbd6d 100644 --- a/modules/osm/tags.js +++ b/modules/osm/tags.js @@ -24,6 +24,7 @@ export var osmOneWayTags = { 'motorway_link': true }, 'junction': { + 'circular': true, 'roundabout': true }, 'man_made': { diff --git a/test/spec/osm/way.js b/test/spec/osm/way.js index 3881358e9b..2460f5fc8b 100644 --- a/test/spec/osm/way.js +++ b/test/spec/osm/way.js @@ -299,6 +299,7 @@ describe('iD.osmWay', function() { expect(iD.Way({tags: { highway: 'motorway' }}).isOneWay(), 'motorway').to.be.true; expect(iD.Way({tags: { highway: 'motorway_link' }}).isOneWay(), 'motorway_link').to.be.true; expect(iD.Way({tags: { junction: 'roundabout' }}).isOneWay(), 'roundabout').to.be.true; + expect(iD.Way({tags: { junction: 'circular' }}).isOneWay(), 'circular').to.be.true; }); it('returns false when the way does not have implied oneway tag', function() { @@ -320,6 +321,7 @@ describe('iD.osmWay', function() { it('returns false when oneway=no overrides implied oneway tag', function() { expect(iD.Way({tags: { junction: 'roundabout', oneway: 'no' }}).isOneWay(), 'roundabout').to.be.false; + expect(iD.Way({tags: { junction: 'circular', oneway: 'no' }}).isOneWay(), 'circular').to.be.false; expect(iD.Way({tags: { highway: 'motorway', oneway: 'no' }}).isOneWay(), 'motorway').to.be.false; }); }); From 536c71dc6b84109f725cca446d257fcbaa221fae Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 14:25:12 -0500 Subject: [PATCH 096/206] Remove duplicate Notary Office preset (closes #4634) --- .../presets/office/lawyer/{notary.json => _notary.json} | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) rename data/presets/presets/office/lawyer/{notary.json => _notary.json} (77%) diff --git a/data/presets/presets/office/lawyer/notary.json b/data/presets/presets/office/lawyer/_notary.json similarity index 77% rename from data/presets/presets/office/lawyer/notary.json rename to data/presets/presets/office/lawyer/_notary.json index baec068e06..1137706d8b 100644 --- a/data/presets/presets/office/lawyer/notary.json +++ b/data/presets/presets/office/lawyer/_notary.json @@ -18,12 +18,6 @@ "key": "office", "value": "notary" }, - "terms": [ - "clerk", - "signature", - "wills", - "deeds", - "estate" - ], + "searchable": false, "name": "Notary Office" } From 1658ada4db15f03d756a7821c3ed8389337cbe5d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 14:25:40 -0500 Subject: [PATCH 097/206] Switch from "commercial" to "suitcase" icon for most offices Probably should have just done this when we did #4590 The "commercial" icon looks more like a retail storefront. --- data/presets.yaml | 8 +- data/presets/presets.json | 97 ++++++++++--------- data/presets/presets/office.json | 2 +- .../presets/office/_administrative.json | 2 +- data/presets/presets/office/_physician.json | 2 +- data/presets/presets/office/accountant.json | 2 +- .../presets/office/adoption_agency.json | 32 +++--- .../presets/office/advertising_agency.json | 44 ++++----- data/presets/presets/office/architect.json | 2 +- data/presets/presets/office/association.json | 44 ++++----- data/presets/presets/office/charity.json | 36 +++---- data/presets/presets/office/company.json | 4 +- data/presets/presets/office/coworking.json | 2 +- .../office/educational_institution.json | 2 +- .../presets/office/employment_agency.json | 2 +- .../presets/office/energy_supplier.json | 42 ++++---- data/presets/presets/office/estate_agent.json | 2 +- data/presets/presets/office/financial.json | 2 +- data/presets/presets/office/forestry.json | 38 ++++---- data/presets/presets/office/foundation.json | 32 +++--- data/presets/presets/office/government.json | 2 +- data/presets/presets/office/guide.json | 40 ++++---- data/presets/presets/office/insurance.json | 2 +- data/presets/presets/office/it.json | 2 +- data/presets/presets/office/lawyer.json | 2 +- .../presets/office/moving_company.json | 36 +++---- data/presets/presets/office/newspaper.json | 2 +- data/presets/presets/office/ngo.json | 2 +- data/presets/presets/office/notary.json | 38 +++++--- .../presets/office/political_party.json | 2 +- .../presets/office/private_investigator.json | 40 ++++---- data/presets/presets/office/quango.json | 48 ++++----- data/presets/presets/office/research.json | 2 +- data/presets/presets/office/surveyor.json | 32 +++--- data/presets/presets/office/tax_advisor.json | 38 ++++---- .../presets/office/telecommunication.json | 9 +- data/presets/presets/office/therapist.json | 2 +- .../presets/presets/office/water_utility.json | 40 ++++---- dist/locales/en.json | 8 +- 39 files changed, 380 insertions(+), 364 deletions(-) diff --git a/data/presets.yaml b/data/presets.yaml index cd0897488c..809ed011d7 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -4109,8 +4109,8 @@ en: terms: '' office/company: # office=company - name: Company Office - terms: '' + name: Corporate Office + terms: '' office/coworking: # office=coworking name: Coworking Space @@ -4182,8 +4182,6 @@ en: office/lawyer/notary: # 'office=lawyer, lawyer=notary' name: Notary Office - # 'terms: clerk,signature,wills,deeds,estate' - terms: '' office/moving_company: # office=moving_company name: Moving Company Office @@ -4201,6 +4199,7 @@ en: office/notary: # office=notary name: Notary Office + # 'terms: clerk,deeds,estate,signature,wills' terms: '' office/physician: # office=physician @@ -4235,6 +4234,7 @@ en: office/telecommunication: # office=telecommunication name: Telecom Office + # 'terms: communication,internet,phone,voice' terms: '' office/therapist: # office=therapist diff --git a/data/presets/presets.json b/data/presets/presets.json index dcebda3708..6107be8356 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -11574,7 +11574,7 @@ "name": "No Exit" }, "office": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "office", @@ -11595,7 +11595,7 @@ "name": "Office" }, "office/administrative": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11614,7 +11614,7 @@ "name": "Administrative Office" }, "office/physician": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11655,7 +11655,7 @@ "searchable": false }, "office/accountant": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11673,7 +11673,7 @@ "name": "Accountant Office" }, "office/adoption_agency": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11691,7 +11691,7 @@ "name": "Adoption Agency" }, "office/advertising_agency": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11715,7 +11715,7 @@ "name": "Advertising Agency" }, "office/architect": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11733,7 +11733,7 @@ "name": "Architect Office" }, "office/association": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11757,7 +11757,7 @@ "name": "Nonprofit Organization Office" }, "office/charity": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11777,7 +11777,7 @@ "name": "Charity Office" }, "office/company": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11793,10 +11793,10 @@ "office": "company" }, "terms": [], - "name": "Company Office" + "name": "Corporate Office" }, "office/coworking": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11824,7 +11824,7 @@ "name": "Coworking Space" }, "office/educational_institution": { - "icon": "commercial", + "icon": "school", "fields": [ "name", "address", @@ -11842,7 +11842,7 @@ "name": "Educational Institution" }, "office/employment_agency": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11862,7 +11862,7 @@ "name": "Employment Agency" }, "office/energy_supplier": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11885,7 +11885,7 @@ "name": "Energy Supplier Office" }, "office/estate_agent": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11903,7 +11903,7 @@ "name": "Real Estate Office" }, "office/financial": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11921,7 +11921,7 @@ "name": "Financial Office" }, "office/forestry": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11942,7 +11942,7 @@ "name": "Forestry Office" }, "office/foundation": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -11960,7 +11960,7 @@ "name": "Foundation Office" }, "office/government": { - "icon": "commercial", + "icon": "town-hall", "fields": [ "name", "government", @@ -12037,7 +12037,7 @@ "name": "Tax and Revenue Office" }, "office/guide": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12059,7 +12059,7 @@ "name": "Tour Guide Office" }, "office/insurance": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12077,7 +12077,7 @@ "name": "Insurance Office" }, "office/it": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12100,7 +12100,7 @@ "name": "Information Technology Office" }, "office/lawyer": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12137,17 +12137,11 @@ "key": "office", "value": "notary" }, - "terms": [ - "clerk", - "signature", - "wills", - "deeds", - "estate" - ], + "searchable": false, "name": "Notary Office" }, "office/moving_company": { - "icon": "commercial", + "icon": "warehouse", "fields": [ "name", "address", @@ -12167,7 +12161,7 @@ "name": "Moving Company Office" }, "office/newspaper": { - "icon": "commercial", + "icon": "library", "fields": [ "name", "address", @@ -12185,7 +12179,7 @@ "name": "Newspaper Office" }, "office/ngo": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12210,7 +12204,7 @@ "name": "NGO Office" }, "office/notary": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12224,11 +12218,17 @@ "tags": { "office": "notary" }, - "terms": [], + "terms": [ + "clerk", + "deeds", + "estate", + "signature", + "wills" + ], "name": "Notary Office" }, "office/political_party": { - "icon": "commercial", + "icon": "town-hall", "fields": [ "name", "address", @@ -12246,7 +12246,7 @@ "name": "Political Party" }, "office/private_investigator": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12268,7 +12268,7 @@ "name": "Private Investigator Office" }, "office/quango": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12294,7 +12294,7 @@ "name": "Quasi-NGO Office" }, "office/research": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12312,7 +12312,7 @@ "name": "Research Office" }, "office/surveyor": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12330,7 +12330,7 @@ "name": "Surveyor Office" }, "office/tax_advisor": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12351,7 +12351,7 @@ "name": "Tax Advisor Office" }, "office/telecommunication": { - "icon": "commercial", + "icon": "telephone", "fields": [ "name", "address", @@ -12365,11 +12365,16 @@ "tags": { "office": "telecommunication" }, - "terms": [], + "terms": [ + "communication", + "internet", + "phone", + "voice" + ], "name": "Telecom Office" }, "office/therapist": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -12389,7 +12394,7 @@ "name": "Therapist Office" }, "office/water_utility": { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office.json b/data/presets/presets/office.json index b978d9192e..19e1cfc3b6 100644 --- a/data/presets/presets/office.json +++ b/data/presets/presets/office.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "office", diff --git a/data/presets/presets/office/_administrative.json b/data/presets/presets/office/_administrative.json index 9db4dbaf49..66830eb8b6 100644 --- a/data/presets/presets/office/_administrative.json +++ b/data/presets/presets/office/_administrative.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/_physician.json b/data/presets/presets/office/_physician.json index 5bf47fc275..14ff84ea5a 100644 --- a/data/presets/presets/office/_physician.json +++ b/data/presets/presets/office/_physician.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/accountant.json b/data/presets/presets/office/accountant.json index 874525af96..de6eaa6d3e 100644 --- a/data/presets/presets/office/accountant.json +++ b/data/presets/presets/office/accountant.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/adoption_agency.json b/data/presets/presets/office/adoption_agency.json index 549b3293ad..50f57b6f20 100644 --- a/data/presets/presets/office/adoption_agency.json +++ b/data/presets/presets/office/adoption_agency.json @@ -1,18 +1,18 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "adoption_agency" - }, - "terms": [], - "name": "Adoption Agency" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "adoption_agency" + }, + "terms": [], + "name": "Adoption Agency" } diff --git a/data/presets/presets/office/advertising_agency.json b/data/presets/presets/office/advertising_agency.json index 55d7f7d55f..e4c01f9183 100644 --- a/data/presets/presets/office/advertising_agency.json +++ b/data/presets/presets/office/advertising_agency.json @@ -1,24 +1,24 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "advertising_agency" - }, - "terms": [ - "ad", - "ad agency", - "advert agency", - "advertising", - "marketing" - ], - "name": "Advertising Agency" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "advertising_agency" + }, + "terms": [ + "ad", + "ad agency", + "advert agency", + "advertising", + "marketing" + ], + "name": "Advertising Agency" } diff --git a/data/presets/presets/office/architect.json b/data/presets/presets/office/architect.json index b40eaf8bb3..cbde17e465 100644 --- a/data/presets/presets/office/architect.json +++ b/data/presets/presets/office/architect.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/association.json b/data/presets/presets/office/association.json index 1a24364ae3..2a2243015c 100644 --- a/data/presets/presets/office/association.json +++ b/data/presets/presets/office/association.json @@ -1,24 +1,24 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "association" - }, - "terms": [ - "association", - "non-profit", - "nonprofit", - "organization", - "society" - ], - "name": "Nonprofit Organization Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "association" + }, + "terms": [ + "association", + "non-profit", + "nonprofit", + "organization", + "society" + ], + "name": "Nonprofit Organization Office" } diff --git a/data/presets/presets/office/charity.json b/data/presets/presets/office/charity.json index 6b4e4d38ea..73e4b52009 100644 --- a/data/presets/presets/office/charity.json +++ b/data/presets/presets/office/charity.json @@ -1,20 +1,20 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "charity" - }, - "terms": [ - "charitable organization" - ], - "name": "Charity Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "charity" + }, + "terms": [ + "charitable organization" + ], + "name": "Charity Office" } diff --git a/data/presets/presets/office/company.json b/data/presets/presets/office/company.json index bb86e90df2..88cfa026be 100644 --- a/data/presets/presets/office/company.json +++ b/data/presets/presets/office/company.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", @@ -15,5 +15,5 @@ "office": "company" }, "terms": [], - "name": "Company Office" + "name": "Corporate Office" } diff --git a/data/presets/presets/office/coworking.json b/data/presets/presets/office/coworking.json index 2b96cee75b..5946a2fb83 100644 --- a/data/presets/presets/office/coworking.json +++ b/data/presets/presets/office/coworking.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/educational_institution.json b/data/presets/presets/office/educational_institution.json index 4e9e6d8aba..6930ca0df1 100644 --- a/data/presets/presets/office/educational_institution.json +++ b/data/presets/presets/office/educational_institution.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "school", "fields": [ "name", "address", diff --git a/data/presets/presets/office/employment_agency.json b/data/presets/presets/office/employment_agency.json index 8753fb5e4b..e7a7e494ee 100644 --- a/data/presets/presets/office/employment_agency.json +++ b/data/presets/presets/office/employment_agency.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/energy_supplier.json b/data/presets/presets/office/energy_supplier.json index 41a2a6387a..9a13dfd47b 100644 --- a/data/presets/presets/office/energy_supplier.json +++ b/data/presets/presets/office/energy_supplier.json @@ -1,23 +1,23 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "energy_supplier" - }, - "terms": [ - "electricity", - "energy company", - "energy utility", - "gas utility" - ], - "name": "Energy Supplier Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "energy_supplier" + }, + "terms": [ + "electricity", + "energy company", + "energy utility", + "gas utility" + ], + "name": "Energy Supplier Office" } diff --git a/data/presets/presets/office/estate_agent.json b/data/presets/presets/office/estate_agent.json index d9655383ce..703d02344b 100644 --- a/data/presets/presets/office/estate_agent.json +++ b/data/presets/presets/office/estate_agent.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/financial.json b/data/presets/presets/office/financial.json index 9072123eda..a3d33aacdc 100644 --- a/data/presets/presets/office/financial.json +++ b/data/presets/presets/office/financial.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/forestry.json b/data/presets/presets/office/forestry.json index 160968fd4e..10d7bb6b16 100644 --- a/data/presets/presets/office/forestry.json +++ b/data/presets/presets/office/forestry.json @@ -1,21 +1,21 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "forestry" - }, - "terms": [ - "forest", - "ranger" - ], - "name": "Forestry Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "forestry" + }, + "terms": [ + "forest", + "ranger" + ], + "name": "Forestry Office" } diff --git a/data/presets/presets/office/foundation.json b/data/presets/presets/office/foundation.json index 29a1ba3264..61dbb958a0 100644 --- a/data/presets/presets/office/foundation.json +++ b/data/presets/presets/office/foundation.json @@ -1,18 +1,18 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "foundation" - }, - "terms": [], - "name": "Foundation Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "foundation" + }, + "terms": [], + "name": "Foundation Office" } diff --git a/data/presets/presets/office/government.json b/data/presets/presets/office/government.json index 31050bd318..389996c598 100644 --- a/data/presets/presets/office/government.json +++ b/data/presets/presets/office/government.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "town-hall", "fields": [ "name", "government", diff --git a/data/presets/presets/office/guide.json b/data/presets/presets/office/guide.json index 6f76a9957e..5c3a2d068c 100644 --- a/data/presets/presets/office/guide.json +++ b/data/presets/presets/office/guide.json @@ -1,22 +1,22 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "guide" - }, - "terms": [ - "dive guide", - "mountain guide", - "tour guide" - ], - "name": "Tour Guide Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "guide" + }, + "terms": [ + "dive guide", + "mountain guide", + "tour guide" + ], + "name": "Tour Guide Office" } diff --git a/data/presets/presets/office/insurance.json b/data/presets/presets/office/insurance.json index 81ce7ff142..57d57ed639 100644 --- a/data/presets/presets/office/insurance.json +++ b/data/presets/presets/office/insurance.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/it.json b/data/presets/presets/office/it.json index 1579159525..3c190ce314 100644 --- a/data/presets/presets/office/it.json +++ b/data/presets/presets/office/it.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/lawyer.json b/data/presets/presets/office/lawyer.json index 40efd07b16..bb370c4c21 100644 --- a/data/presets/presets/office/lawyer.json +++ b/data/presets/presets/office/lawyer.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/moving_company.json b/data/presets/presets/office/moving_company.json index 624d2d3f98..3149684db1 100644 --- a/data/presets/presets/office/moving_company.json +++ b/data/presets/presets/office/moving_company.json @@ -1,20 +1,20 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "moving_company" - }, - "terms": [ - "relocation" - ], - "name": "Moving Company Office" + "icon": "warehouse", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "moving_company" + }, + "terms": [ + "relocation" + ], + "name": "Moving Company Office" } diff --git a/data/presets/presets/office/newspaper.json b/data/presets/presets/office/newspaper.json index 5993e6a9ca..576d4d07a9 100644 --- a/data/presets/presets/office/newspaper.json +++ b/data/presets/presets/office/newspaper.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "library", "fields": [ "name", "address", diff --git a/data/presets/presets/office/ngo.json b/data/presets/presets/office/ngo.json index ffb802ccc2..3a652a3181 100644 --- a/data/presets/presets/office/ngo.json +++ b/data/presets/presets/office/ngo.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/notary.json b/data/presets/presets/office/notary.json index 1ad973630b..91c58e4708 100644 --- a/data/presets/presets/office/notary.json +++ b/data/presets/presets/office/notary.json @@ -1,18 +1,24 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "notary" - }, - "terms": [], - "name": "Notary Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "notary" + }, + "terms": [ + "clerk", + "deeds", + "estate", + "signature", + "wills" + ], + "name": "Notary Office" } diff --git a/data/presets/presets/office/political_party.json b/data/presets/presets/office/political_party.json index d5bdaff9fd..6f74f04049 100644 --- a/data/presets/presets/office/political_party.json +++ b/data/presets/presets/office/political_party.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "town-hall", "fields": [ "name", "address", diff --git a/data/presets/presets/office/private_investigator.json b/data/presets/presets/office/private_investigator.json index a0d40d575c..5665c46161 100644 --- a/data/presets/presets/office/private_investigator.json +++ b/data/presets/presets/office/private_investigator.json @@ -1,22 +1,22 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "private_investigator" - }, - "terms": [ - "PI", - "private eye", - "private detective" - ], - "name": "Private Investigator Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "private_investigator" + }, + "terms": [ + "PI", + "private eye", + "private detective" + ], + "name": "Private Investigator Office" } diff --git a/data/presets/presets/office/quango.json b/data/presets/presets/office/quango.json index 23e3e3a097..9deebf543a 100644 --- a/data/presets/presets/office/quango.json +++ b/data/presets/presets/office/quango.json @@ -1,26 +1,26 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "quango" - }, - "terms": [ - "ngo", - "non government", - "non-government", - "organization", - "organisation", - "quasi autonomous", - "quasi-autonomous" - ], - "name": "Quasi-NGO Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "quango" + }, + "terms": [ + "ngo", + "non government", + "non-government", + "organization", + "organisation", + "quasi autonomous", + "quasi-autonomous" + ], + "name": "Quasi-NGO Office" } diff --git a/data/presets/presets/office/research.json b/data/presets/presets/office/research.json index e7777f9e63..6cb16a010f 100644 --- a/data/presets/presets/office/research.json +++ b/data/presets/presets/office/research.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/surveyor.json b/data/presets/presets/office/surveyor.json index 3b51c5f682..a500320a27 100644 --- a/data/presets/presets/office/surveyor.json +++ b/data/presets/presets/office/surveyor.json @@ -1,18 +1,18 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "surveyor" - }, - "terms": [], - "name": "Surveyor Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "surveyor" + }, + "terms": [], + "name": "Surveyor Office" } diff --git a/data/presets/presets/office/tax_advisor.json b/data/presets/presets/office/tax_advisor.json index 26b67738a1..23bb7ab72b 100644 --- a/data/presets/presets/office/tax_advisor.json +++ b/data/presets/presets/office/tax_advisor.json @@ -1,21 +1,21 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "tax_advisor" - }, - "terms": [ - "tax", - "tax consultant" - ], - "name": "Tax Advisor Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "tax_advisor" + }, + "terms": [ + "tax", + "tax consultant" + ], + "name": "Tax Advisor Office" } diff --git a/data/presets/presets/office/telecommunication.json b/data/presets/presets/office/telecommunication.json index fce97305f0..1160bfce1a 100644 --- a/data/presets/presets/office/telecommunication.json +++ b/data/presets/presets/office/telecommunication.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "telephone", "fields": [ "name", "address", @@ -13,6 +13,11 @@ "tags": { "office": "telecommunication" }, - "terms": [], + "terms": [ + "communication", + "internet", + "phone", + "voice" + ], "name": "Telecom Office" } diff --git a/data/presets/presets/office/therapist.json b/data/presets/presets/office/therapist.json index 0c21e35533..925d31586e 100644 --- a/data/presets/presets/office/therapist.json +++ b/data/presets/presets/office/therapist.json @@ -1,5 +1,5 @@ { - "icon": "commercial", + "icon": "suitcase", "fields": [ "name", "address", diff --git a/data/presets/presets/office/water_utility.json b/data/presets/presets/office/water_utility.json index accfa5651a..0f74a1df5b 100644 --- a/data/presets/presets/office/water_utility.json +++ b/data/presets/presets/office/water_utility.json @@ -1,22 +1,22 @@ { - "icon": "commercial", - "fields": [ - "name", - "address", - "building_area", - "opening_hours", - "operator" - ], - "geometry": [ - "point", - "area" - ], - "tags": { - "office": "water_utility" - }, - "terms": [ - "water board", - "utility" - ], - "name": "Water Utility Office" + "icon": "suitcase", + "fields": [ + "name", + "address", + "building_area", + "opening_hours", + "operator" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "office": "water_utility" + }, + "terms": [ + "water board", + "utility" + ], + "name": "Water Utility Office" } diff --git a/dist/locales/en.json b/dist/locales/en.json index cc3227962d..f6df3fc2c5 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -4859,7 +4859,7 @@ "terms": "charitable organization" }, "office/company": { - "name": "Company Office", + "name": "Corporate Office", "terms": "" }, "office/coworking": { @@ -4924,7 +4924,7 @@ }, "office/lawyer/notary": { "name": "Notary Office", - "terms": "clerk,signature,wills,deeds,estate" + "terms": "" }, "office/moving_company": { "name": "Moving Company Office", @@ -4940,7 +4940,7 @@ }, "office/notary": { "name": "Notary Office", - "terms": "" + "terms": "clerk,deeds,estate,signature,wills" }, "office/political_party": { "name": "Political Party", @@ -4968,7 +4968,7 @@ }, "office/telecommunication": { "name": "Telecom Office", - "terms": "" + "terms": "communication,internet,phone,voice" }, "office/therapist": { "name": "Therapist Office", From 8928e95da2ca2ffd1229e72e5f3db8e7341fa072 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 15:07:35 -0500 Subject: [PATCH 098/206] Allow fallback presets (area, line, point) to appear in the recent list (closes #4612) --- modules/presets/index.js | 60 ++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/modules/presets/index.js b/modules/presets/index.js index 12f8e3a0e9..789c3cb9cf 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -19,14 +19,14 @@ export function presetIndex() { // a presetCollection with methods for // loading new data and returning defaults - var all = presetCollection([]), - defaults = { area: all, line: all, point: all, vertex: all, relation: all }, - fields = {}, - universal = [], - recent = presetCollection([]); + var all = presetCollection([]); + var _defaults = { area: all, line: all, point: all, vertex: all, relation: all }; + var _fields = {}; + var _universal = []; + var _recent = presetCollection([]); // Index of presets by (geometry, tag key). - var index = { + var _index = { point: {}, vertex: {}, line: {}, @@ -43,9 +43,9 @@ export function presetIndex() { geometry = 'point'; } - var geometryMatches = index[geometry], - best = -1, - match; + var geometryMatches = _index[geometry]; + var best = -1; + var match; for (var k in entity.tags) { // If any part of an address is present, @@ -86,9 +86,9 @@ export function presetIndex() { // (see `Way#isArea()`). In other words, the keys of L form the whitelist, // and the subkeys form the blacklist. all.areaKeys = function() { - var areaKeys = {}, - ignore = ['barrier', 'highway', 'footway', 'railway', 'type'], // probably a line.. - presets = _reject(all.collection, 'suggestion'); + var areaKeys = {}; + var ignore = ['barrier', 'highway', 'footway', 'railway', 'type']; // probably a line.. + var presets = _reject(all.collection, 'suggestion'); // whitelist presets.forEach(function(d) { @@ -123,21 +123,23 @@ export function presetIndex() { var d = data.presets; all.collection = []; - recent.collection = []; - fields = {}; - universal = []; - index = { point: {}, vertex: {}, line: {}, area: {}, relation: {} }; + _recent.collection = []; + _fields = {}; + _universal = []; + _index = { point: {}, vertex: {}, line: {}, area: {}, relation: {} }; if (d.fields) { _forEach(d.fields, function(d, id) { - fields[id] = presetField(id, d); - if (d.universal) universal.push(fields[id]); + _fields[id] = presetField(id, d); + if (d.universal) { + _universal.push(_fields[id]); + } }); } if (d.presets) { _forEach(d.presets, function(d, id) { - all.collection.push(presetPreset(id, d, fields)); + all.collection.push(presetPreset(id, d, _fields)); }); } @@ -149,7 +151,7 @@ export function presetIndex() { if (d.defaults) { var getItem = _bind(all.item, all); - defaults = { + _defaults = { area: presetCollection(d.defaults.area.map(getItem)), line: presetCollection(d.defaults.line.map(getItem)), point: presetCollection(d.defaults.point.map(getItem)), @@ -159,11 +161,11 @@ export function presetIndex() { } for (var i = 0; i < all.collection.length; i++) { - var preset = all.collection[i], - geometry = preset.geometry; + var preset = all.collection[i]; + var geometry = preset.geometry; for (var j = 0; j < geometry.length; j++) { - var g = index[geometry[j]]; + var g = _index[geometry[j]]; for (var k in preset.tags) { (g[k] = g[k] || []).push(preset); } @@ -174,23 +176,21 @@ export function presetIndex() { }; all.field = function(id) { - return fields[id]; + return _fields[id]; }; all.universal = function() { - return universal; + return _universal; }; all.defaults = function(geometry, n) { - var rec = recent.matchGeometry(geometry).collection.slice(0, 4), - def = _uniq(rec.concat(defaults[geometry].collection)).slice(0, n - 1); + var rec = _recent.matchGeometry(geometry).collection.slice(0, 4); + var def = _uniq(rec.concat(_defaults[geometry].collection)).slice(0, n - 1); return presetCollection(_uniq(rec.concat(def).concat(all.item(geometry)))); }; all.choose = function(preset) { - if (!preset.isFallback()) { - recent = presetCollection(_uniq([preset].concat(recent.collection))); - } + _recent = presetCollection(_uniq([preset].concat(_recent.collection))); return all; }; From 18bec2be1880b85e4ae778bf5401660a9dbeecda Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 15:42:23 -0500 Subject: [PATCH 099/206] Set Payment Type as universal field, add it to many presets (closes #4437) --- data/presets/fields.json | 3 +- data/presets/fields/payment_multi.json | 3 +- data/presets/presets.json | 6365 +++++++++++------ .../presets/amenity/bicycle_rental.json | 3 +- data/presets/presets/amenity/boat_rental.json | 3 +- data/presets/presets/amenity/car_rental.json | 3 +- data/presets/presets/amenity/car_sharing.json | 3 +- data/presets/presets/amenity/car_wash.json | 3 +- data/presets/presets/amenity/casino.json | 1 + data/presets/presets/amenity/cinema.json | 3 +- .../presets/amenity/driving_school.json | 3 +- data/presets/presets/amenity/fuel.json | 3 +- data/presets/presets/amenity/pharmacy.json | 3 +- data/presets/presets/shop.json | 3 +- data/presets/presets/shop/_fishmonger.json | 3 +- data/presets/presets/shop/_furnace.json | 3 +- data/presets/presets/shop/agrarian.json | 3 +- data/presets/presets/shop/alcohol.json | 1 + data/presets/presets/shop/anime.json | 3 +- data/presets/presets/shop/antiques.json | 3 +- data/presets/presets/shop/appliance.json | 3 +- data/presets/presets/shop/art.json | 3 +- data/presets/presets/shop/baby_goods.json | 3 +- data/presets/presets/shop/bag.json | 3 +- data/presets/presets/shop/bakery.json | 3 +- .../presets/shop/bathroom_furnishing.json | 3 +- data/presets/presets/shop/beauty.json | 3 +- data/presets/presets/shop/beauty/nails.json | 3 +- data/presets/presets/shop/beauty/tanning.json | 3 +- data/presets/presets/shop/bed.json | 3 +- data/presets/presets/shop/beverages.json | 3 +- data/presets/presets/shop/bicycle.json | 3 +- data/presets/presets/shop/bookmaker.json | 3 +- data/presets/presets/shop/books.json | 1 + data/presets/presets/shop/boutique.json | 3 +- data/presets/presets/shop/butcher.json | 3 +- data/presets/presets/shop/candles.json | 3 +- data/presets/presets/shop/car.json | 5 +- data/presets/presets/shop/car_parts.json | 3 +- data/presets/presets/shop/car_repair.json | 3 +- data/presets/presets/shop/carpet.json | 3 +- data/presets/presets/shop/charity.json | 3 +- data/presets/presets/shop/cheese.json | 3 +- data/presets/presets/shop/chemist.json | 3 +- data/presets/presets/shop/chocolate.json | 3 +- data/presets/presets/shop/clothes.json | 3 +- data/presets/presets/shop/coffee.json | 3 +- data/presets/presets/shop/computer.json | 3 +- data/presets/presets/shop/confectionery.json | 3 +- data/presets/presets/shop/convenience.json | 3 +- data/presets/presets/shop/copyshop.json | 3 +- data/presets/presets/shop/cosmetics.json | 3 +- data/presets/presets/shop/craft.json | 3 +- data/presets/presets/shop/curtain.json | 3 +- data/presets/presets/shop/dairy.json | 3 +- data/presets/presets/shop/deli.json | 3 +- .../presets/shop/department_store.json | 3 +- data/presets/presets/shop/doityourself.json | 3 +- data/presets/presets/shop/dry_cleaning.json | 3 +- data/presets/presets/shop/e-cigarette.json | 3 +- data/presets/presets/shop/electronics.json | 3 +- data/presets/presets/shop/erotic.json | 3 +- data/presets/presets/shop/fabric.json | 3 +- data/presets/presets/shop/farm.json | 3 +- data/presets/presets/shop/fashion.json | 3 +- data/presets/presets/shop/florist.json | 3 +- data/presets/presets/shop/frame.json | 3 +- data/presets/presets/shop/furniture.json | 3 +- data/presets/presets/shop/garden_centre.json | 3 +- data/presets/presets/shop/gas.json | 3 +- data/presets/presets/shop/gift.json | 3 +- data/presets/presets/shop/greengrocer.json | 3 +- data/presets/presets/shop/hairdresser.json | 3 +- data/presets/presets/shop/hardware.json | 3 +- data/presets/presets/shop/hearing_aids.json | 3 +- data/presets/presets/shop/herbalist.json | 3 +- data/presets/presets/shop/hifi.json | 3 +- data/presets/presets/shop/houseware.json | 3 +- .../presets/shop/interior_decoration.json | 3 +- data/presets/presets/shop/jewelry.json | 3 +- data/presets/presets/shop/kiosk.json | 3 +- data/presets/presets/shop/kitchen.json | 3 +- data/presets/presets/shop/laundry.json | 3 +- data/presets/presets/shop/leather.json | 3 +- data/presets/presets/shop/locksmith.json | 3 +- data/presets/presets/shop/lottery.json | 3 +- data/presets/presets/shop/massage.json | 3 +- data/presets/presets/shop/medical_supply.json | 3 +- data/presets/presets/shop/mobile_phone.json | 3 +- data/presets/presets/shop/motorcycle.json | 5 +- data/presets/presets/shop/music.json | 3 +- .../presets/shop/musical_instrument.json | 3 +- data/presets/presets/shop/newsagent.json | 3 +- .../presets/shop/nutrition_supplements.json | 3 +- data/presets/presets/shop/optician.json | 3 +- data/presets/presets/shop/organic.json | 3 +- data/presets/presets/shop/outdoor.json | 3 +- data/presets/presets/shop/paint.json | 3 +- data/presets/presets/shop/pastry.json | 5 +- data/presets/presets/shop/pawnbroker.json | 3 +- data/presets/presets/shop/perfumery.json | 3 +- data/presets/presets/shop/pet.json | 3 +- data/presets/presets/shop/photo.json | 3 +- data/presets/presets/shop/pyrotechnics.json | 3 +- data/presets/presets/shop/radiotechnics.json | 3 +- data/presets/presets/shop/religion.json | 5 +- data/presets/presets/shop/scuba_diving.json | 3 +- data/presets/presets/shop/seafood.json | 3 +- data/presets/presets/shop/second_hand.json | 3 +- data/presets/presets/shop/shoes.json | 3 +- data/presets/presets/shop/sports.json | 3 +- data/presets/presets/shop/stationery.json | 3 +- data/presets/presets/shop/storage_rental.json | 3 +- data/presets/presets/shop/supermarket.json | 3 +- data/presets/presets/shop/tailor.json | 3 +- data/presets/presets/shop/tattoo.json | 3 +- data/presets/presets/shop/tea.json | 3 +- data/presets/presets/shop/ticket.json | 3 +- data/presets/presets/shop/tiles.json | 3 +- data/presets/presets/shop/tobacco.json | 3 +- data/presets/presets/shop/toys.json | 3 +- data/presets/presets/shop/trade.json | 3 +- data/presets/presets/shop/travel_agency.json | 3 +- data/presets/presets/shop/tyres.json | 3 +- data/presets/presets/shop/vacuum_cleaner.json | 3 +- data/presets/presets/shop/variety_store.json | 3 +- data/presets/presets/shop/video.json | 3 +- data/presets/presets/shop/video_games.json | 3 +- data/presets/presets/shop/watches.json | 3 +- data/presets/presets/shop/water_sports.json | 3 +- data/presets/presets/shop/weapons.json | 3 +- data/presets/presets/shop/window_blind.json | 3 +- data/presets/presets/shop/wine.json | 3 +- 133 files changed, 4508 insertions(+), 2255 deletions(-) diff --git a/data/presets/fields.json b/data/presets/fields.json index 17def7fd9b..af813c504e 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -1357,7 +1357,8 @@ "payment_multi": { "key": "payment:", "type": "multiCombo", - "label": "Payment Types" + "label": "Payment Types", + "universal": true }, "phases": { "key": "phases", diff --git a/data/presets/fields/payment_multi.json b/data/presets/fields/payment_multi.json index 587fb013a4..1a4b7f6cee 100644 --- a/data/presets/fields/payment_multi.json +++ b/data/presets/fields/payment_multi.json @@ -1,5 +1,6 @@ { "key": "payment:", "type": "multiCombo", - "label": "Payment Types" + "label": "Payment Types", + "universal": true } diff --git a/data/presets/presets.json b/data/presets/presets.json index 6107be8356..f700cf244b 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -950,7 +950,8 @@ "fields": [ "capacity", "network", - "operator" + "operator", + "payment_multi" ], "geometry": [ "point", @@ -1017,7 +1018,8 @@ "amenity/boat_rental": { "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "geometry": [ "point", @@ -1080,7 +1082,8 @@ "icon": "car", "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "geometry": [ "point", @@ -1096,7 +1099,8 @@ "fields": [ "name", "operator", - "capacity" + "capacity", + "payment_multi" ], "geometry": [ "point", @@ -1112,7 +1116,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -1131,6 +1136,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "smoking" ], "geometry": [ @@ -1197,7 +1203,8 @@ "name", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -1534,7 +1541,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -1650,8 +1658,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "geometry": [ "point", @@ -2052,8 +2061,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "geometry": [ "point", @@ -15336,7 +15346,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15355,7 +15366,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15378,7 +15390,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15419,7 +15432,8 @@ "agrarian", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15446,6 +15460,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "geometry": [ @@ -15470,7 +15485,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15495,7 +15511,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15513,7 +15530,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15546,7 +15564,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15569,7 +15588,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15587,7 +15607,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15609,7 +15630,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15627,7 +15649,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15646,7 +15669,8 @@ "address", "building_area", "opening_hours", - "beauty" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15669,7 +15693,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15696,7 +15721,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15719,7 +15745,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15737,7 +15764,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15755,8 +15783,9 @@ "operator", "address", "building_area", + "service/bicycle", "opening_hours", - "service/bicycle" + "payment_multi" ], "geometry": [ "point", @@ -15778,7 +15807,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15800,6 +15830,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "internet_access", "internet_access/fee", "internet_access/ssid" @@ -15821,7 +15852,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15839,7 +15871,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15860,7 +15893,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15878,7 +15912,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15899,8 +15934,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "geometry": [ "point", @@ -15923,9 +15959,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15946,7 +15983,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -15967,8 +16005,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "geometry": [ "point", @@ -15991,7 +16030,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16009,7 +16049,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16032,7 +16073,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16051,7 +16093,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16069,7 +16112,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16087,7 +16131,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16105,7 +16150,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16126,7 +16172,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16144,7 +16191,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16162,7 +16210,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16180,7 +16229,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16203,7 +16253,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16225,7 +16276,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16248,7 +16300,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16271,7 +16324,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16289,7 +16343,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16307,7 +16362,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16325,7 +16381,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16347,7 +16404,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16378,7 +16436,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16400,7 +16459,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16421,7 +16481,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16444,7 +16505,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16462,7 +16524,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16483,7 +16546,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16530,7 +16594,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16553,7 +16618,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16577,7 +16643,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16603,7 +16670,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16624,7 +16692,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16646,7 +16715,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16667,7 +16737,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16685,7 +16756,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16703,7 +16775,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16721,7 +16794,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16743,7 +16817,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16765,7 +16840,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16783,7 +16859,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16806,7 +16883,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16824,7 +16902,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16842,7 +16921,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16860,7 +16940,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16878,7 +16959,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16900,7 +16982,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16939,7 +17022,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16957,7 +17041,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -16975,7 +17060,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17012,7 +17098,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17033,7 +17120,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17055,7 +17143,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17076,7 +17165,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17094,7 +17184,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17112,7 +17203,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17134,7 +17226,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17153,7 +17246,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17176,7 +17270,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17194,7 +17289,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17217,7 +17313,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17235,7 +17332,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17253,7 +17351,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17280,7 +17379,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17302,7 +17402,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17320,7 +17421,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17338,9 +17440,10 @@ "operator", "address", "building_area", - "opening_hours", "religion", - "denomination" + "denomination", + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17358,7 +17461,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17376,7 +17480,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17397,8 +17502,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "geometry": [ "point", @@ -17423,7 +17529,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17441,7 +17548,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17459,7 +17567,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17481,7 +17590,8 @@ "operator", "address", "building", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17499,7 +17609,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17522,7 +17633,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17544,7 +17656,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17562,7 +17675,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17580,7 +17694,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17598,7 +17713,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17616,7 +17732,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17634,7 +17751,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17653,7 +17771,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17671,7 +17790,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17689,7 +17809,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17707,7 +17828,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17725,7 +17847,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17743,7 +17866,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17761,7 +17885,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17782,7 +17907,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17800,7 +17926,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17818,7 +17945,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17842,7 +17970,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -17860,7 +17989,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -30482,7 +30612,8 @@ "fields": [ "capacity", "network", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -30501,7 +30632,8 @@ "fields": [ "capacity", "network", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -30520,7 +30652,8 @@ "fields": [ "capacity", "network", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -30539,7 +30672,8 @@ "fields": [ "capacity", "network", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -30558,7 +30692,8 @@ "fields": [ "capacity", "network", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32509,7 +32644,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32526,7 +32662,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32543,7 +32680,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32560,7 +32698,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32577,7 +32716,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32594,7 +32734,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32611,7 +32752,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32628,7 +32770,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32645,7 +32788,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32662,7 +32806,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32679,7 +32824,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32696,7 +32842,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32713,7 +32860,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32730,7 +32878,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32747,7 +32896,8 @@ ], "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "suggestion": true }, @@ -32765,7 +32915,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32783,7 +32934,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32801,7 +32953,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32819,7 +32972,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32837,7 +32991,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32855,7 +33010,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32873,7 +33029,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32891,7 +33048,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32909,7 +33067,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32927,7 +33086,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32946,7 +33106,8 @@ "name", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32965,7 +33126,8 @@ "name", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -32984,7 +33146,8 @@ "name", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -33003,7 +33166,8 @@ "name", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -33022,7 +33186,8 @@ "name", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -33041,7 +33206,8 @@ "name", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -33281,7 +33447,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -37121,8 +37288,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37141,8 +37309,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37161,8 +37330,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37181,8 +37351,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37201,8 +37372,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37221,8 +37393,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37241,8 +37414,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37261,8 +37435,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37281,8 +37456,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37301,8 +37477,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37321,8 +37498,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37341,8 +37519,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37361,8 +37540,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37381,8 +37561,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37401,8 +37582,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37421,8 +37603,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37441,8 +37624,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37461,8 +37645,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37481,8 +37666,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37501,8 +37687,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37521,8 +37708,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37541,8 +37729,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37561,8 +37750,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37581,8 +37771,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37601,8 +37792,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37621,8 +37813,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37641,8 +37834,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37661,8 +37855,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37681,8 +37876,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37701,8 +37897,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37721,8 +37918,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37741,8 +37939,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37761,8 +37960,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37781,8 +37981,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37801,8 +38002,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37821,8 +38023,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37841,8 +38044,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37861,8 +38065,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37881,8 +38086,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37901,8 +38107,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37921,8 +38128,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37941,8 +38149,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37961,8 +38170,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -37981,8 +38191,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38001,8 +38212,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38021,8 +38233,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38041,8 +38254,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38061,8 +38275,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38081,8 +38296,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38101,8 +38317,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38121,8 +38338,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38141,8 +38359,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38161,8 +38380,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38181,8 +38401,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38201,8 +38422,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38221,8 +38443,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38241,8 +38464,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38261,8 +38485,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38281,8 +38506,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38301,8 +38527,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38321,8 +38548,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38341,8 +38569,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38361,8 +38590,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38381,8 +38611,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38401,8 +38632,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38421,8 +38653,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38441,8 +38674,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38461,8 +38695,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38481,8 +38716,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38501,8 +38737,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38521,8 +38758,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38541,8 +38779,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38561,8 +38800,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38581,8 +38821,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38601,8 +38842,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38621,8 +38863,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38641,8 +38884,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38661,8 +38905,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38681,8 +38926,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38701,8 +38947,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38721,8 +38968,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38741,8 +38989,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38761,8 +39010,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38781,8 +39031,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38801,8 +39052,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38821,8 +39073,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38841,8 +39094,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38861,8 +39115,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38881,8 +39136,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38901,8 +39157,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38921,8 +39178,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38941,8 +39199,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38961,8 +39220,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -38981,8 +39241,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39001,8 +39262,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39021,8 +39283,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39041,8 +39304,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39061,8 +39325,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39081,8 +39346,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39101,8 +39367,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39121,8 +39388,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39141,8 +39409,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39161,8 +39430,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39181,8 +39451,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39201,8 +39472,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39221,8 +39493,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39241,8 +39514,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39261,8 +39535,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39281,8 +39556,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39301,8 +39577,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39321,8 +39598,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39341,8 +39619,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39361,8 +39640,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39381,8 +39661,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39401,8 +39682,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39421,8 +39703,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39441,8 +39724,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39461,8 +39745,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39481,8 +39766,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39501,8 +39787,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39521,8 +39808,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39541,8 +39829,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39561,8 +39850,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39581,8 +39871,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39601,8 +39892,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39621,8 +39913,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39641,8 +39934,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39661,8 +39955,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39681,8 +39976,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39701,8 +39997,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39721,8 +40018,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39741,8 +40039,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39761,8 +40060,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39781,8 +40081,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39801,8 +40102,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39821,8 +40123,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39841,8 +40144,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39861,8 +40165,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39881,8 +40186,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39901,8 +40207,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39921,8 +40228,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39941,8 +40249,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39961,8 +40270,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -39981,8 +40291,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40001,8 +40312,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40021,8 +40333,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40041,8 +40354,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40061,8 +40375,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40081,8 +40396,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40101,8 +40417,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40121,8 +40438,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40141,8 +40459,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40161,8 +40480,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40181,8 +40501,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40201,8 +40522,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40221,8 +40543,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40241,8 +40564,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40261,8 +40585,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40281,8 +40606,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40301,8 +40627,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40321,8 +40648,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40341,8 +40669,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40361,8 +40690,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40381,8 +40711,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40401,8 +40732,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40421,8 +40753,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40441,8 +40774,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40461,8 +40795,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40481,8 +40816,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40501,8 +40837,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40521,8 +40858,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40541,8 +40879,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40561,8 +40900,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40581,8 +40921,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40601,8 +40942,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40621,8 +40963,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40641,8 +40984,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40661,8 +41005,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40681,8 +41026,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40701,8 +41047,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40721,8 +41068,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40741,8 +41089,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40761,8 +41110,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40781,8 +41131,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40801,8 +41152,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40821,8 +41173,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40841,8 +41194,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40861,8 +41215,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40881,8 +41236,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40901,8 +41257,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40921,8 +41278,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40941,8 +41299,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40961,8 +41320,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -40981,8 +41341,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41001,8 +41362,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41021,8 +41383,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41041,8 +41404,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41061,8 +41425,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41081,8 +41446,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41101,8 +41467,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41121,8 +41488,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41141,8 +41509,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41161,8 +41530,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41181,8 +41551,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41201,8 +41572,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41221,8 +41593,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41241,8 +41614,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41261,8 +41635,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41281,8 +41656,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41301,8 +41677,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41321,8 +41698,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41341,8 +41719,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41361,8 +41740,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41381,8 +41761,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41401,8 +41782,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41421,8 +41803,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41441,8 +41824,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41461,8 +41845,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41481,8 +41866,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41501,8 +41887,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41521,8 +41908,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41541,8 +41929,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41561,8 +41950,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41581,8 +41971,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41601,8 +41992,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41621,8 +42013,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41641,8 +42034,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41661,8 +42055,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41681,8 +42076,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41701,8 +42097,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41721,8 +42118,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41741,8 +42139,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41761,8 +42160,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41781,8 +42181,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41801,8 +42202,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41821,8 +42223,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41841,8 +42244,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41861,8 +42265,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41881,8 +42286,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41901,8 +42307,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41921,8 +42328,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41941,8 +42349,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41961,8 +42370,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -41981,8 +42391,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42001,8 +42412,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42021,8 +42433,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42041,8 +42454,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42061,8 +42475,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42081,8 +42496,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42101,8 +42517,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42121,8 +42538,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42141,8 +42559,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42161,8 +42580,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42181,8 +42601,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42201,8 +42622,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42221,8 +42643,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42241,8 +42664,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42261,8 +42685,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42281,8 +42706,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42301,8 +42727,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42321,8 +42748,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42341,8 +42769,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42361,8 +42790,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42381,8 +42811,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42401,8 +42832,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42421,8 +42853,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42441,8 +42874,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42462,8 +42896,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42482,8 +42917,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42502,8 +42938,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -42522,8 +42959,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "suggestion": true }, @@ -44352,8 +44790,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44374,8 +44813,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44396,8 +44836,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44418,8 +44859,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44440,8 +44882,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44462,8 +44905,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44484,8 +44928,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44506,8 +44951,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44528,8 +44974,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44550,8 +44997,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44572,8 +45020,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44594,8 +45043,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44616,8 +45066,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44638,8 +45089,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44660,8 +45112,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44682,8 +45135,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44704,8 +45158,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44726,8 +45181,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44748,8 +45204,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44770,8 +45227,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44792,8 +45250,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44814,8 +45273,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44836,8 +45296,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44858,8 +45319,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44880,8 +45342,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44902,8 +45365,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44924,8 +45388,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44946,8 +45411,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44968,8 +45434,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -44990,8 +45457,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45012,8 +45480,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45034,8 +45503,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45056,8 +45526,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45078,8 +45549,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45100,8 +45572,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45122,8 +45595,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45144,8 +45618,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45166,8 +45641,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45188,8 +45664,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45210,8 +45687,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45232,8 +45710,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45254,8 +45733,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45276,8 +45756,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45298,8 +45779,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45320,8 +45802,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45342,8 +45825,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45364,8 +45848,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45386,8 +45871,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45408,8 +45894,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45430,8 +45917,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45452,8 +45940,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45474,8 +45963,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45496,8 +45986,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45518,8 +46009,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45540,8 +46032,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45562,8 +46055,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45584,8 +46078,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45606,8 +46101,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45628,8 +46124,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45650,8 +46147,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45672,8 +46170,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45694,8 +46193,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45716,8 +46216,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45738,8 +46239,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45760,8 +46262,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45782,8 +46285,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45804,8 +46308,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45826,8 +46331,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45848,8 +46354,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45870,8 +46377,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45892,8 +46400,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45914,8 +46423,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45936,8 +46446,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45958,8 +46469,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -45980,8 +46492,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46002,8 +46515,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46024,8 +46538,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46046,8 +46561,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46068,8 +46584,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46090,8 +46607,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46112,8 +46630,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46134,8 +46653,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46156,8 +46676,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46178,8 +46699,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46200,8 +46722,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46222,8 +46745,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46244,8 +46768,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46266,8 +46791,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46288,8 +46814,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46310,8 +46837,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46332,8 +46860,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46354,8 +46883,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46376,8 +46906,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46398,8 +46929,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46420,8 +46952,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46442,8 +46975,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46464,8 +46998,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46486,8 +47021,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46508,8 +47044,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46530,8 +47067,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46552,8 +47090,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46574,8 +47113,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46596,8 +47136,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46618,8 +47159,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46640,8 +47182,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46662,8 +47205,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46684,8 +47228,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46706,8 +47251,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46728,8 +47274,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46750,8 +47297,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46772,8 +47320,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46794,8 +47343,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46816,8 +47366,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46838,8 +47389,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46860,8 +47412,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46882,8 +47435,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46904,8 +47458,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46926,8 +47481,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46948,8 +47504,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46970,8 +47527,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -46992,8 +47550,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47014,8 +47573,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47036,8 +47596,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47058,8 +47619,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47080,8 +47642,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47102,8 +47665,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47124,8 +47688,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47146,8 +47711,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47168,8 +47734,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47190,8 +47757,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47212,8 +47780,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47234,8 +47803,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47256,8 +47826,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47278,8 +47849,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47300,8 +47872,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47322,8 +47895,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47344,8 +47918,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47366,8 +47941,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47388,8 +47964,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47410,8 +47987,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47432,8 +48010,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47454,8 +48033,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47476,8 +48056,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47498,8 +48079,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47520,8 +48102,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47542,8 +48125,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47564,8 +48148,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47586,8 +48171,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47608,8 +48194,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47630,8 +48217,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47652,8 +48240,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47674,8 +48263,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47696,8 +48286,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47718,8 +48309,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47740,8 +48332,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47762,8 +48355,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47784,8 +48378,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47806,8 +48401,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47828,8 +48424,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47850,8 +48447,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47872,8 +48470,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47894,8 +48493,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47916,8 +48516,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47938,8 +48539,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47960,8 +48562,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -47982,8 +48585,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -48004,8 +48608,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "suggestion": true }, @@ -60292,6 +60897,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60313,6 +60919,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60334,6 +60941,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60355,6 +60963,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60376,6 +60985,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60397,6 +61007,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60418,6 +61029,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60439,6 +61051,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60460,6 +61073,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60481,6 +61095,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60502,6 +61117,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60523,6 +61139,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60544,6 +61161,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60565,6 +61183,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60586,6 +61205,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60607,6 +61227,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60628,6 +61249,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60649,6 +61271,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60670,6 +61293,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60691,6 +61315,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60712,6 +61337,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60733,6 +61359,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60754,6 +61381,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60775,6 +61403,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60796,6 +61425,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60817,6 +61447,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60838,6 +61469,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60859,6 +61491,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "suggestion": true @@ -60879,7 +61512,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -60899,7 +61533,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -60919,7 +61554,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -60939,7 +61575,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -60959,7 +61596,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -60979,7 +61617,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -60999,7 +61638,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61019,7 +61659,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61039,7 +61680,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61059,7 +61701,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61079,7 +61722,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61099,7 +61743,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61119,7 +61764,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61139,7 +61785,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61159,7 +61806,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61179,7 +61827,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61199,7 +61848,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61219,7 +61869,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61239,7 +61890,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61259,7 +61911,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61279,7 +61932,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61299,7 +61953,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61319,7 +61974,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61339,7 +61995,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61359,7 +62016,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61379,7 +62037,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61399,7 +62058,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61419,7 +62079,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61439,7 +62100,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61459,7 +62121,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61479,7 +62142,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61499,7 +62163,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61519,7 +62184,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61539,7 +62205,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61559,7 +62226,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61579,7 +62247,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61599,7 +62268,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61619,7 +62289,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61639,7 +62310,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61659,7 +62331,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61679,7 +62352,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61699,7 +62373,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61719,7 +62394,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61739,7 +62415,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61759,7 +62436,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61779,7 +62457,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61799,7 +62478,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61819,7 +62499,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61839,7 +62520,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61859,7 +62541,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61879,7 +62562,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61899,7 +62583,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61919,7 +62604,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61939,7 +62625,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61959,7 +62646,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61979,7 +62667,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -61999,7 +62688,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62019,7 +62709,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62039,7 +62730,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62059,7 +62751,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62079,7 +62772,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62099,7 +62793,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62119,7 +62814,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62139,7 +62835,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62159,7 +62856,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62179,7 +62877,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62199,7 +62898,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62220,7 +62920,8 @@ "address", "building_area", "opening_hours", - "beauty" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62241,7 +62942,8 @@ "address", "building_area", "opening_hours", - "beauty" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62261,7 +62963,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62281,7 +62984,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62301,7 +63005,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62321,7 +63026,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62341,7 +63047,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62361,7 +63068,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62381,7 +63089,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62401,7 +63110,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62421,7 +63131,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62441,7 +63152,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62461,7 +63173,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62481,7 +63194,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62501,7 +63215,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62521,7 +63236,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62541,8 +63257,9 @@ "operator", "address", "building_area", + "service/bicycle", "opening_hours", - "service/bicycle" + "payment_multi" ], "suggestion": true }, @@ -62562,8 +63279,9 @@ "operator", "address", "building_area", + "service/bicycle", "opening_hours", - "service/bicycle" + "payment_multi" ], "suggestion": true }, @@ -62583,8 +63301,9 @@ "operator", "address", "building_area", + "service/bicycle", "opening_hours", - "service/bicycle" + "payment_multi" ], "suggestion": true }, @@ -62604,7 +63323,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62624,7 +63344,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62644,7 +63365,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62664,7 +63386,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62684,7 +63407,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62704,7 +63428,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62724,7 +63449,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62744,7 +63470,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62764,7 +63491,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62784,7 +63512,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62804,7 +63533,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62824,7 +63554,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62844,7 +63575,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62864,7 +63596,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62884,7 +63617,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62904,7 +63638,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62924,7 +63659,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62944,7 +63680,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62964,7 +63701,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -62984,7 +63722,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63004,9 +63743,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63026,9 +63766,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63048,9 +63789,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63070,9 +63812,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63092,9 +63835,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63114,9 +63858,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63136,9 +63881,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63158,9 +63904,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63180,9 +63927,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63202,9 +63950,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63224,9 +63973,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63246,9 +63996,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63268,9 +64019,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63290,9 +64042,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63312,9 +64065,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63334,9 +64088,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63356,9 +64111,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63378,9 +64134,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63400,9 +64157,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63422,9 +64180,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63444,9 +64203,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63466,9 +64226,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63488,9 +64249,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63510,9 +64272,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63532,9 +64295,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63554,9 +64318,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63576,9 +64341,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63598,9 +64364,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63620,9 +64387,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63642,9 +64410,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63664,7 +64433,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63684,7 +64454,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63704,7 +64475,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63724,7 +64496,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63744,7 +64517,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63764,7 +64538,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63784,7 +64559,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63804,7 +64580,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63824,7 +64601,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63844,7 +64622,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63864,7 +64643,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63884,7 +64664,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63904,7 +64685,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63924,7 +64706,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -63944,8 +64727,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -63965,8 +64749,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -63986,8 +64771,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64007,8 +64793,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64028,8 +64815,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64049,8 +64837,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64070,8 +64859,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64091,8 +64881,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64112,8 +64903,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64133,8 +64925,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64154,8 +64947,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64175,8 +64969,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64196,8 +64991,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64217,8 +65013,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64238,8 +65035,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64259,8 +65057,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64280,8 +65079,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64301,8 +65101,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64322,8 +65123,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64343,8 +65145,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64364,8 +65167,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64385,8 +65189,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64406,8 +65211,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64427,8 +65233,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64448,8 +65255,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64469,8 +65277,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64490,8 +65299,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64511,8 +65321,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64532,8 +65343,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64553,8 +65365,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64574,8 +65387,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64595,8 +65409,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64616,8 +65431,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64637,8 +65453,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64658,8 +65475,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64679,8 +65497,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64700,8 +65519,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64721,8 +65541,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64742,8 +65563,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64763,8 +65585,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "suggestion": true }, @@ -64784,7 +65607,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -64804,7 +65628,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -64824,8 +65649,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "suggestion": true }, @@ -64845,8 +65671,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "suggestion": true }, @@ -64866,8 +65693,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "suggestion": true }, @@ -64887,8 +65715,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "suggestion": true }, @@ -64908,8 +65737,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "suggestion": true }, @@ -64929,8 +65759,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "suggestion": true }, @@ -64950,8 +65781,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "suggestion": true }, @@ -64971,8 +65803,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "suggestion": true }, @@ -64992,7 +65825,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65012,7 +65846,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65032,7 +65867,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65052,7 +65888,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65072,7 +65909,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65092,7 +65930,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65112,7 +65951,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65132,7 +65972,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65152,7 +65993,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65172,7 +66014,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65192,7 +66035,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65212,7 +66056,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65232,7 +66077,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65252,7 +66098,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65272,7 +66119,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65292,7 +66140,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65312,7 +66161,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65332,7 +66182,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65352,7 +66203,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65372,7 +66224,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65392,7 +66245,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65413,7 +66267,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65434,7 +66289,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65455,7 +66311,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65476,7 +66333,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65497,7 +66355,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65518,7 +66377,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65539,7 +66399,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65560,7 +66421,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65581,7 +66443,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65602,7 +66465,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65623,7 +66487,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65644,7 +66509,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65665,7 +66531,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65686,7 +66553,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65707,7 +66575,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65728,7 +66597,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65749,7 +66619,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65770,7 +66641,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65791,7 +66663,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65812,7 +66685,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65833,7 +66707,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65854,7 +66729,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65875,7 +66751,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65896,7 +66773,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65917,7 +66795,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65938,7 +66817,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65959,7 +66839,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -65980,7 +66861,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66001,7 +66883,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66022,7 +66905,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66043,7 +66927,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66064,7 +66949,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66085,7 +66971,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66106,7 +66993,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66127,7 +67015,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66148,7 +67037,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66169,7 +67059,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66190,7 +67081,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66211,7 +67103,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66232,7 +67125,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66253,7 +67147,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66274,7 +67169,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66295,7 +67191,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66316,7 +67213,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66337,7 +67235,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66358,7 +67257,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66379,7 +67279,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66400,7 +67301,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66421,7 +67323,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66442,7 +67345,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66463,7 +67367,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66484,7 +67389,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66505,7 +67411,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66526,7 +67433,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66547,7 +67455,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66568,7 +67477,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66589,7 +67499,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66610,7 +67521,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66631,7 +67543,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66652,7 +67565,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66673,7 +67587,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66694,7 +67609,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66715,7 +67631,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66736,7 +67653,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66757,7 +67675,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66778,7 +67697,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66799,7 +67719,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66820,7 +67741,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66841,7 +67763,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66862,7 +67785,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66883,7 +67807,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66904,7 +67829,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66925,7 +67851,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66946,7 +67873,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66967,7 +67895,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -66988,7 +67917,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67009,7 +67939,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67030,7 +67961,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67051,7 +67983,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67072,7 +68005,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67093,7 +68027,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67114,7 +68049,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67135,7 +68071,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67156,7 +68093,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67177,7 +68115,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67198,7 +68137,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67219,7 +68159,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67240,7 +68181,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67261,7 +68203,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67282,7 +68225,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67303,7 +68247,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67324,7 +68269,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67345,7 +68291,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67366,7 +68313,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67387,7 +68335,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67408,7 +68357,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67429,7 +68379,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67450,7 +68401,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67471,7 +68423,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67492,7 +68445,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67513,7 +68467,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67534,7 +68489,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67555,7 +68511,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67576,7 +68533,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67597,7 +68555,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67618,7 +68577,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67639,7 +68599,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67660,7 +68621,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67681,7 +68643,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67702,7 +68665,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67723,7 +68687,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67744,7 +68709,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67765,7 +68731,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67786,7 +68753,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67807,7 +68775,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67828,7 +68797,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67849,7 +68819,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67870,7 +68841,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67891,7 +68863,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67912,7 +68885,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67933,7 +68907,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67954,7 +68929,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67975,7 +68951,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -67996,7 +68973,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68017,7 +68995,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68038,7 +69017,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68059,7 +69039,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68080,7 +69061,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68101,7 +69083,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68122,7 +69105,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68143,7 +69127,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68164,7 +69149,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68185,7 +69171,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68206,7 +69193,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68227,7 +69215,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68248,7 +69237,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68269,7 +69259,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68290,7 +69281,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68311,7 +69303,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68332,7 +69325,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68353,7 +69347,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68374,7 +69369,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68395,7 +69391,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68416,7 +69413,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68437,7 +69435,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68458,7 +69457,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68479,7 +69479,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68500,7 +69501,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68521,7 +69523,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68542,7 +69545,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68563,7 +69567,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68584,7 +69589,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68605,7 +69611,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68626,7 +69633,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68647,7 +69655,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68668,7 +69677,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68689,7 +69699,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68710,7 +69721,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68731,7 +69743,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68752,7 +69765,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68773,7 +69787,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68794,7 +69809,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68815,7 +69831,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68836,7 +69853,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68857,7 +69875,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68878,7 +69897,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68899,7 +69919,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68920,7 +69941,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68941,7 +69963,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68962,7 +69985,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -68983,7 +70007,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69004,7 +70029,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69025,7 +70051,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69046,7 +70073,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69067,7 +70095,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69088,7 +70117,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69109,7 +70139,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69130,7 +70161,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69151,7 +70183,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69171,7 +70204,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69191,7 +70225,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69211,7 +70246,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69231,7 +70267,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69251,7 +70288,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69271,7 +70309,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69291,7 +70330,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69311,7 +70351,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69331,7 +70372,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69351,7 +70393,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69371,7 +70414,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69391,7 +70435,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69411,7 +70456,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69431,7 +70477,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69451,7 +70498,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69471,7 +70519,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69491,7 +70540,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69511,7 +70561,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69531,7 +70582,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69551,7 +70603,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69571,7 +70624,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69591,7 +70645,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69611,7 +70666,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69631,7 +70687,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69651,7 +70708,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69671,7 +70729,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69691,7 +70750,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69711,7 +70771,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69731,7 +70792,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69751,7 +70813,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69771,7 +70834,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69791,7 +70855,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69811,7 +70876,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69831,7 +70897,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69851,7 +70918,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69871,7 +70939,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69891,7 +70960,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69911,7 +70981,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69931,7 +71002,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69951,7 +71023,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69971,7 +71044,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -69991,7 +71065,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70011,7 +71086,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70031,7 +71107,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70051,7 +71128,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70071,7 +71149,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70091,7 +71170,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70111,7 +71191,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70131,7 +71212,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70151,7 +71233,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70171,7 +71254,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70191,7 +71275,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70211,7 +71296,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70231,7 +71317,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70251,7 +71338,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70271,7 +71359,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70291,7 +71380,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70311,7 +71401,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70331,7 +71422,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70351,7 +71443,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70371,7 +71464,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70391,7 +71485,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70411,7 +71506,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70431,7 +71527,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70451,7 +71548,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70471,7 +71569,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70491,7 +71590,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70511,7 +71611,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70531,7 +71632,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70551,7 +71653,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70571,7 +71674,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70591,7 +71695,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70611,7 +71716,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70631,7 +71737,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70651,7 +71758,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70671,7 +71779,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70691,7 +71800,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70711,7 +71821,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70731,7 +71842,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70751,7 +71863,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70771,7 +71884,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70791,7 +71905,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70811,7 +71926,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70831,7 +71947,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70851,7 +71968,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70871,7 +71989,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70891,7 +72010,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70911,7 +72031,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70931,7 +72052,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70951,7 +72073,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70971,7 +72094,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -70991,7 +72115,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71011,7 +72136,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71031,7 +72157,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71051,7 +72178,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71071,7 +72199,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71091,7 +72220,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71111,7 +72241,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71131,7 +72262,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71151,7 +72283,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71171,7 +72304,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71191,7 +72325,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71211,7 +72346,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71231,7 +72367,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71251,7 +72388,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71271,7 +72409,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71291,7 +72430,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71311,7 +72451,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71331,7 +72472,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71351,7 +72493,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71371,7 +72514,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71391,7 +72535,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71411,7 +72556,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71431,7 +72577,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71451,7 +72598,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71471,7 +72619,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71491,7 +72640,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71511,7 +72661,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71531,7 +72682,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71551,7 +72703,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71571,7 +72724,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71591,7 +72745,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71611,7 +72766,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71631,7 +72787,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71651,7 +72808,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71671,7 +72829,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71691,7 +72850,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71711,7 +72871,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71731,7 +72892,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71751,7 +72913,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71771,7 +72934,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71791,7 +72955,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71811,7 +72976,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71831,7 +72997,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71851,7 +73018,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71871,7 +73039,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71891,7 +73060,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71911,7 +73081,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71931,7 +73102,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71951,7 +73123,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71971,7 +73144,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -71991,7 +73165,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72011,7 +73186,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72031,7 +73207,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72051,7 +73228,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72071,7 +73249,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72091,7 +73270,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72111,7 +73291,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72131,7 +73312,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72151,7 +73333,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72171,7 +73354,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72191,7 +73375,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72211,7 +73396,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72231,7 +73417,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72251,7 +73438,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72271,7 +73459,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72291,7 +73480,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72311,7 +73501,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72331,7 +73522,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72351,7 +73543,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72371,7 +73564,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72391,7 +73585,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72411,7 +73606,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72431,7 +73627,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72451,7 +73648,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72471,7 +73669,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72491,7 +73690,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72511,7 +73711,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72531,7 +73732,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72551,7 +73753,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72571,7 +73774,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72591,7 +73795,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72611,7 +73816,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72631,7 +73837,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72651,7 +73858,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72671,7 +73879,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72691,7 +73900,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72711,7 +73921,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72731,7 +73942,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72751,7 +73963,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72771,7 +73984,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72791,7 +74005,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72811,7 +74026,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72831,7 +74047,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72851,7 +74068,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72871,7 +74089,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72891,7 +74110,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72911,7 +74131,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72931,7 +74152,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72951,7 +74173,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72971,7 +74194,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -72991,7 +74215,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73011,7 +74236,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73031,7 +74257,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73051,7 +74278,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73071,7 +74299,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73091,7 +74320,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73111,7 +74341,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73131,7 +74362,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73151,7 +74383,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73171,7 +74404,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73191,7 +74425,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73211,7 +74446,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73231,7 +74467,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73251,7 +74488,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73271,7 +74509,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73291,7 +74530,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73311,7 +74551,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73331,7 +74572,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73351,7 +74593,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73371,7 +74614,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73391,7 +74635,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73411,7 +74656,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73431,7 +74677,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73451,7 +74698,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73472,7 +74720,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73493,7 +74742,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73513,7 +74763,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73533,7 +74784,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73554,7 +74806,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73574,7 +74827,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73594,7 +74848,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73614,7 +74869,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73635,7 +74891,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73655,7 +74912,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73676,7 +74934,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73696,7 +74955,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73717,7 +74977,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73737,7 +74998,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73757,7 +75019,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73777,7 +75040,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73797,7 +75061,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73817,7 +75082,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73837,7 +75103,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73857,7 +75124,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73877,7 +75145,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73897,7 +75166,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73917,7 +75187,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73937,7 +75208,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73957,7 +75229,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73977,7 +75250,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -73997,7 +75271,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74017,7 +75292,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74037,7 +75313,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74057,7 +75334,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74077,7 +75355,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74097,7 +75376,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74117,7 +75397,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74137,7 +75418,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74157,7 +75439,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74177,7 +75460,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74197,7 +75481,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74217,7 +75502,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74237,7 +75523,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74257,7 +75544,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74277,7 +75565,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74297,7 +75586,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74317,7 +75607,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74337,7 +75628,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74357,7 +75649,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74377,7 +75670,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74397,7 +75691,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74417,7 +75712,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74437,7 +75733,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74457,7 +75754,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74477,7 +75775,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74497,7 +75796,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74517,7 +75817,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74537,7 +75838,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74557,7 +75859,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74577,7 +75880,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74597,7 +75901,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74617,7 +75922,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74637,7 +75943,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74657,7 +75964,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74677,7 +75985,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74697,7 +76006,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74717,7 +76027,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74737,7 +76048,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74757,7 +76069,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74777,7 +76090,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74797,7 +76111,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74817,7 +76132,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74837,7 +76153,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74857,7 +76174,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74877,7 +76195,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74897,7 +76216,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74917,7 +76237,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74937,7 +76258,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74957,7 +76279,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74977,7 +76300,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -74997,7 +76321,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75017,7 +76342,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75037,7 +76363,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75057,7 +76384,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75077,7 +76405,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75097,7 +76426,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75117,7 +76447,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75137,7 +76468,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75157,7 +76489,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75177,7 +76510,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75197,7 +76531,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75217,7 +76552,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75237,7 +76573,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75257,7 +76594,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75277,7 +76615,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75297,7 +76636,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75317,7 +76657,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75337,7 +76678,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75357,7 +76699,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75377,7 +76720,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75397,7 +76741,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75417,7 +76762,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75437,7 +76783,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75457,7 +76804,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75477,7 +76825,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75497,7 +76846,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75517,7 +76867,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75537,7 +76888,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75557,7 +76909,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75577,7 +76930,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75597,7 +76951,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75617,7 +76972,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75637,7 +76993,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75657,7 +77014,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75677,7 +77035,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75697,7 +77056,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75717,7 +77077,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75737,7 +77098,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75757,7 +77119,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75777,7 +77140,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75797,7 +77161,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75817,7 +77182,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75837,7 +77203,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75857,7 +77224,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75877,7 +77245,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75897,7 +77266,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75917,7 +77287,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75937,7 +77308,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75957,7 +77329,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75977,7 +77350,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -75997,7 +77371,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76017,7 +77392,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76037,7 +77413,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76057,7 +77434,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76077,7 +77455,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76097,7 +77476,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76117,7 +77497,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76137,7 +77518,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76157,7 +77539,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76177,7 +77560,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76197,7 +77581,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76217,7 +77602,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76237,7 +77623,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76257,7 +77644,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76277,7 +77665,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76297,7 +77686,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76317,7 +77707,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76337,7 +77728,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76357,7 +77749,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76377,7 +77770,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76397,7 +77791,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76417,7 +77812,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76437,7 +77833,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76457,7 +77854,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76477,7 +77875,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76497,7 +77896,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76517,7 +77917,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76537,7 +77938,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76557,7 +77959,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76577,7 +77980,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76597,7 +78001,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76617,7 +78022,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76700,7 +78106,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76720,7 +78127,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76740,7 +78148,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76760,7 +78169,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76780,7 +78190,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76800,7 +78211,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76820,7 +78232,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76840,7 +78253,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76860,7 +78274,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76880,7 +78295,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76900,7 +78316,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76920,7 +78337,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76940,7 +78358,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76960,7 +78379,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -76980,7 +78400,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77000,7 +78421,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77020,7 +78442,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77040,7 +78463,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77060,7 +78484,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77080,7 +78505,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77100,7 +78526,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77120,7 +78547,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77140,7 +78568,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77160,7 +78589,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77180,7 +78610,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77200,7 +78631,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77220,7 +78652,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77240,7 +78673,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77260,7 +78694,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77280,7 +78715,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77300,7 +78736,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77320,7 +78757,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77340,7 +78778,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77360,7 +78799,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77380,7 +78820,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77400,7 +78841,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77420,7 +78862,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77440,7 +78883,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77460,7 +78904,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77480,7 +78925,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77500,7 +78946,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77520,7 +78967,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77540,7 +78988,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77560,7 +79009,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77580,7 +79030,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77600,7 +79051,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77620,7 +79072,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77640,7 +79093,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77660,7 +79114,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77680,7 +79135,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77700,7 +79156,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77720,7 +79177,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77740,7 +79198,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77760,7 +79219,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77780,7 +79240,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77800,7 +79261,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77820,7 +79282,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77840,7 +79303,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77860,7 +79324,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77880,7 +79345,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77900,7 +79366,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77920,7 +79387,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77940,7 +79408,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77960,7 +79429,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -77980,7 +79450,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78000,7 +79471,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78020,7 +79492,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78040,7 +79513,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78060,7 +79534,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78080,7 +79555,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78100,7 +79576,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78120,7 +79597,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78140,7 +79618,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78160,7 +79639,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78180,7 +79660,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78200,7 +79681,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78220,7 +79702,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78240,7 +79723,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78260,7 +79744,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78280,7 +79765,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78300,7 +79786,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78320,7 +79807,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78340,7 +79828,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78360,7 +79849,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78380,7 +79870,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78400,7 +79891,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78420,7 +79912,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78440,7 +79933,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78460,7 +79954,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78480,7 +79975,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78500,7 +79996,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78520,7 +80017,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78540,7 +80038,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78560,7 +80059,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78580,7 +80080,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78600,7 +80101,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78620,7 +80122,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78640,7 +80143,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78660,7 +80164,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78680,7 +80185,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78700,7 +80206,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78720,7 +80227,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78740,7 +80248,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78760,7 +80269,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78780,7 +80290,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78800,7 +80311,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78820,7 +80332,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78840,7 +80353,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78860,7 +80374,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78880,7 +80395,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78900,7 +80416,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78920,7 +80437,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78940,7 +80458,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78960,7 +80479,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -78980,7 +80500,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79000,7 +80521,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79020,7 +80542,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79040,7 +80563,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79060,7 +80584,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79080,7 +80605,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79100,7 +80626,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79120,7 +80647,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79140,7 +80668,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79160,7 +80689,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79180,7 +80710,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79200,7 +80731,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79220,7 +80752,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79240,7 +80773,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79260,7 +80794,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79280,7 +80815,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79300,7 +80836,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79320,7 +80857,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79360,7 +80898,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79380,7 +80919,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79400,7 +80940,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79420,7 +80961,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79440,7 +80982,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79460,7 +81003,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79480,7 +81024,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79500,7 +81045,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79520,7 +81066,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79540,7 +81087,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79560,7 +81108,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79580,7 +81129,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79600,7 +81150,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79620,7 +81171,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79640,7 +81192,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79660,7 +81213,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79680,7 +81234,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79700,7 +81255,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79720,7 +81276,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79740,7 +81297,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79760,7 +81318,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79780,7 +81339,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79800,7 +81360,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79820,7 +81381,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79840,7 +81402,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79860,7 +81423,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79880,7 +81444,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79900,7 +81465,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79920,7 +81486,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79940,7 +81507,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79960,7 +81528,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -79980,7 +81549,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80000,7 +81570,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80020,7 +81591,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80040,7 +81612,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80060,7 +81633,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80080,7 +81654,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80100,7 +81675,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80120,7 +81696,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80140,7 +81717,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80160,7 +81738,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80180,7 +81759,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80200,7 +81780,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80220,7 +81801,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80240,7 +81822,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80260,7 +81843,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80280,7 +81864,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80300,7 +81885,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80320,7 +81906,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80340,7 +81927,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80360,7 +81948,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80380,7 +81969,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80400,7 +81990,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80441,7 +82032,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80461,7 +82053,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80481,7 +82074,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80501,7 +82095,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80521,7 +82116,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80541,7 +82137,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80561,7 +82158,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80581,7 +82179,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80601,7 +82200,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80621,7 +82221,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80641,7 +82242,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80661,7 +82263,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80681,7 +82284,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80701,7 +82305,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80721,7 +82326,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80741,7 +82347,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80761,7 +82368,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80781,7 +82389,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80801,7 +82410,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80821,7 +82431,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80841,7 +82452,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80861,7 +82473,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80881,7 +82494,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80901,7 +82515,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80921,7 +82536,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80941,7 +82557,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80961,7 +82578,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -80981,7 +82599,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81001,7 +82620,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81021,7 +82641,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81041,7 +82662,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81061,7 +82683,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81081,7 +82704,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81101,7 +82725,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81121,7 +82746,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81141,7 +82767,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81161,7 +82788,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81181,7 +82809,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81201,7 +82830,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81221,7 +82851,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81241,7 +82872,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81261,7 +82893,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81281,7 +82914,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81301,7 +82935,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81321,7 +82956,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81341,7 +82977,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81361,7 +82998,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81381,7 +83019,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81401,7 +83040,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81421,7 +83061,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81441,7 +83082,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81461,7 +83103,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81481,7 +83124,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81501,7 +83145,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81521,7 +83166,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81541,7 +83187,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81561,7 +83208,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81581,7 +83229,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81601,7 +83250,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81621,7 +83271,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81641,7 +83292,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81661,8 +83313,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "suggestion": true }, @@ -81682,8 +83335,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "suggestion": true }, @@ -81703,7 +83357,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81723,7 +83378,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81743,7 +83399,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81763,7 +83420,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81783,7 +83441,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81803,7 +83462,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81823,7 +83483,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81843,7 +83504,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81863,7 +83525,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81883,7 +83546,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81903,7 +83567,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81923,7 +83588,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81943,7 +83609,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81963,7 +83630,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -81983,7 +83651,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82003,7 +83672,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82023,7 +83693,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82043,7 +83714,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82063,7 +83735,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82083,7 +83756,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82103,7 +83777,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82123,7 +83798,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82143,7 +83819,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82163,7 +83840,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82183,7 +83861,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82203,7 +83882,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82223,7 +83903,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82243,7 +83924,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82263,7 +83945,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82283,7 +83966,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82303,7 +83987,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82323,7 +84008,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82343,7 +84029,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82363,7 +84050,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82383,7 +84071,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82403,7 +84092,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82423,7 +84113,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82443,7 +84134,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82463,7 +84155,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82483,7 +84176,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82503,7 +84197,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82523,7 +84218,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82543,7 +84239,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82563,7 +84260,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82583,7 +84281,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82603,7 +84302,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82623,7 +84323,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82643,7 +84344,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82663,7 +84365,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82683,7 +84386,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82703,7 +84407,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82723,7 +84428,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82743,7 +84449,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82763,7 +84470,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82783,7 +84491,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82803,7 +84512,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82823,7 +84533,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82843,7 +84554,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82863,7 +84575,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82883,7 +84596,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82903,7 +84617,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82923,7 +84638,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82943,7 +84659,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82963,7 +84680,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -82983,7 +84701,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83003,7 +84722,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83023,7 +84743,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83043,7 +84764,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83063,7 +84785,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83083,7 +84806,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83103,7 +84827,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83123,7 +84848,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83143,7 +84869,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83163,7 +84890,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83183,7 +84911,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83203,7 +84932,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83223,7 +84953,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83243,7 +84974,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83263,7 +84995,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83283,7 +85016,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83303,7 +85037,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83323,7 +85058,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83343,7 +85079,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83363,7 +85100,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83383,7 +85121,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83403,7 +85142,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83423,7 +85163,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83443,7 +85184,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83463,7 +85205,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83483,7 +85226,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83503,7 +85247,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83523,7 +85268,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83543,7 +85289,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83563,7 +85310,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83583,7 +85331,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83603,7 +85352,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83623,7 +85373,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83643,7 +85394,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83663,7 +85415,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83683,7 +85436,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83703,7 +85457,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83723,7 +85478,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83743,7 +85499,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83763,7 +85520,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83783,7 +85541,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83803,7 +85562,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83823,7 +85583,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83843,7 +85604,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83863,7 +85625,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83883,7 +85646,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83903,7 +85667,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83923,7 +85688,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83943,7 +85709,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83963,7 +85730,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -83983,7 +85751,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84003,7 +85772,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84023,7 +85793,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84043,7 +85814,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84063,7 +85835,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84083,7 +85856,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84103,7 +85877,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84123,7 +85898,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84143,7 +85919,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84163,7 +85940,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84183,7 +85961,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84203,7 +85982,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84223,7 +86003,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84243,7 +86024,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84263,7 +86045,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84283,7 +86066,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84303,7 +86087,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84323,7 +86108,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84343,7 +86129,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84363,7 +86150,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84383,7 +86171,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84403,7 +86192,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84423,7 +86213,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84443,7 +86234,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84463,7 +86255,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84483,7 +86276,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84503,7 +86297,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84523,7 +86318,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84543,7 +86339,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84563,7 +86360,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84583,7 +86381,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84603,7 +86402,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84623,7 +86423,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84643,7 +86444,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84663,7 +86465,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84683,7 +86486,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84703,7 +86507,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84723,7 +86528,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84743,7 +86549,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84763,7 +86570,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84783,7 +86591,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84803,7 +86612,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84823,7 +86633,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84843,7 +86654,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84863,7 +86675,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84883,7 +86696,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84903,7 +86717,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84923,7 +86738,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84943,7 +86759,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84963,7 +86780,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -84983,7 +86801,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85003,7 +86822,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85023,7 +86843,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85043,7 +86864,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85063,7 +86885,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85083,7 +86906,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85103,7 +86927,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85123,7 +86948,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85143,7 +86969,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85163,7 +86990,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85183,7 +87011,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85203,7 +87032,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85223,7 +87053,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85243,7 +87074,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85263,7 +87095,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85283,7 +87116,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85303,7 +87137,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85323,7 +87158,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85343,7 +87179,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85363,7 +87200,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85383,7 +87221,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85403,7 +87242,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85423,7 +87263,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85443,7 +87284,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85463,7 +87305,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85483,7 +87326,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85503,7 +87347,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85523,7 +87368,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85543,7 +87389,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85563,7 +87410,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85583,7 +87431,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85603,7 +87452,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85623,7 +87473,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85643,7 +87494,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85663,7 +87515,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85683,7 +87536,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85703,7 +87557,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85723,7 +87578,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85743,7 +87599,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85763,7 +87620,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85783,7 +87641,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85803,7 +87662,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85823,7 +87683,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85843,7 +87704,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85863,7 +87725,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85883,7 +87746,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85903,7 +87767,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85923,7 +87788,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85943,7 +87809,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85963,7 +87830,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -85983,7 +87851,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86003,7 +87872,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86023,7 +87893,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86043,7 +87914,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86063,7 +87935,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86083,7 +87956,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86103,7 +87977,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86123,7 +87998,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86143,7 +88019,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86163,7 +88040,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86183,7 +88061,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86203,7 +88082,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86223,7 +88103,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86243,7 +88124,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86263,7 +88145,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86283,7 +88166,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86303,7 +88187,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86323,7 +88208,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86343,7 +88229,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86363,7 +88250,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86383,7 +88271,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86403,7 +88292,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86423,7 +88313,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86443,7 +88334,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86463,7 +88355,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86483,7 +88376,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86503,7 +88397,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86523,7 +88418,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86543,7 +88439,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86563,7 +88460,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86583,7 +88481,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86603,7 +88502,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86623,7 +88523,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86643,7 +88544,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86663,7 +88565,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86683,7 +88586,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86703,7 +88607,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86723,7 +88628,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86743,7 +88649,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86763,7 +88670,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86783,7 +88691,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86803,7 +88712,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86823,7 +88733,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86843,7 +88754,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86863,7 +88775,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86883,7 +88796,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86903,7 +88817,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86923,7 +88838,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86943,7 +88859,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86963,7 +88880,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -86983,7 +88901,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87003,7 +88922,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87023,7 +88943,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87043,7 +88964,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87063,7 +88985,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87083,7 +89006,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87103,7 +89027,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87123,7 +89048,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87143,7 +89069,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87163,7 +89090,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87183,7 +89111,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87203,7 +89132,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87223,7 +89153,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87243,7 +89174,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87263,7 +89195,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87283,7 +89216,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87303,7 +89237,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87323,7 +89258,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87343,7 +89279,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87363,7 +89300,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87383,7 +89321,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87403,7 +89342,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87423,7 +89363,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87443,7 +89384,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87463,7 +89405,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87483,7 +89426,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87503,7 +89447,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87523,7 +89468,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87543,7 +89489,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87563,7 +89510,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87583,7 +89531,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87603,7 +89552,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87623,7 +89573,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87643,7 +89594,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87663,7 +89615,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87683,7 +89636,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87703,7 +89657,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87723,7 +89678,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87743,7 +89699,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87763,7 +89720,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87783,7 +89741,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87803,7 +89762,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87823,7 +89783,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87843,7 +89804,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87863,7 +89825,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87883,7 +89846,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87903,7 +89867,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87923,7 +89888,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87943,7 +89909,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87963,7 +89930,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -87983,7 +89951,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88003,7 +89972,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88023,7 +89993,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88043,7 +90014,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88063,7 +90035,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88083,7 +90056,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88103,7 +90077,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88123,7 +90098,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88143,7 +90119,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88163,7 +90140,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88183,7 +90161,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88203,7 +90182,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88223,7 +90203,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88243,7 +90224,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88263,7 +90245,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88283,7 +90266,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88303,7 +90287,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88323,7 +90308,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88343,7 +90329,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88363,7 +90350,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88383,7 +90371,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88403,7 +90392,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88423,7 +90413,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88443,7 +90434,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88463,7 +90455,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88483,7 +90476,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88503,7 +90497,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88523,7 +90518,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88543,7 +90539,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88563,7 +90560,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88583,7 +90581,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88603,7 +90602,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88623,7 +90623,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88643,7 +90644,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88663,7 +90665,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88683,7 +90686,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88703,7 +90707,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88723,7 +90728,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88743,7 +90749,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88763,7 +90770,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88783,7 +90791,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88803,7 +90812,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88823,7 +90833,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88843,7 +90854,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88863,7 +90875,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88883,7 +90896,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88903,7 +90917,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88923,7 +90938,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88943,7 +90959,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88963,7 +90980,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -88983,7 +91001,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89003,7 +91022,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89023,7 +91043,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89043,7 +91064,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89063,7 +91085,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89083,7 +91106,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89103,7 +91127,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89123,7 +91148,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89143,7 +91169,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89163,7 +91190,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89183,7 +91211,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89203,7 +91232,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89223,7 +91253,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89243,7 +91274,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89263,7 +91295,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89283,7 +91316,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89303,7 +91337,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89323,7 +91358,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89343,7 +91379,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89363,7 +91400,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89383,7 +91421,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89403,7 +91442,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89423,7 +91463,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89443,7 +91484,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89463,7 +91505,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89483,7 +91526,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89503,7 +91547,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89523,7 +91568,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89543,7 +91589,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89563,7 +91610,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89583,7 +91631,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89603,7 +91652,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89623,7 +91673,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89643,7 +91694,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89663,7 +91715,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89683,7 +91736,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89703,7 +91757,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89723,7 +91778,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89743,7 +91799,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89763,7 +91820,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89783,7 +91841,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89803,7 +91862,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89823,7 +91883,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89843,7 +91904,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89863,7 +91925,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89883,7 +91946,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89903,7 +91967,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89923,7 +91988,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89943,7 +92009,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89963,7 +92030,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -89983,7 +92051,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90003,7 +92072,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90023,7 +92093,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90043,7 +92114,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90063,7 +92135,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90083,7 +92156,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90103,7 +92177,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90123,7 +92198,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90143,7 +92219,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90163,7 +92240,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90183,7 +92261,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90203,7 +92282,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90223,7 +92303,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90243,7 +92324,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90263,7 +92345,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90283,7 +92366,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90303,7 +92387,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90323,7 +92408,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90343,7 +92429,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90363,7 +92450,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90383,7 +92471,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90403,7 +92492,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90423,7 +92513,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90443,7 +92534,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90463,7 +92555,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90483,7 +92576,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90503,7 +92597,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90523,7 +92618,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90543,7 +92639,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90563,7 +92660,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90583,7 +92681,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90603,7 +92702,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90623,7 +92723,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90643,7 +92744,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90663,7 +92765,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90683,7 +92786,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90703,7 +92807,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90723,7 +92828,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90743,7 +92849,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90763,7 +92870,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90783,7 +92891,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90803,7 +92912,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90823,7 +92933,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90843,7 +92954,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90863,7 +92975,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90883,7 +92996,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90903,7 +93017,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90923,7 +93038,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90943,7 +93059,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90963,7 +93080,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -90983,7 +93101,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -91003,7 +93122,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, @@ -91023,7 +93143,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "suggestion": true }, diff --git a/data/presets/presets/amenity/bicycle_rental.json b/data/presets/presets/amenity/bicycle_rental.json index 3a47e707ce..f597cd9dd8 100644 --- a/data/presets/presets/amenity/bicycle_rental.json +++ b/data/presets/presets/amenity/bicycle_rental.json @@ -3,7 +3,8 @@ "fields": [ "capacity", "network", - "operator" + "operator", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/amenity/boat_rental.json b/data/presets/presets/amenity/boat_rental.json index 64a5b45c69..50bddc0e7c 100644 --- a/data/presets/presets/amenity/boat_rental.json +++ b/data/presets/presets/amenity/boat_rental.json @@ -1,7 +1,8 @@ { "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/amenity/car_rental.json b/data/presets/presets/amenity/car_rental.json index dbf1912a2c..d0bf0cd2d3 100644 --- a/data/presets/presets/amenity/car_rental.json +++ b/data/presets/presets/amenity/car_rental.json @@ -2,7 +2,8 @@ "icon": "car", "fields": [ "name", - "operator" + "operator", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/amenity/car_sharing.json b/data/presets/presets/amenity/car_sharing.json index e2aebbbd38..8b59a52e1e 100644 --- a/data/presets/presets/amenity/car_sharing.json +++ b/data/presets/presets/amenity/car_sharing.json @@ -3,7 +3,8 @@ "fields": [ "name", "operator", - "capacity" + "capacity", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/amenity/car_wash.json b/data/presets/presets/amenity/car_wash.json index e129302278..f55eb0ab64 100644 --- a/data/presets/presets/amenity/car_wash.json +++ b/data/presets/presets/amenity/car_wash.json @@ -3,7 +3,8 @@ "fields": [ "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/amenity/casino.json b/data/presets/presets/amenity/casino.json index ed16cd0fb4..8322025ef6 100644 --- a/data/presets/presets/amenity/casino.json +++ b/data/presets/presets/amenity/casino.json @@ -6,6 +6,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "smoking" ], "geometry": [ diff --git a/data/presets/presets/amenity/cinema.json b/data/presets/presets/amenity/cinema.json index 19eca302fc..d7612a98c8 100644 --- a/data/presets/presets/amenity/cinema.json +++ b/data/presets/presets/amenity/cinema.json @@ -4,7 +4,8 @@ "name", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/amenity/driving_school.json b/data/presets/presets/amenity/driving_school.json index c5ca11fd94..211394d26e 100644 --- a/data/presets/presets/amenity/driving_school.json +++ b/data/presets/presets/amenity/driving_school.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/amenity/fuel.json b/data/presets/presets/amenity/fuel.json index 1c973ab1c2..379e924d48 100644 --- a/data/presets/presets/amenity/fuel.json +++ b/data/presets/presets/amenity/fuel.json @@ -4,8 +4,9 @@ "name", "operator", "address", + "fuel_multi", "opening_hours", - "fuel_multi" + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/amenity/pharmacy.json b/data/presets/presets/amenity/pharmacy.json index 0346098593..8fa7d06289 100644 --- a/data/presets/presets/amenity/pharmacy.json +++ b/data/presets/presets/amenity/pharmacy.json @@ -5,8 +5,9 @@ "operator", "address", "building_area", + "drive_through", "opening_hours", - "drive_through" + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop.json b/data/presets/presets/shop.json index f7a9d6bd49..f78e40705f 100644 --- a/data/presets/presets/shop.json +++ b/data/presets/presets/shop.json @@ -6,7 +6,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/_fishmonger.json b/data/presets/presets/shop/_fishmonger.json index 411b239816..13b25cb424 100644 --- a/data/presets/presets/shop/_fishmonger.json +++ b/data/presets/presets/shop/_fishmonger.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/_furnace.json b/data/presets/presets/shop/_furnace.json index 1c774b9dd6..8afd2e3529 100644 --- a/data/presets/presets/shop/_furnace.json +++ b/data/presets/presets/shop/_furnace.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/agrarian.json b/data/presets/presets/shop/agrarian.json index 8696682aa6..17eef7a25b 100644 --- a/data/presets/presets/shop/agrarian.json +++ b/data/presets/presets/shop/agrarian.json @@ -6,7 +6,8 @@ "agrarian", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/alcohol.json b/data/presets/presets/shop/alcohol.json index 903534f8ea..5b50ac5ddf 100644 --- a/data/presets/presets/shop/alcohol.json +++ b/data/presets/presets/shop/alcohol.json @@ -6,6 +6,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "drive_through" ], "geometry": [ diff --git a/data/presets/presets/shop/anime.json b/data/presets/presets/shop/anime.json index bbb19cb61e..2b1ae641e3 100644 --- a/data/presets/presets/shop/anime.json +++ b/data/presets/presets/shop/anime.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/antiques.json b/data/presets/presets/shop/antiques.json index fe31708494..a6425624b3 100644 --- a/data/presets/presets/shop/antiques.json +++ b/data/presets/presets/shop/antiques.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/appliance.json b/data/presets/presets/shop/appliance.json index aa3321b0b9..443c51e670 100644 --- a/data/presets/presets/shop/appliance.json +++ b/data/presets/presets/shop/appliance.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/art.json b/data/presets/presets/shop/art.json index dd0c36e00e..b9edbd8925 100644 --- a/data/presets/presets/shop/art.json +++ b/data/presets/presets/shop/art.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/baby_goods.json b/data/presets/presets/shop/baby_goods.json index 059c0527d7..7432b5f8c1 100644 --- a/data/presets/presets/shop/baby_goods.json +++ b/data/presets/presets/shop/baby_goods.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/bag.json b/data/presets/presets/shop/bag.json index 064ffc0333..a766e04ddd 100644 --- a/data/presets/presets/shop/bag.json +++ b/data/presets/presets/shop/bag.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/bakery.json b/data/presets/presets/shop/bakery.json index 70c41a4127..6de918f6c5 100644 --- a/data/presets/presets/shop/bakery.json +++ b/data/presets/presets/shop/bakery.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/bathroom_furnishing.json b/data/presets/presets/shop/bathroom_furnishing.json index 4751ac9bb0..794ab38176 100644 --- a/data/presets/presets/shop/bathroom_furnishing.json +++ b/data/presets/presets/shop/bathroom_furnishing.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/beauty.json b/data/presets/presets/shop/beauty.json index 880b9eb817..5bbdefc8d2 100644 --- a/data/presets/presets/shop/beauty.json +++ b/data/presets/presets/shop/beauty.json @@ -6,7 +6,8 @@ "address", "building_area", "opening_hours", - "beauty" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/beauty/nails.json b/data/presets/presets/shop/beauty/nails.json index 83032a90ac..64539feaca 100644 --- a/data/presets/presets/shop/beauty/nails.json +++ b/data/presets/presets/shop/beauty/nails.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/beauty/tanning.json b/data/presets/presets/shop/beauty/tanning.json index 7f0755a513..d61daf11bd 100644 --- a/data/presets/presets/shop/beauty/tanning.json +++ b/data/presets/presets/shop/beauty/tanning.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/bed.json b/data/presets/presets/shop/bed.json index 177b58596c..e032c4ac16 100644 --- a/data/presets/presets/shop/bed.json +++ b/data/presets/presets/shop/bed.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/beverages.json b/data/presets/presets/shop/beverages.json index cec13c010d..ba3cc0ffe3 100644 --- a/data/presets/presets/shop/beverages.json +++ b/data/presets/presets/shop/beverages.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/bicycle.json b/data/presets/presets/shop/bicycle.json index e641708b39..17b63c4e00 100644 --- a/data/presets/presets/shop/bicycle.json +++ b/data/presets/presets/shop/bicycle.json @@ -5,8 +5,9 @@ "operator", "address", "building_area", + "service/bicycle", "opening_hours", - "service/bicycle" + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/bookmaker.json b/data/presets/presets/shop/bookmaker.json index e6aa0b7257..f25eeb22be 100644 --- a/data/presets/presets/shop/bookmaker.json +++ b/data/presets/presets/shop/bookmaker.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/books.json b/data/presets/presets/shop/books.json index 34b7815a4e..67878d65ad 100644 --- a/data/presets/presets/shop/books.json +++ b/data/presets/presets/shop/books.json @@ -6,6 +6,7 @@ "address", "building_area", "opening_hours", + "payment_multi", "internet_access", "internet_access/fee", "internet_access/ssid" diff --git a/data/presets/presets/shop/boutique.json b/data/presets/presets/shop/boutique.json index 42f24fdc79..651a3c8c98 100644 --- a/data/presets/presets/shop/boutique.json +++ b/data/presets/presets/shop/boutique.json @@ -6,7 +6,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/butcher.json b/data/presets/presets/shop/butcher.json index 19246d0542..cbe5f9d986 100644 --- a/data/presets/presets/shop/butcher.json +++ b/data/presets/presets/shop/butcher.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/candles.json b/data/presets/presets/shop/candles.json index 78ffd74e19..ff43f6be63 100644 --- a/data/presets/presets/shop/candles.json +++ b/data/presets/presets/shop/candles.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/car.json b/data/presets/presets/shop/car.json index 5bc5b10e45..f4f9006249 100644 --- a/data/presets/presets/shop/car.json +++ b/data/presets/presets/shop/car.json @@ -5,9 +5,10 @@ "operator", "address", "building_area", - "opening_hours", "second_hand", - "service/vehicle" + "service/vehicle", + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/car_parts.json b/data/presets/presets/shop/car_parts.json index 6629f2033e..1e31507097 100644 --- a/data/presets/presets/shop/car_parts.json +++ b/data/presets/presets/shop/car_parts.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/car_repair.json b/data/presets/presets/shop/car_repair.json index 026476d793..fe95c44746 100644 --- a/data/presets/presets/shop/car_repair.json +++ b/data/presets/presets/shop/car_repair.json @@ -5,8 +5,9 @@ "operator", "address", "building_area", + "service/vehicle", "opening_hours", - "service/vehicle" + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/carpet.json b/data/presets/presets/shop/carpet.json index 6fe884bce4..66d4c71dc2 100644 --- a/data/presets/presets/shop/carpet.json +++ b/data/presets/presets/shop/carpet.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/charity.json b/data/presets/presets/shop/charity.json index 7ca3d463dd..e6088e1c11 100644 --- a/data/presets/presets/shop/charity.json +++ b/data/presets/presets/shop/charity.json @@ -5,8 +5,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/cheese.json b/data/presets/presets/shop/cheese.json index 395ff30b5b..a05a8aed54 100644 --- a/data/presets/presets/shop/cheese.json +++ b/data/presets/presets/shop/cheese.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/chemist.json b/data/presets/presets/shop/chemist.json index d1b68e3b4a..ab27fb0472 100644 --- a/data/presets/presets/shop/chemist.json +++ b/data/presets/presets/shop/chemist.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/chocolate.json b/data/presets/presets/shop/chocolate.json index 384b249562..3a48072beb 100644 --- a/data/presets/presets/shop/chocolate.json +++ b/data/presets/presets/shop/chocolate.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/clothes.json b/data/presets/presets/shop/clothes.json index 19c58ba725..fcf6551d35 100644 --- a/data/presets/presets/shop/clothes.json +++ b/data/presets/presets/shop/clothes.json @@ -6,7 +6,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/coffee.json b/data/presets/presets/shop/coffee.json index 12678c8134..c9bf22d748 100644 --- a/data/presets/presets/shop/coffee.json +++ b/data/presets/presets/shop/coffee.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/computer.json b/data/presets/presets/shop/computer.json index 249e96f154..226a260c2a 100644 --- a/data/presets/presets/shop/computer.json +++ b/data/presets/presets/shop/computer.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/confectionery.json b/data/presets/presets/shop/confectionery.json index 599399ca39..8a665ca2e1 100644 --- a/data/presets/presets/shop/confectionery.json +++ b/data/presets/presets/shop/confectionery.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/convenience.json b/data/presets/presets/shop/convenience.json index 3294631058..91b80d453f 100644 --- a/data/presets/presets/shop/convenience.json +++ b/data/presets/presets/shop/convenience.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/copyshop.json b/data/presets/presets/shop/copyshop.json index e1837596d3..513125c401 100644 --- a/data/presets/presets/shop/copyshop.json +++ b/data/presets/presets/shop/copyshop.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/cosmetics.json b/data/presets/presets/shop/cosmetics.json index 18c1eca01c..9f5ee9e25c 100644 --- a/data/presets/presets/shop/cosmetics.json +++ b/data/presets/presets/shop/cosmetics.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/craft.json b/data/presets/presets/shop/craft.json index c275c3afe2..4ac87257af 100644 --- a/data/presets/presets/shop/craft.json +++ b/data/presets/presets/shop/craft.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/curtain.json b/data/presets/presets/shop/curtain.json index e1a2fc7d74..5c68c8536f 100644 --- a/data/presets/presets/shop/curtain.json +++ b/data/presets/presets/shop/curtain.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/dairy.json b/data/presets/presets/shop/dairy.json index 3ca4e86204..818f0a8e2e 100644 --- a/data/presets/presets/shop/dairy.json +++ b/data/presets/presets/shop/dairy.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/deli.json b/data/presets/presets/shop/deli.json index 4d1983e7c5..465148d739 100644 --- a/data/presets/presets/shop/deli.json +++ b/data/presets/presets/shop/deli.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/department_store.json b/data/presets/presets/shop/department_store.json index e7178bee9d..03396c2fcc 100644 --- a/data/presets/presets/shop/department_store.json +++ b/data/presets/presets/shop/department_store.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/doityourself.json b/data/presets/presets/shop/doityourself.json index f45b78ae4f..f482befe8f 100644 --- a/data/presets/presets/shop/doityourself.json +++ b/data/presets/presets/shop/doityourself.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/dry_cleaning.json b/data/presets/presets/shop/dry_cleaning.json index d339020927..8cbd2bc2e2 100644 --- a/data/presets/presets/shop/dry_cleaning.json +++ b/data/presets/presets/shop/dry_cleaning.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/e-cigarette.json b/data/presets/presets/shop/e-cigarette.json index a33ba69d1c..3f7b5dea73 100644 --- a/data/presets/presets/shop/e-cigarette.json +++ b/data/presets/presets/shop/e-cigarette.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/electronics.json b/data/presets/presets/shop/electronics.json index e1fc590faa..b5e3ea98d4 100644 --- a/data/presets/presets/shop/electronics.json +++ b/data/presets/presets/shop/electronics.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/erotic.json b/data/presets/presets/shop/erotic.json index 943c511faf..9fa9397c0f 100644 --- a/data/presets/presets/shop/erotic.json +++ b/data/presets/presets/shop/erotic.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/fabric.json b/data/presets/presets/shop/fabric.json index 9938e9a930..a188ed7f91 100644 --- a/data/presets/presets/shop/fabric.json +++ b/data/presets/presets/shop/fabric.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/farm.json b/data/presets/presets/shop/farm.json index 713a752073..68f40d74f9 100644 --- a/data/presets/presets/shop/farm.json +++ b/data/presets/presets/shop/farm.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/fashion.json b/data/presets/presets/shop/fashion.json index e9793a6e34..63deaf413f 100644 --- a/data/presets/presets/shop/fashion.json +++ b/data/presets/presets/shop/fashion.json @@ -6,7 +6,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/florist.json b/data/presets/presets/shop/florist.json index 2ab61a1d8d..dd774e0924 100644 --- a/data/presets/presets/shop/florist.json +++ b/data/presets/presets/shop/florist.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/frame.json b/data/presets/presets/shop/frame.json index 118cf773b9..2ebfbbe424 100644 --- a/data/presets/presets/shop/frame.json +++ b/data/presets/presets/shop/frame.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/furniture.json b/data/presets/presets/shop/furniture.json index 45bd879f6d..ee41edca78 100644 --- a/data/presets/presets/shop/furniture.json +++ b/data/presets/presets/shop/furniture.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/garden_centre.json b/data/presets/presets/shop/garden_centre.json index f0d879991f..a57eb9a886 100644 --- a/data/presets/presets/shop/garden_centre.json +++ b/data/presets/presets/shop/garden_centre.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/gas.json b/data/presets/presets/shop/gas.json index 4562b25cdc..713bd6dff2 100644 --- a/data/presets/presets/shop/gas.json +++ b/data/presets/presets/shop/gas.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/gift.json b/data/presets/presets/shop/gift.json index a1bfa0072a..da444402a6 100644 --- a/data/presets/presets/shop/gift.json +++ b/data/presets/presets/shop/gift.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/greengrocer.json b/data/presets/presets/shop/greengrocer.json index 228cc64894..0178e5f86c 100644 --- a/data/presets/presets/shop/greengrocer.json +++ b/data/presets/presets/shop/greengrocer.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/hairdresser.json b/data/presets/presets/shop/hairdresser.json index 71ed03877d..1844ae8b68 100644 --- a/data/presets/presets/shop/hairdresser.json +++ b/data/presets/presets/shop/hairdresser.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/hardware.json b/data/presets/presets/shop/hardware.json index a11fb4d51d..8623b593ac 100644 --- a/data/presets/presets/shop/hardware.json +++ b/data/presets/presets/shop/hardware.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/hearing_aids.json b/data/presets/presets/shop/hearing_aids.json index 120b94ab3d..7d9d4aa5cc 100644 --- a/data/presets/presets/shop/hearing_aids.json +++ b/data/presets/presets/shop/hearing_aids.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/herbalist.json b/data/presets/presets/shop/herbalist.json index 2187bed2c8..4530126e1f 100644 --- a/data/presets/presets/shop/herbalist.json +++ b/data/presets/presets/shop/herbalist.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/hifi.json b/data/presets/presets/shop/hifi.json index 2b150eaa51..c5ab7074b5 100644 --- a/data/presets/presets/shop/hifi.json +++ b/data/presets/presets/shop/hifi.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/houseware.json b/data/presets/presets/shop/houseware.json index 9531bf14c6..04890140a8 100644 --- a/data/presets/presets/shop/houseware.json +++ b/data/presets/presets/shop/houseware.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/interior_decoration.json b/data/presets/presets/shop/interior_decoration.json index db61b2567a..1a5fefbb99 100644 --- a/data/presets/presets/shop/interior_decoration.json +++ b/data/presets/presets/shop/interior_decoration.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/jewelry.json b/data/presets/presets/shop/jewelry.json index 371f26ce9c..93bcfb1579 100644 --- a/data/presets/presets/shop/jewelry.json +++ b/data/presets/presets/shop/jewelry.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/kiosk.json b/data/presets/presets/shop/kiosk.json index eebbfc0918..10ddc8b912 100644 --- a/data/presets/presets/shop/kiosk.json +++ b/data/presets/presets/shop/kiosk.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/kitchen.json b/data/presets/presets/shop/kitchen.json index 1c3d8aefcb..b2f95ea7a7 100644 --- a/data/presets/presets/shop/kitchen.json +++ b/data/presets/presets/shop/kitchen.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/laundry.json b/data/presets/presets/shop/laundry.json index 936ada3749..2de6f67131 100644 --- a/data/presets/presets/shop/laundry.json +++ b/data/presets/presets/shop/laundry.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/leather.json b/data/presets/presets/shop/leather.json index 5adc70c491..576ef709dd 100644 --- a/data/presets/presets/shop/leather.json +++ b/data/presets/presets/shop/leather.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/locksmith.json b/data/presets/presets/shop/locksmith.json index b0feec2f99..90fd0e3c57 100644 --- a/data/presets/presets/shop/locksmith.json +++ b/data/presets/presets/shop/locksmith.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/lottery.json b/data/presets/presets/shop/lottery.json index f22009c9bf..1d38ad0099 100644 --- a/data/presets/presets/shop/lottery.json +++ b/data/presets/presets/shop/lottery.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/massage.json b/data/presets/presets/shop/massage.json index e2efdb7980..7c6717dda0 100644 --- a/data/presets/presets/shop/massage.json +++ b/data/presets/presets/shop/massage.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/medical_supply.json b/data/presets/presets/shop/medical_supply.json index dd31441bda..4d34d66a80 100644 --- a/data/presets/presets/shop/medical_supply.json +++ b/data/presets/presets/shop/medical_supply.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/mobile_phone.json b/data/presets/presets/shop/mobile_phone.json index e2246c4712..3e314b3501 100644 --- a/data/presets/presets/shop/mobile_phone.json +++ b/data/presets/presets/shop/mobile_phone.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/motorcycle.json b/data/presets/presets/shop/motorcycle.json index a1d5d6a9e7..0eddf08bab 100644 --- a/data/presets/presets/shop/motorcycle.json +++ b/data/presets/presets/shop/motorcycle.json @@ -5,12 +5,13 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", "area" - ], + ], "terms": [ "bike" ], diff --git a/data/presets/presets/shop/music.json b/data/presets/presets/shop/music.json index b5b48747e6..0632d138c0 100644 --- a/data/presets/presets/shop/music.json +++ b/data/presets/presets/shop/music.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/musical_instrument.json b/data/presets/presets/shop/musical_instrument.json index ff21c90c41..759bc37aa1 100644 --- a/data/presets/presets/shop/musical_instrument.json +++ b/data/presets/presets/shop/musical_instrument.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/newsagent.json b/data/presets/presets/shop/newsagent.json index e6de7c32da..fcc811cb0f 100644 --- a/data/presets/presets/shop/newsagent.json +++ b/data/presets/presets/shop/newsagent.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/nutrition_supplements.json b/data/presets/presets/shop/nutrition_supplements.json index bbc3d1985f..7f3023b09a 100644 --- a/data/presets/presets/shop/nutrition_supplements.json +++ b/data/presets/presets/shop/nutrition_supplements.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/optician.json b/data/presets/presets/shop/optician.json index 577388bba6..93bca54496 100644 --- a/data/presets/presets/shop/optician.json +++ b/data/presets/presets/shop/optician.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/organic.json b/data/presets/presets/shop/organic.json index 85d2b535b6..236090c7cb 100644 --- a/data/presets/presets/shop/organic.json +++ b/data/presets/presets/shop/organic.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/outdoor.json b/data/presets/presets/shop/outdoor.json index 3d1cf2d700..8312b60969 100644 --- a/data/presets/presets/shop/outdoor.json +++ b/data/presets/presets/shop/outdoor.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/paint.json b/data/presets/presets/shop/paint.json index 971214c8a8..a7ee7b8d96 100644 --- a/data/presets/presets/shop/paint.json +++ b/data/presets/presets/shop/paint.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/pastry.json b/data/presets/presets/shop/pastry.json index 6e62a45bad..28d4c7a844 100644 --- a/data/presets/presets/shop/pastry.json +++ b/data/presets/presets/shop/pastry.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", @@ -20,4 +21,4 @@ "cakery" ], "name": "Pastry Shop" -} \ No newline at end of file +} diff --git a/data/presets/presets/shop/pawnbroker.json b/data/presets/presets/shop/pawnbroker.json index 2f38fb9aa2..1c1528c385 100644 --- a/data/presets/presets/shop/pawnbroker.json +++ b/data/presets/presets/shop/pawnbroker.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/perfumery.json b/data/presets/presets/shop/perfumery.json index 3d4d8990ac..8914fe0935 100644 --- a/data/presets/presets/shop/perfumery.json +++ b/data/presets/presets/shop/perfumery.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/pet.json b/data/presets/presets/shop/pet.json index d10903b600..00aab3e090 100644 --- a/data/presets/presets/shop/pet.json +++ b/data/presets/presets/shop/pet.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/photo.json b/data/presets/presets/shop/photo.json index f76951aa70..4d08ee0250 100644 --- a/data/presets/presets/shop/photo.json +++ b/data/presets/presets/shop/photo.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/pyrotechnics.json b/data/presets/presets/shop/pyrotechnics.json index 5ff936d6bd..0acddf7db4 100644 --- a/data/presets/presets/shop/pyrotechnics.json +++ b/data/presets/presets/shop/pyrotechnics.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/radiotechnics.json b/data/presets/presets/shop/radiotechnics.json index ea51f414ec..4e234f85dc 100644 --- a/data/presets/presets/shop/radiotechnics.json +++ b/data/presets/presets/shop/radiotechnics.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/religion.json b/data/presets/presets/shop/religion.json index ea60532f3c..e7ff5bae77 100644 --- a/data/presets/presets/shop/religion.json +++ b/data/presets/presets/shop/religion.json @@ -5,9 +5,10 @@ "operator", "address", "building_area", - "opening_hours", "religion", - "denomination" + "denomination", + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/scuba_diving.json b/data/presets/presets/shop/scuba_diving.json index 6c09d91029..46c11253e4 100644 --- a/data/presets/presets/shop/scuba_diving.json +++ b/data/presets/presets/shop/scuba_diving.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/seafood.json b/data/presets/presets/shop/seafood.json index ef6769e8fd..598c7963db 100644 --- a/data/presets/presets/shop/seafood.json +++ b/data/presets/presets/shop/seafood.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/second_hand.json b/data/presets/presets/shop/second_hand.json index 613c33d532..bf575f1330 100644 --- a/data/presets/presets/shop/second_hand.json +++ b/data/presets/presets/shop/second_hand.json @@ -5,8 +5,9 @@ "operator", "address", "building_area", + "second_hand", "opening_hours", - "second_hand" + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/shoes.json b/data/presets/presets/shop/shoes.json index 8e5a912acc..2f2f083246 100644 --- a/data/presets/presets/shop/shoes.json +++ b/data/presets/presets/shop/shoes.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/sports.json b/data/presets/presets/shop/sports.json index d8e46a33bf..cc9c7b8fb1 100644 --- a/data/presets/presets/shop/sports.json +++ b/data/presets/presets/shop/sports.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/stationery.json b/data/presets/presets/shop/stationery.json index be7eecb755..ef7a3d6755 100644 --- a/data/presets/presets/shop/stationery.json +++ b/data/presets/presets/shop/stationery.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/storage_rental.json b/data/presets/presets/shop/storage_rental.json index c035064805..988d96e212 100644 --- a/data/presets/presets/shop/storage_rental.json +++ b/data/presets/presets/shop/storage_rental.json @@ -5,7 +5,8 @@ "operator", "address", "building", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/supermarket.json b/data/presets/presets/shop/supermarket.json index cf134415ab..c1177f4a1a 100644 --- a/data/presets/presets/shop/supermarket.json +++ b/data/presets/presets/shop/supermarket.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/tailor.json b/data/presets/presets/shop/tailor.json index 4f4584ffb7..07489d17c5 100644 --- a/data/presets/presets/shop/tailor.json +++ b/data/presets/presets/shop/tailor.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/tattoo.json b/data/presets/presets/shop/tattoo.json index 72718b1f4c..36b6e6859f 100644 --- a/data/presets/presets/shop/tattoo.json +++ b/data/presets/presets/shop/tattoo.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/tea.json b/data/presets/presets/shop/tea.json index 51cc4e02c7..6f69e61127 100644 --- a/data/presets/presets/shop/tea.json +++ b/data/presets/presets/shop/tea.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/ticket.json b/data/presets/presets/shop/ticket.json index 0683c6e17a..63da442ba6 100644 --- a/data/presets/presets/shop/ticket.json +++ b/data/presets/presets/shop/ticket.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/tiles.json b/data/presets/presets/shop/tiles.json index f2c4393e1f..c9c54552ef 100644 --- a/data/presets/presets/shop/tiles.json +++ b/data/presets/presets/shop/tiles.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/tobacco.json b/data/presets/presets/shop/tobacco.json index 5fa14b9937..1263b61366 100644 --- a/data/presets/presets/shop/tobacco.json +++ b/data/presets/presets/shop/tobacco.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/toys.json b/data/presets/presets/shop/toys.json index f1983d2cf4..d289afdd9e 100644 --- a/data/presets/presets/shop/toys.json +++ b/data/presets/presets/shop/toys.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/trade.json b/data/presets/presets/shop/trade.json index 214a79302c..5b3bd7396e 100644 --- a/data/presets/presets/shop/trade.json +++ b/data/presets/presets/shop/trade.json @@ -6,7 +6,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/travel_agency.json b/data/presets/presets/shop/travel_agency.json index 830e76191b..794f409696 100644 --- a/data/presets/presets/shop/travel_agency.json +++ b/data/presets/presets/shop/travel_agency.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/tyres.json b/data/presets/presets/shop/tyres.json index 357fe5b7fd..4e8539f594 100644 --- a/data/presets/presets/shop/tyres.json +++ b/data/presets/presets/shop/tyres.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/vacuum_cleaner.json b/data/presets/presets/shop/vacuum_cleaner.json index 6c4f3dac3b..f08c8997a6 100644 --- a/data/presets/presets/shop/vacuum_cleaner.json +++ b/data/presets/presets/shop/vacuum_cleaner.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/variety_store.json b/data/presets/presets/shop/variety_store.json index 4e81923f58..1a511686cd 100644 --- a/data/presets/presets/shop/variety_store.json +++ b/data/presets/presets/shop/variety_store.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/video.json b/data/presets/presets/shop/video.json index 2646e79751..7ee6714b12 100644 --- a/data/presets/presets/shop/video.json +++ b/data/presets/presets/shop/video.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/video_games.json b/data/presets/presets/shop/video_games.json index 8a06cfa91e..fc3cc003da 100644 --- a/data/presets/presets/shop/video_games.json +++ b/data/presets/presets/shop/video_games.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/watches.json b/data/presets/presets/shop/watches.json index f2f5a876d2..b40922e5cc 100644 --- a/data/presets/presets/shop/watches.json +++ b/data/presets/presets/shop/watches.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/water_sports.json b/data/presets/presets/shop/water_sports.json index 75c6ddf4fd..f0e49567af 100644 --- a/data/presets/presets/shop/water_sports.json +++ b/data/presets/presets/shop/water_sports.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/weapons.json b/data/presets/presets/shop/weapons.json index a5c3b6061f..66f7cffb3c 100644 --- a/data/presets/presets/shop/weapons.json +++ b/data/presets/presets/shop/weapons.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/window_blind.json b/data/presets/presets/shop/window_blind.json index 3ed9a6494a..5920528ed5 100644 --- a/data/presets/presets/shop/window_blind.json +++ b/data/presets/presets/shop/window_blind.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", diff --git a/data/presets/presets/shop/wine.json b/data/presets/presets/shop/wine.json index 38b45edc67..40078b8f25 100644 --- a/data/presets/presets/shop/wine.json +++ b/data/presets/presets/shop/wine.json @@ -5,7 +5,8 @@ "operator", "address", "building_area", - "opening_hours" + "opening_hours", + "payment_multi" ], "geometry": [ "point", From d2ffde8b484c70db1f7a7d9351da2b01bb4c9899 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 2 Jan 2018 14:21:02 +0000 Subject: [PATCH 100/206] chore(package): update rollup to version 0.53.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15524b40ae..520f63b0a3 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "npm-run-all": "^4.0.0", "phantomjs-prebuilt": "~2.1.11", "request": "^2.81.0", - "rollup": "0.53.2", + "rollup": "0.53.3", "rollup-plugin-commonjs": "8.2.6", "rollup-plugin-includepaths": "0.2.2", "rollup-plugin-json": "2.2.0", From 31770b3957b58c012f53e7990f5b455e38991f38 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 16:29:05 -0500 Subject: [PATCH 101/206] Add Car Pooling preset (closes #4623) --- data/presets.yaml | 4 ++++ data/presets/presets.json | 16 ++++++++++++++++ data/presets/presets/amenity/car_pooling.json | 16 ++++++++++++++++ data/taginfo.json | 4 ++++ dist/locales/en.json | 4 ++++ 5 files changed, 44 insertions(+) create mode 100644 data/presets/presets/amenity/car_pooling.json diff --git a/data/presets.yaml b/data/presets.yaml index 809ed011d7..acffd3d613 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -1822,6 +1822,10 @@ en: name: Cafe # 'terms: bistro,coffee,tea' terms: '' + amenity/car_pooling: + # amenity=car_pooling + name: Car Pooling + terms: '' amenity/car_rental: # amenity=car_rental name: Car Rental diff --git a/data/presets/presets.json b/data/presets/presets.json index f700cf244b..e8b4454859 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -1078,6 +1078,22 @@ }, "name": "Cafe" }, + "amenity/car_pooling": { + "icon": "car", + "fields": [ + "name", + "operator", + "capacity" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "amenity": "car_pooling" + }, + "name": "Car Pooling" + }, "amenity/car_rental": { "icon": "car", "fields": [ diff --git a/data/presets/presets/amenity/car_pooling.json b/data/presets/presets/amenity/car_pooling.json new file mode 100644 index 0000000000..9c06de8ee3 --- /dev/null +++ b/data/presets/presets/amenity/car_pooling.json @@ -0,0 +1,16 @@ +{ + "icon": "car", + "fields": [ + "name", + "operator", + "capacity" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "amenity": "car_pooling" + }, + "name": "Car Pooling" +} diff --git a/data/taginfo.json b/data/taginfo.json index 9d34fec620..1ea45a2447 100644 --- a/data/taginfo.json +++ b/data/taginfo.json @@ -223,6 +223,10 @@ "key": "amenity", "value": "cafe" }, + { + "key": "amenity", + "value": "car_pooling" + }, { "key": "amenity", "value": "car_rental" diff --git a/dist/locales/en.json b/dist/locales/en.json index f6df3fc2c5..0925348730 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -2846,6 +2846,10 @@ "name": "Cafe", "terms": "bistro,coffee,tea" }, + "amenity/car_pooling": { + "name": "Car Pooling", + "terms": "" + }, "amenity/car_rental": { "name": "Car Rental", "terms": "" From baeff8f59c90b2583950c223c91f4c3074948a4a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 17:55:25 -0500 Subject: [PATCH 102/206] Don't autocomplete a longer value if search matches a value exactly (closes #4549) --- modules/lib/d3.combobox.js | 113 +++++++++++++++++++---------------- test/spec/lib/d3.combobox.js | 57 +++++++++++------- 2 files changed, 98 insertions(+), 72 deletions(-) diff --git a/modules/lib/d3.combobox.js b/modules/lib/d3.combobox.js index baa9d4cb45..92cd0a7364 100644 --- a/modules/lib/d3.combobox.js +++ b/modules/lib/d3.combobox.js @@ -14,15 +14,15 @@ import { export function d3combobox() { - var dispatch = d3_dispatch('accept'), - container = d3_select(document.body), - data = [], - suggestions = [], - minItems = 2, - caseSensitive = false; - - var fetcher = function(val, cb) { - cb(data.filter(function(d) { + var dispatch = d3_dispatch('accept'); + var _container = d3_select(document.body); + var _data = []; + var _suggestions = []; + var _minItems = 2; + var _caseSensitive = false; + + var _fetcher = function(val, cb) { + cb(_data.filter(function(d) { return d.value .toString() .toLowerCase() @@ -31,11 +31,11 @@ export function d3combobox() { }; var combobox = function(input, attachTo) { - var idx = -1, - wrapper = container - .selectAll('div.combobox') - .filter(function(d) { return d === input.node(); }), - shown = !wrapper.empty(); + var idx = -1; + var wrapper = _container + .selectAll('div.combobox') + .filter(function(d) { return d === input.node(); }); + var shown = !wrapper.empty(); input .classed('combobox-input', true) @@ -45,17 +45,17 @@ export function d3combobox() { .on('keyup.typeahead', keyup) .on('input.typeahead', change) .each(function() { - var parent = this.parentNode, - sibling = this.nextSibling; + var parent = this.parentNode; + var sibling = this.nextSibling; var caret = d3_select(parent).selectAll('.combobox-caret') .filter(function(d) { return d === input.node(); }) .data([input.node()]); caret = caret.enter() - .insert('div', function() { return sibling; }) + .insert('div', function() { return sibling; }) .attr('class', 'combobox-caret') - .merge(caret); + .merge(caret); caret .on('mousedown', function () { @@ -82,7 +82,7 @@ export function d3combobox() { function show() { if (!shown) { - wrapper = container + wrapper = _container .insert('div', ':first-child') .datum(input.node()) .attr('class', 'combobox') @@ -177,17 +177,17 @@ export function d3combobox() { } function nav(dir) { - if (!suggestions.length) return; - idx = Math.max(Math.min(idx + dir, suggestions.length - 1), 0); - input.property('value', suggestions[idx].value); + if (!_suggestions.length) return; + idx = Math.max(Math.min(idx + dir, _suggestions.length - 1), 0); + input.property('value', _suggestions[idx].value); render(); ensureVisible(); } function value() { - var value = input.property('value'), - start = input.property('selectionStart'), - end = input.property('selectionEnd'); + var value = input.property('value'); + var start = input.property('selectionStart'); + var end = input.property('selectionEnd'); if (start && end) { value = value.substring(0, start); @@ -197,32 +197,45 @@ export function d3combobox() { } function fetch(v, cb) { - fetcher.call(input, v, function(_) { - suggestions = _; + _fetcher.call(input, v, function(_) { + _suggestions = _; cb(); }); } function autocomplete() { - var v = caseSensitive ? value() : value().toLowerCase(); + var v = _caseSensitive ? value() : value().toLowerCase(); idx = -1; if (!v) return; - for (var i = 0; i < suggestions.length; i++) { - var suggestion = suggestions[i].value, - compare = caseSensitive ? suggestion : suggestion.toLowerCase(); + var best = -1; + var suggestion, compare; - if (compare.indexOf(v) === 0) { - idx = i; - input.property('value', suggestion); - input.node().setSelectionRange(v.length, suggestion.length); - return; + for (var i = 0; i < _suggestions.length; i++) { + suggestion = _suggestions[i].value; + compare = _caseSensitive ? suggestion : suggestion.toLowerCase(); + + // if search string matches suggestion exactly, pick it.. + if (compare === v) { + best = i; + break; + + // otherwise lock in the first result that starts with the search string.. + } else if (best === -1 && compare.indexOf(v) === 0) { + best = i; } } + + if (best !== -1) { + idx = best; + suggestion = _suggestions[best].value; + input.property('value', suggestion); + input.node().setSelectionRange(v.length, suggestion.length); + } } function render() { - if (suggestions.length >= minItems && document.activeElement === input.node()) { + if (_suggestions.length >= _minItems && document.activeElement === input.node()) { show(); } else { hide(); @@ -231,7 +244,7 @@ export function d3combobox() { var options = wrapper .selectAll('a.combobox-option') - .data(suggestions, function(d) { return d.value; }); + .data(_suggestions, function(d) { return d.value; }); options.exit() .remove(); @@ -248,8 +261,8 @@ export function d3combobox() { .order(); - var node = attachTo ? attachTo.node() : input.node(), - rect = node.getBoundingClientRect(); + var node = attachTo ? attachTo.node() : input.node(); + var rect = node.getBoundingClientRect(); wrapper .style('left', rect.left + 'px') @@ -277,32 +290,32 @@ export function d3combobox() { }; combobox.fetcher = function(_) { - if (!arguments.length) return fetcher; - fetcher = _; + if (!arguments.length) return _fetcher; + _fetcher = _; return combobox; }; combobox.data = function(_) { - if (!arguments.length) return data; - data = _; + if (!arguments.length) return _data; + _data = _; return combobox; }; combobox.minItems = function(_) { - if (!arguments.length) return minItems; - minItems = _; + if (!arguments.length) return _minItems; + _minItems = _; return combobox; }; combobox.caseSensitive = function(_) { - if (!arguments.length) return caseSensitive; - caseSensitive = _; + if (!arguments.length) return _caseSensitive; + _caseSensitive = _; return combobox; }; combobox.container = function(_) { - if (!arguments.length) return container; - container = _; + if (!arguments.length) return _container; + _container = _; return combobox; }; diff --git a/test/spec/lib/d3.combobox.js b/test/spec/lib/d3.combobox.js index b04f839a83..7cc633ca5e 100644 --- a/test/spec/lib/d3.combobox.js +++ b/test/spec/lib/d3.combobox.js @@ -2,16 +2,18 @@ describe('d3.combobox', function() { var body, container, content, input, combobox; var data = [ + {title: 'foobar', value: 'foobar'}, {title: 'foo', value: 'foo'}, {title: 'bar', value: 'bar'}, - {title: 'Baz', value: 'Baz'} + {title: 'Baz', value: 'Baz'}, + {title: 'test', value: 'test'} ]; function simulateKeypress(key) { - var keyCode = iD.lib.d3keybinding.keyCodes[key], - value = input.property('value'), - start = input.property('selectionStart'), - finis = input.property('selectionEnd'); + var keyCode = iD.lib.d3keybinding.keyCodes[key]; + var value = input.property('value'); + var start = input.property('selectionStart'); + var finis = input.property('selectionEnd'); iD.d3.customEvent(happen.makeEvent({ type: 'keydown', @@ -101,36 +103,37 @@ describe('d3.combobox', function() { it('shows a menu of entries on focus', function() { input.call(combobox.data(data)); focusTypeahead(input); - expect(body.selectAll('.combobox-option').nodes().length).to.equal(3); - expect(body.selectAll('.combobox-option').text()).to.equal('foo'); + expect(body.selectAll('.combobox-option').nodes().length).to.equal(5); + expect(body.selectAll('.combobox-option').text()).to.equal('foobar'); }); it('filters entries to those matching the value', function() { input.property('value', 'b').call(combobox.data(data)); focusTypeahead(input); - expect(body.selectAll('.combobox-option').size()).to.equal(2); - expect(body.selectAll('.combobox-option').nodes()[0].text).to.equal('bar'); - expect(body.selectAll('.combobox-option').nodes()[1].text).to.equal('Baz'); + expect(body.selectAll('.combobox-option').size()).to.equal(3); + expect(body.selectAll('.combobox-option').nodes()[0].text).to.equal('foobar'); + expect(body.selectAll('.combobox-option').nodes()[1].text).to.equal('bar'); + expect(body.selectAll('.combobox-option').nodes()[2].text).to.equal('Baz'); }); it('shows no menu on focus if it would contain only one item', function() { - input.property('value', 'f').call(combobox.data(data)); + input.property('value', 't').call(combobox.data(data)); focusTypeahead(input); expect(body.selectAll('.combobox-option').size()).to.equal(0); }); it('shows menu on focus if it would contain at least minItems items', function() { combobox.minItems(1); - input.property('value', 'f').call(combobox.data(data)); + input.property('value', 't').call(combobox.data(data)); focusTypeahead(input); expect(body.selectAll('.combobox-option').size()).to.equal(1); }); it('shows all entries when clicking on the caret', function() { - input.property('value', 'foo').call(combobox.data(data)); + input.property('value', 'foobar').call(combobox.data(data)); body.selectAll('.combobox-caret').dispatch('mousedown'); - expect(body.selectAll('.combobox-option').size()).to.equal(3); - expect(body.selectAll('.combobox-option').text()).to.equal('foo'); + expect(body.selectAll('.combobox-option').size()).to.equal(5); + expect(body.selectAll('.combobox-option').text()).to.equal('foobar'); }); it('is initially shown with no selection', function() { @@ -139,7 +142,7 @@ describe('d3.combobox', function() { expect(body.selectAll('.combobox-option.selected').size()).to.equal(0); }); - it('selects the first option matching the input', function() { + it('selects the first option that matches the input', function() { input.call(combobox.data(data)); focusTypeahead(input); simulateKeypress('b'); @@ -147,6 +150,16 @@ describe('d3.combobox', function() { expect(body.selectAll('.combobox-option.selected').text()).to.equal('bar'); }); + it('prefers an option that exactly matches the input over the first option', function() { + input.call(combobox.data(data)); + focusTypeahead(input); + simulateKeypress('f'); + simulateKeypress('o'); + simulateKeypress('o'); + expect(body.selectAll('.combobox-option.selected').size()).to.equal(1); + expect(body.selectAll('.combobox-option.selected').text()).to.equal('foo'); // skip foobar + }); + it('selects the completed portion of the value', function() { input.call(combobox.data(data)); focusTypeahead(input); @@ -217,18 +230,18 @@ describe('d3.combobox', function() { simulateKeypress('↓'); expect(body.selectAll('.combobox-option.selected').size()).to.equal(1); - expect(body.selectAll('.combobox-option.selected').text()).to.equal('foo'); - expect(input.property('value')).to.equal('foo'); + expect(body.selectAll('.combobox-option.selected').text()).to.equal('foobar'); + expect(input.property('value')).to.equal('foobar'); simulateKeypress('↓'); expect(body.selectAll('.combobox-option.selected').size()).to.equal(1); - expect(body.selectAll('.combobox-option.selected').text()).to.equal('bar'); - expect(input.property('value')).to.equal('bar'); + expect(body.selectAll('.combobox-option.selected').text()).to.equal('foo'); + expect(input.property('value')).to.equal('foo'); simulateKeypress('↑'); expect(body.selectAll('.combobox-option.selected').size()).to.equal(1); - expect(body.selectAll('.combobox-option.selected').text()).to.equal('foo'); - expect(input.property('value')).to.equal('foo'); + expect(body.selectAll('.combobox-option.selected').text()).to.equal('foobar'); + expect(input.property('value')).to.equal('foobar'); }); it('emits accepted event with selected datum on ⇥', function(done) { From 48834a0ea564b4b1575797912d689809831593c3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 2 Jan 2018 23:41:21 -0500 Subject: [PATCH 103/206] Add reset buttons for display sliders --- modules/ui/background.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/ui/background.js b/modules/ui/background.js index 2ddf0d81c7..cf194a3583 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -62,6 +62,8 @@ export function uiBackground(context) { .property('value', val); _displayOptions.selectAll('.' + d + '-value') .text(Math.floor(val * 100) + '%'); + _displayOptions.selectAll('.' + d + '-reset') + .classed('disabled', val === 1); _options[d] = val; context.background()[d](val); @@ -281,6 +283,16 @@ export function uiBackground(context) { setDisplayOption(d, val); }); + controlsEnter + .append('button') + .attr('title', t('background.reset')) + .attr('class', function(d) { return d + '-reset disabled'; }) + .on('click', function(d) { + if (d3_event.button !== 0) return; + setDisplayOption(d, 1); + }) + .call(svgIcon('#icon-' + (textDirection === 'rtl' ? 'redo' : 'undo'))); + // add minimap toggle var minimapEnter = containerEnter From 53aa2973e48a363e286261af8a6be5c561860ad2 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 3 Jan 2018 01:47:25 -0500 Subject: [PATCH 104/206] Refactor display options controls to uiBackgroundDisplayOptions --- modules/ui/background.js | 135 ++------------------ modules/ui/background_display_options.js | 152 +++++++++++++++++++++++ modules/ui/index.js | 1 + 3 files changed, 164 insertions(+), 124 deletions(-) create mode 100644 modules/ui/background_display_options.js diff --git a/modules/ui/background.js b/modules/ui/background.js index cf194a3583..3fd003d3f2 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -10,69 +10,36 @@ import { select as d3_select } from 'd3-selection'; - import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t, textDirection } from '../util/locale'; import { svgIcon } from '../svg'; +import { uiBackgroundDisplayOptions } from './background_display_options'; import { uiBackgroundOffset } from './background_offset'; import { uiCmd } from './cmd'; import { uiDisclosure } from './disclosure'; import { uiHelp } from './help'; import { uiMapData } from './map_data'; -import { uiMapInMap } from './map_in_map'; import { uiTooltipHtml } from './tooltipHtml'; -import { utilDetect } from '../util/detect'; -import { utilSetTransform, utilCallWhenIdle } from '../util'; +import { utilCallWhenIdle } from '../util'; import { tooltip } from '../util/tooltip'; export function uiBackground(context) { var key = t('background.key'); - var detected = utilDetect(); - - var _options = { - brightness: (context.storage('background-opacity') !== null) ? - (+context.storage('background-opacity')) : 1.0, - contrast: 1, - saturation: 1, - sharpness: 1 - }; + var _customSource = context.background().findSource('custom'); var _previousBackground; var _shown = false; var _backgroundList = d3_select(null); var _overlayList = d3_select(null); - var _displayOptions = d3_select(null); + var _displayOptionsContainer = d3_select(null); var _offsetContainer = d3_select(null); + var backgroundDisplayOptions = uiBackgroundDisplayOptions(context); var backgroundOffset = uiBackgroundOffset(context); - function clamp(x, min, max) { return Math.max(min, Math.min(x, max)); } - - - function setDisplayOption(d, val) { - if (!val && d3_event && d3_event.target) { - val = d3_event.target.value; - } - - val = clamp(val, 0.25, 2); - _displayOptions.selectAll('.' + d + '-input') - .property('value', val); - _displayOptions.selectAll('.' + d + '-value') - .text(Math.floor(val * 100) + '%'); - _displayOptions.selectAll('.' + d + '-reset') - .classed('disabled', val === 1); - - _options[d] = val; - context.background()[d](val); - - if (d === 'brightness') { - context.storage('background-opacity', val); - } - } - function setTooltips(selection) { selection.each(function(d, i, nodes) { @@ -247,84 +214,6 @@ export function uiBackground(context) { } - function renderDisplayOptions(selection) { - var container = selection.selectAll('display-options-container') - .data([0]); - - var containerEnter = container.enter() - .append('div') - .attr('class', 'display-options-container controls-list'); - - var sliders = ['brightness', 'contrast', 'saturation', 'sharpness']; - - var controls = containerEnter.selectAll('.display-control') - .data(sliders); - - var controlsEnter = controls.enter() - .append('div') - .attr('class', function(d) { return 'display-control display-control-' + d; }); - - controlsEnter - .append('h5') - .text(function(d) { return t('background.' + d); }) - .append('span') - .attr('class', function(d) { return d + '-value'; }); - - controlsEnter - .append('input') - .attr('class', function(d) { return d + '-input'; }) - .attr('type', 'range') - .attr('min', '0.25') - .attr('max', '2') - .attr('step', '0.05') - .property('value', function(d) { return _options[d]; }) - .on('input', function(d) { - var val = d3_select(this).property('value'); - setDisplayOption(d, val); - }); - - controlsEnter - .append('button') - .attr('title', t('background.reset')) - .attr('class', function(d) { return d + '-reset disabled'; }) - .on('click', function(d) { - if (d3_event.button !== 0) return; - setDisplayOption(d, 1); - }) - .call(svgIcon('#icon-' + (textDirection === 'rtl' ? 'redo' : 'undo'))); - - - // add minimap toggle - var minimapEnter = containerEnter - .append('div') - .attr('class', 'minimap-toggle-wrap'); - - var minimapLabelEnter = minimapEnter - .append('label') - .call(tooltip() - .html(true) - .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) - .placement('top') - ); - - minimapLabelEnter - .classed('minimap-toggle', true) - .append('input') - .attr('type', 'checkbox') - .on('change', function() { - uiMapInMap.toggle(); - d3_event.preventDefault(); - }); - - minimapLabelEnter - .append('span') - .text(t('background.minimap.description')); - - _displayOptions = containerEnter - .merge(container); - } - - function update() { _backgroundList .call(drawListItems, 'radio', chooseBackground, function(d) { return !d.isHidden() && !d.overlay; }); @@ -332,6 +221,9 @@ export function uiBackground(context) { _overlayList .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; }); + _displayOptionsContainer + .call(backgroundDisplayOptions); + _offsetContainer .call(backgroundOffset); } @@ -443,14 +335,10 @@ export function uiBackground(context) { .content(renderOverlayList) ); - // display settings - pane + // display options + _displayOptionsContainer = pane .append('div') - .attr('class', 'background-display-options-container') - .call(uiDisclosure(context, 'background_display_options', true) - .title(t('background.display_options')) - .content(renderDisplayOptions) - ); + .attr('class', 'background-display-options'); // offset controls _offsetContainer = pane @@ -467,7 +355,6 @@ export function uiBackground(context) { update(); - setDisplayOption('brightness', _options.brightness); var keybinding = d3_keybinding('background') .on(key, togglePane) diff --git a/modules/ui/background_display_options.js b/modules/ui/background_display_options.js new file mode 100644 index 0000000000..ecfa8fddac --- /dev/null +++ b/modules/ui/background_display_options.js @@ -0,0 +1,152 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + + +import { t, textDirection } from '../util/locale'; +import { svgIcon } from '../svg'; +import { uiDisclosure } from './disclosure'; +import { uiMapInMap } from './map_in_map'; +import { uiTooltipHtml } from './tooltipHtml'; +import { tooltip } from '../util/tooltip'; + + +export function uiBackgroundDisplayOptions(context) { + var _selection = d3_select(null); + var sliders = ['brightness', 'contrast', 'saturation', 'sharpness']; + + var _options = { + brightness: (context.storage('background-opacity') !== null) ? + (+context.storage('background-opacity')) : 1.0, + contrast: 1, + saturation: 1, + sharpness: 1 + }; + + + function clamp(x, min, max) { + return Math.max(min, Math.min(x, max)); + } + + + function updateValue(d, val) { + if (!val && d3_event && d3_event.target) { + val = d3_event.target.value; + } + + val = clamp(val, 0.25, 2); + + _options[d] = val; + context.background()[d](val); + + if (d === 'brightness') { + context.storage('background-opacity', val); + } + + _selection + .call(render); + } + + + function render(selection) { + var container = selection.selectAll('.display-options-container') + .data([0]); + + var containerEnter = container.enter() + .append('div') + .attr('class', 'display-options-container controls-list'); + + // add slider controls + var slidersEnter = containerEnter.selectAll('.display-control') + .data(sliders) + .enter() + .append('div') + .attr('class', function(d) { return 'display-control display-control-' + d; }); + + slidersEnter + .append('h5') + .text(function(d) { return t('background.' + d); }) + .append('span') + .attr('class', function(d) { return 'display-option-value display-option-value-' + d; }); + + slidersEnter + .append('input') + .attr('class', function(d) { return 'display-option-input display-option-input-' + d; }) + .attr('type', 'range') + .attr('min', '0.25') + .attr('max', '2') + .attr('step', '0.05') + .on('input', function(d) { + var val = d3_select(this).property('value'); + updateValue(d, val); + }); + + slidersEnter + .append('button') + .attr('title', t('background.reset')) + .attr('class', function(d) { return 'display-option-reset display-option-reset-' + d; }) + .on('click', function(d) { + if (d3_event.button !== 0) return; + updateValue(d, 1); + }) + .call(svgIcon('#icon-' + (textDirection === 'rtl' ? 'redo' : 'undo'))); + + + // add minimap toggle + var minimapEnter = containerEnter + .append('div') + .attr('class', 'minimap-toggle-wrap'); + + var minimapLabelEnter = minimapEnter + .append('label') + .call(tooltip() + .html(true) + .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) + .placement('top') + ); + + minimapLabelEnter + .classed('minimap-toggle', true) + .append('input') + .attr('type', 'checkbox') + .on('change', function() { + uiMapInMap.toggle(); + d3_event.preventDefault(); + }); + + minimapLabelEnter + .append('span') + .text(t('background.minimap.description')); + + + // update + container = containerEnter + .merge(container); + + container.selectAll('.display-option-input') + .property('value', function(d) { return _options[d]; }); + + container.selectAll('.display-option-value') + .text(function(d) { return Math.floor(_options[d] * 100) + '%'; }); + + container.selectAll('.display-option-reset') + .classed('disabled', function(d) { return _options[d] === 1; }); + } + + + function backgroundDisplayOptions(selection) { + _selection = selection; + + selection + .call(uiDisclosure(context, 'background_display_options', true) + .title(t('background.display_options')) + .content(render) + ); + } + + // setDisplayOption('brightness', _options.brightness); + + + return backgroundDisplayOptions; +} diff --git a/modules/ui/index.js b/modules/ui/index.js index f1affc4fde..8a3dd80c64 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -2,6 +2,7 @@ export { uiInit } from './init'; export { uiAccount } from './account'; export { uiAttribution } from './attribution'; export { uiBackground } from './background'; +export { uiBackgroundDisplayOptions } from './background_display_options'; export { uiBackgroundOffset } from './background_offset'; export { uiChangesetEditor } from './changeset_editor'; export { uiCmd } from './cmd'; From 46ebe025555fe82930f5d27fa9866e19759f1cfc Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 3 Jan 2018 14:35:27 -0500 Subject: [PATCH 105/206] Clean up styles, move minimap toggle below background imagery list --- css/80_app.css | 45 +++++++++++++++--------- modules/ui/background.js | 42 +++++++++++++++++++--- modules/ui/background_display_options.js | 41 ++++----------------- modules/ui/map_in_map.js | 7 ++-- 4 files changed, 76 insertions(+), 59 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index e0c851e13c..a7e430fe38 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2319,20 +2319,38 @@ div.full-screen > button:hover { text-overflow: ellipsis; } -.minimap-toggle { - display: block; - padding: 5px 10px; - cursor: pointer; - color: #7092ff; - border-top: 1px solid #ccc; + +/* Background Display Options */ + +.display-options-container { + padding: 10px; } -.minimap-toggle.active { - background: #e8ebff; +.display-control h5 { + padding-bottom: 0; + padding-top: 10px; } -.minimap-toggle:hover { - background-color: #ececec; +.display-control h5 span { + margin: 5px; +} + +.display-control .display-option-input { + height: 20px; + width: 160px; +} + +.display-control button { + height: 30px; + width: 30px; + margin-left: 5px; + margin-right: 0px; + vertical-align: text-bottom; + border-radius: 4px; +} +[dir='rtl'] .display-control button { + margin-left: 0px; + margin-right: 5px; } @@ -2463,13 +2481,6 @@ div.full-screen > button:hover { border-top: 5px solid #222; } -.display-options-container { - padding: 10px; -} - -.display-options-container h5 span { - margin: 5px; -} .map-data-control .layer-list button, .background-control .layer-list button { diff --git a/modules/ui/background.js b/modules/ui/background.js index 3fd003d3f2..cbd5bfa151 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -20,6 +20,7 @@ import { uiCmd } from './cmd'; import { uiDisclosure } from './disclosure'; import { uiHelp } from './help'; import { uiMapData } from './map_data'; +import { uiMapInMap } from './map_in_map'; import { uiTooltipHtml } from './tooltipHtml'; import { utilCallWhenIdle } from '../util'; import { tooltip } from '../util/tooltip'; @@ -43,11 +44,11 @@ export function uiBackground(context) { function setTooltips(selection) { selection.each(function(d, i, nodes) { - var item = d3_select(this).select('label'), - span = item.select('span'), - placement = (i < nodes.length / 2) ? 'bottom' : 'top', - description = d.description(), - isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); + var item = d3_select(this).select('label'); + var span = item.select('span'); + var placement = (i < nodes.length / 2) ? 'bottom' : 'top'; + var description = d.description(); + var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); if (d === _previousBackground) { item.call(tooltip() @@ -194,11 +195,42 @@ export function uiBackground(context) { var container = selection.selectAll('layer-background-list') .data([0]); + // the background list _backgroundList = container.enter() .append('ul') .attr('class', 'layer-list layer-background-list') .attr('dir', 'auto') .merge(container); + + + // add minimap toggle below list + var minimapEnter = selection.selectAll('minimap-toggle-list') + .data([0]) + .enter() + .append('ul') + .attr('class', 'layer-list minimap-toggle-list') + .append('li') + .attr('class', 'layer minimap-toggle-item'); + + var minimapLabelEnter = minimapEnter + .append('label') + .call(tooltip() + .html(true) + .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) + .placement('top') + ); + + minimapLabelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { + d3_event.preventDefault(); + uiMapInMap.toggle(); + }); + + minimapLabelEnter + .append('span') + .text(t('background.minimap.description')); } diff --git a/modules/ui/background_display_options.js b/modules/ui/background_display_options.js index ecfa8fddac..9dfbfc80cc 100644 --- a/modules/ui/background_display_options.js +++ b/modules/ui/background_display_options.js @@ -7,18 +7,15 @@ import { import { t, textDirection } from '../util/locale'; import { svgIcon } from '../svg'; import { uiDisclosure } from './disclosure'; -import { uiMapInMap } from './map_in_map'; -import { uiTooltipHtml } from './tooltipHtml'; -import { tooltip } from '../util/tooltip'; export function uiBackgroundDisplayOptions(context) { var _selection = d3_select(null); var sliders = ['brightness', 'contrast', 'saturation', 'sharpness']; + var storedOpacity = context.storage('background-opacity'); var _options = { - brightness: (context.storage('background-opacity') !== null) ? - (+context.storage('background-opacity')) : 1.0, + brightness: (storedOpacity !== null ? (+storedOpacity) : 1), contrast: 1, saturation: 1, sharpness: 1 @@ -93,33 +90,6 @@ export function uiBackgroundDisplayOptions(context) { .call(svgIcon('#icon-' + (textDirection === 'rtl' ? 'redo' : 'undo'))); - // add minimap toggle - var minimapEnter = containerEnter - .append('div') - .attr('class', 'minimap-toggle-wrap'); - - var minimapLabelEnter = minimapEnter - .append('label') - .call(tooltip() - .html(true) - .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) - .placement('top') - ); - - minimapLabelEnter - .classed('minimap-toggle', true) - .append('input') - .attr('type', 'checkbox') - .on('change', function() { - uiMapInMap.toggle(); - d3_event.preventDefault(); - }); - - minimapLabelEnter - .append('span') - .text(t('background.minimap.description')); - - // update container = containerEnter .merge(container); @@ -132,6 +102,11 @@ export function uiBackgroundDisplayOptions(context) { container.selectAll('.display-option-reset') .classed('disabled', function(d) { return _options[d] === 1; }); + + // first time only, set brightness if needed + if (containerEnter.size() && _options.brightness !== 1) { + context.background().brightness(_options.brightness); + } } @@ -145,8 +120,6 @@ export function uiBackgroundDisplayOptions(context) { ); } - // setDisplayOption('brightness', _options.brightness); - return backgroundDisplayOptions; } diff --git a/modules/ui/map_in_map.js b/modules/ui/map_in_map.js index 4a6609c982..e7b70ec5ed 100644 --- a/modules/ui/map_in_map.js +++ b/modules/ui/map_in_map.js @@ -285,9 +285,10 @@ export function uiMapInMap(context) { isHidden = !isHidden; - var label = d3_select('.minimap-toggle'); - label.classed('active', !isHidden) - .select('input').property('checked', !isHidden); + d3_select('.minimap-toggle-item') + .classed('active', !isHidden) + .select('input') + .property('checked', !isHidden); if (isHidden) { wrap From 04fa29cfe6b8c745e64964dee86aa6111a2e3746 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 3 Jan 2018 15:23:47 -0500 Subject: [PATCH 106/206] Move link to imagery faq, reword as "Imagery Info / Report a Problem" (closes #4546) --- css/80_app.css | 1 + data/core.yaml | 2 +- dist/locales/en.json | 2 +- modules/ui/background.js | 37 ++++++++++++++++++++----------------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index a7e430fe38..a64c2d8212 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2246,6 +2246,7 @@ div.full-screen > button:hover { .imagery-faq { margin-bottom: 10px; + white-space: nowrap; } .layer-list, .controls-list { diff --git a/data/core.yaml b/data/core.yaml index 7b593ce3d4..4ccb518fc6 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -380,7 +380,7 @@ en: custom_button: Edit custom background custom_prompt: "Enter a tile URL template. Valid tokens are:\n - {zoom}/{z}, {x}, {y} for Z/X/Y tile scheme\n - {ty} for flipped TMS-style Y coordinates\n - {u} for quadtile scheme\n - {switch:a,b,c} for DNS server multiplexing\n\nExample:\n{example}" overlays: Overlays - imagery_source_faq: Where does this imagery come from? + imagery_source_faq: Imagery Info / Report a Problem reset: reset display_options: Display Options brightness: Brightness diff --git a/dist/locales/en.json b/dist/locales/en.json index 306c8ac3d0..f944f71946 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -468,7 +468,7 @@ "custom_button": "Edit custom background", "custom_prompt": "Enter a tile URL template. Valid tokens are:\n - {zoom}/{z}, {x}, {y} for Z/X/Y tile scheme\n - {ty} for flipped TMS-style Y coordinates\n - {u} for quadtile scheme\n - {switch:a,b,c} for DNS server multiplexing\n\nExample:\n{example}", "overlays": "Overlays", - "imagery_source_faq": "Where does this imagery come from?", + "imagery_source_faq": "Imagery Info / Report a Problem", "reset": "reset", "display_options": "Display Options", "brightness": "Brightness", diff --git a/modules/ui/background.js b/modules/ui/background.js index cbd5bfa151..d41bd9d240 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -192,10 +192,11 @@ export function uiBackground(context) { function renderBackgroundList(selection) { - var container = selection.selectAll('layer-background-list') - .data([0]); // the background list + var container = selection.selectAll('.layer-background-list') + .data([0]); + _backgroundList = container.enter() .append('ul') .attr('class', 'layer-list layer-background-list') @@ -204,7 +205,7 @@ export function uiBackground(context) { // add minimap toggle below list - var minimapEnter = selection.selectAll('minimap-toggle-list') + var minimapEnter = selection.selectAll('.minimap-toggle-list') .data([0]) .enter() .append('ul') @@ -231,11 +232,26 @@ export function uiBackground(context) { minimapLabelEnter .append('span') .text(t('background.minimap.description')); + + + // "Info / Report a Problem" link + selection.selectAll('.imagery-faq') + .data([0]) + .enter() + .append('div') + .attr('class', 'imagery-faq') + .append('a') + .attr('target', '_blank') + .attr('tabindex', -1) + .call(svgIcon('#icon-out-link', 'inline')) + .attr('href', 'https://github.com/openstreetmap/iD/blob/master/FAQ.md#how-can-i-report-an-issue-with-background-imagery') + .append('span') + .text(t('background.imagery_source_faq')); } function renderOverlayList(selection) { - var container = selection.selectAll('layer-overlay-list') + var container = selection.selectAll('.layer-overlay-list') .data([0]); _overlayList = container.enter() @@ -345,19 +361,6 @@ export function uiBackground(context) { .content(renderBackgroundList) ); - // "Where does this imagery come from?" - // pane - // .append('div') - // .attr('class', 'imagery-faq') - // .append('a') - // .attr('target', '_blank') - // .attr('tabindex', -1) - // .call(svgIcon('#icon-out-link', 'inline')) - // .attr('href', 'https://github.com/openstreetmap/iD/blob/master/FAQ.md#how-can-i-report-an-issue-with-background-imagery') - // .append('span') - // .text(t('background.imagery_source_faq')); - - // overlay list pane .append('div') From 9155641e5e2ae821bad4b61565b5335f8ee9f9ff Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 3 Jan 2018 15:28:08 -0500 Subject: [PATCH 107/206] Remove background opacity pngs so #4575 will merge cleanly --- dist/img/background-pattern-1.png | Bin 76 -> 0 bytes dist/img/background-pattern-opacity.png | Bin 79 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dist/img/background-pattern-1.png delete mode 100644 dist/img/background-pattern-opacity.png diff --git a/dist/img/background-pattern-1.png b/dist/img/background-pattern-1.png deleted file mode 100644 index e3cbe81790868d9e4a70bbe40d57ea5ec59ad95a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzv!k#XUAso@kJL>=cD>q1J2++`B Ys27kt=4qef36y5=boFyt=akR{07R$~5&!@I diff --git a/dist/img/background-pattern-opacity.png b/dist/img/background-pattern-opacity.png deleted file mode 100644 index 4ca1ff201e91976bb3c1a524921f1eccbb2d1bf2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79 zcmeAS@N?(olHy`uVBq!ia0vp^Y#_|R1SIp Date: Wed, 3 Jan 2018 16:52:29 -0500 Subject: [PATCH 108/206] Render points as vertices at zoom >= 18 (was > 18) (closes #4642) --- modules/svg/vertices.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index c6843a3af6..c242decf93 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -151,7 +151,7 @@ export function svgVertices(projection, context) { // Directional vertices get viewfields var dgroups = groups.filter(function(d) { return getDirections(d); }) .selectAll('.viewfieldgroup') - .data(function data(d) { return zoom < 18 ? [] : [d]; }, osmEntity.key); + .data(function data(d) { return zoom >= 18 ? [d] : []; }, osmEntity.key); // exit dgroups.exit() @@ -261,7 +261,7 @@ export function svgVertices(projection, context) { function renderAsVertex(entity, graph, wireframe, zoom) { var geometry = entity.geometry(graph); return geometry === 'vertex' || (geometry === 'point' && ( - wireframe || (zoom > 18 && entity.directions(graph, projection).length) + wireframe || (zoom >= 18 && entity.directions(graph, projection).length) )); } From 1111c6b0c1babd65e4efa91330b754709f2ecbf1 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 3 Jan 2018 23:37:35 -0500 Subject: [PATCH 109/206] Fallback to event.keyCode if event.key is outside ISO-Latin-1 (closes #4618) --- modules/lib/d3.keybinding.js | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/modules/lib/d3.keybinding.js b/modules/lib/d3.keybinding.js index 47c1b3eb16..8fb2f73729 100644 --- a/modules/lib/d3.keybinding.js +++ b/modules/lib/d3.keybinding.js @@ -51,22 +51,34 @@ export function d3keybinding(namespace) { function matches(binding, testShift) { var event = d3_event; + var isMatch = false; + var tryKeyCode = true; + + // Prefer a match on `KeyboardEvent.key` if (event.key !== undefined) { + tryKeyCode = (event.key.charCodeAt(0) > 255); // outside ISO-Latin-1 + isMatch = true; + if (binding.event.key === undefined) { - return false; + isMatch = false; } else if (Array.isArray(binding.event.key)) { if (binding.event.key.map(function(s) { return s.toLowerCase(); }).indexOf(event.key.toLowerCase()) === -1) - return false; + isMatch = false; } else { if (event.key.toLowerCase() !== binding.event.key.toLowerCase()) - return false; + isMatch = false; } - } else { - // check keycodes if browser doesn't support KeyboardEvent.key - if (event.keyCode !== binding.event.keyCode) - return false; } + // Fallback match on `KeyboardEvent.keyCode`, can happen if: + // - browser doesn't support `KeyboardEvent.key` + // - `KeyboardEvent.key` is outside ISO-Latin-1 range (cyrillic?) + if (!isMatch && tryKeyCode) { + isMatch = (event.keyCode === binding.event.keyCode); + } + + if (!isMatch) return false; + // test modifier keys if (!(event.ctrlKey && event.altKey)) { // if both are set, assume AltGr and skip it - #4096 if (event.ctrlKey !== binding.event.modifiers.ctrlKey) return false; @@ -117,8 +129,8 @@ export function d3keybinding(namespace) { var code = arr[i]; var binding = { event: { - key: undefined, - keyCode: 0, // only for browsers that don't support KeyboardEvent.key + key: undefined, // preferred + keyCode: 0, // fallback modifiers: { shiftKey: false, ctrlKey: false, From 77ec49d959f57c267109fc339d826c448d20652e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 4 Jan 2018 10:53:09 -0500 Subject: [PATCH 110/206] Add some more building presets (closes #4505) --- data/presets.yaml | 33 +++++ data/presets/presets.json | 135 ++++++++++++++++++ data/presets/presets/building/bungalow.json | 20 +++ data/presets/presets/building/civic.json | 17 +++ data/presets/presets/building/farm.json | 16 +++ data/presets/presets/building/ruins.json | 16 +++ data/presets/presets/building/service.json | 16 +++ data/presets/presets/building/stadium.json | 17 +++ data/presets/presets/building/temple.json | 16 +++ .../presets/building/transportation.json | 17 +++ data/taginfo.json | 32 +++++ dist/locales/en.json | 32 +++++ 12 files changed, 367 insertions(+) create mode 100644 data/presets/presets/building/bungalow.json create mode 100644 data/presets/presets/building/civic.json create mode 100644 data/presets/presets/building/farm.json create mode 100644 data/presets/presets/building/ruins.json create mode 100644 data/presets/presets/building/service.json create mode 100644 data/presets/presets/building/stadium.json create mode 100644 data/presets/presets/building/temple.json create mode 100644 data/presets/presets/building/transportation.json diff --git a/data/presets.yaml b/data/presets.yaml index acffd3d613..28997a9996 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -2483,6 +2483,11 @@ en: # building=barn name: Barn terms: '' + building/bungalow: + # building=bungalow + name: Bungalow + # 'terms: home,detached' + terms: '' building/bunker: # building=bunker name: Bunker @@ -2502,6 +2507,10 @@ en: # building=church name: Church Building terms: '' + building/civic: + # building=civic + name: Civic Building + terms: '' building/college: # building=college name: College Building @@ -2527,6 +2536,10 @@ en: building/entrance: # building=entrance name: Entrance/Exit + building/farm: + # building=farm + name: Farm Building + terms: '' building/garage: # building=garage name: Garage @@ -2585,6 +2598,10 @@ en: # building=roof name: Roof terms: '' + building/ruins: + # building=ruins + name: Building Ruins + terms: '' building/school: # building=school name: School Building @@ -2595,6 +2612,10 @@ en: name: Semi-Detached House # 'terms: home,double,duplex,twin,family,residence,dwelling' terms: '' + building/service: + # building=service + name: Service Building + terms: '' building/shed: # building=shed name: Shed @@ -2603,10 +2624,18 @@ en: # building=stable name: Stable terms: '' + building/stadium: + # building=stadium + name: Stadium Building + terms: '' building/static_caravan: # building=static_caravan name: Static Mobile Home terms: '' + building/temple: + # building=temple + name: Temple Building + terms: '' building/terrace: # building=terrace name: Row Houses @@ -2615,6 +2644,10 @@ en: building/train_station: # building=train_station name: Train Station + building/transportation: + # building=transportation + name: Transportation Building + terms: '' building/university: # building=university name: University Building diff --git a/data/presets/presets.json b/data/presets/presets.json index e8b4454859..415fc8d7c0 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -4247,6 +4247,26 @@ "matchScore": 0.5, "name": "Barn" }, + "building/bungalow": { + "icon": "home", + "fields": [ + "name", + "address", + "levels" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "bungalow" + }, + "terms": [ + "home", + "detached" + ], + "matchScore": 0.5, + "name": "Bungalow" + }, "building/cabin": { "icon": "home", "fields": [ @@ -4311,6 +4331,23 @@ "matchScore": 0.5, "name": "Church Building" }, + "building/civic": { + "icon": "building", + "fields": [ + "name", + "address", + "levels", + "smoking" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "civic" + }, + "matchScore": 0.5, + "name": "Civic Building" + }, "building/college": { "icon": "building", "fields": [ @@ -4403,6 +4440,22 @@ "matchScore": 0.5, "name": "Dormitory" }, + "building/farm": { + "icon": "farm", + "fields": [ + "name", + "address", + "levels" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "farm" + }, + "matchScore": 0.5, + "name": "Farm Building" + }, "building/garage": { "icon": "warehouse", "fields": [ @@ -4634,6 +4687,22 @@ "matchScore": 0.5, "name": "Roof" }, + "building/ruins": { + "icon": "poi-ruins", + "fields": [ + "name", + "address", + "levels" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "ruins" + }, + "matchScore": 0.5, + "name": "Building Ruins" + }, "building/school": { "icon": "building", "fields": [ @@ -4681,6 +4750,22 @@ "matchScore": 0.5, "name": "Semi-Detached House" }, + "building/service": { + "icon": "home", + "fields": [ + "name", + "address", + "levels" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "service" + }, + "matchScore": 0.5, + "name": "Service Building" + }, "building/shed": { "icon": "home", "fields": [ @@ -4713,6 +4798,23 @@ "matchScore": 0.5, "name": "Stable" }, + "building/stadium": { + "icon": "stadium", + "fields": [ + "name", + "address", + "levels", + "smoking" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "stadium" + }, + "matchScore": 0.5, + "name": "Stadium Building" + }, "building/static_caravan": { "icon": "home", "fields": [ @@ -4729,6 +4831,22 @@ "matchScore": 0.5, "name": "Static Mobile Home" }, + "building/temple": { + "icon": "place-of-worship", + "fields": [ + "name", + "address", + "levels" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "temple" + }, + "matchScore": 0.5, + "name": "Temple Building" + }, "building/terrace": { "icon": "building", "fields": [ @@ -4753,6 +4871,23 @@ "matchScore": 0.5, "name": "Row Houses" }, + "building/transportation": { + "icon": "building", + "fields": [ + "name", + "address", + "levels", + "smoking" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "transportation" + }, + "matchScore": 0.5, + "name": "Transportation Building" + }, "building/university": { "icon": "building", "fields": [ diff --git a/data/presets/presets/building/bungalow.json b/data/presets/presets/building/bungalow.json new file mode 100644 index 0000000000..81e586d8d8 --- /dev/null +++ b/data/presets/presets/building/bungalow.json @@ -0,0 +1,20 @@ +{ + "icon": "home", + "fields": [ + "name", + "address", + "levels" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "bungalow" + }, + "terms": [ + "home", + "detached" + ], + "matchScore": 0.5, + "name": "Bungalow" +} diff --git a/data/presets/presets/building/civic.json b/data/presets/presets/building/civic.json new file mode 100644 index 0000000000..f5e4dc1a1e --- /dev/null +++ b/data/presets/presets/building/civic.json @@ -0,0 +1,17 @@ +{ + "icon": "building", + "fields": [ + "name", + "address", + "levels", + "smoking" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "civic" + }, + "matchScore": 0.5, + "name": "Civic Building" +} diff --git a/data/presets/presets/building/farm.json b/data/presets/presets/building/farm.json new file mode 100644 index 0000000000..ac93662863 --- /dev/null +++ b/data/presets/presets/building/farm.json @@ -0,0 +1,16 @@ +{ + "icon": "farm", + "fields": [ + "name", + "address", + "levels" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "farm" + }, + "matchScore": 0.5, + "name": "Farm Building" +} diff --git a/data/presets/presets/building/ruins.json b/data/presets/presets/building/ruins.json new file mode 100644 index 0000000000..ab88c6944d --- /dev/null +++ b/data/presets/presets/building/ruins.json @@ -0,0 +1,16 @@ +{ + "icon": "poi-ruins", + "fields": [ + "name", + "address", + "levels" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "ruins" + }, + "matchScore": 0.5, + "name": "Building Ruins" +} diff --git a/data/presets/presets/building/service.json b/data/presets/presets/building/service.json new file mode 100644 index 0000000000..fc69060ae7 --- /dev/null +++ b/data/presets/presets/building/service.json @@ -0,0 +1,16 @@ +{ + "icon": "home", + "fields": [ + "name", + "address", + "levels" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "service" + }, + "matchScore": 0.5, + "name": "Service Building" +} diff --git a/data/presets/presets/building/stadium.json b/data/presets/presets/building/stadium.json new file mode 100644 index 0000000000..b6a9fda2f4 --- /dev/null +++ b/data/presets/presets/building/stadium.json @@ -0,0 +1,17 @@ +{ + "icon": "stadium", + "fields": [ + "name", + "address", + "levels", + "smoking" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "stadium" + }, + "matchScore": 0.5, + "name": "Stadium Building" +} diff --git a/data/presets/presets/building/temple.json b/data/presets/presets/building/temple.json new file mode 100644 index 0000000000..534a59d478 --- /dev/null +++ b/data/presets/presets/building/temple.json @@ -0,0 +1,16 @@ +{ + "icon": "place-of-worship", + "fields": [ + "name", + "address", + "levels" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "temple" + }, + "matchScore": 0.5, + "name": "Temple Building" +} diff --git a/data/presets/presets/building/transportation.json b/data/presets/presets/building/transportation.json new file mode 100644 index 0000000000..6465dfd60f --- /dev/null +++ b/data/presets/presets/building/transportation.json @@ -0,0 +1,17 @@ +{ + "icon": "building", + "fields": [ + "name", + "address", + "levels", + "smoking" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "transportation" + }, + "matchScore": 0.5, + "name": "Transportation Building" +} diff --git a/data/taginfo.json b/data/taginfo.json index 1ea45a2447..d03317a08c 100644 --- a/data/taginfo.json +++ b/data/taginfo.json @@ -788,6 +788,10 @@ "key": "building", "value": "barn" }, + { + "key": "building", + "value": "bungalow" + }, { "key": "building", "value": "cabin" @@ -804,6 +808,10 @@ "key": "building", "value": "church" }, + { + "key": "building", + "value": "civic" + }, { "key": "building", "value": "college" @@ -824,6 +832,10 @@ "key": "building", "value": "dormitory" }, + { + "key": "building", + "value": "farm" + }, { "key": "building", "value": "garage" @@ -880,6 +892,10 @@ "key": "building", "value": "roof" }, + { + "key": "building", + "value": "ruins" + }, { "key": "building", "value": "school" @@ -888,6 +904,10 @@ "key": "building", "value": "semidetached_house" }, + { + "key": "building", + "value": "service" + }, { "key": "building", "value": "shed" @@ -896,14 +916,26 @@ "key": "building", "value": "stable" }, + { + "key": "building", + "value": "stadium" + }, { "key": "building", "value": "static_caravan" }, + { + "key": "building", + "value": "temple" + }, { "key": "building", "value": "terrace" }, + { + "key": "building", + "value": "transportation" + }, { "key": "building", "value": "university" diff --git a/dist/locales/en.json b/dist/locales/en.json index e009184a17..7ed619a623 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -3420,6 +3420,10 @@ "name": "Barn", "terms": "" }, + "building/bungalow": { + "name": "Bungalow", + "terms": "home,detached" + }, "building/cabin": { "name": "Cabin", "terms": "" @@ -3436,6 +3440,10 @@ "name": "Church Building", "terms": "" }, + "building/civic": { + "name": "Civic Building", + "terms": "" + }, "building/college": { "name": "College Building", "terms": "university" @@ -3456,6 +3464,10 @@ "name": "Dormitory", "terms": "" }, + "building/farm": { + "name": "Farm Building", + "terms": "" + }, "building/garage": { "name": "Garage", "terms": "" @@ -3512,6 +3524,10 @@ "name": "Roof", "terms": "" }, + "building/ruins": { + "name": "Building Ruins", + "terms": "" + }, "building/school": { "name": "School Building", "terms": "academy,elementary school,middle school,high school" @@ -3520,6 +3536,10 @@ "name": "Semi-Detached House", "terms": "home,double,duplex,twin,family,residence,dwelling" }, + "building/service": { + "name": "Service Building", + "terms": "" + }, "building/shed": { "name": "Shed", "terms": "" @@ -3528,14 +3548,26 @@ "name": "Stable", "terms": "" }, + "building/stadium": { + "name": "Stadium Building", + "terms": "" + }, "building/static_caravan": { "name": "Static Mobile Home", "terms": "" }, + "building/temple": { + "name": "Temple Building", + "terms": "" + }, "building/terrace": { "name": "Row Houses", "terms": "home,terrace,brownstone,family,residence,dwelling" }, + "building/transportation": { + "name": "Transportation Building", + "terms": "" + }, "building/university": { "name": "University Building", "terms": "college" From c908807f6449c699a9d2594ab33b8a5c108536da Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 4 Jan 2018 13:40:13 -0500 Subject: [PATCH 111/206] Render `oneway=alternating`, `oneway=reversible` with dual arrows (closes #4291) --- modules/geo/vector.js | 6 +++--- modules/osm/way.js | 14 +++++++++++-- modules/svg/defs.js | 2 +- modules/svg/helpers.js | 46 ++++++++++++++++++++++++++++++------------ test/spec/osm/way.js | 8 ++++++++ 5 files changed, 57 insertions(+), 19 deletions(-) diff --git a/modules/geo/vector.js b/modules/geo/vector.js index 0e7929b3ba..708bb5f66f 100644 --- a/modules/geo/vector.js +++ b/modules/geo/vector.js @@ -13,9 +13,9 @@ export function geoVecSubtract(a, b) { return [ a[0] - b[0], a[1] - b[1] ]; } -// vector multiplication -export function geoVecScale(a, b) { - return [ a[0] * b, a[1] * b ]; +// vector scaling +export function geoVecScale(a, mag) { + return [ a[0] * mag, a[1] * mag ]; } // vector rounding (was: geoRoundCoordinates) diff --git a/modules/osm/way.js b/modules/osm/way.js index 14e4e410ce..28dbccf95b 100644 --- a/modules/osm/way.js +++ b/modules/osm/way.js @@ -108,8 +108,18 @@ _extend(osmWay.prototype, { isOneWay: function() { // explicit oneway tag.. - if (['yes', '1', '-1'].indexOf(this.tags.oneway) !== -1) { return true; } - if (['no', '0'].indexOf(this.tags.oneway) !== -1) { return false; } + var values = { + 'yes': true, + '1': true, + '-1': true, + 'reversible': true, + 'alternating': true, + 'no': false, + '0': false + }; + if (values[this.tags.oneway] !== undefined) { + return values[this.tags.oneway]; + } // implied oneway tag.. for (var key in this.tags) { diff --git a/modules/svg/defs.js b/modules/svg/defs.js index 65f37a7ba3..ff6e2209ec 100644 --- a/modules/svg/defs.js +++ b/modules/svg/defs.js @@ -31,7 +31,7 @@ export function svgDefs(context) { .append('marker') .attr('id', 'oneway-marker') .attr('viewBox', '0 0 10 5') - .attr('refX', 5) + .attr('refX', 2.5) .attr('refY', 2.5) .attr('markerWidth', 2) .attr('markerHeight', 2) diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index 3ca51ea8dc..4cee3047bf 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -6,7 +6,11 @@ import { geoStream as d3_geoStream } from 'd3-geo'; -import { geoVecLength } from '../geo'; +import { + geoVecAdd, + geoVecAngle, + geoVecLength +} from '../geo'; // Touch targets control which other vertices we can drag a vertex onto. @@ -72,6 +76,8 @@ export function svgOneWaySegments(projection, graph, dt) { coordinates.reverse(); } + var isReversible = (entity.tags.oneway === 'reversible' || entity.tags.oneway === 'alternating'); + d3_geoStream({ type: 'LineString', coordinates: coordinates @@ -85,27 +91,41 @@ export function svgOneWaySegments(projection, graph, dt) { var span = geoVecLength(a, b) - offset; if (span >= 0) { - var angle = Math.atan2(b[1] - a[1], b[0] - a[0]); - var dx = dt * Math.cos(angle); - var dy = dt * Math.sin(angle); + var heading = geoVecAngle(a, b); + var dx = dt * Math.cos(heading); + var dy = dt * Math.sin(heading); var p = [ - a[0] + offset * Math.cos(angle), - a[1] + offset * Math.sin(angle) + a[0] + offset * Math.cos(heading), + a[1] + offset * Math.sin(heading) ]; - var segment = 'M' + a[0] + ',' + a[1] + 'L' + p[0] + ',' + p[1]; + // gather coordinates + var coord = [a, p]; for (span -= dt; span >= 0; span -= dt) { - p[0] += dx; - p[1] += dy; - segment += 'L' + p[0] + ',' + p[1]; + p = geoVecAdd(p, [dx, dy]); + coord.push(p); } + coord.push(b); + + // generate svg paths + var segment = ''; + var j; - segment += 'L' + b[0] + ',' + b[1]; - segments.push({id: entity.id, index: i, d: segment}); + for (j = 0; j < coord.length; j++) { + segment += (j === 0 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1]; + } + segments.push({ id: entity.id, index: i++, d: segment }); + + if (isReversible) { + segment = ''; + for (j = coord.length - 1; j >= 0; j--) { + segment += (j === coord.length - 1 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1]; + } + segments.push({ id: entity.id, index: i++, d: segment }); + } } offset = -span; - i++; } a = b; diff --git a/test/spec/osm/way.js b/test/spec/osm/way.js index 2460f5fc8b..2680e0deac 100644 --- a/test/spec/osm/way.js +++ b/test/spec/osm/way.js @@ -293,6 +293,14 @@ describe('iD.osmWay', function() { expect(iD.Way({tags: { oneway: '-1' }}).isOneWay(), 'oneway -1').to.be.true; }); + it('returns true when the way has tag oneway=reversible', function() { + expect(iD.Way({tags: { oneway: 'reversible' }}).isOneWay(), 'oneway reversible').to.be.true; + }); + + it('returns true when the way has tag oneway=alternating', function() { + expect(iD.Way({tags: { oneway: 'alternating' }}).isOneWay(), 'oneway alternating').to.be.true; + }); + it('returns true when the way has implied oneway tag (waterway=river, waterway=stream, etc)', function() { expect(iD.Way({tags: { waterway: 'river' }}).isOneWay(), 'river').to.be.true; expect(iD.Way({tags: { waterway: 'stream' }}).isOneWay(), 'stream').to.be.true; From b2eb982044c682656e101889d4b009eada8c0fbe Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 4 Jan 2018 13:41:35 -0500 Subject: [PATCH 112/206] Add support for oneway alternating/reversible to oneway check fields Also allow checkbox field to display non-standard values (i.e. not 'yes' or 'no) in the field label --- data/presets.yaml | 8 +++ data/presets/fields.json | 8 ++- data/presets/fields/oneway.json | 4 +- data/presets/fields/oneway_yes.json | 4 +- dist/locales/en.json | 8 ++- modules/ui/fields/check.js | 75 +++++++++++++++++------------ 6 files changed, 70 insertions(+), 37 deletions(-) diff --git a/data/presets.yaml b/data/presets.yaml index 28997a9996..7577852d3e 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -953,8 +953,12 @@ en: # oneway=* label: One Way options: + # oneway=alternating + alternating: Alternating # oneway=no 'no': 'No' + # oneway=reversible + reversible: Reversible # oneway=undefined undefined: Assumed to be No # oneway=yes @@ -963,8 +967,12 @@ en: # oneway=* label: One Way options: + # oneway=alternating + alternating: Alternating # oneway=no 'no': 'No' + # oneway=reversible + reversible: Reversible # oneway=undefined undefined: Assumed to be Yes # oneway=yes diff --git a/data/presets/fields.json b/data/presets/fields.json index af813c504e..90b49d4ccc 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -1295,7 +1295,9 @@ "options": { "undefined": "Assumed to be Yes", "yes": "Yes", - "no": "No" + "no": "No", + "reversible": "Reversible", + "alternating": "Alternating" } } }, @@ -1307,7 +1309,9 @@ "options": { "undefined": "Assumed to be No", "yes": "Yes", - "no": "No" + "no": "No", + "reversible": "Reversible", + "alternating": "Alternating" } } }, diff --git a/data/presets/fields/oneway.json b/data/presets/fields/oneway.json index 851dfbfcf6..b2c30c9fd2 100644 --- a/data/presets/fields/oneway.json +++ b/data/presets/fields/oneway.json @@ -6,7 +6,9 @@ "options": { "undefined": "Assumed to be No", "yes": "Yes", - "no": "No" + "no": "No", + "reversible": "Reversible", + "alternating": "Alternating" } } } diff --git a/data/presets/fields/oneway_yes.json b/data/presets/fields/oneway_yes.json index cc978924e7..1914f203ab 100644 --- a/data/presets/fields/oneway_yes.json +++ b/data/presets/fields/oneway_yes.json @@ -6,7 +6,9 @@ "options": { "undefined": "Assumed to be Yes", "yes": "Yes", - "no": "No" + "no": "No", + "reversible": "Reversible", + "alternating": "Alternating" } } } diff --git a/dist/locales/en.json b/dist/locales/en.json index 7ed619a623..1e13481be0 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -2068,7 +2068,9 @@ "options": { "undefined": "Assumed to be Yes", "yes": "Yes", - "no": "No" + "no": "No", + "reversible": "Reversible", + "alternating": "Alternating" } }, "oneway": { @@ -2076,7 +2078,9 @@ "options": { "undefined": "Assumed to be No", "yes": "Yes", - "no": "No" + "no": "No", + "reversible": "Reversible", + "alternating": "Alternating" } }, "opening_hours": { diff --git a/modules/ui/fields/check.js b/modules/ui/fields/check.js index 8656748ee3..eb60d66465 100644 --- a/modules/ui/fields/check.js +++ b/modules/ui/fields/check.js @@ -16,17 +16,19 @@ export { uiFieldCheck as uiFieldOnewayCheck }; export function uiFieldCheck(field, context) { - var dispatch = d3_dispatch('change'), - options = field.strings && field.strings.options, - values = [], - texts = [], - input = d3_select(null), - text = d3_select(null), - label = d3_select(null), - reverser = d3_select(null), - impliedYes, - entityId, - value; + var dispatch = d3_dispatch('change'); + var options = field.strings && field.strings.options; + var values = []; + var texts = []; + + var input = d3_select(null); + var text = d3_select(null); + var label = d3_select(null); + var reverser = d3_select(null); + + var _impliedYes; + var _entityID; + var _value; if (options) { @@ -46,15 +48,15 @@ export function uiFieldCheck(field, context) { // Checks tags to see whether an undefined value is "Assumed to be Yes" function checkImpliedYes() { - impliedYes = (field.id === 'oneway_yes'); + _impliedYes = (field.id === 'oneway_yes'); // hack: pretend `oneway` field is a `oneway_yes` field // where implied oneway tag exists (e.g. `junction=roundabout`) #2220, #1841 if (field.id === 'oneway') { - var entity = context.entity(entityId); + var entity = context.entity(_entityID); for (var key in entity.tags) { if (key in osmOneWayTags && (entity.tags[key] in osmOneWayTags[key])) { - impliedYes = true; + _impliedYes = true; texts[0] = t('presets.fields.oneway_yes.options.undefined'); break; } @@ -65,18 +67,18 @@ export function uiFieldCheck(field, context) { function reverserHidden() { if (!d3_select('div.inspector-hover').empty()) return true; - return !(value === 'yes' || (impliedYes && !value)); + return !(_value === 'yes' || (_impliedYes && !_value)); } function reverserSetText(selection) { - var entity = context.hasEntity(entityId); + var entity = context.hasEntity(_entityID); if (reverserHidden() || !entity) return selection; - var first = entity.first(), - last = entity.isClosed() ? entity.nodes[entity.nodes.length - 2] : entity.last(), - pseudoDirection = first < last, - icon = pseudoDirection ? '#icon-forward' : '#icon-backward'; + var first = entity.first(); + var last = entity.isClosed() ? entity.nodes[entity.nodes.length - 2] : entity.last(); + var pseudoDirection = first < last; + var icon = pseudoDirection ? '#icon-forward' : '#icon-backward'; selection.selectAll('.reverser-span') .text(t('inspector.check.reverser')) @@ -125,7 +127,7 @@ export function uiFieldCheck(field, context) { input .on('click', function() { var t = {}; - t[field.key] = values[(values.indexOf(value) + 1) % values.length]; + t[field.key] = values[(values.indexOf(_value) + 1) % values.length]; dispatch.call('change', this, t); d3_event.stopPropagation(); }); @@ -139,7 +141,7 @@ export function uiFieldCheck(field, context) { d3_event.preventDefault(); d3_event.stopPropagation(); context.perform( - actionReverse(entityId), + actionReverse(_entityID), t('operations.reverse.annotation') ); d3_select(this) @@ -150,29 +152,40 @@ export function uiFieldCheck(field, context) { check.entity = function(_) { - if (!arguments.length) return context.hasEntity(entityId); - entityId = _.id; + if (!arguments.length) return context.hasEntity(_entityID); + _entityID = _.id; return check; }; check.tags = function(tags) { + + function isChecked(val) { + return val !== 'no' && val !== '' && val !== undefined && val !== null; + } + + function textFor(val) { + if (val === '') val = undefined; + var index = values.indexOf(val); + return (index !== -1 ? texts[index] : ('"' + val + '"')); + } + checkImpliedYes(); - value = tags[field.key] && tags[field.key].toLowerCase(); + _value = tags[field.key] && tags[field.key].toLowerCase(); - if (field.type === 'onewayCheck' && (value === '1' || value === '-1')) { - value = 'yes'; + if (field.type === 'onewayCheck' && (_value === '1' || _value === '-1')) { + _value = 'yes'; } input - .property('indeterminate', field.type !== 'defaultCheck' && !value) - .property('checked', value === 'yes'); + .property('indeterminate', field.type !== 'defaultCheck' && !_value) + .property('checked', isChecked(_value)); text - .text(texts[values.indexOf(value)]); + .text(textFor(_value)); label - .classed('set', !!value); + .classed('set', !!_value); if (field.type === 'onewayCheck') { reverser From 286c09366aadb29165c575300dab02a38fc6e503 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 4 Jan 2018 15:55:53 -0500 Subject: [PATCH 113/206] Remove unused var (eslint) --- modules/ui/intro/welcome.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ui/intro/welcome.js b/modules/ui/intro/welcome.js index 8187998e72..81e1f9b09b 100644 --- a/modules/ui/intro/welcome.js +++ b/modules/ui/intro/welcome.js @@ -137,7 +137,7 @@ export function uiIntroWelcome(context, reveal) { ); } - + chapter.enter = function() { welcome(); }; @@ -145,7 +145,7 @@ export function uiIntroWelcome(context, reveal) { chapter.exit = function() { listener.off(); - var tooltip = d3_select('.curtain-tooltip.intro-mouse') + d3_select('.curtain-tooltip.intro-mouse') .selectAll('.counter') .remove(); }; From 954227be53e467ea316ea9662941fb3ac18b8c28 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 4 Jan 2018 16:12:40 -0500 Subject: [PATCH 114/206] Don't let modal button cover up header bottom border --- css/80_app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/80_app.css b/css/80_app.css index 2e14ac1d01..e104870257 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -713,7 +713,7 @@ button.save.has-count .count::before { position: absolute; right: 0; top: 0; - height: 60px; + height: 59px; z-index: 50; } [dir='rtl'] .modal > button { From 8914d1ce367876863b11abd5fb94281411f267cc Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 4 Jan 2018 16:13:13 -0500 Subject: [PATCH 115/206] Variable cleanups and formatting --- modules/modes/save.js | 53 +++++++++++++++++++++---------------------- modules/ui/commit.js | 52 +++++++++++++++++++++--------------------- modules/ui/loading.js | 25 ++++++++++---------- 3 files changed, 65 insertions(+), 65 deletions(-) diff --git a/modules/modes/save.js b/modules/modes/save.js index 4fb3908a06..212fca0d1c 100644 --- a/modules/modes/save.js +++ b/modules/modes/save.js @@ -67,18 +67,17 @@ export function modeSave(context) { function save(changeset, tryAgain) { - - var osm = context.connection(), - loading = uiLoading(context).message(t('save.uploading')).blocking(true), - history = context.history(), - origChanges = history.changes(actionDiscardTags(history.difference())), - localGraph = context.graph(), - remoteGraph = coreGraph(history.base(), true), - modified = _filter(history.difference().summary(), {changeType: 'modified'}), - toCheck = _map(_map(modified, 'entity'), 'id'), - toLoad = withChildNodes(toCheck, localGraph), - conflicts = [], - errors = []; + var osm = context.connection(); + var loading = uiLoading(context).message(t('save.uploading')).blocking(true); + var history = context.history(); + var origChanges = history.changes(actionDiscardTags(history.difference())); + var localGraph = context.graph(); + var remoteGraph = coreGraph(history.base(), true); + var modified = _filter(history.difference().summary(), {changeType: 'modified'}); + var toCheck = _map(_map(modified, 'entity'), 'id'); + var toLoad = withChildNodes(toCheck, localGraph); + var conflicts = []; + var errors = []; if (!osm) return; @@ -100,8 +99,8 @@ export function modeSave(context) { var entity = graph.entity(id); if (entity.type === 'way') { try { - var cn = graph.childNodes(entity); - result.push.apply(result, _map(_filter(cn, 'version'), 'id')); + var children = graph.childNodes(entity); + result.push.apply(result, _map(_filter(children, 'version'), 'id')); } catch (err) { /* eslint-disable no-console */ if (typeof console !== 'undefined') console.error(err); @@ -172,8 +171,8 @@ export function modeSave(context) { var children = _union(local.nodes, remote.nodes); for (var i = 0; i < children.length; i++) { - var a = localGraph.hasEntity(children[i]), - b = remoteGraph.hasEntity(children[i]); + var a = localGraph.hasEntity(children[i]); + var b = remoteGraph.hasEntity(children[i]); if (a && b && a.version !== b.version) return false; } @@ -183,23 +182,23 @@ export function modeSave(context) { } _each(toCheck, function(id) { - var local = localGraph.entity(id), - remote = remoteGraph.entity(id); + var local = localGraph.entity(id); + var remote = remoteGraph.entity(id); if (compareVersions(local, remote)) return; - var action = actionMergeRemoteChanges, - merge = action(id, localGraph, remoteGraph, formatUser); + var action = actionMergeRemoteChanges; + var merge = action(id, localGraph, remoteGraph, formatUser); history.replace(merge); var mergeConflicts = merge.conflicts(); if (!mergeConflicts.length) return; // merged safely - var forceLocal = action(id, localGraph, remoteGraph).withOption('force_local'), - forceRemote = action(id, localGraph, remoteGraph).withOption('force_remote'), - keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore')), - keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete')); + var forceLocal = action(id, localGraph, remoteGraph).withOption('force_local'); + var forceRemote = action(id, localGraph, remoteGraph).withOption('force_remote'); + var keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore')); + var keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete')); conflicts.push({ id: id, @@ -328,9 +327,9 @@ export function modeSave(context) { .classed('hide-toggle', true) .text(function(d) { return d.msg || t('save.unknown_error_details'); }) .on('click', function() { - var error = d3_select(this), - detail = d3_select(this.nextElementSibling), - exp = error.classed('expanded'); + var error = d3_select(this); + var detail = d3_select(this.nextElementSibling); + var exp = error.classed('expanded'); detail.style('display', exp ? 'none' : 'block'); error.classed('expanded', !exp); diff --git a/modules/ui/commit.js b/modules/ui/commit.js index e7d3837f50..d4af06590e 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -16,9 +16,9 @@ import { utilDetect } from '../util/detect'; import { utilRebind } from '../util'; -var changeset; +var _changeset; var readOnlyTags = [ - /^changesets_count$/, + /^_changesets_count$/, /^created_by$/, /^ideditor:/, /^imagery_used$/, @@ -32,9 +32,9 @@ var hashtagRegex = /(#[^\u2000-\u206F\u2E00-\u2E7F\s\\'!"#$%()*,.\/:;<=>?@\[\]^` export function uiCommit(context) { - var dispatch = d3_dispatch('cancel', 'save'), - userDetails, - _selection; + var dispatch = d3_dispatch('cancel', 'save'); + var _userDetails; + var _selection; var changesetEditor = uiChangesetEditor(context) .on('change', changeTags); @@ -51,16 +51,16 @@ export function uiCommit(context) { if (!osm) return; // expire stored comment and hashtags after cutoff datetime - #3947 - var commentDate = +context.storage('commentDate') || 0, - currDate = Date.now(), - cutoff = 2 * 86400 * 1000; // 2 days + var commentDate = +context.storage('commentDate') || 0; + var currDate = Date.now(); + var cutoff = 2 * 86400 * 1000; // 2 days if (commentDate > currDate || currDate - commentDate > cutoff) { context.storage('comment', null); context.storage('hashtags', null); } var tags; - if (!changeset) { + if (!_changeset) { var detected = utilDetect(); tags = { comment: context.storage('comment') || '', @@ -78,12 +78,12 @@ export function uiCommit(context) { tags.hashtags = hashtags; } - changeset = new osmChangeset({ tags: tags }); + _changeset = new osmChangeset({ tags: tags }); } - tags = _clone(changeset.tags); + tags = _clone(_changeset.tags); tags.imagery_used = context.history().imageryUsed().join(';').substr(0, 255); - changeset = changeset.update({ tags: tags }); + _changeset = _changeset.update({ tags: tags }); var header = selection.selectAll('.header') .data([0]); @@ -114,7 +114,7 @@ export function uiCommit(context) { changesetSection .call(changesetEditor - .changesetID(changeset.id) + .changesetID(_changeset.id) .tags(tags) ); @@ -146,7 +146,7 @@ export function uiCommit(context) { var userLink = d3_select(document.createElement('div')); - userDetails = user; + _userDetails = user; if (user.image_url) { userLink @@ -195,7 +195,7 @@ export function uiCommit(context) { .merge(requestReviewEnter); var requestReviewInput = requestReview.selectAll('input') - .property('checked', isReviewRequested(changeset.tags)) + .property('checked', isReviewRequested(_changeset.tags)) .on('change', toggleRequestReview); @@ -238,7 +238,7 @@ export function uiCommit(context) { return (n && n.value.length) ? null : true; }) .on('click.save', function() { - dispatch.call('save', this, changeset); + dispatch.call('save', this, _changeset); }); @@ -256,7 +256,7 @@ export function uiCommit(context) { .call(rawTagEditor .expanded(expanded) .readOnlyTags(readOnlyTags) - .tags(_clone(changeset.tags)) + .tags(_clone(_changeset.tags)) ); @@ -273,7 +273,7 @@ export function uiCommit(context) { .call(rawTagEditor .expanded(expanded) .readOnlyTags(readOnlyTags) - .tags(_clone(changeset.tags)) + .tags(_clone(_changeset.tags)) ); } } @@ -299,8 +299,8 @@ export function uiCommit(context) { function findHashtags(tags, commentOnly) { - var inComment = commentTags(), - inHashTags = hashTags(); + var inComment = commentTags(); + var inHashTags = hashTags(); if (inComment !== null) { // when hashtags are detected in comment... context.storage('hashtags', null); // always remove stored hashtags - #4304 @@ -340,7 +340,7 @@ export function uiCommit(context) { function updateChangeset(changed, onInput) { - var tags = _clone(changeset.tags); + var tags = _clone(_changeset.tags); _forEach(changed, function(v, k) { k = k.trim().substr(0, 255); @@ -371,8 +371,8 @@ export function uiCommit(context) { } // always update userdetails, just in case user reauthenticates as someone else - if (userDetails && userDetails.changesets_count !== undefined) { - var changesetsCount = parseInt(userDetails.changesets_count, 10) + 1; // #4283 + if (_userDetails && _userDetails.changesets_count !== undefined) { + var changesetsCount = parseInt(_userDetails.changesets_count, 10) + 1; // #4283 tags.changesets_count = String(changesetsCount); // first 100 edits - new user @@ -397,14 +397,14 @@ export function uiCommit(context) { delete tags.changesets_count; } - if (!_isEqual(changeset.tags, tags)) { - changeset = changeset.update({ tags: tags }); + if (!_isEqual(_changeset.tags, tags)) { + _changeset = _changeset.update({ tags: tags }); } } commit.reset = function() { - changeset = null; + _changeset = null; }; diff --git a/modules/ui/loading.js b/modules/ui/loading.js index 0b567fc71e..899c56ae6e 100644 --- a/modules/ui/loading.js +++ b/modules/ui/loading.js @@ -1,16 +1,17 @@ +import { select as d3_select } from 'd3-selection'; import { uiModal } from './modal'; export function uiLoading(context) { - var message = '', - blocking = false, - modalSelection; + var _modalSelection = d3_select(null); + var _message = ''; + var _blocking = false; var loading = function(selection) { - modalSelection = uiModal(selection, blocking); + _modalSelection = uiModal(selection, _blocking); - var loadertext = modalSelection.select('.content') + var loadertext = _modalSelection.select('.content') .classed('loading-modal', true) .append('div') .attr('class', 'modal-section fillL'); @@ -22,9 +23,9 @@ export function uiLoading(context) { loadertext .append('h3') - .text(message); + .text(_message); - modalSelection.select('button.close') + _modalSelection.select('button.close') .attr('class', 'hide'); return loading; @@ -32,21 +33,21 @@ export function uiLoading(context) { loading.message = function(_) { - if (!arguments.length) return message; - message = _; + if (!arguments.length) return _message; + _message = _; return loading; }; loading.blocking = function(_) { - if (!arguments.length) return blocking; - blocking = _; + if (!arguments.length) return _blocking; + _blocking = _; return loading; }; loading.close = function() { - modalSelection.remove(); + _modalSelection.remove(); }; From 437893ebb8b31e033e6544f0cc343725c4d6a0fd Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 4 Jan 2018 23:27:00 -0500 Subject: [PATCH 116/206] Don't reenter putChangeset, allow reuse of open changeset id We are going to start trying an opportunistic save, then only start the conflict resolution stuff if the server returns a 409. Reusing an already open changeset makes sense in this situation. --- modules/services/osm.js | 230 +++++++++++++++++++++------------------- 1 file changed, 123 insertions(+), 107 deletions(-) diff --git a/modules/services/osm.js b/modules/services/osm.js index af96685a33..d284d72f21 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -25,25 +25,26 @@ import { import { utilRebind, utilIdleWorker } from '../util'; -var dispatch = d3_dispatch('authLoading', 'authDone', 'change', 'loading', 'loaded'), - urlroot = 'https://www.openstreetmap.org', - blacklists = ['.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*'], - inflight = {}, - loadedTiles = {}, - entityCache = {}, - connectionId = 1, - tileZoom = 16, - oauth = osmAuth({ - url: urlroot, - oauth_consumer_key: '5A043yRSEugj4DJ5TljuapfnrflWDte8jTOcWLlT', - oauth_secret: 'aB3jKq1TRsCOUrfOIZ6oQMEDmv2ptV76PA54NGLL', - loading: authLoading, - done: authDone - }), - rateLimitError, - userChangesets, - userDetails, - off; +var dispatch = d3_dispatch('authLoading', 'authDone', 'change', 'loading', 'loaded'); +var urlroot = 'https://www.openstreetmap.org'; +var oauth = osmAuth({ + url: urlroot, + oauth_consumer_key: '5A043yRSEugj4DJ5TljuapfnrflWDte8jTOcWLlT', + oauth_secret: 'aB3jKq1TRsCOUrfOIZ6oQMEDmv2ptV76PA54NGLL', + loading: authLoading, + done: authDone +}); + +var _blacklists = ['.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*']; +var _tiles = { loaded: {}, inflight: {} }; +var _changeset = {}; +var _entityCache = {}; +var _connectionID = 1; +var _tileZoom = 16; +var _rateLimitError; +var _userChangesets; +var _userDetails; +var _off; function authLoading() { @@ -64,15 +65,15 @@ function abortRequest(i) { function getLoc(attrs) { - var lon = attrs.lon && attrs.lon.value, - lat = attrs.lat && attrs.lat.value; + var lon = attrs.lon && attrs.lon.value; + var lat = attrs.lat && attrs.lat.value; return [parseFloat(lon), parseFloat(lat)]; } function getNodes(obj) { - var elems = obj.getElementsByTagName('nd'), - nodes = new Array(elems.length); + var elems = obj.getElementsByTagName('nd'); + var nodes = new Array(elems.length); for (var i = 0, l = elems.length; i < l; i++) { nodes[i] = 'n' + elems[i].attributes.ref.value; } @@ -81,8 +82,8 @@ function getNodes(obj) { function getTags(obj) { - var elems = obj.getElementsByTagName('tag'), - tags = {}; + var elems = obj.getElementsByTagName('tag'); + var tags = {}; for (var i = 0, l = elems.length; i < l; i++) { var attrs = elems[i].attributes; tags[attrs.k.value] = attrs.v.value; @@ -93,8 +94,8 @@ function getTags(obj) { function getMembers(obj) { - var elems = obj.getElementsByTagName('member'), - members = new Array(elems.length); + var elems = obj.getElementsByTagName('member'); + var members = new Array(elems.length); for (var i = 0, l = elems.length; i < l; i++) { var attrs = elems[i].attributes; members[i] = { @@ -164,14 +165,14 @@ function parse(xml, callback, options) { options = _extend({ cache: true }, options); if (!xml || !xml.childNodes) return; - var root = xml.childNodes[0], - children = root.childNodes; + var root = xml.childNodes[0]; + var children = root.childNodes; function parseChild(child) { var parser = parsers[child.nodeName]; if (parser) { var uid = osmEntity.id.fromOSM(child.nodeName, child.attributes.id.value); - if (options.cache && entityCache[uid]) { + if (options.cache && _entityCache[uid]) { return null; } return parser(child, uid); @@ -190,20 +191,21 @@ export default { reset: function() { - connectionId++; - userChangesets = undefined; - userDetails = undefined; - rateLimitError = undefined; - _forEach(inflight, abortRequest); - entityCache = {}; - loadedTiles = {}; - inflight = {}; + _connectionID++; + _userChangesets = undefined; + _userDetails = undefined; + _rateLimitError = undefined; + _forEach(_tiles.inflight, abortRequest); + if (_changeset.inflight) abortRequest(_changeset.inflight); + _tiles = { loaded: {}, inflight: {} }; + _changeset = {}; + _entityCache = {}; return this; }, getConnectionId: function() { - return connectionId; + return _connectionID; }, @@ -239,7 +241,7 @@ export default { loadFromAPI: function(path, callback, options) { options = _extend({ cache: true }, options); var that = this; - var cid = connectionId; + var cid = _connectionID; function done(err, xml) { if (that.getConnectionId() !== cid) { @@ -260,9 +262,9 @@ export default { } else { // 509 Bandwidth Limit Exceeded, 429 Too Many Requests // Set the rateLimitError flag and trigger a warning.. - if (!isAuthenticated && !rateLimitError && err && + if (!isAuthenticated && !_rateLimitError && err && (err.status === 509 || err.status === 429)) { - rateLimitError = err; + _rateLimitError = err; dispatch.call('change'); } @@ -271,7 +273,7 @@ export default { parse(xml, function (entities) { if (options.cache) { for (var i in entities) { - entityCache[entities[i].id] = true; + _entityCache[entities[i].id] = true; } } callback(null, entities); @@ -290,9 +292,9 @@ export default { loadEntity: function(id, callback) { - var type = osmEntity.id.type(id), - osmID = osmEntity.id.toOSM(id), - options = { cache: false }; + var type = osmEntity.id.type(id); + var osmID = osmEntity.id.toOSM(id); + var options = { cache: false }; this.loadFromAPI( '/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : ''), @@ -305,9 +307,9 @@ export default { loadEntityVersion: function(id, version, callback) { - var type = osmEntity.id.type(id), - osmID = osmEntity.id.toOSM(id), - options = { cache: false }; + var type = osmEntity.id.type(id); + var osmID = osmEntity.id.toOSM(id); + var options = { cache: false }; this.loadFromAPI( '/api/0.6/' + type + '/' + osmID + '/' + version, @@ -323,9 +325,9 @@ export default { var that = this; _forEach(_groupBy(_uniq(ids), osmEntity.id.type), function(v, k) { - var type = k + 's', - osmIDs = _map(v, osmEntity.id.toOSM), - options = { cache: false }; + var type = k + 's'; + var osmIDs = _map(v, osmEntity.id.toOSM); + var options = { cache: false }; _forEach(_chunk(osmIDs, 150), function(arr) { that.loadFromAPI( @@ -346,19 +348,28 @@ export default { putChangeset: function(changeset, changes, callback) { + if (_changeset.inflight) { + return callback({ message: 'Changeset already inflight', status: -2 }); + } + var that = this; - var cid = connectionId; + var cid = _connectionID; + + if (_changeset.open) { // reuse existing open changeset.. + createdChangeset(null, _changeset.open); + } else { // open a new changeset.. + _changeset.inflight = oauth.xhr({ + method: 'PUT', + path: '/api/0.6/changeset/create', + options: { header: { 'Content-Type': 'text/xml' } }, + content: JXON.stringify(changeset.asJXON()) + }, createdChangeset); + } - // Create the changeset.. - oauth.xhr({ - method: 'PUT', - path: '/api/0.6/changeset/create', - options: { header: { 'Content-Type': 'text/xml' } }, - content: JXON.stringify(changeset.asJXON()) - }, createdChangeset); + function createdChangeset(err, changesetID) { + _changeset.inflight = null; - function createdChangeset(err, changeset_id) { if (err) { return callback(err); } @@ -366,12 +377,13 @@ export default { return callback({ message: 'Connection Switched', status: -1 }); } - changeset = changeset.update({ id: changeset_id }); + _changeset.open = changesetID; + changeset = changeset.update({ id: changesetID }); // Upload the changeset.. - oauth.xhr({ + _changeset.inflight = oauth.xhr({ method: 'POST', - path: '/api/0.6/changeset/' + changeset_id + '/upload', + path: '/api/0.6/changeset/' + changesetID + '/upload', options: { header: { 'Content-Type': 'text/xml' } }, content: JXON.stringify(changeset.osmChangeJXON(changes)) }, uploadedChangeset); @@ -379,6 +391,8 @@ export default { function uploadedChangeset(err) { + _changeset.inflight = null; + if (err) return callback(err); // Upload was successful, safe to call the callback. @@ -387,6 +401,8 @@ export default { callback(null, changeset); }, 2500); + _changeset.open = null; + // At this point, we don't really care if the connection was switched.. // Only try to close the changeset if we're still talking to the same server. if (that.getConnectionId() === cid) { @@ -402,13 +418,13 @@ export default { userDetails: function(callback) { - if (userDetails) { - callback(undefined, userDetails); + if (_userDetails) { + callback(undefined, _userDetails); return; } var that = this; - var cid = connectionId; + var cid = _connectionID; function done(err, user_details) { if (err) { @@ -418,29 +434,29 @@ export default { return callback({ message: 'Connection Switched', status: -1 }); } - var u = user_details.getElementsByTagName('user')[0], - img = u.getElementsByTagName('img'), - image_url = ''; + var u = user_details.getElementsByTagName('user')[0]; + var img = u.getElementsByTagName('img'); + var image_url = ''; if (img && img[0] && img[0].getAttribute('href')) { image_url = img[0].getAttribute('href'); } - var changesets = u.getElementsByTagName('changesets'), - changesets_count = 0; + var changesets = u.getElementsByTagName('changesets'); + var changesets_count = 0; if (changesets && changesets[0] && changesets[0].getAttribute('count')) { changesets_count = changesets[0].getAttribute('count'); } - userDetails = { + _userDetails = { id: u.attributes.id.value, display_name: u.attributes.display_name.value, image_url: image_url, changesets_count: changesets_count }; - callback(undefined, userDetails); + callback(undefined, _userDetails); } oauth.xhr({ method: 'GET', path: '/api/0.6/user/details' }, done); @@ -448,13 +464,13 @@ export default { userChangesets: function(callback) { - if (userChangesets) { - callback(undefined, userChangesets); + if (_userChangesets) { + callback(undefined, _userChangesets); return; } var that = this; - var cid = connectionId; + var cid = _connectionID; this.userDetails(function(err, user) { if (err) { @@ -472,7 +488,7 @@ export default { return callback({ message: 'Connection Switched', status: -1 }); } - userChangesets = Array.prototype.map.call( + _userChangesets = Array.prototype.map.call( changesets.getElementsByTagName('changeset'), function (changeset) { return { tags: getTags(changeset) }; @@ -482,7 +498,7 @@ export default { return comment && comment !== ''; }); - callback(undefined, userChangesets); + callback(undefined, _userChangesets); } oauth.xhr({ method: 'GET', path: '/api/0.6/changesets?user=' + user.id }, done); @@ -492,7 +508,7 @@ export default { status: function(callback) { var that = this; - var cid = connectionId; + var cid = _connectionID; function done(xml) { if (that.getConnectionId() !== cid) { @@ -509,12 +525,12 @@ export default { } } if (regexes.length) { - blacklists = regexes; + _blacklists = regexes; } - if (rateLimitError) { - callback(rateLimitError, 'rateLimited'); + if (_rateLimitError) { + callback(_rateLimitError, 'rateLimited'); } else { var apiStatus = xml.getElementsByTagName('status'); var val = apiStatus[0].getAttribute('api'); @@ -530,31 +546,31 @@ export default { imageryBlacklists: function() { - return blacklists; + return _blacklists; }, tileZoom: function(_) { - if (!arguments.length) return tileZoom; - tileZoom = _; + if (!arguments.length) return _tileZoom; + _tileZoom = _; return this; }, loadTiles: function(projection, dimensions, callback) { - if (off) return; + if (_off) return; var that = this; var s = projection.scale() * 2 * Math.PI; var z = Math.max(Math.log(s) / Math.log(2) - 8, 0); - var ts = 256 * Math.pow(2, z - tileZoom); + var ts = 256 * Math.pow(2, z - _tileZoom); var origin = [ s / 2 - projection.translate()[0], s / 2 - projection.translate()[1] ]; var tiles = d3_geoTile() - .scaleExtent([tileZoom, tileZoom]) + .scaleExtent([_tileZoom, _tileZoom]) .scale(s) .size(dimensions) .translate(projection.translate())() @@ -570,36 +586,36 @@ export default { }; }); - _filter(inflight, function(v, i) { + _filter(_tiles.inflight, function(v, i) { var wanted = _find(tiles, function(tile) { return i === tile.id; }); - if (!wanted) delete inflight[i]; + if (!wanted) delete _tiles.inflight[i]; return !wanted; }).map(abortRequest); tiles.forEach(function(tile) { var id = tile.id; - if (loadedTiles[id] || inflight[id]) return; + if (_tiles.loaded[id] || _tiles.inflight[id]) return; - if (_isEmpty(inflight)) { + if (_isEmpty(_tiles.inflight)) { dispatch.call('loading'); } - inflight[id] = that.loadFromAPI( + _tiles.inflight[id] = that.loadFromAPI( '/api/0.6/map?bbox=' + tile.extent.toParam(), function(err, parsed) { - delete inflight[id]; + delete _tiles.inflight[id]; if (!err) { - loadedTiles[id] = true; + _tiles.loaded[id] = true; } if (callback) { callback(err, _extend({ data: parsed }, tile)); } - if (_isEmpty(inflight)) { + if (_isEmpty(_tiles.inflight)) { dispatch.call('loaded'); } } @@ -625,21 +641,21 @@ export default { toggle: function(_) { - off = !_; + _off = !_; return this; }, loadedTiles: function(_) { - if (!arguments.length) return loadedTiles; - loadedTiles = _; + if (!arguments.length) return _tiles.loaded; + _tiles.loaded = _; return this; }, logout: function() { - userChangesets = undefined; - userDetails = undefined; + _userChangesets = undefined; + _userDetails = undefined; oauth.logout(); dispatch.call('change'); return this; @@ -648,9 +664,9 @@ export default { authenticate: function(callback) { var that = this; - var cid = connectionId; - userChangesets = undefined; - userDetails = undefined; + var cid = _connectionID; + _userChangesets = undefined; + _userDetails = undefined; function done(err, res) { if (err) { @@ -661,10 +677,10 @@ export default { if (callback) callback({ message: 'Connection Switched', status: -1 }); return; } - rateLimitError = undefined; + _rateLimitError = undefined; dispatch.call('change'); if (callback) callback(err, res); - that.userChangesets(function() {}); // eagerly load user details/changesets + that._userChangesets(function() {}); // eagerly load user details/changesets } return oauth.authenticate(done); From a63c4a72fe3b215b0c78c92e1ccecfd871e7e598 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 5 Jan 2018 14:40:59 -0500 Subject: [PATCH 117/206] Improvements to save flow - Attempt fast save first, only perform conflict resolution if necessary (re: #3056) - Block reentry of save, and dont keep focus on save button (closes #4641) - Refactor modeSave() for code clarity (avoid shared state in closure variables) --- modules/modes/save.js | 351 ++++++++++++++++++++++------------------ modules/services/osm.js | 10 +- modules/ui/commit.js | 1 + modules/ui/confirm.js | 4 +- modules/ui/conflicts.js | 51 +++--- 5 files changed, 230 insertions(+), 187 deletions(-) diff --git a/modules/modes/save.js b/modules/modes/save.js index 212fca0d1c..73479e72a9 100644 --- a/modules/modes/save.js +++ b/modules/modes/save.js @@ -1,7 +1,6 @@ import _clone from 'lodash-es/clone'; import _difference from 'lodash-es/difference'; import _filter from 'lodash-es/filter'; -import _each from 'lodash-es/each'; import _map from 'lodash-es/map'; import _reduce from 'lodash-es/reduce'; import _union from 'lodash-es/union'; @@ -44,18 +43,27 @@ import { } from '../util'; +var _isSaving = false; -export function modeSave(context) { - var mode = { - id: 'save' - }; +export function modeSave(context) { + var mode = { id: 'save' }; var keybinding = d3_keybinding('select'); + var loading = uiLoading(context) + .message(t('save.uploading')) + .blocking(true); + var commit = uiCommit(context) .on('cancel', cancel) .on('save', save); + var _toCheck = []; + var _toLoad = []; + var _conflicts = []; + var _errors = []; + var _origChanges; + function cancel(selectedID) { if (selectedID) { @@ -66,33 +74,50 @@ export function modeSave(context) { } - function save(changeset, tryAgain) { + function save(changeset, tryAgain, checkConflicts) { + // Guard against accidentally entering save code twice - #4641 + if (_isSaving && !tryAgain) return; + var osm = context.connection(); - var loading = uiLoading(context).message(t('save.uploading')).blocking(true); + if (!osm) return; + + _isSaving = true; + context.container().call(loading); // block input + var history = context.history(); - var origChanges = history.changes(actionDiscardTags(history.difference())); var localGraph = context.graph(); var remoteGraph = coreGraph(history.base(), true); - var modified = _filter(history.difference().summary(), {changeType: 'modified'}); - var toCheck = _map(_map(modified, 'entity'), 'id'); - var toLoad = withChildNodes(toCheck, localGraph); - var conflicts = []; - var errors = []; - if (!osm) return; + _conflicts = []; + _errors = []; + + // Store original changes, in case user wants to download them as an .osc file + _origChanges = history.changes(actionDiscardTags(history.difference())); + // First time, `history.perform` a no-op action. + // Any conflict resolutions will be done as `history.replace` if (!tryAgain) { - history.perform(actionNoop()); // checkpoint + history.perform(actionNoop()); } - context.container().call(loading); + // Attempt a fast upload.. If there are conflicts, re-enter with `checkConflicts = true` + if (!checkConflicts) { + upload(changeset); - if (toCheck.length) { - osm.loadMultiple(toLoad, loaded); + // Do the full (slow) conflict check.. } else { - upload(); + var modified = _filter(history.difference().summary(), { changeType: 'modified' }); + _toCheck = _map(_map(modified, 'entity'), 'id'); + _toLoad = withChildNodes(_toCheck, localGraph); + if (_toCheck.length) { + osm.loadMultiple(_toLoad, loaded); + } else { + upload(changeset); + } } + return; + function withChildNodes(ids, graph) { return _uniq(_reduce(ids, function(result, id) { @@ -111,13 +136,12 @@ export function modeSave(context) { }, _clone(ids))); } - // Reload modified entities into an alternate graph and check for conflicts.. function loaded(err, result) { - if (errors.length) return; + if (_errors.length) return; if (err) { - errors.push({ + _errors.push({ msg: err.responseText, details: [ t('save.status_code', { code: err.status }) ] }); @@ -125,35 +149,35 @@ export function modeSave(context) { } else { var loadMore = []; - _each(result.data, function(entity) { + result.data.forEach(function(entity) { remoteGraph.replace(entity); - toLoad = _without(toLoad, entity.id); + _toLoad = _without(_toLoad, entity.id); // Because loadMultiple doesn't download /full like loadEntity, // need to also load children that aren't already being checked.. if (!entity.visible) return; if (entity.type === 'way') { loadMore.push.apply(loadMore, - _difference(entity.nodes, toCheck, toLoad, loadMore)); + _difference(entity.nodes, _toCheck, _toLoad, loadMore)); } else if (entity.type === 'relation' && entity.isMultipolygon()) { loadMore.push.apply(loadMore, - _difference(_map(entity.members, 'id'), toCheck, toLoad, loadMore)); + _difference(_map(entity.members, 'id'), _toCheck, _toLoad, loadMore)); } }); if (loadMore.length) { - toLoad.push.apply(toLoad, loadMore); + _toLoad.push.apply(_toLoad, loadMore); osm.loadMultiple(loadMore, loaded); } - if (!toLoad.length) { - checkConflicts(); + if (!_toLoad.length) { + detectConflicts(); } } } - function checkConflicts() { + function detectConflicts() { function choice(id, text, action) { return { id: id, text: text, action: function() { history.replace(action); } }; } @@ -164,16 +188,14 @@ export function modeSave(context) { return utilDisplayName(entity) || (utilDisplayType(entity.id) + ' ' + entity.id); } - function compareVersions(local, remote) { + function sameVersions(local, remote) { if (local.version !== remote.version) return false; if (local.type === 'way') { var children = _union(local.nodes, remote.nodes); - for (var i = 0; i < children.length; i++) { var a = localGraph.hasEntity(children[i]); var b = remoteGraph.hasEntity(children[i]); - if (a && b && a.version !== b.version) return false; } } @@ -181,11 +203,11 @@ export function modeSave(context) { return true; } - _each(toCheck, function(id) { + _toCheck.forEach(function(id) { var local = localGraph.entity(id); var remote = remoteGraph.entity(id); - if (compareVersions(local, remote)) return; + if (sameVersions(local, remote)) return; var action = actionMergeRemoteChanges; var merge = action(id, localGraph, remoteGraph, formatUser); @@ -200,7 +222,7 @@ export function modeSave(context) { var keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore')); var keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete')); - conflicts.push({ + _conflicts.push({ id: id, name: entityName(local), details: mergeConflicts, @@ -212,163 +234,179 @@ export function modeSave(context) { }); }); - upload(); + upload(changeset); } + } - function upload() { - if (conflicts.length) { - conflicts.sort(function(a,b) { return b.id.localeCompare(a.id); }); - showConflicts(); - } else if (errors.length) { - showErrors(); - } else { - var changes = history.changes(actionDiscardTags(history.difference())); - if (changes.modified.length || changes.created.length || changes.deleted.length) { - osm.putChangeset(changeset, changes, uploadCallback); - } else { // changes were insignificant or reverted by user - d3_select('.inspector-wrap *').remove(); - loading.close(); - context.flush(); - cancel(); - } + function upload(changeset) { + var osm = context.connection(); + if (!osm) { + _errors.push({ msg: 'No OSM Service' }); + } + + if (_conflicts.length) { + _conflicts.sort(function(a, b) { return b.id.localeCompare(a.id); }); + showConflicts(changeset); + + } else if (_errors.length) { + showErrors(); + + } else { + var history = context.history(); + var changes = history.changes(actionDiscardTags(history.difference())); + if (changes.modified.length || changes.created.length || changes.deleted.length) { + osm.putChangeset(changeset, changes, uploadCallback); + } else { // changes were insignificant or reverted by user + d3_select('.inspector-wrap *').remove(); + loading.close(); + _isSaving = false; + context.flush(); + cancel(); } } + } - function uploadCallback(err, changeset) { - if (err) { - errors.push({ + function uploadCallback(err, changeset) { + if (err) { + if (err.status === 409) { // 409 Conflict + save(changeset, true, true); // tryAgain = true, checkConflicts = true + } else { + _errors.push({ msg: err.responseText, details: [ t('save.status_code', { code: err.status }) ] }); showErrors(); - } else { - history.clearSaved(); - success(changeset); - // Add delay to allow for postgres replication #1646 #2678 - window.setTimeout(function() { - d3_select('.inspector-wrap *').remove(); - loading.close(); - context.flush(); - }, 2500); } + + } else { + context.history().clearSaved(); + success(changeset); + // Add delay to allow for postgres replication #1646 #2678 + window.setTimeout(function() { + d3_select('.inspector-wrap *').remove(); + loading.close(); + _isSaving = false; + context.flush(); + }, 2500); } + } - function showConflicts() { - var selection = context.container() - .select('#sidebar') - .append('div') - .attr('class','sidebar-component'); - - loading.close(); - - selection.call(uiConflicts(context) - .list(conflicts) - .origChanges(origChanges) - .on('cancel', function() { - history.pop(); - selection.remove(); - }) - .on('save', function() { - for (var i = 0; i < conflicts.length; i++) { - if (conflicts[i].chosen === 1) { // user chose "keep theirs" - var entity = context.hasEntity(conflicts[i].id); - if (entity && entity.type === 'way') { - var children = _uniq(entity.nodes); - for (var j = 0; j < children.length; j++) { - history.replace(actionRevert(children[j])); - } + function showConflicts(changeset) { + var history = context.history(); + var selection = context.container() + .select('#sidebar') + .append('div') + .attr('class','sidebar-component'); + + loading.close(); + _isSaving = false; + + var ui = uiConflicts(context) + .conflictList(_conflicts) + .origChanges(_origChanges) + .on('cancel', function() { + history.pop(); + selection.remove(); + }) + .on('save', function() { + for (var i = 0; i < _conflicts.length; i++) { + if (_conflicts[i].chosen === 1) { // user chose "keep theirs" + var entity = context.hasEntity(_conflicts[i].id); + if (entity && entity.type === 'way') { + var children = _uniq(entity.nodes); + for (var j = 0; j < children.length; j++) { + history.replace(actionRevert(children[j])); } - history.replace(actionRevert(conflicts[i].id)); } + history.replace(actionRevert(_conflicts[i].id)); } + } - selection.remove(); - save(changeset, true); - }) - ); - } + selection.remove(); + save(changeset, true, false); // tryAgain = true, checkConflicts = false + }); + selection.call(ui); + } - function showErrors() { - var selection = uiConfirm(context.container()); - history.pop(); - loading.close(); + function showErrors() { + var selection = uiConfirm(context.container()); - selection - .select('.modal-section.header') - .append('h3') - .text(t('save.error')); + context.history().pop(); + loading.close(); + _isSaving = false; - addErrors(selection, errors); - selection.okButton(); - } + selection + .select('.modal-section.header') + .append('h3') + .text(t('save.error')); + addErrors(selection, _errors); + selection.okButton(); + } - function addErrors(selection, data) { - var message = selection - .select('.modal-section.message-text'); - var items = message - .selectAll('.error-container') - .data(data); + function addErrors(selection, data) { + var message = selection + .select('.modal-section.message-text'); - var enter = items.enter() - .append('div') - .attr('class', 'error-container'); + var items = message + .selectAll('.error-container') + .data(data); - enter - .append('a') - .attr('class', 'error-description') - .attr('href', '#') - .classed('hide-toggle', true) - .text(function(d) { return d.msg || t('save.unknown_error_details'); }) - .on('click', function() { - var error = d3_select(this); - var detail = d3_select(this.nextElementSibling); - var exp = error.classed('expanded'); + var enter = items.enter() + .append('div') + .attr('class', 'error-container'); - detail.style('display', exp ? 'none' : 'block'); - error.classed('expanded', !exp); + enter + .append('a') + .attr('class', 'error-description') + .attr('href', '#') + .classed('hide-toggle', true) + .text(function(d) { return d.msg || t('save.unknown_error_details'); }) + .on('click', function() { + d3_event.preventDefault(); - d3_event.preventDefault(); - }); + var error = d3_select(this); + var detail = d3_select(this.nextElementSibling); + var exp = error.classed('expanded'); - var details = enter - .append('div') - .attr('class', 'error-detail-container') - .style('display', 'none'); - - details - .append('ul') - .attr('class', 'error-detail-list') - .selectAll('li') - .data(function(d) { return d.details || []; }) - .enter() - .append('li') - .attr('class', 'error-detail-item') - .text(function(d) { return d; }); - - items.exit() - .remove(); - } + detail.style('display', exp ? 'none' : 'block'); + error.classed('expanded', !exp); + }); + var details = enter + .append('div') + .attr('class', 'error-detail-container') + .style('display', 'none'); + + details + .append('ul') + .attr('class', 'error-detail-list') + .selectAll('li') + .data(function(d) { return d.details || []; }) + .enter() + .append('li') + .attr('class', 'error-detail-item') + .text(function(d) { return d; }); + + items.exit() + .remove(); } function success(changeset) { commit.reset(); - context.enter(modeBrowse(context) - .sidebar(uiSuccess(context) - .changeset(changeset) - .on('cancel', function() { - context.ui().sidebar.hide(); - }) - ) - ); + + var ui = uiSuccess(context) + .changeset(changeset) + .on('cancel', function() { context.ui().sidebar.hide(); }); + + context.enter(modeBrowse(context).sidebar(ui)); } @@ -404,6 +442,9 @@ export function modeSave(context) { mode.exit = function() { + loading.close(); + _isSaving = false; + keybinding.off(); context.container().selectAll('#content') diff --git a/modules/services/osm.js b/modules/services/osm.js index d284d72f21..89b45013dd 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -349,7 +349,7 @@ export default { putChangeset: function(changeset, changes, callback) { if (_changeset.inflight) { - return callback({ message: 'Changeset already inflight', status: -2 }); + return callback({ message: 'Changeset already inflight', status: -2 }, changeset); } var that = this; @@ -371,10 +371,10 @@ export default { _changeset.inflight = null; if (err) { - return callback(err); + return callback(err, changeset); } if (that.getConnectionId() !== cid) { - return callback({ message: 'Connection Switched', status: -1 }); + return callback({ message: 'Connection Switched', status: -1 }, changeset); } _changeset.open = changesetID; @@ -393,7 +393,7 @@ export default { function uploadedChangeset(err) { _changeset.inflight = null; - if (err) return callback(err); + if (err) return callback(err, changeset); // Upload was successful, safe to call the callback. // Add delay to allow for postgres replication #1646 #2678 @@ -680,7 +680,7 @@ export default { _rateLimitError = undefined; dispatch.call('change'); if (callback) callback(err, res); - that._userChangesets(function() {}); // eagerly load user details/changesets + that.userChangesets(function() {}); // eagerly load user details/changesets } return oauth.authenticate(done); diff --git a/modules/ui/commit.js b/modules/ui/commit.js index d4af06590e..be1ef0e72e 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -238,6 +238,7 @@ export function uiCommit(context) { return (n && n.value.length) ? null : true; }) .on('click.save', function() { + this.blur(); // avoid keeping focus on the button - #4641 dispatch.call('save', this, _changeset); }); diff --git a/modules/ui/confirm.js b/modules/ui/confirm.js index aa1238a20b..2fd0e43836 100644 --- a/modules/ui/confirm.js +++ b/modules/ui/confirm.js @@ -27,7 +27,9 @@ export function uiConfirm(selection) { .on('click.confirm', function() { modalSelection.remove(); }) - .text(t('confirm.okay')); + .text(t('confirm.okay')) + .node() + .focus(); return modalSelection; }; diff --git a/modules/ui/conflicts.js b/modules/ui/conflicts.js index 2cfe3448e9..21a0aa2b7c 100644 --- a/modules/ui/conflicts.js +++ b/modules/ui/conflicts.js @@ -16,9 +16,9 @@ import { utilRebind } from '../util/rebind'; export function uiConflicts(context) { - var dispatch = d3_dispatch('cancel', 'save'), - origChanges, - conflictList; + var dispatch = d3_dispatch('cancel', 'save'); + var _origChanges; + var _conflictList; function conflicts(selection) { @@ -47,14 +47,14 @@ export function uiConflicts(context) { // Download changes link - var detected = utilDetect(), - changeset = new osmChangeset(); + var detected = utilDetect(); + var changeset = new osmChangeset(); - delete changeset.id; // Export without chnageset_id + delete changeset.id; // Export without changeset_id - var data = JXON.stringify(changeset.osmChangeJXON(origChanges)), - blob = new Blob([data], {type: 'text/xml;charset=utf-8;'}), - fileName = 'changes.osc'; + var data = JXON.stringify(changeset.osmChangeJXON(_origChanges)); + var blob = new Blob([data], { type: 'text/xml;charset=utf-8;' }); + var fileName = 'changes.osc'; var linkEnter = conflictsHelp.selectAll('.download-changes') .data([0]) @@ -99,7 +99,7 @@ export function uiConflicts(context) { buttons .append('button') - .attr('disabled', conflictList.length > 1) + .attr('disabled', _conflictList.length > 1) .attr('class', 'action conflicts-button col6') .text(t('save.title')) .on('click.try_again', function() { dispatch.call('save'); }); @@ -113,12 +113,12 @@ export function uiConflicts(context) { function showConflict(selection, index) { - if (index < 0 || index >= conflictList.length) return; + if (index < 0 || index >= _conflictList.length) return; var parent = d3_select(selection.node().parentNode); // enable save button if this is the last conflict being reviewed.. - if (index === conflictList.length - 1) { + if (index === _conflictList.length - 1) { window.setTimeout(function() { parent.select('.conflicts-button') .attr('disabled', null); @@ -132,7 +132,7 @@ export function uiConflicts(context) { var item = selection .selectAll('.conflict') - .data([conflictList[index]]); + .data([_conflictList[index]]); var enter = item.enter() .append('div') @@ -141,7 +141,7 @@ export function uiConflicts(context) { enter .append('h4') .attr('class', 'conflict-count') - .text(t('save.conflict.count', { num: index + 1, total: conflictList.length })); + .text(t('save.conflict.count', { num: index + 1, total: _conflictList.length })); enter .append('a') @@ -183,11 +183,11 @@ export function uiConflicts(context) { .attr('class', 'conflict-nav-button action col6') .attr('disabled', function(d, i) { return (i === 0 && index === 0) || - (i === 1 && index === conflictList.length - 1) || null; + (i === 1 && index === _conflictList.length - 1) || null; }) .on('click', function(d, i) { - var container = parent.select('.conflict-container'), - sign = (i === 0 ? -1 : 1); + var container = parent.select('.conflict-container'); + var sign = (i === 0 ? -1 : 1); container .selectAll('.conflict') @@ -249,8 +249,8 @@ export function uiConflicts(context) { .selectAll('input') .property('checked', function(d) { return d === datum; }); - var extent = geoExtent(), - entity; + var extent = geoExtent(); + var entity; entity = context.graph().hasEntity(datum.id); if (entity) extent._extend(entity.extent(context.graph())); @@ -275,8 +275,7 @@ export function uiConflicts(context) { } else { context.map().zoomTo(entity); } - context.surface().selectAll( - utilEntityOrMemberSelector([entity.id], context.graph())) + context.surface().selectAll(utilEntityOrMemberSelector([entity.id], context.graph())) .classed('hover', true); } } @@ -293,16 +292,16 @@ export function uiConflicts(context) { // choice(id, keepTheirs, forceRemote) // ] // } - conflicts.list = function(_) { - if (!arguments.length) return conflictList; - conflictList = _; + conflicts.conflictList = function(_) { + if (!arguments.length) return _conflictList; + _conflictList = _; return conflicts; }; conflicts.origChanges = function(_) { - if (!arguments.length) return origChanges; - origChanges = _; + if (!arguments.length) return _origChanges; + _origChanges = _; return conflicts; }; From ac86869b4a463a7a1d7a77a16f7a739148c92ae3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 5 Jan 2018 18:11:49 -0500 Subject: [PATCH 118/206] Add conflict checking progress, guard code for user authentication --- data/core.yaml | 1 + dist/locales/en.json | 1 + modules/modes/save.js | 64 +++++++++++++++++++++++++++++++++++------ modules/services/osm.js | 16 +++++++++-- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 9261276afd..08021a33fd 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -470,6 +470,7 @@ en: status_code: "Server returned status code {code}" unknown_error_details: "Please ensure you are connected to the internet." uploading: Uploading changes to OpenStreetMap... + conflict_progress: "Checking for conflicts: {num} of {total}" unsaved_changes: You have unsaved changes conflict: header: Resolve conflicting edits diff --git a/dist/locales/en.json b/dist/locales/en.json index 1e13481be0..976862a902 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -582,6 +582,7 @@ "status_code": "Server returned status code {code}", "unknown_error_details": "Please ensure you are connected to the internet.", "uploading": "Uploading changes to OpenStreetMap...", + "conflict_progress": "Checking for conflicts: {num} of {total}", "unsaved_changes": "You have unsaved changes", "conflict": { "header": "Resolve conflicting edits", diff --git a/modules/modes/save.js b/modules/modes/save.js index 73479e72a9..99d4641a03 100644 --- a/modules/modes/save.js +++ b/modules/modes/save.js @@ -60,6 +60,9 @@ export function modeSave(context) { var _toCheck = []; var _toLoad = []; + var _toLoadCount = 0; + var _toLoadTotal = 0; + var _conflicts = []; var _errors = []; var _origChanges; @@ -76,13 +79,33 @@ export function modeSave(context) { function save(changeset, tryAgain, checkConflicts) { // Guard against accidentally entering save code twice - #4641 - if (_isSaving && !tryAgain) return; + if (_isSaving && !tryAgain) { + return; + } var osm = context.connection(); - if (!osm) return; + if (!osm) { + cancel(); + return; + } + + // If user somehow got logged out mid-save, try to reauthenticate.. + // This can happen if they were logged in from before, but the tokens are no longer valid. + if (!osm.authenticated()) { + osm.authenticate(function(err) { + if (err) { + cancel(); // quit save mode.. + } else { + save(changeset, tryAgain, checkConflicts); // continue where we left off.. + } + }); + return; + } - _isSaving = true; - context.container().call(loading); // block input + if (!_isSaving) { + context.container().call(loading); // block input + _isSaving = true; + } var history = context.history(); var localGraph = context.graph(); @@ -109,7 +132,11 @@ export function modeSave(context) { var modified = _filter(history.difference().summary(), { changeType: 'modified' }); _toCheck = _map(_map(modified, 'entity'), 'id'); _toLoad = withChildNodes(_toCheck, localGraph); + _toLoadCount = 0; + _toLoadTotal = _toLoad.length; + if (_toCheck.length) { + showProgress(_toLoadCount, _toLoadTotal); osm.loadMultiple(_toLoad, loaded); } else { upload(changeset); @@ -142,12 +169,13 @@ export function modeSave(context) { if (err) { _errors.push({ - msg: err.responseText, + msg: err.message || err.responseText, details: [ t('save.status_code', { code: err.status }) ] }); showErrors(); } else { + var loadMore = []; result.data.forEach(function(entity) { remoteGraph.replace(entity); @@ -165,6 +193,10 @@ export function modeSave(context) { } }); + _toLoadCount += result.data.length; + _toLoadTotal += loadMore.length; + showProgress(_toLoadCount, _toLoadTotal); + if (loadMore.length) { _toLoad.push.apply(_toLoad, loadMore); osm.loadMultiple(loadMore, loaded); @@ -274,7 +306,7 @@ export function modeSave(context) { save(changeset, true, true); // tryAgain = true, checkConflicts = true } else { _errors.push({ - msg: err.responseText, + msg: err.message || err.responseText, details: [ t('save.status_code', { code: err.status }) ] }); showErrors(); @@ -294,6 +326,20 @@ export function modeSave(context) { } + function showProgress(num, total) { + var modal = context.container().select('.loading-modal .modal-section'); + var progress = modal.selectAll('.progress') + .data([0]); + + // enter/update + progress.enter() + .append('div') + .attr('class', 'progress') + .merge(progress) + .text(t('save.conflict_progress', { num: num, total: total })); + } + + function showConflicts(changeset) { var history = context.history(); var selection = context.container() @@ -425,7 +471,10 @@ export function modeSave(context) { .attr('class', 'inactive'); var osm = context.connection(); - if (!osm) return; + if (!osm) { + cancel(); + return; + } if (osm.authenticated()) { done(); @@ -442,7 +491,6 @@ export function modeSave(context) { mode.exit = function() { - loading.close(); _isSaving = false; keybinding.off(); diff --git a/modules/services/osm.js b/modules/services/osm.js index 89b45013dd..9ead1f5b0c 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -253,8 +253,7 @@ export default { // 400 Bad Request, 401 Unauthorized, 403 Forbidden // Logout and retry the request.. - if (isAuthenticated && err && - (err.status === 400 || err.status === 401 || err.status === 403)) { + if (isAuthenticated && err && (err.status === 400 || err.status === 401 || err.status === 403)) { that.logout(); that.loadFromAPI(path, callback); @@ -371,6 +370,10 @@ export default { _changeset.inflight = null; if (err) { + // 400 Bad Request, 401 Unauthorized, 403 Forbidden.. + if (err.status === 400 || err.status === 401 || err.status === 403) { + that.logout(); + } return callback(err, changeset); } if (that.getConnectionId() !== cid) { @@ -428,12 +431,17 @@ export default { function done(err, user_details) { if (err) { + // 400 Bad Request, 401 Unauthorized, 403 Forbidden.. + if (err.status === 400 || err.status === 401 || err.status === 403) { + that.logout(); + } return callback(err); } if (that.getConnectionId() !== cid) { return callback({ message: 'Connection Switched', status: -1 }); } + var u = user_details.getElementsByTagName('user')[0]; var img = u.getElementsByTagName('img'); var image_url = ''; @@ -482,6 +490,10 @@ export default { function done(err, changesets) { if (err) { + // 400 Bad Request, 401 Unauthorized, 403 Forbidden.. + if (err.status === 400 || err.status === 401 || err.status === 403) { + that.logout(); + } return callback(err); } if (that.getConnectionId() !== cid) { From a22cfe64b8136adfc3045f661cc0d5a8fb8fbe67 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 6 Jan 2018 00:30:26 -0500 Subject: [PATCH 119/206] Avoid loading circular relations by storing ids in _loaded object (re: #3056) --- modules/modes/save.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/modules/modes/save.js b/modules/modes/save.js index 99d4641a03..bb52804a5b 100644 --- a/modules/modes/save.js +++ b/modules/modes/save.js @@ -1,5 +1,4 @@ import _clone from 'lodash-es/clone'; -import _difference from 'lodash-es/difference'; import _filter from 'lodash-es/filter'; import _map from 'lodash-es/map'; import _reduce from 'lodash-es/reduce'; @@ -60,6 +59,7 @@ export function modeSave(context) { var _toCheck = []; var _toLoad = []; + var _loaded = {}; var _toLoadCount = 0; var _toLoadTotal = 0; @@ -132,11 +132,13 @@ export function modeSave(context) { var modified = _filter(history.difference().summary(), { changeType: 'modified' }); _toCheck = _map(_map(modified, 'entity'), 'id'); _toLoad = withChildNodes(_toCheck, localGraph); + _loaded = {}; _toLoadCount = 0; _toLoadTotal = _toLoad.length; if (_toCheck.length) { showProgress(_toLoadCount, _toLoadTotal); + _toLoad.forEach(function(id) { _loaded[id] = false; }); osm.loadMultiple(_toLoad, loaded); } else { upload(changeset); @@ -163,6 +165,7 @@ export function modeSave(context) { }, _clone(ids))); } + // Reload modified entities into an alternate graph and check for conflicts.. function loaded(err, result) { if (_errors.length) return; @@ -175,21 +178,34 @@ export function modeSave(context) { showErrors(); } else { - var loadMore = []; + result.data.forEach(function(entity) { remoteGraph.replace(entity); + _loaded[entity.id] = true; _toLoad = _without(_toLoad, entity.id); + if (!entity.visible) return; + // Because loadMultiple doesn't download /full like loadEntity, // need to also load children that aren't already being checked.. - if (!entity.visible) return; + var i, id; if (entity.type === 'way') { - loadMore.push.apply(loadMore, - _difference(entity.nodes, _toCheck, _toLoad, loadMore)); + for (i = 0; i < entity.nodes.length; i++) { + id = entity.nodes[i]; + if (_loaded[id] === undefined) { + _loaded[id] = false; + loadMore.push(id); + } + } } else if (entity.type === 'relation' && entity.isMultipolygon()) { - loadMore.push.apply(loadMore, - _difference(_map(entity.members, 'id'), _toCheck, _toLoad, loadMore)); + for (i = 0; i < entity.members.length; i++) { + id = entity.members[i].id; + if (_loaded[id] === undefined) { + _loaded[id] = false; + loadMore.push(id); + } + } } }); From 590487d23703cd58d313bc255f130b3930d3669d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 6 Jan 2018 23:16:48 -0500 Subject: [PATCH 120/206] Fix escape keybind when conflicts ui is active (re: 4351) --- modules/modes/save.js | 28 +++++++++++++++++++--------- modules/ui/conflicts.js | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/modules/modes/save.js b/modules/modes/save.js index bb52804a5b..f5198b41bf 100644 --- a/modules/modes/save.js +++ b/modules/modes/save.js @@ -47,7 +47,7 @@ var _isSaving = false; export function modeSave(context) { var mode = { id: 'save' }; - var keybinding = d3_keybinding('select'); + var keybinding = d3_keybinding('save'); var loading = uiLoading(context) .message(t('save.uploading')) @@ -103,6 +103,7 @@ export function modeSave(context) { } if (!_isSaving) { + keybindingOff(); context.container().call(loading); // block input _isSaving = true; } @@ -372,6 +373,7 @@ export function modeSave(context) { .on('cancel', function() { history.pop(); selection.remove(); + keybindingOn(); }) .on('save', function() { for (var i = 0; i < _conflicts.length; i++) { @@ -396,12 +398,12 @@ export function modeSave(context) { function showErrors() { - var selection = uiConfirm(context.container()); - + keybindingOn(); context.history().pop(); loading.close(); _isSaving = false; + var selection = uiConfirm(context.container()); selection .select('.modal-section.header') .append('h3') @@ -472,16 +474,24 @@ export function modeSave(context) { } + function keybindingOn() { + d3_select(document) + .call(keybinding.on('⎋', cancel, true)); + } + + + function keybindingOff() { + d3_select(document) + .call(keybinding.off); + } + + mode.enter = function() { function done() { context.ui().sidebar.show(commit); } - keybinding - .on('⎋', cancel, true); - - d3_select(document) - .call(keybinding); + keybindingOn(); context.container().selectAll('#content') .attr('class', 'inactive'); @@ -509,7 +519,7 @@ export function modeSave(context) { mode.exit = function() { _isSaving = false; - keybinding.off(); + keybindingOff(); context.container().selectAll('#content') .attr('class', 'active'); diff --git a/modules/ui/conflicts.js b/modules/ui/conflicts.js index 21a0aa2b7c..2180b235eb 100644 --- a/modules/ui/conflicts.js +++ b/modules/ui/conflicts.js @@ -5,6 +5,8 @@ import { select as d3_select } from 'd3-selection'; +import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; + import { t } from '../util/locale'; import { JXON } from '../util/jxon'; import { geoExtent } from '../geo'; @@ -17,11 +19,35 @@ import { utilRebind } from '../util/rebind'; export function uiConflicts(context) { var dispatch = d3_dispatch('cancel', 'save'); + var keybinding = d3_keybinding('conflicts'); var _origChanges; var _conflictList; + function keybindingOn() { + d3_select(document) + .call(keybinding.on('⎋', cancel, true)); + } + + function keybindingOff() { + d3_select(document) + .call(keybinding.off); + } + + function tryAgain() { + keybindingOff(); + dispatch.call('save'); + } + + function cancel() { + keybindingOff(); + dispatch.call('cancel'); + } + + function conflicts(selection) { + keybindingOn(); + var header = selection .append('div') .attr('class', 'header fillL'); @@ -29,7 +55,7 @@ export function uiConflicts(context) { header .append('button') .attr('class', 'fr') - .on('click', function() { dispatch.call('cancel'); }) + .on('click', cancel) .call(svgIcon('#icon-close')); header @@ -102,13 +128,13 @@ export function uiConflicts(context) { .attr('disabled', _conflictList.length > 1) .attr('class', 'action conflicts-button col6') .text(t('save.title')) - .on('click.try_again', function() { dispatch.call('save'); }); + .on('click.try_again', tryAgain); buttons .append('button') .attr('class', 'secondary-action conflicts-button col6') .text(t('confirm.cancel')) - .on('click.cancel', function() { dispatch.call('cancel'); }); + .on('click.cancel', cancel); } @@ -149,8 +175,8 @@ export function uiConflicts(context) { .attr('href', '#') .text(function(d) { return d.name; }) .on('click', function(d) { - zoomToEntity(d.id); d3_event.preventDefault(); + zoomToEntity(d.id); }); var details = enter @@ -186,6 +212,8 @@ export function uiConflicts(context) { (i === 1 && index === _conflictList.length - 1) || null; }) .on('click', function(d, i) { + d3_event.preventDefault(); + var container = parent.select('.conflict-container'); var sign = (i === 0 ? -1 : 1); @@ -195,8 +223,6 @@ export function uiConflicts(context) { container .call(showConflict, index + sign); - - d3_event.preventDefault(); }); item.exit() From 1b973f3c9bd01c531a7326f288f76ba265e48d90 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 7 Jan 2018 23:46:54 -0500 Subject: [PATCH 121/206] Be more careful about enter/update selection in conflict resolution ui This fixes one of the issues in #4351 where the radio button was not selected. This was likely introduced during the upgrade to d3 v4, now that enter selections do not automatically flow into update anymore. (the fix is to add a `merge` to ensure that the `selection.each` actually has some things to iterate over) --- modules/ui/conflicts.js | 72 ++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/modules/ui/conflicts.js b/modules/ui/conflicts.js index 2180b235eb..885b054792 100644 --- a/modules/ui/conflicts.js +++ b/modules/ui/conflicts.js @@ -13,8 +13,12 @@ import { geoExtent } from '../geo'; import { osmChangeset } from '../osm'; import { svgIcon } from '../svg'; import { utilDetect } from '../util/detect'; -import { utilEntityOrMemberSelector } from '../util'; -import { utilRebind } from '../util/rebind'; + +import { + utilEntityOrMemberSelector, + utilRebind, + utilWrap +} from '../util'; export function uiConflicts(context) { @@ -48,25 +52,29 @@ export function uiConflicts(context) { function conflicts(selection) { keybindingOn(); - var header = selection + var headerEnter = selection.selectAll('.header') + .data([0]) + .enter() .append('div') .attr('class', 'header fillL'); - header + headerEnter .append('button') .attr('class', 'fr') .on('click', cancel) .call(svgIcon('#icon-close')); - header + headerEnter .append('h3') .text(t('save.conflict.header')); - var body = selection + var bodyEnter = selection.selectAll('.body') + .data([0]) + .enter() .append('div') .attr('class', 'body fillL'); - var conflictsHelp = body + var conflictsHelpEnter = bodyEnter .append('div') .attr('class', 'conflicts-help') .text(t('save.conflict.help')); @@ -82,9 +90,7 @@ export function uiConflicts(context) { var blob = new Blob([data], { type: 'text/xml;charset=utf-8;' }); var fileName = 'changes.osc'; - var linkEnter = conflictsHelp.selectAll('.download-changes') - .data([0]) - .enter() + var linkEnter = conflictsHelpEnter.selectAll('.download-changes') .append('a') .attr('class', 'download-changes'); @@ -107,30 +113,30 @@ export function uiConflicts(context) { .text(t('save.conflict.download_changes')); - body + bodyEnter .append('div') .attr('class', 'conflict-container fillL3') .call(showConflict, 0); - body + bodyEnter .append('div') .attr('class', 'conflicts-done') .attr('opacity', 0) .style('display', 'none') .text(t('save.conflict.done')); - var buttons = body + var buttonsEnter = bodyEnter .append('div') .attr('class','buttons col12 joined conflicts-buttons'); - buttons + buttonsEnter .append('button') .attr('disabled', _conflictList.length > 1) .attr('class', 'action conflicts-button col6') .text(t('save.title')) .on('click.try_again', tryAgain); - buttons + buttonsEnter .append('button') .attr('class', 'secondary-action conflicts-button col6') .text(t('confirm.cancel')) @@ -139,7 +145,7 @@ export function uiConflicts(context) { function showConflict(selection, index) { - if (index < 0 || index >= _conflictList.length) return; + index = utilWrap(index, _conflictList.length); var parent = d3_select(selection.node().parentNode); @@ -156,20 +162,23 @@ export function uiConflicts(context) { }, 250); } - var item = selection + var conflict = selection .selectAll('.conflict') .data([_conflictList[index]]); - var enter = item.enter() + conflict.exit() + .remove(); + + var conflictEnter = conflict.enter() .append('div') .attr('class', 'conflict'); - enter + conflictEnter .append('h4') .attr('class', 'conflict-count') .text(t('save.conflict.count', { num: index + 1, total: _conflictList.length })); - enter + conflictEnter .append('a') .attr('class', 'conflict-description') .attr('href', '#') @@ -179,7 +188,7 @@ export function uiConflicts(context) { zoomToEntity(d.id); }); - var details = enter + var details = conflictEnter .append('div') .attr('class', 'conflict-detail-container'); @@ -214,7 +223,7 @@ export function uiConflicts(context) { .on('click', function(d, i) { d3_event.preventDefault(); - var container = parent.select('.conflict-container'); + var container = parent.selectAll('.conflict-container'); var sign = (i === 0 ? -1 : 1); container @@ -225,8 +234,6 @@ export function uiConflicts(context) { .call(showConflict, index + sign); }); - item.exit() - .remove(); } @@ -237,14 +244,15 @@ export function uiConflicts(context) { .selectAll('li') .data(function(d) { return d.choices || []; }); - var enter = choices.enter() + // enter + var choicesEnter = choices.enter() .append('li') .attr('class', 'layer'); - var label = enter + var labelEnter = choicesEnter .append('label'); - label + labelEnter .append('input') .attr('type', 'radio') .attr('name', function(d) { return d.id; }) @@ -254,14 +262,18 @@ export function uiConflicts(context) { choose(ul, d); }); - label + labelEnter .append('span') .text(function(d) { return d.text; }); - choices + // update + choicesEnter + .merge(choices) .each(function(d, i) { var ul = this.parentNode; - if (ul.__data__.chosen === i) choose(ul, d); + if (ul.__data__.chosen === i) { + choose(ul, d); + } }); } From 6ec6c319482f666e6b3d3c459f04f8fe49f85eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt?= Date: Mon, 8 Jan 2018 15:16:11 +0100 Subject: [PATCH 122/206] Restore fr lu mo addressing addr:unit is not standard in addressing for France in France : 800 addr:unit / 4 000 000 addr:housenumber = 0,0002 (0,02%) This partly reverts https://github.com/openstreetmap/iD/pull/4235/commits/de6b4fb6a20b2a3438b4159192e2b54682fab2e5 in https://github.com/openstreetmap/iD/pull/4235 --- data/address-formats.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/address-formats.json b/data/address-formats.json index 1c644c4811..491f036624 100644 --- a/data/address-formats.json +++ b/data/address-formats.json @@ -53,7 +53,7 @@ { "countryCodes": ["fr", "lu", "mo"], "format": [ - ["unit","housenumber", "street"], + ["housenumber", "street"], ["postcode", "city"] ] }, From 8472f99347da3eedc0aa2bdf99dcb38ceaeb9df3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 8 Jan 2018 09:49:32 -0500 Subject: [PATCH 123/206] Zooming out should not exit save mode (closes #4664) --- modules/renderer/map.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/renderer/map.js b/modules/renderer/map.js index a4ebac8a43..bac563eeb8 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -344,7 +344,12 @@ export function rendererMap(context) { function editOff() { context.features().resetStats(); surface.selectAll('.layer-osm *').remove(); - context.enter(modeBrowse(context)); + + var mode = context.mode(); + if (mode && mode.id !== 'save') { + context.enter(modeBrowse(context)); + } + dispatch.call('drawn', this, {full: true}); } From 58eaca2aa0a06154d49de580a63a975d094d708d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 8 Jan 2018 12:09:17 -0500 Subject: [PATCH 124/206] Ignore area closing segment during move when validating geometry (closes #4655) --- modules/behavior/draw_way.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 6c43c6ea9f..ee2cde230b 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -60,21 +60,28 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.replace(actionMoveNode(end.id, loc)); end = context.entity(end.id); + checkGeometry(true); // skipLast = true + } + - // check if this movement causes the geometry to break - var doBlock = invalidGeometry(end, context.graph()); + // Check whether this edit causes the geometry to break. + // If so, class the surface with a nope cursor. + // `skipLast` - include closing segment in the check, see #4655 + function checkGeometry(skipLast) { + var doBlock = isInvalidGeometry(end, context.graph(), skipLast); context.surface() .classed('nope', doBlock); } - function invalidGeometry(entity, graph) { + function isInvalidGeometry(entity, graph, skipLast) { var parents = graph.parentWays(entity); for (var i = 0; i < parents.length; i++) { var parent = parents[i]; var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); if (parent.isClosed()) { + if (skipLast) nodes.pop(); // disregard closing segment - #4655 if (geoHasSelfIntersections(nodes, entity.id)) { return true; } @@ -183,6 +190,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { annotation ); + checkGeometry(false); // skipLast = false context.enter(mode); }; @@ -202,6 +210,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { annotation ); + checkGeometry(false); // skipLast = false context.enter(mode); }; @@ -220,6 +229,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { annotation ); + checkGeometry(false); // skipLast = false context.enter(mode); }; @@ -228,6 +238,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // If the way has enough nodes to be valid, it's selected. // Otherwise, delete everything and return to browse mode. drawWay.finish = function() { + checkGeometry(false); // skipLast = false if (context.surface().classed('nope')) { return; // can't click here } From ea9643e08b900dd5e480c968b9b9e50760b65730 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 8 Jan 2018 15:47:29 -0500 Subject: [PATCH 125/206] Allow Alt/option key to disable geometry check and nope cursor (re: #4646) --- css/20_map.css | 4 +-- modules/behavior/draw_way.js | 68 +++++++++++++++++++++++++++++++++--- modules/modes/drag_node.js | 60 +++++++++++++++++++++++++++---- 3 files changed, 120 insertions(+), 12 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index d162eef120..f5d8e168ae 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -34,8 +34,8 @@ } /* `.target-nope` objects are explicitly forbidden to join to */ -.node.target.target-nope, -.way.target.target-nope { +.surface:not(.nope-disabled) .node.target.target-nope, +.surface:not(.nope-disabled) .way.target.target-nope { cursor: not-allowed; } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index ee2cde230b..009a2a5ada 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -1,5 +1,12 @@ import { t } from '../util/locale'; +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; + import { actionAddMidpoint, actionMoveNode, @@ -34,11 +41,40 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { _tempEdits++; + + function keydown() { + if (d3_event.keyCode === d3_keybinding.modifierCodes.alt) { + if (context.surface().classed('nope')) { + context.surface() + .classed('nope-suppressed', true); + } + context.surface() + .classed('nope', false) + .classed('nope-disabled', true); + } + } + + + function keyup() { + if (d3_event.keyCode === d3_keybinding.modifierCodes.alt) { + if (context.surface().classed('nope-suppressed')) { + context.surface() + .classed('nope', true); + } + context.surface() + .classed('nope-suppressed', false) + .classed('nope-disabled', false); + } + } + + // related code // - `mode/drag_node.js` `doMode()` // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` function move(datum) { + context.surface().classed('nope-disabled', d3_event.altKey); + var nodeLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc; var nodeGroups = datum && datum.properties && datum.properties.nodes; var loc = context.map().mouseCoordinates(); @@ -68,9 +104,18 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // If so, class the surface with a nope cursor. // `skipLast` - include closing segment in the check, see #4655 function checkGeometry(skipLast) { - var doBlock = isInvalidGeometry(end, context.graph(), skipLast); - context.surface() - .classed('nope', doBlock); + var nopeDisabled = context.surface().classed('nope-disabled'); + var isInvalid = isInvalidGeometry(end, context.graph(), skipLast); + + if (nopeDisabled) { + context.surface() + .classed('nope', false) + .classed('nope-suppressed', isInvalid); + } else { + context.surface() + .classed('nope', isInvalid) + .classed('nope-suppressed', false); + } } @@ -122,6 +167,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { .on('cancel', drawWay.cancel) .on('finish', drawWay.finish); + d3_select(window) + .on('keydown.drawWay', keydown) + .on('keyup.drawWay', keyup); + context.map() .dblclickEnable(false) .on('drawn.draw', setActiveElements); @@ -153,6 +202,15 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { .selectAll('.active') .classed('active', false); + surface + .classed('nope', false) + .classed('nope-suppressed', false) + .classed('nope-disabled', false); + + d3_select(window) + .on('keydown.hover', null) + .on('keyup.hover', null); + context.history() .on('undone.draw', null); }; @@ -274,7 +332,9 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { }, 1000); context.surface() - .classed('nope', false); + .classed('nope', false) + .classed('nope-disabled', false) + .classed('nope-suppressed', false); context.enter(modeBrowse(context)); }; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index faf603f66e..201515ed2d 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -5,6 +5,8 @@ import { select as d3_select } from 'd3-selection'; +import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; + import { t } from '../util/locale'; import { @@ -83,6 +85,32 @@ export function modeDragNode(context) { } + function keydown() { + if (d3_event.keyCode === d3_keybinding.modifierCodes.alt) { + if (context.surface().classed('nope')) { + context.surface() + .classed('nope-suppressed', true); + } + context.surface() + .classed('nope', false) + .classed('nope-disabled', true); + } + } + + + function keyup() { + if (d3_event.keyCode === d3_keybinding.modifierCodes.alt) { + if (context.surface().classed('nope-suppressed')) { + context.surface() + .classed('nope', true); + } + context.surface() + .classed('nope-suppressed', false) + .classed('nope-disabled', false); + } + } + + function start(entity) { _wasMidpoint = entity.type === 'midpoint'; var hasHidden = context.features().hasHiddenConnections(entity, context.graph()); @@ -173,15 +201,23 @@ export function modeDragNode(context) { // check if this movement causes the geometry to break - var doBlock = invalidGeometry(entity, context.graph()); - context.surface() - .classed('nope', doBlock); + var nopeDisabled = context.surface().classed('nope-disabled'); + var isInvalid = isInvalidGeometry(entity, context.graph()); + if (nopeDisabled) { + context.surface() + .classed('nope', false) + .classed('nope-suppressed', isInvalid); + } else { + context.surface() + .classed('nope', isInvalid) + .classed('nope-suppressed', false); + } _lastLoc = loc; } - function invalidGeometry(entity, graph) { + function isInvalidGeometry(entity, graph) { var parents = graph.parentWays(entity); var i, j, k; @@ -223,7 +259,7 @@ export function modeDragNode(context) { // If we still haven't tested this node's parent way for self-intersections. // (because it's not a member of a multipolygon), test it now. - if (activeIndex !== null && parent.isClosed()) { + if (activeIndex === null && parent.isClosed()) { nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) { return true; @@ -238,8 +274,10 @@ export function modeDragNode(context) { function move(entity) { if (_isCancelled) return; - d3_event.sourceEvent.stopPropagation(); + + context.surface().classed('nope-disabled', d3_event.sourceEvent.altKey); + _lastLoc = context.projection.invert(d3_event.point); doMove(entity); @@ -337,6 +375,10 @@ export function modeDragNode(context) { context.install(hover); context.install(edit); + d3_select(window) + .on('keydown.drawWay', keydown) + .on('keyup.drawWay', keyup); + context.history() .on('undone.drag-node', cancel); }; @@ -347,6 +389,10 @@ export function modeDragNode(context) { context.uninstall(hover); context.uninstall(edit); + d3_select(window) + .on('keydown.hover', null) + .on('keyup.hover', null); + context.history() .on('undone.drag-node', null); @@ -357,6 +403,8 @@ export function modeDragNode(context) { context.surface() .classed('nope', false) + .classed('nope-suppressed', false) + .classed('nope-disabled', false) .selectAll('.active') .classed('active', false); From 49eb46fefbf4646a2849b960fcea48af79969d89 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 8 Jan 2018 17:04:30 -0500 Subject: [PATCH 126/206] Add epsilon parameter to geoVecEqual --- modules/geo/vector.js | 8 ++++++-- test/spec/geo/vector.js | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/modules/geo/vector.js b/modules/geo/vector.js index 708bb5f66f..e67a6d1761 100644 --- a/modules/geo/vector.js +++ b/modules/geo/vector.js @@ -1,6 +1,10 @@ // vector equals -export function geoVecEqual(a, b) { - return (a[0] === b[0]) && (a[1] === b[1]); +export function geoVecEqual(a, b, epsilon) { + if (epsilon) { + return (Math.abs(a[0] - b[0]) <= epsilon) && (Math.abs(a[1] - b[1]) <= epsilon); + } else { + return (a[0] === b[0]) && (a[1] === b[1]); + } } // vector addition diff --git a/test/spec/geo/vector.js b/test/spec/geo/vector.js index 7b69aab003..20764e80a6 100644 --- a/test/spec/geo/vector.js +++ b/test/spec/geo/vector.js @@ -1,11 +1,15 @@ describe('iD.geo - vector', function() { describe('geoVecEqual', function() { - it('tests vectors for equality', function() { + it('tests vectors for exact equality', function() { expect(iD.geoVecEqual([1, 2], [1, 2])).to.be.true; expect(iD.geoVecEqual([1, 2], [1, 0])).to.be.false; expect(iD.geoVecEqual([1, 2], [2, 1])).to.be.false; }); + it('tests vectors for equality within epsilon', function() { + expect(iD.geoVecEqual([1, 2], [1.0000001, 2.0000001], 1e-5)).to.be.true; + expect(iD.geoVecEqual([1, 2], [1.0000001, 2.0000001], 1e-8)).to.be.false; + }); }); describe('geoVecAdd', function() { From e661281e38b1a337e60670a8112b116deb1d0836 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 8 Jan 2018 19:17:50 -0500 Subject: [PATCH 127/206] Prevent self intersecting lines without a junction node (closes #4646) --- modules/behavior/draw_way.js | 12 +++++------- modules/geo/geom.js | 14 ++++++++++++-- modules/modes/drag_node.js | 2 +- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 009a2a5ada..3a684f9f6d 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -96,7 +96,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.replace(actionMoveNode(end.id, loc)); end = context.entity(end.id); - checkGeometry(true); // skipLast = true + checkGeometry(origWay.isClosed()); // skipLast = true when drawing areas } @@ -125,11 +125,9 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { for (var i = 0; i < parents.length; i++) { var parent = parents[i]; var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); - if (parent.isClosed()) { - if (skipLast) nodes.pop(); // disregard closing segment - #4655 - if (geoHasSelfIntersections(nodes, entity.id)) { - return true; - } + if (skipLast) nodes.pop(); // disregard closing segment - #4655 + if (geoHasSelfIntersections(nodes, entity.id)) { + return true; } } @@ -296,7 +294,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // If the way has enough nodes to be valid, it's selected. // Otherwise, delete everything and return to browse mode. drawWay.finish = function() { - checkGeometry(false); // skipLast = false + checkGeometry(true); // skipLast = true if (context.surface().classed('nope')) { return; // can't click here } diff --git a/modules/geo/geom.js b/modules/geo/geom.js index da76cb6075..ed097c3756 100644 --- a/modules/geo/geom.js +++ b/modules/geo/geom.js @@ -105,8 +105,18 @@ export function geoHasSelfIntersections(nodes, activeID) { if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) || geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) { continue; - } else if (geoLineIntersection(p, q)) { - return true; + } + + var hit = geoLineIntersection(p, q); + if (hit) { + var epsilon = 1e-8; + // skip if the hit is at the segment's endpoint + if (geoVecEqual(p[1], hit, epsilon) || geoVecEqual(p[0], hit, epsilon) || + geoVecEqual(q[1], hit, epsilon) || geoVecEqual(q[0], hit, epsilon) ) { + continue; + } else { + return true; + } } } } diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 201515ed2d..5a93ba5268 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -259,7 +259,7 @@ export function modeDragNode(context) { // If we still haven't tested this node's parent way for self-intersections. // (because it's not a member of a multipolygon), test it now. - if (activeIndex === null && parent.isClosed()) { + if (activeIndex === null) { nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) { return true; From fcb2a0aa855fc389af29b6e8140cd043b5107a11 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 9 Jan 2018 00:20:41 +0000 Subject: [PATCH 128/206] fix(package): update marked to version 0.3.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 520f63b0a3..d1f3f47071 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@mapbox/togeojson": "0.16.0", "diacritics": "1.3.0", "lodash-es": "4.17.4", - "marked": "0.3.9", + "marked": "0.3.12", "node-diff3": "bhousel/node-diff3.git#v0.1.0", "osm-auth": "1.0.2", "rbush": "2.0.2", From f339c57838852f427dbab625b8c4d5fcf1257ee0 Mon Sep 17 00:00:00 2001 From: Andrey Golovin Date: Tue, 9 Jan 2018 13:26:54 +0200 Subject: [PATCH 129/206] Remove addr:unit for addresses in Ukraine --- data/address-formats.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/address-formats.json b/data/address-formats.json index 491f036624..94d24fa281 100644 --- a/data/address-formats.json +++ b/data/address-formats.json @@ -130,7 +130,7 @@ "countryCodes": ["ua"], "format": [ ["housenumber", "postcode"], - ["street", "unit"] + ["street"] ] }, { From f0a27bc1ec9079f5349deca81b41653d80fd914b Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 9 Jan 2018 10:07:21 -0500 Subject: [PATCH 130/206] Simplify way segmentation and fix bug with adjacent segment type (closes #4669) Now instead of creating MultiLineString targets, we just create a bunch of LineString targets. This makes the code simpler, and anyway the entity is still there in `properties` for drawing code to decide what to do with the target. Incidentally, this change allows iD to support an extrusion operation. (Because each way segment has its own unique GeoJSON target now) --- modules/behavior/draw.js | 8 +-- modules/behavior/draw_way.js | 31 +++++------ modules/modes/drag_node.js | 25 ++++----- modules/modes/draw_area.js | 4 +- modules/modes/draw_line.js | 4 +- modules/svg/helpers.js | 103 +++++++++++------------------------ modules/svg/vertices.js | 11 ++-- 7 files changed, 69 insertions(+), 117 deletions(-) diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 5866d3c263..d372a66524 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -128,12 +128,12 @@ export function behaviorDraw(context) { // - `behavior/draw_way.js` `move()` function click() { var d = datum(); - var target = d && d.id && context.hasEntity(d.id); - + var target = d && d.properties && d.properties.entity; var trySnap = geoViewportEdge(context.mouse(), context.map().dimensions()) === null; + if (trySnap) { if (target && target.type === 'node') { // Snap to a node - dispatch.call('clickNode', this, target); + dispatch.call('clickNode', this, target, d); return; } else if (target && target.type === 'way') { // Snap to a way @@ -142,7 +142,7 @@ export function behaviorDraw(context) { ); if (choice) { var edge = [target.nodes[choice.index - 1], target.nodes[choice.index]]; - dispatch.call('clickWay', this, choice.loc, edge); + dispatch.call('clickWay', this, choice.loc, edge, d); return; } } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 3a684f9f6d..1444584e14 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -75,22 +75,17 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function move(datum) { context.surface().classed('nope-disabled', d3_event.altKey); - var nodeLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc; - var nodeGroups = datum && datum.properties && datum.properties.nodes; + var targetLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc; + var targetNodes = datum && datum.properties && datum.properties.nodes; var loc = context.map().mouseCoordinates(); - if (nodeLoc) { // snap to node/vertex - a point target with `.loc` - loc = nodeLoc; - - } else if (nodeGroups) { // snap to way - a line target with `.nodes` - var best = Infinity; - for (var i = 0; i < nodeGroups.length; i++) { - var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); - var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, end.id); - if (choice && choice.distance < best) { - best = choice.distance; - loc = choice.loc; - } + if (targetLoc) { // snap to node/vertex - a point target with `.loc` + loc = targetLoc; + + } else if (targetNodes) { // snap to way - a line target with `.nodes` + var choice = geoChooseEdge(targetNodes, context.mouse(), context.projection, end.id); + if (choice) { + loc = choice.loc; } } @@ -252,8 +247,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing way. - drawWay.addWay = function(loc, edge) { - if (context.surface().classed('nope')) { + drawWay.addWay = function(loc, edge, d) { + if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) { return; // can't click here } @@ -272,8 +267,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing node and continue drawing. - drawWay.addNode = function(node) { - if (context.surface().classed('nope')) { + drawWay.addNode = function(node, d) { + if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) { return; // can't click here } diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 5a93ba5268..fc4b772760 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -175,21 +175,16 @@ export function modeDragNode(context) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` var d = datum(); - var nodeLoc = d && d.properties && d.properties.entity && d.properties.entity.loc; - var nodeGroups = d && d.properties && d.properties.nodes; - - if (nodeLoc) { // snap to node/vertex - a point target with `.loc` - loc = nodeLoc; - - } else if (nodeGroups) { // snap to way - a line target with `.nodes` - var best = Infinity; - for (var i = 0; i < nodeGroups.length; i++) { - var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); - var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, entity.id); - if (choice && choice.distance < best) { - best = choice.distance; - loc = choice.loc; - } + var targetLoc = d && d.properties && d.properties.entity && d.properties.entity.loc; + var targetNodes = d && d.properties && d.properties.nodes; + + if (targetLoc) { // snap to node/vertex - a point target with `.loc` + loc = targetLoc; + + } else if (targetNodes) { // snap to way - a line target with `.nodes` + var choice = geoChooseEdge(targetNodes, context.mouse(), context.projection, end.id); + if (choice) { + loc = choice.loc; } } } diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 0890e3592b..a06908faa9 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -19,14 +19,14 @@ export function modeDrawArea(context, wayId, startGraph) { var addNode = behavior.addNode; - behavior.addNode = function(node) { + behavior.addNode = function(node, d) { var length = way.nodes.length; var penultimate = length > 2 ? way.nodes[length - 2] : null; if (node.id === way.first() || node.id === penultimate) { behavior.finish(); } else { - addNode(node); + addNode(node, d); } }; diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index ca567601ee..83c2a9ae03 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -20,11 +20,11 @@ export function modeDrawLine(context, wayId, startGraph, affix) { .tail(t('modes.draw_line.tail')); var addNode = behavior.addNode; - behavior.addNode = function(node) { + behavior.addNode = function(node, d) { if (node.id === headId) { behavior.finish(); } else { - addNode(node); + addNode(node, d); } }; diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index 4cee3047bf..bf6647e66f 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -205,102 +205,65 @@ export function svgRelationMemberTags(graph) { export function svgSegmentWay(way, graph, activeID) { var features = { passive: [], active: [] }; - var coordGroups = { passive: [], active: [] }; - var nodeGroups = { passive: [], active: [] }; - var coords = []; - var nodes = []; - var startType = null; // 0 = active, 1 = passive, 2 = adjacent - var currType = null; // 0 = active, 1 = passive, 2 = adjacent - var node; + var start = {}; + var end = {}; + var node, type; for (var i = 0; i < way.nodes.length; i++) { - if (way.nodes[i] === activeID) { // vertex is the activeID - coords = []; // draw no segment here - nodes = []; - startType = null; - continue; - } - node = graph.entity(way.nodes[i]); - currType = svgPassiveVertex(node, graph, activeID); - - if (startType === null) { - startType = currType; - } - - if (currType !== startType) { // line changes here - try to save a segment - - if (coords.length > 0) { // finish previous segment - coords.push(node.loc); - nodes.push(node.id); - if (startType === 2 || currType === 2) { // one adjacent vertex - coordGroups.active.push(coords); - nodeGroups.active.push(nodes); - } else if (startType === 0 && currType === 0) { // both active vertices - coordGroups.active.push(coords); - nodeGroups.active.push(nodes); - } else { - coordGroups.passive.push(coords); - nodeGroups.passive.push(nodes); - } + type = svgPassiveVertex(node, graph, activeID); + end = { node: node, type: type }; + + if (start.type !== undefined) { + if (start.node.id === activeID || end.node.id === activeID) { + // push nothing + } else if (start.type === 2 || end.type === 2) { // one adjacent vertex + pushActive(start, end, i); + } else if (start.type === 0 && end.type === 0) { // both active vertices + pushActive(start, end, i); + } else { + pushPassive(start, end, i); } - - coords = []; - nodes = []; - startType = currType; } - coords.push(node.loc); - nodes.push(node.id); + start = end; } - // complete whatever segment we ended on - if (coords.length > 1) { - if (startType === 2 || currType === 2) { // one adjacent vertex - coordGroups.active.push(coords); - nodeGroups.active.push(nodes); - } else if (startType === 0 && currType === 0) { // both active vertices - coordGroups.active.push(coords); - nodeGroups.active.push(nodes); - } else { - coordGroups.passive.push(coords); - nodeGroups.passive.push(nodes); - } - } + return features; - if (coordGroups.passive.length) { - features.passive.push({ + + function pushActive(start, end, index) { + features.active.push({ type: 'Feature', - id: way.id, + id: way.id + '-' + index + '-nope', properties: { + nope: true, target: true, entity: way, - nodes: nodeGroups.passive + nodes: [start.node, end.node], + index: index }, geometry: { - type: 'MultiLineString', - coordinates: coordGroups.passive + type: 'LineString', + coordinates: [start.node.loc, end.node.loc] } }); } - if (coordGroups.active.length) { - features.active.push({ + function pushPassive(start, end, index) { + features.passive.push({ type: 'Feature', - id: way.id + '-nope', // break the ids on purpose + id: way.id + '-' + index, properties: { target: true, entity: way, - nodes: nodeGroups.active, - nope: true, - originalID: way.id + nodes: [start.node, end.node], + index: index }, geometry: { - type: 'MultiLineString', - coordinates: coordGroups.active + type: 'LineString', + coordinates: [start.node.loc, end.node.loc] } }); } - - return features; } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index c242decf93..22b4477e83 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -192,7 +192,7 @@ export function svgVertices(projection, context) { if (activeID === node.id) return; // draw no target on the activeID var vertexType = svgPassiveVertex(node, graph, activeID); - if (vertexType !== 0) { // passive or adjacent - allow to connect + if (vertexType !== 0) { // passive or adjacent - allow to connect data.targets.push({ type: 'Feature', id: node.id, @@ -205,12 +205,11 @@ export function svgVertices(projection, context) { } else { data.nopes.push({ type: 'Feature', - id: node.id + '-nope', // break the ids on purpose + id: node.id + '-nope', properties: { - target: true, - entity: node, nope: true, - originalID: node.id + target: true, + entity: node }, geometry: node.asGeoJSON() }); @@ -248,7 +247,7 @@ export function svgVertices(projection, context) { // enter/update nopes.enter() .append('circle') - .attr('r', function(d) { return (_radii[d.properties.originalID] || radiuses.shadow[3]); }) + .attr('r', function(d) { return (_radii[d.properties.entity.id] || radiuses.shadow[3]); }) .merge(nopes) .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; }) .attr('transform', getTransform); From 528739b9b0edead9eb611dae1fe6a42d979a4e1a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 9 Jan 2018 10:15:18 -0500 Subject: [PATCH 131/206] Drop `unit` from default address format --- data/address-formats.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/address-formats.json b/data/address-formats.json index 94d24fa281..dddf3cecda 100644 --- a/data/address-formats.json +++ b/data/address-formats.json @@ -2,7 +2,7 @@ "dataAddressFormats": [ { "format": [ - ["housenumber", "street", "unit"], + ["housenumber", "street"], ["city", "postcode"] ] }, From b1df1ca701788331fab738c32e664c6f0d4a019e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt?= Date: Tue, 9 Jan 2018 23:17:37 +0100 Subject: [PATCH 132/206] remove addr:unit field for gb, ie, si, tr as per comments in #4235 --- data/address-formats.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/data/address-formats.json b/data/address-formats.json index dddf3cecda..43024db05d 100644 --- a/data/address-formats.json +++ b/data/address-formats.json @@ -10,7 +10,7 @@ "countryCodes": ["gb"], "format": [ ["housename"], - ["housenumber", "street", "unit"], + ["housenumber", "street"], ["city", "postcode"] ] }, @@ -18,13 +18,13 @@ "countryCodes": ["ie"], "format": [ ["housename"], - ["housenumber", "street", "unit"], + ["housenumber", "street"], ["city"], ["postcode"] ] }, { - "countryCodes": ["at", "ch", "de"], + "countryCodes": ["at", "ch", "de", "si"], "format": [ ["street", "housenumber"], ["postcode", "city"] @@ -34,11 +34,11 @@ "countryCodes": [ "ad", "ba", "be", "cz", "dk", "es", "fi", "gr", "hr", "is", "it", "li", - "nl", "no", "pt", "se", "si", "sk", - "sm", "va" + "nl", "no", "pt", "se", "sk", "sm", + "va" ], "format": [ - ["unit","street", "housenumber"], + ["street", "housenumber", "unit"], ["postcode", "city"] ] }, @@ -122,7 +122,7 @@ "countryCodes": ["tr"], "format": [ ["neighbourhood"], - ["street", "housenumber", "unit"], + ["street", "housenumber"], ["postcode", "district", "city"] ] }, From 748abdb9502a12b366145e78942d4a0800992074 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 9 Jan 2018 11:41:54 -0500 Subject: [PATCH 133/206] Code formatting --- test/spec/actions/split.js | 986 ++++++++++++++++++---------------- test/spec/osm/multipolygon.js | 196 +++---- 2 files changed, 613 insertions(+), 569 deletions(-) diff --git a/test/spec/actions/split.js b/test/spec/actions/split.js index e722929fda..d09148499c 100644 --- a/test/spec/actions/split.js +++ b/test/spec/actions/split.js @@ -1,581 +1,615 @@ describe('iD.actionSplit', function () { beforeEach(function () { - iD.areaKeys = iD.Context().presets().areaKeys(); + iD.areaKeys = iD.coreContext().presets().areaKeys(); }); + describe('#disabled', function () { it('returns falsy for a non-end node of a single way', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}) + ]); expect(iD.actionSplit('b').disabled(graph)).not.to.be.ok; }); it('returns falsy for an intersection of two ways', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Way({id: '-', nodes: ['a', '*', 'b']}), - iD.Way({id: '|', nodes: ['c', '*', 'd']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), + iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) + ]); expect(iD.actionSplit('*').disabled(graph)).not.to.be.ok; }); it('returns falsy for an intersection of two ways with parent way specified', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Way({id: '-', nodes: ['a', '*', 'b']}), - iD.Way({id: '|', nodes: ['c', '*', 'd']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), + iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) + ]); expect(iD.actionSplit('*').limitWays(['-']).disabled(graph)).not.to.be.ok; }); it('returns falsy for a self-intersection', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']}) + ]); expect(iD.actionSplit('a').disabled(graph)).not.to.be.ok; }); it('returns \'not_eligible\' for the first node of a single way', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Way({id: '-', nodes: ['a', 'b']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}) + ]); expect(iD.actionSplit('a').disabled(graph)).to.equal('not_eligible'); }); it('returns \'not_eligible\' for the last node of a single way', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Way({id: '-', nodes: ['a', 'b']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}) + ]); expect(iD.actionSplit('b').disabled(graph)).to.equal('not_eligible'); }); it('returns \'not_eligible\' for an intersection of two ways with non-parent way specified', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Way({id: '-', nodes: ['a', '*', 'b']}), - iD.Way({id: '|', nodes: ['c', '*', 'd']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), + iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) + ]); expect(iD.actionSplit('*').limitWays(['-', '=']).disabled(graph)).to.equal('not_eligible'); }); }); - it('creates a new way with the appropriate nodes', function () { - // Situation: - // a ---- b ---- c - // - // Split at b. - // - // Expected result: - // a ---- b ==== c - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}) - ]); - - graph = iD.actionSplit('b', ['='])(graph); - expect(graph.entity('-').nodes).to.eql(['a', 'b']); - expect(graph.entity('=').nodes).to.eql(['b', 'c']); - }); + describe('ways', function () { - it('copies tags to the new way', function () { - var tags = {highway: 'residential'}, - graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c'], tags: tags}) + it('creates a new way with the appropriate nodes', function () { + // Situation: + // a ---- b ---- c + // + // Split at b. + // + // Expected result: + // a ---- b ==== c + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}) ]); - graph = iD.actionSplit('b', ['='])(graph); + graph = iD.actionSplit('b', ['='])(graph); - // Immutable tags => should be shared by identity. - expect(graph.entity('-').tags).to.equal(tags); - expect(graph.entity('=').tags).to.equal(tags); - }); + expect(graph.entity('-').nodes).to.eql(['a', 'b']); + expect(graph.entity('=').nodes).to.eql(['b', 'c']); + }); - it('splits a way at a T-junction', function () { - // Situation: - // a ---- b ---- c - // | - // d - // - // Split at b. - // - // Expected result: - // a ---- b ==== c - // | - // d - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - iD.Way({id: '|', nodes: ['d', 'b']}) + it('copies tags to the new way', function () { + var tags = {highway: 'residential'}; + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c'], tags: tags}) ]); - graph = iD.actionSplit('b', ['='])(graph); + graph = iD.actionSplit('b', ['='])(graph); - expect(graph.entity('-').nodes).to.eql(['a', 'b']); - expect(graph.entity('=').nodes).to.eql(['b', 'c']); - expect(graph.entity('|').nodes).to.eql(['d', 'b']); - }); + // Immutable tags => should be shared by identity. + expect(graph.entity('-').tags).to.equal(tags); + expect(graph.entity('=').tags).to.equal(tags); + }); - it('splits multiple ways at an intersection', function () { - // Situation: - // c - // | - // a ---- * ---- b - // ¦ - // d - // - // Split at b. - // - // Expected result: - // c - // | - // a ---- * ==== b - // ¦ - // d - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Way({id: '-', nodes: ['a', '*', 'b']}), - iD.Way({id: '|', nodes: ['c', '*', 'd']}) + it('splits a way at a T-junction', function () { + // Situation: + // a ---- b ---- c + // | + // d + // + // Split at b. + // + // Expected result: + // a ---- b ==== c + // | + // d + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '|', nodes: ['d', 'b']}) ]); - graph = iD.actionSplit('*', ['=', '¦'])(graph); - - expect(graph.entity('-').nodes).to.eql(['a', '*']); - expect(graph.entity('=').nodes).to.eql(['*', 'b']); - expect(graph.entity('|').nodes).to.eql(['c', '*']); - expect(graph.entity('¦').nodes).to.eql(['*', 'd']); - }); - - it('splits the specified ways at an intersection', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Way({id: '-', nodes: ['a', '*', 'b']}), - iD.Way({id: '|', nodes: ['c', '*', 'd']}) - ]); + graph = iD.actionSplit('b', ['='])(graph); - var g1 = iD.actionSplit('*', ['=']).limitWays(['-'])(graph); - expect(g1.entity('-').nodes).to.eql(['a', '*']); - expect(g1.entity('=').nodes).to.eql(['*', 'b']); - expect(g1.entity('|').nodes).to.eql(['c', '*', 'd']); - - var g2 = iD.actionSplit('*', ['¦']).limitWays(['|'])(graph); - expect(g2.entity('-').nodes).to.eql(['a', '*', 'b']); - expect(g2.entity('|').nodes).to.eql(['c', '*']); - expect(g2.entity('¦').nodes).to.eql(['*', 'd']); - - var g3 = iD.actionSplit('*', ['=', '¦']).limitWays(['-', '|'])(graph); - expect(g3.entity('-').nodes).to.eql(['a', '*']); - expect(g3.entity('=').nodes).to.eql(['*', 'b']); - expect(g3.entity('|').nodes).to.eql(['c', '*']); - expect(g3.entity('¦').nodes).to.eql(['*', 'd']); - }); + expect(graph.entity('-').nodes).to.eql(['a', 'b']); + expect(graph.entity('=').nodes).to.eql(['b', 'c']); + expect(graph.entity('|').nodes).to.eql(['d', 'b']); + }); - it('splits self-intersecting ways', function () { - // Situation: - // b - // / | - // / | - // c - a -- d - // - // Split at a. - // - // Expected result: - // b - // / | - // / | - // c - a == d - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']}) + it('splits multiple ways at an intersection', function () { + // Situation: + // c + // | + // a ---- * ---- b + // ¦ + // d + // + // Split at b. + // + // Expected result: + // c + // | + // a ---- * ==== b + // ¦ + // d + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), + iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) ]); - graph = iD.actionSplit('a', ['='])(graph); + graph = iD.actionSplit('*', ['=', '¦'])(graph); - expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c', 'a']); - expect(graph.entity('=').nodes).to.eql(['a', 'd']); - }); + expect(graph.entity('-').nodes).to.eql(['a', '*']); + expect(graph.entity('=').nodes).to.eql(['*', 'b']); + expect(graph.entity('|').nodes).to.eql(['c', '*']); + expect(graph.entity('¦').nodes).to.eql(['*', 'd']); + }); - it('splits a closed way at the given point and its antipode', function () { - // Situation: - // a ---- b - // | | - // d ---- c - // - // Split at a. - // - // Expected result: - // a ---- b - // || | - // d ==== c - // - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0,1]}), - iD.Node({id: 'b', loc: [1,1]}), - iD.Node({id: 'c', loc: [1,0]}), - iD.Node({id: 'd', loc: [0,0]}), - iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + it('splits the specified ways at an intersection', function () { + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), + iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) ]); - var g1 = iD.actionSplit('a', ['='])(graph); - expect(g1.entity('-').nodes).to.eql(['a', 'b', 'c']); - expect(g1.entity('=').nodes).to.eql(['c', 'd', 'a']); + var g1 = iD.actionSplit('*', ['=']).limitWays(['-'])(graph); + expect(g1.entity('-').nodes).to.eql(['a', '*']); + expect(g1.entity('=').nodes).to.eql(['*', 'b']); + expect(g1.entity('|').nodes).to.eql(['c', '*', 'd']); + + var g2 = iD.actionSplit('*', ['¦']).limitWays(['|'])(graph); + expect(g2.entity('-').nodes).to.eql(['a', '*', 'b']); + expect(g2.entity('|').nodes).to.eql(['c', '*']); + expect(g2.entity('¦').nodes).to.eql(['*', 'd']); + + var g3 = iD.actionSplit('*', ['=', '¦']).limitWays(['-', '|'])(graph); + expect(g3.entity('-').nodes).to.eql(['a', '*']); + expect(g3.entity('=').nodes).to.eql(['*', 'b']); + expect(g3.entity('|').nodes).to.eql(['c', '*']); + expect(g3.entity('¦').nodes).to.eql(['*', 'd']); + }); - var g2 = iD.actionSplit('b', ['='])(graph); - expect(g2.entity('-').nodes).to.eql(['b', 'c', 'd']); - expect(g2.entity('=').nodes).to.eql(['d', 'a', 'b']); + it('splits self-intersecting ways', function () { + // Situation: + // b + // / | + // / | + // c - a -- d + // + // Split at a. + // + // Expected result: + // b + // / | + // / | + // c - a == d + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']}) + ]); - var g3 = iD.actionSplit('c', ['='])(graph); - expect(g3.entity('-').nodes).to.eql(['c', 'd', 'a']); - expect(g3.entity('=').nodes).to.eql(['a', 'b', 'c']); + graph = iD.actionSplit('a', ['='])(graph); - var g4 = iD.actionSplit('d', ['='])(graph); - expect(g4.entity('-').nodes).to.eql(['d', 'a', 'b']); - expect(g4.entity('=').nodes).to.eql(['b', 'c', 'd']); - }); + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c', 'a']); + expect(graph.entity('=').nodes).to.eql(['a', 'd']); + }); - it('splits an area by converting it to a multipolygon', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0,1]}), - iD.Node({id: 'b', loc: [1,1]}), - iD.Node({id: 'c', loc: [1,0]}), - iD.Node({id: 'd', loc: [0,0]}), - iD.Way({id: '-', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}) + it('splits a closed way at the given point and its antipode', function () { + // Situation: + // a ---- b + // | | + // d ---- c + // + // Split at a. + // + // Expected result: + // a ---- b + // || | + // d ==== c + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0,1]}), + iD.osmNode({id: 'b', loc: [1,1]}), + iD.osmNode({id: 'c', loc: [1,0]}), + iD.osmNode({id: 'd', loc: [0,0]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) ]); - graph = iD.actionSplit('a', ['='])(graph); - expect(graph.entity('-').tags).to.eql({}); - expect(graph.entity('=').tags).to.eql({}); - expect(graph.parentRelations(graph.entity('-'))).to.have.length(1); - - var relation = graph.parentRelations(graph.entity('-'))[0]; - expect(relation.tags).to.eql({type: 'multipolygon', building: 'yes'}); - expect(relation.members).to.eql([ - {id: '-', role: 'outer', type: 'way'}, - {id: '=', role: 'outer', type: 'way'} - ]); - }); + var g1 = iD.actionSplit('a', ['='])(graph); + expect(g1.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(g1.entity('=').nodes).to.eql(['c', 'd', 'a']); - it('splits only the line of a node shared by a line and an area', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0,1]}), - iD.Node({id: 'b', loc: [1,1]}), - iD.Node({id: 'c', loc: [1,0]}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - iD.Way({id: '=', nodes: ['a', 'b', 'c', 'a'], tags: {area: 'yes'}}) - ]); + var g2 = iD.actionSplit('b', ['='])(graph); + expect(g2.entity('-').nodes).to.eql(['b', 'c', 'd']); + expect(g2.entity('=').nodes).to.eql(['d', 'a', 'b']); - graph = iD.actionSplit('b', ['~'])(graph); + var g3 = iD.actionSplit('c', ['='])(graph); + expect(g3.entity('-').nodes).to.eql(['c', 'd', 'a']); + expect(g3.entity('=').nodes).to.eql(['a', 'b', 'c']); - expect(graph.entity('-').nodes).to.eql(['a', 'b']); - expect(graph.entity('~').nodes).to.eql(['b', 'c']); - expect(graph.entity('=').nodes).to.eql(['a', 'b', 'c', 'a']); - expect(graph.parentRelations(graph.entity('='))).to.have.length(0); + var g4 = iD.actionSplit('d', ['='])(graph); + expect(g4.entity('-').nodes).to.eql(['d', 'a', 'b']); + expect(g4.entity('=').nodes).to.eql(['b', 'c', 'd']); + }); }); - it('adds the new way to parent relations (no connections)', function () { - // Situation: - // a ---- b ---- c - // Relation: [----] - // - // Split at b. - // - // Expected result: - // a ---- b ==== c - // Relation: [----, ====] - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - iD.Relation({id: 'r', members: [{id: '-', type: 'way', role: 'forward'}]}) - ]); - - graph = iD.actionSplit('b', ['='])(graph); - expect(graph.entity('r').members).to.eql([ - {id: '-', type: 'way', role: 'forward'}, - {id: '=', type: 'way', role: 'forward'} - ]); - }); + describe('relations', function () { - it('adds the new way to parent relations (forward order)', function () { - // Situation: - // a ---- b ---- c ~~~~ d - // Relation: [----, ~~~~] - // - // Split at b. - // - // Expected result: - // a ---- b ==== c ~~~~ d - // Relation: [----, ====, ~~~~] - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - iD.Way({id: '~', nodes: ['c', 'd']}), - iD.Relation({id: 'r', members: [{id: '-', type: 'way'}, {id: '~', type: 'way'}]}) + it('handles incomplete relations', function () { + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmRelation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) ]); - graph = iD.actionSplit('b', ['='])(graph); - - var ids = graph.entity('r').members.map(function(m) { return m.id; }); - expect(ids).to.have.ordered.members(['-', '=', '~']); - }); + graph = iD.actionSplit('b', ['='])(graph); - it('adds the new way to parent relations (reverse order)', function () { - // Situation: - // a ---- b ---- c ~~~~ d - // Relation: [~~~~, ----] - // - // Split at b. - // - // Expected result: - // a ---- b ==== c ~~~~ d - // Relation: [~~~~, ====, ----] - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - iD.Way({id: '~', nodes: ['c', 'd']}), - iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) - ]); + var ids = graph.entity('r').members.map(function(m) { return m.id; }); + expect(ids).to.have.ordered.members(['~', '-', '=']); + }); - graph = iD.actionSplit('b', ['='])(graph); - var ids = graph.entity('r').members.map(function(m) { return m.id; }); - expect(ids).to.have.ordered.members(['~', '=', '-']); - }); + describe('member ordering', function () { + + it('adds the new way to parent relations (no connections)', function () { + // Situation: + // a ---- b ---- c + // Relation: [----] + // + // Split at b. + // + // Expected result: + // a ---- b ==== c + // Relation: [----, ====] + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmRelation({id: 'r', members: [{id: '-', type: 'way', role: 'forward'}]}) + ]); - it('handles incomplete relations', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) - ]); + graph = iD.actionSplit('b', ['='])(graph); - graph = iD.actionSplit('b', ['='])(graph); + expect(graph.entity('r').members).to.eql([ + {id: '-', type: 'way', role: 'forward'}, + {id: '=', type: 'way', role: 'forward'} + ]); + }); + + it('adds the new way to parent relations (forward order)', function () { + // Situation: + // a ---- b ---- c ~~~~ d + // Relation: [----, ~~~~] + // + // Split at b. + // + // Expected result: + // a ---- b ==== c ~~~~ d + // Relation: [----, ====, ~~~~] + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [{id: '-', type: 'way'}, {id: '~', type: 'way'}]}) + ]); - var ids = graph.entity('r').members.map(function(m) { return m.id; }); - expect(ids).to.have.ordered.members(['~', '-', '=']); - }); + graph = iD.actionSplit('b', ['='])(graph); + + var ids = graph.entity('r').members.map(function(m) { return m.id; }); + expect(ids).to.have.ordered.members(['-', '=', '~']); + }); + + it('adds the new way to parent relations (reverse order)', function () { + // Situation: + // a ---- b ---- c ~~~~ d + // Relation: [~~~~, ----] + // + // Split at b. + // + // Expected result: + // a ---- b ==== c ~~~~ d + // Relation: [~~~~, ====, ----] + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) + ]); - it('converts simple multipolygon to a proper multipolygon', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({'id': '-', nodes: ['a', 'b', 'c'], tags: {natural: 'water'}}), - iD.Relation({id: 'r', members: [{id: '-', type: 'way', role: 'outer'}], tags: {type: 'multipolygon'}}) - ]); + graph = iD.actionSplit('b', ['='])(graph); - graph = iD.actionSplit('b', ['='])(graph); + var ids = graph.entity('r').members.map(function(m) { return m.id; }); + expect(ids).to.have.ordered.members(['~', '=', '-']); + }); + }); - expect(graph.entity('-').tags).to.eql({}); - expect(graph.entity('r').tags).to.eql({type: 'multipolygon', natural: 'water'}); - var ids = graph.entity('r').members.map(function(m) { return m.id; }); - expect(ids).to.have.ordered.members(['-', '=']); - }); - ['restriction', 'restriction:bus'].forEach(function (type) { - it('updates a restriction\'s \'from\' role', function () { - // Situation: - // a ----> b ----> c ~~~~ d - // A restriction from ---- to ~~~~ via c. - // - // Split at b. - // - // Expected result: - // a ----> b ====> c ~~~~ d - // A restriction from ==== to ~~~~ via c. - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - iD.Way({id: '~', nodes: ['c', 'd']}), - iD.Relation({id: 'r', tags: {type: type}, members: [ - {id: '-', role: 'from', type: 'way'}, - {id: '~', role: 'to', type: 'way'}, - {id: 'c', role: 'via', type: 'node'} - ]}) + describe('type = multipolygon', function () { + + it('splits an area by converting it to a multipolygon', function () { + // Situation: + // a ---- b + // | | + // d ---- c + // + // Split at a. + // + // Expected result: + // a ---- b + // || | + // d ==== c + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0,1]}), + iD.osmNode({id: 'b', loc: [1,1]}), + iD.osmNode({id: 'c', loc: [1,0]}), + iD.osmNode({id: 'd', loc: [0,0]}), + iD.osmWay({id: '-', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}) ]); - graph = iD.actionSplit('b', ['='])(graph); + graph = iD.actionSplit('a', ['='])(graph); + expect(graph.entity('-').tags).to.eql({}); + expect(graph.entity('=').tags).to.eql({}); + expect(graph.parentRelations(graph.entity('-'))).to.have.length(1); - expect(graph.entity('r').members).to.eql([ - {id: '=', role: 'from', type: 'way'}, - {id: '~', role: 'to', type: 'way'}, - {id: 'c', role: 'via', type: 'node'} - ]); - }); + var relation = graph.parentRelations(graph.entity('-'))[0]; + expect(relation.tags).to.eql({type: 'multipolygon', building: 'yes'}); + expect(relation.members).to.eql([ + {id: '-', role: 'outer', type: 'way'}, + {id: '=', role: 'outer', type: 'way'} + ]); + }); + + it('splits only the line of a node shared by a line and an area', function () { + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0,1]}), + iD.osmNode({id: 'b', loc: [1,1]}), + iD.osmNode({id: 'c', loc: [1,0]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '=', nodes: ['a', 'b', 'c', 'a'], tags: {area: 'yes'}}) + ]); - it('updates a restriction\'s \'to\' role', function () { - // Situation: - // a ----> b ----> c ~~~~ d - // A restriction from ~~~~ to ---- via c. - // - // Split at b. - // - // Expected result: - // a ----> b ====> c ~~~~ d - // A restriction from ~~~~ to ==== via c. - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - iD.Way({id: '~', nodes: ['c', 'd']}), - iD.Relation({id: 'r', tags: {type: type}, members: [ - {id: '~', role: 'from', type: 'way'}, - {id: '-', role: 'to', type: 'way'}, - {id: 'c', role: 'via', type: 'node'} - ]}) + graph = iD.actionSplit('b', ['~'])(graph); + + expect(graph.entity('-').nodes).to.eql(['a', 'b']); + expect(graph.entity('~').nodes).to.eql(['b', 'c']); + expect(graph.entity('=').nodes).to.eql(['a', 'b', 'c', 'a']); + expect(graph.parentRelations(graph.entity('='))).to.have.length(0); + }); + + it('converts simple multipolygon to a proper multipolygon', function () { + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({'id': '-', nodes: ['a', 'b', 'c'], tags: {natural: 'water'}}), + iD.osmRelation({id: 'r', members: [{id: '-', type: 'way', role: 'outer'}], tags: {type: 'multipolygon'}}) ]); - graph = iD.actionSplit('b', ['='])(graph); + graph = iD.actionSplit('b', ['='])(graph); - expect(graph.entity('r').members).to.eql([ - {id: '~', role: 'from', type: 'way'}, - {id: '=', role: 'to', type: 'way'}, - {id: 'c', role: 'via', type: 'node'} - ]); + expect(graph.entity('-').tags).to.eql({}); + expect(graph.entity('r').tags).to.eql({type: 'multipolygon', natural: 'water'}); + var ids = graph.entity('r').members.map(function(m) { return m.id; }); + expect(ids).to.have.ordered.members(['-', '=']); + }); }); - it('updates both \'to\' and \'from\' roles for u-turn restrictions', function () { - // Situation: - // a ----> b ----> c ~~~~ d - // A restriction from ---- to ---- via c. - // - // Split at b. - // - // Expected result: - // a ----> b ====> c ~~~~ d - // A restriction from ==== to ==== via c. - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - iD.Way({id: '~', nodes: ['c', 'd']}), - iD.Relation({id: 'r', tags: {type: type}, members: [ - {id: '-', role: 'from', type: 'way'}, - {id: '-', role: 'to', type: 'way'}, + ['restriction', 'restriction:bus'].forEach(function (type) { + describe('type = ' + type, function () { + + it('updates a restriction\'s \'from\' role', function () { + // Situation: + // a ----> b ----> c ~~~~ d + // A restriction from ---- to ~~~~ via c. + // + // Split at b. + // + // Expected result: + // a ----> b ====> c ~~~~ d + // A restriction from ==== to ~~~~ via c. + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', tags: {type: type}, members: [ + {id: '-', role: 'from', type: 'way'}, + {id: '~', role: 'to', type: 'way'}, + {id: 'c', role: 'via', type: 'node'} + ]}) + ]); + + graph = iD.actionSplit('b', ['='])(graph); + + expect(graph.entity('r').members).to.eql([ + {id: '=', role: 'from', type: 'way'}, + {id: '~', role: 'to', type: 'way'}, {id: 'c', role: 'via', type: 'node'} - ]}) - ]); - - graph = iD.actionSplit('b', ['='])(graph); - - expect(graph.entity('r').members).to.eql([ - {id: '=', role: 'from', type: 'way'}, - {id: '=', role: 'to', type: 'way'}, - {id: 'c', role: 'via', type: 'node'} - ]); - }); - - it('leaves unaffected restrictions unchanged', function () { - // Situation: - // a <---- b <---- c ~~~~ d - // A restriction from ---- to ~~~~ via c. - // - // Split at b. - // - // Expected result: - // a <==== b <---- c ~~~~ d - // A restriction from ---- to ~~~~ via c. - // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['c', 'b', 'a']}), - iD.Way({id: '~', nodes: ['c', 'd']}), - iD.Relation({id: 'r', tags: {type: type}, members: [ + ]); + }); + + it('updates a restriction\'s \'to\' role', function () { + // Situation: + // a ----> b ----> c ~~~~ d + // A restriction from ~~~~ to ---- via c. + // + // Split at b. + // + // Expected result: + // a ----> b ====> c ~~~~ d + // A restriction from ~~~~ to ==== via c. + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', tags: {type: type}, members: [ + {id: '~', role: 'from', type: 'way'}, + {id: '-', role: 'to', type: 'way'}, + {id: 'c', role: 'via', type: 'node'} + ]}) + ]); + + graph = iD.actionSplit('b', ['='])(graph); + + expect(graph.entity('r').members).to.eql([ + {id: '~', role: 'from', type: 'way'}, + {id: '=', role: 'to', type: 'way'}, + {id: 'c', role: 'via', type: 'node'} + ]); + }); + + + it('updates both \'to\' and \'from\' roles for u-turn restrictions', function () { + // Situation: + // a ----> b ----> c ~~~~ d + // A restriction from ---- to ---- via c. + // + // Split at b. + // + // Expected result: + // a ----> b ====> c ~~~~ d + // A restriction from ==== to ==== via c. + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', tags: {type: type}, members: [ + {id: '-', role: 'from', type: 'way'}, + {id: '-', role: 'to', type: 'way'}, + {id: 'c', role: 'via', type: 'node'} + ]}) + ]); + + graph = iD.actionSplit('b', ['='])(graph); + + expect(graph.entity('r').members).to.eql([ + {id: '=', role: 'from', type: 'way'}, + {id: '=', role: 'to', type: 'way'}, + {id: 'c', role: 'via', type: 'node'} + ]); + }); + + it('leaves unaffected restrictions unchanged', function () { + // Situation: + // a <---- b <---- c ~~~~ d + // A restriction from ---- to ~~~~ via c. + // + // Split at b. + // + // Expected result: + // a <==== b <---- c ~~~~ d + // A restriction from ---- to ~~~~ via c. + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['c', 'b', 'a']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', tags: {type: type}, members: [ + {id: '-', role: 'from', type: 'way'}, + {id: '~', role: 'to', type: 'way'}, + {id: 'c', role: 'via', type: 'node'} + ]}) + ]); + + graph = iD.actionSplit('b', ['='])(graph); + + expect(graph.entity('r').members).to.eql([ {id: '-', role: 'from', type: 'way'}, {id: '~', role: 'to', type: 'way'}, {id: 'c', role: 'via', type: 'node'} - ]}) - ]); - - graph = iD.actionSplit('b', ['='])(graph); + ]); + }); + }); - expect(graph.entity('r').members).to.eql([ - {id: '-', role: 'from', type: 'way'}, - {id: '~', role: 'to', type: 'way'}, - {id: 'c', role: 'via', type: 'node'} - ]); }); }); }); diff --git a/test/spec/osm/multipolygon.js b/test/spec/osm/multipolygon.js index 6d0c9fb0b8..aea1d8a686 100644 --- a/test/spec/osm/multipolygon.js +++ b/test/spec/osm/multipolygon.js @@ -1,91 +1,101 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() { it('returns the parent relation of a simple multipolygon outer', function() { - var outer = iD.Way({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {type: 'multipolygon'}, - members: [{id: outer.id, role: 'outer'}]}), - graph = iD.Graph([outer, relation]); + var outer = iD.osmWay({tags: {'natural':'wood'}}); + var relation = iD.osmRelation( + {tags: {type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]} + ); + var graph = iD.coreGraph([outer, relation]); expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.equal(relation); }); it('returns the parent relation of a simple multipolygon outer, assuming role outer if unspecified', function() { - var outer = iD.Way({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {type: 'multipolygon'}, - members: [{id: outer.id}]}), - graph = iD.Graph([outer, relation]); + var outer = iD.osmWay({tags: {'natural':'wood'}}); + var relation = iD.osmRelation( + {tags: {type: 'multipolygon'}, members: [{id: outer.id}]} + ); + var graph = iD.coreGraph([outer, relation]); expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.equal(relation); }); it('returns false if entity is not a way', function() { - var outer = iD.Node({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {type: 'multipolygon'}, - members: [{id: outer.id, role: 'outer'}]}), - graph = iD.Graph([outer, relation]); + var outer = iD.osmNode({tags: {'natural':'wood'}}); + var relation = iD.osmRelation( + {tags: {type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]} + ); + var graph = iD.coreGraph([outer, relation]); expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.be.false; }); it('returns false if entity does not have interesting tags', function() { - var outer = iD.Way({tags: {'tiger:reviewed':'no'}}), - relation = iD.Relation({tags: {type: 'multipolygon'}, - members: [{id: outer.id, role: 'outer'}]}), - graph = iD.Graph([outer, relation]); + var outer = iD.osmWay({tags: {'tiger:reviewed':'no'}}); + var relation = iD.osmRelation( + {tags: {type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]} + ); + var graph = iD.coreGraph([outer, relation]); expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.be.false; }); it('returns false if entity does not have a parent relation', function() { - var outer = iD.Way({tags: {'natural':'wood'}}), - graph = iD.Graph([outer]); + var outer = iD.osmWay({tags: {'natural':'wood'}}); + var graph = iD.coreGraph([outer]); expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.be.false; }); it('returns false if the parent is not a multipolygon', function() { - var outer = iD.Way({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {type: 'route'}, - members: [{id: outer.id, role: 'outer'}]}), - graph = iD.Graph([outer, relation]); + var outer = iD.osmWay({tags: {'natural':'wood'}}); + var relation = iD.osmRelation( + {tags: {type: 'route'}, members: [{id: outer.id, role: 'outer'}]} + ); + var graph = iD.coreGraph([outer, relation]); expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.be.false; }); it('returns false if the parent has interesting tags', function() { - var outer = iD.Way({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {natural: 'wood', type: 'multipolygon'}, - members: [{id: outer.id, role: 'outer'}]}), - graph = iD.Graph([outer, relation]); + var outer = iD.osmWay({tags: {'natural':'wood'}}); + var relation = iD.osmRelation( + {tags: {natural: 'wood', type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]} + ); + var graph = iD.coreGraph([outer, relation]); expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.be.false; }); it('returns the parent relation of a simple multipolygon outer, ignoring uninteresting parent tags', function() { - var outer = iD.Way({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {'tiger:reviewed':'no', type: 'multipolygon'}, - members: [{id: outer.id, role: 'outer'}]}), - graph = iD.Graph([outer, relation]); + var outer = iD.osmWay({tags: {'natural':'wood'}}); + var relation = iD.osmRelation( + {tags: {'tiger:reviewed':'no', type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]} + ); + var graph = iD.coreGraph([outer, relation]); expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.equal(relation); }); it('returns false if the parent has multiple outer ways', function() { - var outer1 = iD.Way({tags: {'natural':'wood'}}), - outer2 = iD.Way({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {type: 'multipolygon'}, - members: [{id: outer1.id, role: 'outer'}, {id: outer2.id, role: 'outer'}]}), - graph = iD.Graph([outer1, outer2, relation]); + var outer1 = iD.osmWay({tags: {'natural':'wood'}}); + var outer2 = iD.osmWay({tags: {'natural':'wood'}}); + var relation = iD.osmRelation( + {tags: {type: 'multipolygon'}, members: [{id: outer1.id, role: 'outer'}, {id: outer2.id, role: 'outer'}]} + ); + var graph = iD.coreGraph([outer1, outer2, relation]); expect(iD.osmIsSimpleMultipolygonOuterMember(outer1, graph)).to.be.false; expect(iD.osmIsSimpleMultipolygonOuterMember(outer2, graph)).to.be.false; }); it('returns false if the parent has multiple outer ways, assuming role outer if unspecified', function() { - var outer1 = iD.Way({tags: {'natural':'wood'}}), - outer2 = iD.Way({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {type: 'multipolygon'}, - members: [{id: outer1.id}, {id: outer2.id}]}), - graph = iD.Graph([outer1, outer2, relation]); + var outer1 = iD.osmWay({tags: {'natural':'wood'}}); + var outer2 = iD.osmWay({tags: {'natural':'wood'}}); + var relation = iD.osmRelation( + {tags: {type: 'multipolygon'}, members: [{id: outer1.id}, {id: outer2.id}]} + ); + var graph = iD.coreGraph([outer1, outer2, relation]); expect(iD.osmIsSimpleMultipolygonOuterMember(outer1, graph)).to.be.false; expect(iD.osmIsSimpleMultipolygonOuterMember(outer2, graph)).to.be.false; }); it('returns false if the entity is not an outer', function() { - var inner = iD.Way({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {type: 'multipolygon'}, - members: [{id: inner.id, role: 'inner'}]}), - graph = iD.Graph([inner, relation]); + var inner = iD.osmWay({tags: {'natural':'wood'}}); + var relation = iD.osmRelation( + {tags: {type: 'multipolygon'}, members: [{id: inner.id, role: 'inner'}]} + ); + var graph = iD.coreGraph([inner, relation]); expect(iD.osmIsSimpleMultipolygonOuterMember(inner, graph)).to.be.false; }); }); @@ -93,28 +103,28 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() { describe('iD.osmSimpleMultipolygonOuterMember', function() { it('returns the outer member of a simple multipolygon', function() { - var inner = iD.Way(), - outer = iD.Way({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {type: 'multipolygon'}, members: [ - {id: outer.id, role: 'outer'}, - {id: inner.id, role: 'inner'}] - }), - graph = iD.Graph([inner, outer, relation]); + var inner = iD.osmWay(); + var outer = iD.osmWay({tags: {'natural':'wood'}}); + var relation = iD.osmRelation({tags: {type: 'multipolygon'}, members: [ + {id: outer.id, role: 'outer'}, + {id: inner.id, role: 'inner'}] + }); + var graph = iD.coreGraph([inner, outer, relation]); expect(iD.osmSimpleMultipolygonOuterMember(inner, graph)).to.equal(outer); expect(iD.osmSimpleMultipolygonOuterMember(outer, graph)).to.equal(outer); }); it('returns falsy for a complex multipolygon', function() { - var inner = iD.Way(), - outer1 = iD.Way({tags: {'natural':'wood'}}), - outer2 = iD.Way({tags: {'natural':'wood'}}), - relation = iD.Relation({tags: {type: 'multipolygon'}, members: [ - {id: outer1.id, role: 'outer'}, - {id: outer2.id, role: 'outer'}, - {id: inner.id, role: 'inner'}] - }), - graph = iD.Graph([inner, outer1, outer2, relation]); + var inner = iD.osmWay(); + var outer1 = iD.osmWay({tags: {'natural':'wood'}}); + var outer2 = iD.osmWay({tags: {'natural':'wood'}}); + var relation = iD.osmRelation({tags: {type: 'multipolygon'}, members: [ + {id: outer1.id, role: 'outer'}, + {id: outer2.id, role: 'outer'}, + {id: inner.id, role: 'inner'}] + }); + var graph = iD.coreGraph([inner, outer1, outer2, relation]); expect(iD.osmSimpleMultipolygonOuterMember(inner, graph)).not.to.be.ok; expect(iD.osmSimpleMultipolygonOuterMember(outer1, graph)).not.to.be.ok; @@ -122,12 +132,12 @@ describe('iD.osmSimpleMultipolygonOuterMember', function() { }); it('handles incomplete relations', function() { - var way = iD.Way({id: 'w'}), - relation = iD.Relation({id: 'r', tags: {type: 'multipolygon'}, members: [ - {id: 'o', role: 'outer'}, - {id: 'w', role: 'inner'}] - }), - graph = iD.Graph([way, relation]); + var way = iD.osmWay({id: 'w'}); + var relation = iD.osmRelation({id: 'r', tags: {type: 'multipolygon'}, members: [ + {id: 'o', role: 'outer'}, + {id: 'w', role: 'inner'}] + }); + var graph = iD.coreGraph([way, relation]); expect(iD.osmSimpleMultipolygonOuterMember(way, graph)).not.to.be.ok; }); @@ -136,11 +146,11 @@ describe('iD.osmSimpleMultipolygonOuterMember', function() { describe('iD.osmJoinWays', function() { it('returns an array of members with nodes properties', function() { - var node = iD.Node({loc: [0, 0]}), - way = iD.Way({nodes: [node.id]}), - member = {id: way.id, type: 'way'}, - graph = iD.Graph([node, way]), - result = iD.osmJoinWays([member], graph); + var node = iD.osmNode({loc: [0, 0]}); + var way = iD.osmWay({nodes: [node.id]}); + var member = {id: way.id, type: 'way'}; + var graph = iD.coreGraph([node, way]); + var result = iD.osmJoinWays([member], graph); expect(result.length).to.equal(1); expect(result[0].nodes.length).to.equal(1); @@ -150,16 +160,16 @@ describe('iD.osmJoinWays', function() { }); it('returns the members in the correct order', function() { - // a<===b--->c~~~>d - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [0, 0]}), - iD.Node({id: 'c', loc: [0, 0]}), - iD.Node({id: 'd', loc: [0, 0]}), - iD.Way({id: '=', nodes: ['b', 'a']}), - iD.Way({id: '-', nodes: ['b', 'c']}), - iD.Way({id: '~', nodes: ['c', 'd']}), - iD.Relation({id: 'r', members: [ + // a <=== b ---> c ~~~> d + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [0, 0]}), + iD.osmWay({id: '=', nodes: ['b', 'a']}), + iD.osmWay({id: '-', nodes: ['b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [ {id: '-', type: 'way'}, {id: '~', type: 'way'}, {id: '=', type: 'way'} @@ -176,28 +186,28 @@ describe('iD.osmJoinWays', function() { // Expected result: // a --> b --> c // tags on === reversed - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['c', 'b'], tags: {'oneway': 'yes', 'lanes:forward': 2}}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['c', 'b'], tags: {'oneway': 'yes', 'lanes:forward': 2}}) + ]); var result = iD.osmJoinWays([graph.entity('-'), graph.entity('=')], graph); expect(result[0][1].tags).to.eql({'oneway': '-1', 'lanes:backward': 2}); }); it('ignores non-way members', function() { - var node = iD.Node({loc: [0, 0]}), - member = {id: 'n', type: 'node'}, - graph = iD.Graph([node]); + var node = iD.osmNode({loc: [0, 0]}); + var member = {id: 'n', type: 'node'}; + var graph = iD.coreGraph([node]); expect(iD.osmJoinWays([member], graph)).to.eql([]); }); it('ignores incomplete members', function() { - var member = {id: 'w', type: 'way'}, - graph = iD.Graph(); + var member = {id: 'w', type: 'way'}; + var graph = iD.coreGraph(); expect(iD.osmJoinWays([member], graph)).to.eql([]); }); }); From 07262fa7118c8cf4e111ebe173bafbeafaf9928a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 9 Jan 2018 23:57:44 -0500 Subject: [PATCH 134/206] Add tests for #4589 --- test/spec/actions/add_member.js | 158 ++++++++++++++++++++++---------- test/spec/actions/split.js | 62 +++++++++++++ test/spec/osm/multipolygon.js | 32 +++++++ 3 files changed, 203 insertions(+), 49 deletions(-) diff --git a/test/spec/actions/add_member.js b/test/spec/actions/add_member.js index e5a5801c66..0d220b59a4 100644 --- a/test/spec/actions/add_member.js +++ b/test/spec/actions/add_member.js @@ -1,7 +1,7 @@ describe('iD.actionAddMember', function() { it('adds an member to a relation at the specified index', function() { - var r = iD.Relation({members: [{id: '1'}, {id: '3'}]}), - g = iD.actionAddMember(r.id, {id: '2'}, 1)(iD.Graph([r])); + var r = iD.osmRelation({members: [{id: '1'}, {id: '3'}]}); + var g = iD.actionAddMember(r.id, {id: '2'}, 1)(iD.coreGraph([r])); expect(g.entity(r.id).members).to.eql([{id: '1'}, {id: '2'}, {id: '3'}]); }); @@ -10,81 +10,141 @@ describe('iD.actionAddMember', function() { return graph.entity('r').members.map(function (m) { return m.id; }); } - specify('no members', function() { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [0, 0]}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Relation({id: 'r'}) + it('adds the member to a relation with no members', function() { + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmRelation({id: 'r'}) ]); graph = iD.actionAddMember('r', {id: '-', type: 'way'})(graph); expect(members(graph)).to.eql(['-']); }); - specify('not connecting', function() { - // a--->b c===>d - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [0, 0]}), - iD.Node({id: 'c', loc: [0, 0]}), - iD.Node({id: 'd', loc: [0, 0]}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['c', 'd']}), - iD.Relation({id: 'r', members: [{id: '-', type: 'way'}]}) + it('appends the member if the ways are not connecting', function() { + // Before: ---> + // After: ---> ... ===> + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [0, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [ + {id: '-', type: 'way'} + ]}) ]); graph = iD.actionAddMember('r', {id: '=', type: 'way'})(graph); expect(members(graph)).to.eql(['-', '=']); }); - specify('connecting at end', function() { - // a--->b===>c - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [0, 0]}), - iD.Node({id: 'c', loc: [0, 0]}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['b', 'c']}), - iD.Relation({id: 'r', members: [{id: '-', type: 'way'}]}) + it('appends the member if the way connects at end', function() { + // Before: ---> + // After: ---> ===> + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmRelation({id: 'r', members: [ + {id: '-', type: 'way'} + ]}) ]); graph = iD.actionAddMember('r', {id: '=', type: 'way'})(graph); expect(members(graph)).to.eql(['-', '=']); }); - specify('connecting at beginning', function() { - // a===>b--->c~~~>d - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [0, 0]}), - iD.Node({id: 'c', loc: [0, 0]}), - iD.Node({id: 'd', loc: [0, 0]}), - iD.Way({id: '=', nodes: ['a', 'b']}), - iD.Way({id: '-', nodes: ['b', 'c']}), - iD.Way({id: '~', nodes: ['c', 'd']}), - iD.Relation({id: 'r', members: [{id: '-', type: 'way'}, {id: '~', type: 'way'}]}) + it('inserts the member if the way connects at beginning', function() { + // Before: ---> ~~~> + // After: ===> ---> ~~~> + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [0, 0]}), + iD.osmWay({id: '=', nodes: ['a', 'b']}), + iD.osmWay({id: '-', nodes: ['b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [ + {id: '-', type: 'way'}, + {id: '~', type: 'way'} + ]}) ]); graph = iD.actionAddMember('r', {id: '=', type: 'way'})(graph); expect(members(graph)).to.eql(['=', '-', '~']); }); - specify('connecting in middle', function() { - // a--->b===>c~~~>d - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [0, 0]}), - iD.Node({id: 'c', loc: [0, 0]}), - iD.Node({id: 'd', loc: [0, 0]}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['b', 'c']}), - iD.Way({id: '~', nodes: ['c', 'd']}), - iD.Relation({id: 'r', members: [{id: '-', type: 'way'}, {id: '~', type: 'way'}]}) + it('inserts the member if the way connects in middle', function() { + // Before: ---> ~~~> + // After: ---> ===> ~~~> + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [0, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [ + {id: '-', type: 'way'}, + {id: '~', type: 'way'} + ]}) ]); graph = iD.actionAddMember('r', {id: '=', type: 'way'})(graph); expect(members(graph)).to.eql(['-', '=', '~']); }); + + it('inserts the member multiple times if the way exists multiple times (middle)', function() { + // Before: ---> ~~~> ---> + // After: ---> ===> ~~~> ===> ---> + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [0, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [ + {id: '-', type: 'way'}, + {id: '~', type: 'way'}, + {id: '-', type: 'way'} + ]}) + ]); + + graph = iD.actionAddMember('r', {id: '=', type: 'way'})(graph); + expect(members(graph)).to.eql(['-', '=', '~', '=', '-']); + }); + + it('inserts the member multiple times if the way exists multiple times (beginning/end)', function() { + // Before: ===> ~~~> ===> + // After: ---> ===> ~~~> ===> ---> + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [0, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [ + {id: '=', type: 'way'}, + {id: '~', type: 'way'}, + {id: '=', type: 'way'} + ]}) + ]); + + graph = iD.actionAddMember('r', {id: '-', type: 'way'})(graph); + expect(members(graph)).to.eql(['-', '=', '~', '=', '-']); + }); + + }); }); diff --git a/test/spec/actions/split.js b/test/spec/actions/split.js index d09148499c..cc5fd31c41 100644 --- a/test/spec/actions/split.js +++ b/test/spec/actions/split.js @@ -394,6 +394,68 @@ describe('iD.actionSplit', function () { var ids = graph.entity('r').members.map(function(m) { return m.id; }); expect(ids).to.have.ordered.members(['~', '=', '-']); }); + + it('adds the new way to parent relations (unsplit way belongs multiple times)', function () { + // Situation: + // a ---- b ---- c ~~~~ d + // Relation: [~~~~, ----, ~~~~] + // + // Split at b. + // + // Expected result: + // a ---- b ==== c ~~~~ d + // Relation: [~~~~, ====, ----, ====, ~~~~] + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [ + {id: '~', type: 'way'}, + {id: '-', type: 'way'}, + {id: '~', type: 'way'} + ]}) + ]); + + graph = iD.actionSplit('b', ['='])(graph); + + var ids = graph.entity('r').members.map(function(m) { return m.id; }); + expect(ids).to.have.ordered.members(['~', '=', '-', '=', '~']); + }); + + it('adds the new way to parent relations (split way belongs multiple times)', function () { + // Situation: + // a ---- b ---- c ~~~~ d + // Relation: [----, ~~~~, ----] + // + // Split at b. + // + // Expected result: + // a ---- b ==== c ~~~~ d + // Relation: [----, ====, ~~~~, ====, ----] + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [ + {id: '-', type: 'way'}, + {id: '~', type: 'way'}, + {id: '-', type: 'way'} + ]}) + ]); + + graph = iD.actionSplit('b', ['='])(graph); + + var ids = graph.entity('r').members.map(function(m) { return m.id; }); + expect(ids).to.have.ordered.members(['-', '=', '~', '=', '-']); + }); }); diff --git a/test/spec/osm/multipolygon.js b/test/spec/osm/multipolygon.js index aea1d8a686..ea0fd9c3d3 100644 --- a/test/spec/osm/multipolygon.js +++ b/test/spec/osm/multipolygon.js @@ -210,4 +210,36 @@ describe('iD.osmJoinWays', function() { var graph = iD.coreGraph(); expect(iD.osmJoinWays([member], graph)).to.eql([]); }); + + it('understands doubled-back relation members', function() { + // e + // / \ + // a <=== b ---> c ~~~> d + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [0, 0]}), + iD.osmNode({id: 'e', loc: [0, 0]}), + iD.osmWay({id: '=', nodes: ['b', 'a']}), + iD.osmWay({id: '-', nodes: ['b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmWay({id: '\\', nodes: ['d', 'e']}), + iD.osmWay({id: '/', nodes: ['c', 'e']}), + iD.osmRelation({id: 'r', members: [ + {id: '=', type: 'way'}, + {id: '-', type: 'way'}, + {id: '~', type: 'way'}, + {id: '\\', type: 'way'}, + {id: '/', type: 'way'}, + {id: '-', type: 'way'}, + {id: '=', type: 'way'} + ]}) + ]); + + var result = iD.osmJoinWays(graph.entity('r').members, graph); + var ids = result[0].map(function (w) { return w.id; }); + expect(ids).to.have.ordered.members(['=', '-', '~', '\\', '/', '-', '=']); + }); + }); From c796a9de1288990ddfbe6668c0fc467a7dabb2c6 Mon Sep 17 00:00:00 2001 From: Nicolas Decoster Date: Wed, 10 Jan 2018 15:42:56 +0100 Subject: [PATCH 135/206] Increase GPX labels readability --- css/20_map.css | 10 +++++++++- modules/svg/gpx.js | 33 +++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index f5d8e168ae..8c3296b349 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -202,6 +202,7 @@ text.pointlabel { dominant-baseline: auto; } +text.gpxlabel-halo, .layer-labels-halo text { opacity: 0.7; stroke: #fff; @@ -259,6 +260,13 @@ path.gpx { fill: none; } -text.gpx { +text.gpxlabel { fill: #ff26d4; } + +text.gpxlabel-halo, +text.gpxlabel { + font-size: 12px; + font-weight: bold; + dominant-baseline: middle; +} \ No newline at end of file diff --git a/modules/svg/gpx.js b/modules/svg/gpx.js index f1274203c7..3d273986a3 100644 --- a/modules/svg/gpx.js +++ b/modules/svg/gpx.js @@ -57,7 +57,6 @@ export function svgGpx(projection, context, dispatch) { svgGpx.initialized = true; } - function drawGpx(selection) { var geojson = svgGpx.geojson, enabled = svgGpx.enabled; @@ -93,16 +92,26 @@ export function svgGpx(projection, context, dispatch) { .attr('d', path); - var labels = layer.selectAll('text') - .data(showLabels && geojson.features ? geojson.features : []); - - labels.exit() - .remove(); - - labels = labels.enter() - .append('text') - .attr('class', 'gpx') - .merge(labels); + function createLabels(layer, textClass, data) { + var labels = layer.selectAll('text.' + textClass) + .data(data); + + labels.exit() + .remove(); + + labels = labels.enter() + .append('text') + .attr('class', textClass) + .merge(labels); + + return labels; + } + + var labelsData = showLabels && geojson.features ? geojson.features : []; + createLabels(layer, 'gpxlabel-halo', labelsData); + createLabels(layer, 'gpxlabel', labelsData); + + labels = layer.selectAll('text'); labels .text(function(d) { @@ -110,7 +119,7 @@ export function svgGpx(projection, context, dispatch) { }) .attr('x', function(d) { var centroid = path.centroid(d); - return centroid[0] + 7; + return centroid[0] + 11; }) .attr('y', function(d) { var centroid = path.centroid(d); From 8f6cb207fcb624afe9a194a71ed6178c410da41a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 11 Jan 2018 21:42:29 -0500 Subject: [PATCH 136/206] Much expanded tests for osmJoinWays --- test/spec/actions/add_member.js | 30 ++-- test/spec/osm/multipolygon.js | 297 ++++++++++++++++++++++++-------- 2 files changed, 244 insertions(+), 83 deletions(-) diff --git a/test/spec/actions/add_member.js b/test/spec/actions/add_member.js index 0d220b59a4..95e9d98a6b 100644 --- a/test/spec/actions/add_member.js +++ b/test/spec/actions/add_member.js @@ -23,8 +23,8 @@ describe('iD.actionAddMember', function() { }); it('appends the member if the ways are not connecting', function() { - // Before: ---> - // After: ---> ... ===> + // Before: a ---> b + // After: a ---> b .. c ===> d var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), iD.osmNode({id: 'b', loc: [0, 0]}), @@ -42,8 +42,8 @@ describe('iD.actionAddMember', function() { }); it('appends the member if the way connects at end', function() { - // Before: ---> - // After: ---> ===> + // Before: a ---> b + // After: a ---> b ===> c var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), iD.osmNode({id: 'b', loc: [0, 0]}), @@ -60,8 +60,8 @@ describe('iD.actionAddMember', function() { }); it('inserts the member if the way connects at beginning', function() { - // Before: ---> ~~~> - // After: ===> ---> ~~~> + // Before: b ---> c ~~~> d + // After: a ===> b ---> c ~~~> d var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), iD.osmNode({id: 'b', loc: [0, 0]}), @@ -81,8 +81,8 @@ describe('iD.actionAddMember', function() { }); it('inserts the member if the way connects in middle', function() { - // Before: ---> ~~~> - // After: ---> ===> ~~~> + // Before: a ---> b .. c ~~~> d + // After: a ---> b ===> c ~~~> d var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), iD.osmNode({id: 'b', loc: [0, 0]}), @@ -102,8 +102,8 @@ describe('iD.actionAddMember', function() { }); it('inserts the member multiple times if the way exists multiple times (middle)', function() { - // Before: ---> ~~~> ---> - // After: ---> ===> ~~~> ===> ---> + // Before: a ---> b .. c ~~~> d <~~~ c .. b <--- a + // After: a ---> b ===> c ~~~> d <~~~ c <=== b <--- a var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), iD.osmNode({id: 'b', loc: [0, 0]}), @@ -115,17 +115,18 @@ describe('iD.actionAddMember', function() { iD.osmRelation({id: 'r', members: [ {id: '-', type: 'way'}, {id: '~', type: 'way'}, + {id: '~', type: 'way'}, {id: '-', type: 'way'} ]}) ]); graph = iD.actionAddMember('r', {id: '=', type: 'way'})(graph); - expect(members(graph)).to.eql(['-', '=', '~', '=', '-']); + expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']); }); it('inserts the member multiple times if the way exists multiple times (beginning/end)', function() { - // Before: ===> ~~~> ===> - // After: ---> ===> ~~~> ===> ---> + // Before: b ===> c ~~~> d <~~~ c <=== b + // After: a ---> b ===> c ~~~> d <~~~ c <=== b <--- a var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), iD.osmNode({id: 'b', loc: [0, 0]}), @@ -137,12 +138,13 @@ describe('iD.actionAddMember', function() { iD.osmRelation({id: 'r', members: [ {id: '=', type: 'way'}, {id: '~', type: 'way'}, + {id: '~', type: 'way'}, {id: '=', type: 'way'} ]}) ]); graph = iD.actionAddMember('r', {id: '-', type: 'way'})(graph); - expect(members(graph)).to.eql(['-', '=', '~', '=', '-']); + expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']); }); diff --git a/test/spec/osm/multipolygon.js b/test/spec/osm/multipolygon.js index ea0fd9c3d3..b71249c74e 100644 --- a/test/spec/osm/multipolygon.js +++ b/test/spec/osm/multipolygon.js @@ -145,56 +145,122 @@ describe('iD.osmSimpleMultipolygonOuterMember', function() { describe('iD.osmJoinWays', function() { + function getIDs(objects) { + return objects.map(function(node) { return node.id; }); + } + it('returns an array of members with nodes properties', function() { - var node = iD.osmNode({loc: [0, 0]}); - var way = iD.osmWay({nodes: [node.id]}); - var member = {id: way.id, type: 'way'}; + var node = iD.osmNode({id: 'a', loc: [0, 0]}); + var way = iD.osmWay({id: '-', nodes: ['a']}); + var member = {id: '-', type: 'way'}; var graph = iD.coreGraph([node, way]); + var result = iD.osmJoinWays([member], graph); expect(result.length).to.equal(1); - expect(result[0].nodes.length).to.equal(1); - expect(result[0].nodes[0]).to.equal(node); + expect(getIDs(result[0].nodes)).to.eql(['a']); expect(result[0].length).to.equal(1); - expect(result[0][0]).to.equal(member); - }); - - it('returns the members in the correct order', function() { - // a <=== b ---> c ~~~> d - var graph = iD.coreGraph([ - iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [0, 0]}), - iD.osmNode({id: 'c', loc: [0, 0]}), - iD.osmNode({id: 'd', loc: [0, 0]}), - iD.osmWay({id: '=', nodes: ['b', 'a']}), - iD.osmWay({id: '-', nodes: ['b', 'c']}), - iD.osmWay({id: '~', nodes: ['c', 'd']}), - iD.osmRelation({id: 'r', members: [ - {id: '-', type: 'way'}, - {id: '~', type: 'way'}, - {id: '=', type: 'way'} - ]}) - ]); - - var result = iD.osmJoinWays(graph.entity('r').members, graph); - var ids = result[0].map(function (w) { return w.id; }); - expect(ids).to.have.ordered.members(['=', '-', '~']); + expect(result[0][0]).to.have.own.property('id', '-'); + expect(result[0][0]).to.have.own.property('type', 'way'); + }); + + it('joins ways', function() { + // + // a ---> b ===> c + // + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [1, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var w1 = iD.osmWay({id: '-', nodes: ['a', 'b']}); + var w2 = iD.osmWay({id: '=', nodes: ['b', 'c']}); + var graph = iD.coreGraph([a, b, c, w1, w2]); + + var result = iD.osmJoinWays([w1, w2], graph); + expect(result.length).to.equal(1); + expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); + expect(result[0].length).to.equal(2); + expect(result[0][0]).to.eql(w1); + expect(result[0][1]).to.eql(w2); + }); + + it('joins relation members', function() { + // + // a ---> b ===> c + // r: ['-', '='] + // + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [1, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var w1 = iD.osmWay({id: '-', nodes: ['a', 'b']}); + var w2 = iD.osmWay({id: '=', nodes: ['b', 'c']}); + var r = iD.osmRelation({id: 'r', members: [ + {id: '-', type: 'way'}, + {id: '=', type: 'way'} + ]}); + var graph = iD.coreGraph([a, b, c, w1, w2, r]); + + var result = iD.osmJoinWays(r.members, graph); + expect(result.length).to.equal(1); + expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); + expect(result[0].length).to.equal(2); + expect(result[0][0]).to.have.own.property('id', '-'); + expect(result[0][0]).to.have.own.property('type', 'way'); + expect(result[0][1]).to.have.own.property('id', '='); + expect(result[0][1]).to.have.own.property('type', 'way'); + }); + + it('returns joined members in the correct order', function() { + // + // a <=== b ---> c ~~~> d + // r: ['-', '~', '='] + // + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [1, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [3, 0]}); + var w1 = iD.osmWay({id: '-', nodes: ['b', 'c']}); + var w2 = iD.osmWay({id: '=', nodes: ['b', 'a']}); + var w3 = iD.osmWay({id: '~', nodes: ['c', 'd']}); + var r = iD.osmRelation({id: 'r', members: [ + {id: '-', type: 'way'}, + {id: '~', type: 'way'}, + {id: '=', type: 'way'} + ]}); + var graph = iD.coreGraph([a, b, c, d, w1, w2, w3, r]); + + var result = iD.osmJoinWays(r.members, graph); + expect(result.length).to.equal(1); + expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c', 'd']); + expect(result[0].length).to.equal(3); + expect(result[0][0]).to.have.own.property('id', '='); + expect(result[0][0]).to.have.own.property('type', 'way'); + expect(result[0][1]).to.have.own.property('id', '-'); + expect(result[0][1]).to.have.own.property('type', 'way'); + expect(result[0][2]).to.have.own.property('id', '~'); + expect(result[0][2]).to.have.own.property('type', 'way'); }); it('reverses member tags of reversed segements', function() { - // a --> b <== c - // Expected result: - // a --> b --> c - // tags on === reversed - var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmWay({id: '-', nodes: ['a', 'b']}), - iD.osmWay({id: '=', nodes: ['c', 'b'], tags: {'oneway': 'yes', 'lanes:forward': 2}}) - ]); - - var result = iD.osmJoinWays([graph.entity('-'), graph.entity('=')], graph); + // + // Source: + // a ---> b <=== c + // Result: + // a ---> b ===> c (and b === c reversed) + // + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [1, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var w1 = iD.osmWay({id: '-', nodes: ['a', 'b']}); + var w2 = iD.osmWay({id: '=', nodes: ['c', 'b'], tags: {'oneway': 'yes', 'lanes:forward': 2}}); + var graph = iD.coreGraph([a, b, c, w1, w2]); + + var result = iD.osmJoinWays([w1, w2], graph); + expect(result.length).to.equal(1); + expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); + expect(result[0].length).to.equal(2); + expect(result[0][0]).to.eql(w1); + expect(result[0][1]).to.be.an.instanceof(iD.osmWay); + expect(result[0][1].nodes).to.eql(['b', 'c']); expect(result[0][1].tags).to.eql({'oneway': '-1', 'lanes:backward': 2}); }); @@ -211,35 +277,128 @@ describe('iD.osmJoinWays', function() { expect(iD.osmJoinWays([member], graph)).to.eql([]); }); + it('returns multiple arrays for disjoint ways', function() { + // + // b + // / \ d ---> e ===> f + // a c + // + var a = iD.osmNode({id: 'a', loc: [0, -1]}); + var b = iD.osmNode({id: 'b', loc: [1, 1]}); + var c = iD.osmNode({id: 'c', loc: [2, -1]}); + var d = iD.osmNode({id: 'd', loc: [5, 0]}); + var e = iD.osmNode({id: 'e', loc: [6, 0]}); + var f = iD.osmNode({id: 'f', loc: [7, 0]}); + var w1 = iD.osmWay({id: '/', nodes: ['a', 'b']}); + var w2 = iD.osmWay({id: '\\', nodes: ['b', 'c']}); + var w3 = iD.osmWay({id: '-', nodes: ['d', 'e']}); + var w4 = iD.osmWay({id: '=', nodes: ['e', 'f']}); + var graph = iD.coreGraph([a, b, c, d, e, f, w1, w2, w3, w4]); + + var result = iD.osmJoinWays([w1, w2, w3, w4], graph); + + expect(result.length).to.equal(2); + expect(result[0].length).to.equal(2); + expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); + expect(result[0][0]).to.eql(w1); + expect(result[0][1]).to.eql(w2); + + expect(result[1].length).to.equal(2); + expect(getIDs(result[1].nodes)).to.eql(['d', 'e', 'f']); + expect(result[1][0]).to.eql(w3); + expect(result[1][1]).to.eql(w4); + }); + + it('returns multiple arrays for disjoint relations', function() { + // + // b + // / \ + // a c d ---> e ===> f + // + // r: ['/', '\', '-', '='] + // + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [1, 1]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [5, 0]}); + var e = iD.osmNode({id: 'e', loc: [6, 0]}); + var f = iD.osmNode({id: 'f', loc: [7, 0]}); + var w1 = iD.osmWay({id: '/', nodes: ['a', 'b']}); + var w2 = iD.osmWay({id: '\\', nodes: ['b', 'c']}); + var w3 = iD.osmWay({id: '-', nodes: ['d', 'e']}); + var w4 = iD.osmWay({id: '=', nodes: ['e', 'f']}); + var r = iD.osmRelation({id: 'r', members: [ + {id: '/', type: 'way'}, + {id: '\\', type: 'way'}, + {id: '-', type: 'way'}, + {id: '=', type: 'way'} + ]}); + var graph = iD.coreGraph([a, b, c, d, e, f, w1, w2, w3, w4, r]); + var result = iD.osmJoinWays(r.members, graph); + + expect(result.length).to.equal(2); + expect(result[0].length).to.equal(2); + expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); + expect(result[0][0]).to.have.own.property('id', '/'); + expect(result[0][0]).to.have.own.property('type', 'way'); + expect(result[0][1]).to.have.own.property('id', '\\'); + expect(result[0][1]).to.have.own.property('type', 'way'); + + expect(result[1].length).to.equal(2); + expect(getIDs(result[1].nodes)).to.eql(['d', 'e', 'f']); + expect(result[1][0]).to.have.own.property('id', '-'); + expect(result[1][0]).to.have.own.property('type', 'way'); + expect(result[1][1]).to.have.own.property('id', '='); + expect(result[1][1]).to.have.own.property('type', 'way'); + }); + it('understands doubled-back relation members', function() { - // e - // / \ - // a <=== b ---> c ~~~> d - var graph = iD.coreGraph([ - iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [0, 0]}), - iD.osmNode({id: 'c', loc: [0, 0]}), - iD.osmNode({id: 'd', loc: [0, 0]}), - iD.osmNode({id: 'e', loc: [0, 0]}), - iD.osmWay({id: '=', nodes: ['b', 'a']}), - iD.osmWay({id: '-', nodes: ['b', 'c']}), - iD.osmWay({id: '~', nodes: ['c', 'd']}), - iD.osmWay({id: '\\', nodes: ['d', 'e']}), - iD.osmWay({id: '/', nodes: ['c', 'e']}), - iD.osmRelation({id: 'r', members: [ - {id: '=', type: 'way'}, - {id: '-', type: 'way'}, - {id: '~', type: 'way'}, - {id: '\\', type: 'way'}, - {id: '/', type: 'way'}, - {id: '-', type: 'way'}, - {id: '=', type: 'way'} - ]}) - ]); - - var result = iD.osmJoinWays(graph.entity('r').members, graph); - var ids = result[0].map(function (w) { return w.id; }); - expect(ids).to.have.ordered.members(['=', '-', '~', '\\', '/', '-', '=']); + // + // e + // / \ + // a <=== b ---> c ~~~> d + // + // r: ['=', '-', '~', '\', '/', '-', '='] + // + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [1, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var d = iD.osmNode({id: 'd', loc: [4, 0]}); + var e = iD.osmNode({id: 'e', loc: [3, 1]}); + var w1 = iD.osmWay({id: '=', nodes: ['b', 'a']}); + var w2 = iD.osmWay({id: '-', nodes: ['b', 'c']}); + var w3 = iD.osmWay({id: '~', nodes: ['c', 'd']}); + var w4 = iD.osmWay({id: '\\', nodes: ['d', 'e']}); + var w5 = iD.osmWay({id: '/', nodes: ['c', 'e']}); + var r = iD.osmRelation({id: 'r', members: [ + {id: '=', type: 'way'}, + {id: '-', type: 'way'}, + {id: '~', type: 'way'}, + {id: '\\', type: 'way'}, + {id: '/', type: 'way'}, + {id: '-', type: 'way'}, + {id: '=', type: 'way'} + ]}); + var graph = iD.coreGraph([a, b, c, d, e, w1, w2, w3, w4, w5, r]); + + var result = iD.osmJoinWays(r.members, graph); + expect(result.length).to.equal(1); + expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c', 'd', 'e', 'c', 'b', 'a']); + expect(result[0].length).to.equal(7); + expect(result[0][0]).to.have.own.property('id', '='); + expect(result[0][0]).to.have.own.property('type', 'way'); + expect(result[0][1]).to.have.own.property('id', '-'); + expect(result[0][1]).to.have.own.property('type', 'way'); + expect(result[0][2]).to.have.own.property('id', '~'); + expect(result[0][2]).to.have.own.property('type', 'way'); + expect(result[0][3]).to.have.own.property('id', '\\'); + expect(result[0][3]).to.have.own.property('type', 'way'); + expect(result[0][4]).to.have.own.property('id', '/'); + expect(result[0][4]).to.have.own.property('type', 'way'); + expect(result[0][5]).to.have.own.property('id', '-'); + expect(result[0][5]).to.have.own.property('type', 'way'); + expect(result[0][6]).to.have.own.property('id', '='); + expect(result[0][6]).to.have.own.property('type', 'way'); }); }); From e8a68205abfc283252433512b24d81cca453a95a Mon Sep 17 00:00:00 2001 From: Lukas Toggenburger Date: Fri, 12 Jan 2018 11:58:58 +0100 Subject: [PATCH 137/206] Add ref:isil=* for libraries --- data/presets/fields/ref/isil.json | 5 +++++ data/presets/presets/amenity/library.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 data/presets/fields/ref/isil.json diff --git a/data/presets/fields/ref/isil.json b/data/presets/fields/ref/isil.json new file mode 100644 index 0000000000..a071099d33 --- /dev/null +++ b/data/presets/fields/ref/isil.json @@ -0,0 +1,5 @@ +{ + "key": "ref:isil", + "type": "text", + "label": "International Standard Identifier for Libraries and Related Organisations" +} diff --git a/data/presets/presets/amenity/library.json b/data/presets/presets/amenity/library.json index 0f3a48c770..f6fc301cd1 100644 --- a/data/presets/presets/amenity/library.json +++ b/data/presets/presets/amenity/library.json @@ -8,7 +8,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "geometry": [ "point", From 4544bbc1f5b2cc3e636751db655d2ba280198fa8 Mon Sep 17 00:00:00 2001 From: Lukas Toggenburger Date: Fri, 12 Jan 2018 17:31:05 +0100 Subject: [PATCH 138/206] Shorten label of field ref:isil=* --- data/presets/fields/ref/isil.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/presets/fields/ref/isil.json b/data/presets/fields/ref/isil.json index a071099d33..fcf0a0ffe2 100644 --- a/data/presets/fields/ref/isil.json +++ b/data/presets/fields/ref/isil.json @@ -1,5 +1,5 @@ { "key": "ref:isil", "type": "text", - "label": "International Standard Identifier for Libraries and Related Organisations" + "label": "ISIL Code" } From 0fd801d750733e01764dab2fe477e69323451213 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 12 Jan 2018 17:23:56 -0500 Subject: [PATCH 139/206] Prefer to join member ways in a way that preserves their order (re #4589) Strongly prefer to generate a forward path that preserves the order of the members array. For multipolygons and most relations, member order does not matter - but for routes, it does. If we started this sequence backwards (i.e. next member way attaches to the start node and not the end node), reverse the initial way before continuing. --- modules/osm/multipolygon.js | 161 ++++++++++++++++++++++++++-------- test/spec/osm/multipolygon.js | 88 +++++++++++-------- 2 files changed, 171 insertions(+), 78 deletions(-) diff --git a/modules/osm/multipolygon.js b/modules/osm/multipolygon.js index 05f37bf959..92ed38a228 100644 --- a/modules/osm/multipolygon.js +++ b/modules/osm/multipolygon.js @@ -82,13 +82,7 @@ export function osmSimpleMultipolygonOuterMember(entity, graph) { // Incomplete members (those for which `graph.hasEntity(element.id)` returns // false) and non-way members are ignored. // -export function osmJoinWays(array, graph) { - var joined = [], member, current, nodes, first, last, i, how, what; - - array = array.filter(function(member) { - return member.type === 'way' && graph.hasEntity(member.id); - }); - +export function osmJoinWays(toJoin, graph) { function resolve(member) { return graph.childNodes(graph.entity(member.id)); } @@ -97,52 +91,141 @@ export function osmJoinWays(array, graph) { return member.tags ? actionReverse(member.id, { reverseOneway: true })(graph).entity(member.id) : member; } - while (array.length) { - member = array.shift(); - current = [member]; - current.nodes = nodes = resolve(member).slice(); - joined.push(current); - while (array.length && nodes[0] !== nodes[nodes.length - 1]) { - first = nodes[0]; - last = nodes[nodes.length - 1]; + // make a copy containing only the ways to join + toJoin = toJoin.filter(function(member) { + return member.type === 'way' && graph.hasEntity(member.id); + }); - for (i = 0; i < array.length; i++) { - member = array[i]; - what = resolve(member); + var sequences = []; + + while (toJoin.length) { + // start a new sequence + var way = toJoin.shift(); + var currWays = [way]; + var currNodes = resolve(way).slice(); + var doneSequence = false; + + // add to it + while (toJoin.length && !doneSequence) { + var start = currNodes[0]; + var end = currNodes[currNodes.length - 1]; + var fn = null; + var nodes = null; + var i; + + // find the next way + for (i = 0; i < toJoin.length; i++) { + way = toJoin[i]; + nodes = resolve(way); + + // Strongly prefer to generate a forward path that preserves the order + // of the members array. For multipolygons and most relations, member + // order does not matter - but for routes, it does. If we started this + // sequence backwards (i.e. next member way attaches to the start node + // and not the end node), reverse the initial way before continuing. + if (currWays.length === 1 && nodes[0] !== end && nodes[nodes.length - 1] !== end && + (nodes[nodes.length - 1] === start || nodes[0] === start) + ) { + currWays[0] = reverse(currWays[0]); + currNodes.reverse(); + start = currNodes[0]; + end = currNodes[currNodes.length - 1]; + } - if (last === what[0]) { - how = nodes.push; - what = what.slice(1); + if (nodes[0] === end) { + fn = currNodes.push; // join to end + nodes = nodes.slice(1); break; - } else if (last === what[what.length - 1]) { - how = nodes.push; - what = what.slice(0, -1).reverse(); - member = reverse(member); + } else if (nodes[nodes.length - 1] === end) { + fn = currNodes.push; // join to end + nodes = nodes.slice(0, -1).reverse(); + way = reverse(way); break; - } else if (first === what[what.length - 1]) { - how = nodes.unshift; - what = what.slice(0, -1); + } else if (nodes[nodes.length - 1] === start) { + fn = currNodes.unshift; // join to beginning + nodes = nodes.slice(0, -1); break; - } else if (first === what[0]) { - how = nodes.unshift; - what = what.slice(1).reverse(); - member = reverse(member); + } else if (nodes[0] === start) { + fn = currNodes.unshift; // join to beginning + nodes = nodes.slice(1).reverse(); + way = reverse(way); break; } else { - what = how = null; + fn = nodes = null; } } - if (!what) - break; // No more joinable ways. + if (!nodes) { + doneSequence = true; // couldn't find a joinable way + break; + } - how.apply(current, [member]); - how.apply(nodes, what); + fn.apply(currWays, [way]); + fn.apply(currNodes, nodes); - array.splice(i, 1); + toJoin.splice(i, 1); } + + + currWays.nodes = currNodes; + sequences.push(currWays); } - return joined; + return sequences; + + + // var joined = []; + + // while (array.length) { + // var member = array.shift(); + // var current = [member]; + // var nodes = resolve(member).slice(); + + // current.nodes = nodes; + // joined.push(current); + + // while (array.length && nodes[0] !== nodes[nodes.length - 1]) { + // var first = nodes[0]; + // var last = nodes[nodes.length - 1]; + // var how, what, i; + + // for (i = 0; i < array.length; i++) { + // member = array[i]; + // what = resolve(member); + + // if (last === what[0]) { + // how = nodes.push; + // what = what.slice(1); + // break; + // } else if (last === what[what.length - 1]) { + // how = nodes.push; + // what = what.slice(0, -1).reverse(); + // member = reverse(member); + // break; + // } else if (first === what[what.length - 1]) { + // how = nodes.unshift; + // what = what.slice(0, -1); + // break; + // } else if (first === what[0]) { + // how = nodes.unshift; + // what = what.slice(1).reverse(); + // member = reverse(member); + // break; + // } else { + // what = how = null; + // } + // } + + // if (!what) + // break; // No more joinable ways. + + // how.apply(current, [member]); + // how.apply(nodes, what); + + // array.splice(i, 1); + // } + // } + + // return joined; } diff --git a/test/spec/osm/multipolygon.js b/test/spec/osm/multipolygon.js index b71249c74e..6f5211ab7a 100644 --- a/test/spec/osm/multipolygon.js +++ b/test/spec/osm/multipolygon.js @@ -160,8 +160,7 @@ describe('iD.osmJoinWays', function() { expect(result.length).to.equal(1); expect(getIDs(result[0].nodes)).to.eql(['a']); expect(result[0].length).to.equal(1); - expect(result[0][0]).to.have.own.property('id', '-'); - expect(result[0][0]).to.have.own.property('type', 'way'); + expect(result[0][0]).to.eql(member); }); it('joins ways', function() { @@ -203,10 +202,8 @@ describe('iD.osmJoinWays', function() { expect(result.length).to.equal(1); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); expect(result[0].length).to.equal(2); - expect(result[0][0]).to.have.own.property('id', '-'); - expect(result[0][0]).to.have.own.property('type', 'way'); - expect(result[0][1]).to.have.own.property('id', '='); - expect(result[0][1]).to.have.own.property('type', 'way'); + expect(result[0][0]).to.eql({id: '-', type: 'way'}); + expect(result[0][1]).to.eql({id: '=', type: 'way'}); }); it('returns joined members in the correct order', function() { @@ -232,12 +229,9 @@ describe('iD.osmJoinWays', function() { expect(result.length).to.equal(1); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c', 'd']); expect(result[0].length).to.equal(3); - expect(result[0][0]).to.have.own.property('id', '='); - expect(result[0][0]).to.have.own.property('type', 'way'); - expect(result[0][1]).to.have.own.property('id', '-'); - expect(result[0][1]).to.have.own.property('type', 'way'); - expect(result[0][2]).to.have.own.property('id', '~'); - expect(result[0][2]).to.have.own.property('type', 'way'); + expect(result[0][0]).to.eql({id: '=', type: 'way'}); + expect(result[0][1]).to.eql({id: '-', type: 'way'}); + expect(result[0][2]).to.eql({id: '~', type: 'way'}); }); it('reverses member tags of reversed segements', function() { @@ -245,7 +239,7 @@ describe('iD.osmJoinWays', function() { // Source: // a ---> b <=== c // Result: - // a ---> b ===> c (and b === c reversed) + // a ---> b ===> c (and === reversed) // var a = iD.osmNode({id: 'a', loc: [0, 0]}); var b = iD.osmNode({id: 'b', loc: [1, 0]}); @@ -264,6 +258,31 @@ describe('iD.osmJoinWays', function() { expect(result[0][1].tags).to.eql({'oneway': '-1', 'lanes:backward': 2}); }); + it('reverses the initial segment to preserve member order', function() { + // + // Source: + // a <--- b ===> c + // Result: + // a ---> b ===> c (and --- reversed) + // + var a = iD.osmNode({id: 'a', loc: [0, 0]}); + var b = iD.osmNode({id: 'b', loc: [1, 0]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); + var w1 = iD.osmWay({id: '-', nodes: ['b', 'a'], tags: {'oneway': 'yes', 'lanes:forward': 2}}); + var w2 = iD.osmWay({id: '=', nodes: ['b', 'c']}); + var graph = iD.coreGraph([a, b, c, w1, w2]); + + var result = iD.osmJoinWays([w1, w2], graph); + expect(result.length).to.equal(1); + expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); + expect(result[0].length).to.equal(2); + expect(result[0][0]).to.be.an.instanceof(iD.osmWay); + expect(result[0][0].nodes).to.eql(['a', 'b']); + expect(result[0][0].tags).to.eql({'oneway': '-1', 'lanes:backward': 2}); + expect(result[0][1]).to.eql(w2); + }); + + it('ignores non-way members', function() { var node = iD.osmNode({loc: [0, 0]}); var member = {id: 'n', type: 'node'}; @@ -280,12 +299,12 @@ describe('iD.osmJoinWays', function() { it('returns multiple arrays for disjoint ways', function() { // // b - // / \ d ---> e ===> f - // a c + // / \ + // a c d ---> e ===> f // - var a = iD.osmNode({id: 'a', loc: [0, -1]}); + var a = iD.osmNode({id: 'a', loc: [0, 0]}); var b = iD.osmNode({id: 'b', loc: [1, 1]}); - var c = iD.osmNode({id: 'c', loc: [2, -1]}); + var c = iD.osmNode({id: 'c', loc: [2, 0]}); var d = iD.osmNode({id: 'd', loc: [5, 0]}); var e = iD.osmNode({id: 'e', loc: [6, 0]}); var f = iD.osmNode({id: 'f', loc: [7, 0]}); @@ -298,6 +317,7 @@ describe('iD.osmJoinWays', function() { var result = iD.osmJoinWays([w1, w2, w3, w4], graph); expect(result.length).to.equal(2); + expect(result[0].length).to.equal(2); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); expect(result[0][0]).to.eql(w1); @@ -337,19 +357,16 @@ describe('iD.osmJoinWays', function() { var result = iD.osmJoinWays(r.members, graph); expect(result.length).to.equal(2); + expect(result[0].length).to.equal(2); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); - expect(result[0][0]).to.have.own.property('id', '/'); - expect(result[0][0]).to.have.own.property('type', 'way'); - expect(result[0][1]).to.have.own.property('id', '\\'); - expect(result[0][1]).to.have.own.property('type', 'way'); + expect(result[0][0]).to.eql({id: '/', type: 'way'}); + expect(result[0][1]).to.eql({id: '\\', type: 'way'}); expect(result[1].length).to.equal(2); expect(getIDs(result[1].nodes)).to.eql(['d', 'e', 'f']); - expect(result[1][0]).to.have.own.property('id', '-'); - expect(result[1][0]).to.have.own.property('type', 'way'); - expect(result[1][1]).to.have.own.property('id', '='); - expect(result[1][1]).to.have.own.property('type', 'way'); + expect(result[1][0]).to.eql({id: '-', type: 'way'}); + expect(result[1][1]).to.eql({id: '=', type: 'way'}); }); it('understands doubled-back relation members', function() { @@ -385,20 +402,13 @@ describe('iD.osmJoinWays', function() { expect(result.length).to.equal(1); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c', 'd', 'e', 'c', 'b', 'a']); expect(result[0].length).to.equal(7); - expect(result[0][0]).to.have.own.property('id', '='); - expect(result[0][0]).to.have.own.property('type', 'way'); - expect(result[0][1]).to.have.own.property('id', '-'); - expect(result[0][1]).to.have.own.property('type', 'way'); - expect(result[0][2]).to.have.own.property('id', '~'); - expect(result[0][2]).to.have.own.property('type', 'way'); - expect(result[0][3]).to.have.own.property('id', '\\'); - expect(result[0][3]).to.have.own.property('type', 'way'); - expect(result[0][4]).to.have.own.property('id', '/'); - expect(result[0][4]).to.have.own.property('type', 'way'); - expect(result[0][5]).to.have.own.property('id', '-'); - expect(result[0][5]).to.have.own.property('type', 'way'); - expect(result[0][6]).to.have.own.property('id', '='); - expect(result[0][6]).to.have.own.property('type', 'way'); + expect(result[0][0]).to.eql({id: '=', type: 'way'}); + expect(result[0][1]).to.eql({id: '-', type: 'way'}); + expect(result[0][2]).to.eql({id: '~', type: 'way'}); + expect(result[0][3]).to.eql({id: '\\', type: 'way'}); + expect(result[0][4]).to.eql({id: '/', type: 'way'}); + expect(result[0][5]).to.eql({id: '-', type: 'way'}); + expect(result[0][6]).to.eql({id: '=', type: 'way'}); }); }); From 8dbb6eb20c022a185182e31d85b34dd3d186eb14 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 13 Jan 2018 01:45:46 -0500 Subject: [PATCH 140/206] Return reversal actions performed by osmJoinWays (see #4688) --- modules/osm/multipolygon.js | 63 ++++------------------------------- test/spec/osm/multipolygon.js | 12 ++++++- 2 files changed, 17 insertions(+), 58 deletions(-) diff --git a/modules/osm/multipolygon.js b/modules/osm/multipolygon.js index 92ed38a228..2292f65f36 100644 --- a/modules/osm/multipolygon.js +++ b/modules/osm/multipolygon.js @@ -1,5 +1,6 @@ import { actionReverse } from '../actions/reverse'; import { osmIsInterestingTag } from './tags'; +import { osmWay } from './way'; // For fixing up rendering of multipolygons with tags on the outer member. @@ -87,8 +88,10 @@ export function osmJoinWays(toJoin, graph) { return graph.childNodes(graph.entity(member.id)); } - function reverse(member) { - return member.tags ? actionReverse(member.id, { reverseOneway: true })(graph).entity(member.id) : member; + function reverse(which) { + var action = actionReverse(which.id, { reverseOneway: true }); + sequences.actions.push(action); + return (which instanceof osmWay) ? action(graph).entity(which.id) : which; } @@ -98,6 +101,7 @@ export function osmJoinWays(toJoin, graph) { }); var sequences = []; + sequences.actions = []; while (toJoin.length) { // start a new sequence @@ -173,59 +177,4 @@ export function osmJoinWays(toJoin, graph) { } return sequences; - - - // var joined = []; - - // while (array.length) { - // var member = array.shift(); - // var current = [member]; - // var nodes = resolve(member).slice(); - - // current.nodes = nodes; - // joined.push(current); - - // while (array.length && nodes[0] !== nodes[nodes.length - 1]) { - // var first = nodes[0]; - // var last = nodes[nodes.length - 1]; - // var how, what, i; - - // for (i = 0; i < array.length; i++) { - // member = array[i]; - // what = resolve(member); - - // if (last === what[0]) { - // how = nodes.push; - // what = what.slice(1); - // break; - // } else if (last === what[what.length - 1]) { - // how = nodes.push; - // what = what.slice(0, -1).reverse(); - // member = reverse(member); - // break; - // } else if (first === what[what.length - 1]) { - // how = nodes.unshift; - // what = what.slice(0, -1); - // break; - // } else if (first === what[0]) { - // how = nodes.unshift; - // what = what.slice(1).reverse(); - // member = reverse(member); - // break; - // } else { - // what = how = null; - // } - // } - - // if (!what) - // break; // No more joinable ways. - - // how.apply(current, [member]); - // how.apply(nodes, what); - - // array.splice(i, 1); - // } - // } - - // return joined; } diff --git a/test/spec/osm/multipolygon.js b/test/spec/osm/multipolygon.js index 6f5211ab7a..1cb467d726 100644 --- a/test/spec/osm/multipolygon.js +++ b/test/spec/osm/multipolygon.js @@ -158,6 +158,7 @@ describe('iD.osmJoinWays', function() { var result = iD.osmJoinWays([member], graph); expect(result.length).to.equal(1); + expect(result.actions).to.eql([]); expect(getIDs(result[0].nodes)).to.eql(['a']); expect(result[0].length).to.equal(1); expect(result[0][0]).to.eql(member); @@ -176,6 +177,7 @@ describe('iD.osmJoinWays', function() { var result = iD.osmJoinWays([w1, w2], graph); expect(result.length).to.equal(1); + expect(result.actions).to.eql([]); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); expect(result[0].length).to.equal(2); expect(result[0][0]).to.eql(w1); @@ -200,6 +202,7 @@ describe('iD.osmJoinWays', function() { var result = iD.osmJoinWays(r.members, graph); expect(result.length).to.equal(1); + expect(result.actions).to.eql([]); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); expect(result[0].length).to.equal(2); expect(result[0][0]).to.eql({id: '-', type: 'way'}); @@ -227,6 +230,7 @@ describe('iD.osmJoinWays', function() { var result = iD.osmJoinWays(r.members, graph); expect(result.length).to.equal(1); + expect(result.actions.length).to.equal(1); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c', 'd']); expect(result[0].length).to.equal(3); expect(result[0][0]).to.eql({id: '=', type: 'way'}); @@ -250,6 +254,7 @@ describe('iD.osmJoinWays', function() { var result = iD.osmJoinWays([w1, w2], graph); expect(result.length).to.equal(1); + expect(result.actions.length).to.equal(1); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); expect(result[0].length).to.equal(2); expect(result[0][0]).to.eql(w1); @@ -274,6 +279,7 @@ describe('iD.osmJoinWays', function() { var result = iD.osmJoinWays([w1, w2], graph); expect(result.length).to.equal(1); + expect(result.actions.length).to.equal(1); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); expect(result[0].length).to.equal(2); expect(result[0][0]).to.be.an.instanceof(iD.osmWay); @@ -317,6 +323,7 @@ describe('iD.osmJoinWays', function() { var result = iD.osmJoinWays([w1, w2, w3, w4], graph); expect(result.length).to.equal(2); + expect(result.actions).to.eql([]); expect(result[0].length).to.equal(2); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); @@ -357,6 +364,7 @@ describe('iD.osmJoinWays', function() { var result = iD.osmJoinWays(r.members, graph); expect(result.length).to.equal(2); + expect(result.actions).to.eql([]); expect(result[0].length).to.equal(2); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c']); @@ -399,7 +407,9 @@ describe('iD.osmJoinWays', function() { var graph = iD.coreGraph([a, b, c, d, e, w1, w2, w3, w4, w5, r]); var result = iD.osmJoinWays(r.members, graph); - expect(result.length).to.equal(1); + expect(result.length).to.equal(3); + expect(result.actions.length).to.equal(1); + expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c', 'd', 'e', 'c', 'b', 'a']); expect(result[0].length).to.equal(7); expect(result[0][0]).to.eql({id: '=', type: 'way'}); From 075b85c81d50fceb4fa247d173e991678586c833 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 14 Jan 2018 14:49:57 -0500 Subject: [PATCH 141/206] Apply reversal actions in `actionJoin` (closes #4688) --- modules/actions/join.js | 34 +++++++++++++++++----------------- test/spec/actions/join.js | 12 ++++++------ test/spec/osm/multipolygon.js | 4 ++-- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/modules/actions/join.js b/modules/actions/join.js index 5749ffbba4..421cda3ce6 100644 --- a/modules/actions/join.js +++ b/modules/actions/join.js @@ -1,13 +1,8 @@ import _extend from 'lodash-es/extend'; import _groupBy from 'lodash-es/groupBy'; -import _map from 'lodash-es/map'; import { actionDeleteWay } from './delete_way'; - -import { - osmIsInterestingTag, - osmJoinWays -} from '../osm'; +import { osmIsInterestingTag, osmJoinWays } from '../osm'; // Join ways at the end node they share. @@ -27,25 +22,30 @@ export function actionJoin(ids) { var action = function(graph) { - var ways = ids.map(graph.entity, graph), - survivor = ways[0]; + var ways = ids.map(graph.entity, graph); + var survivorID = ways[0].id; // Prefer to keep an existing way. for (var i = 0; i < ways.length; i++) { if (!ways[i].isNew()) { - survivor = ways[i]; + survivorID = ways[i].id; break; } } - var joined = osmJoinWays(ways, graph)[0]; + var sequences = osmJoinWays(ways, graph); + var joined = sequences[0]; + + // We might need to reverse some of these ways before joining them. #4688 + // `joined.actions` property will contain any actions we need to apply. + graph = sequences.actions.reduce(function(g, action) { return action(g); }, graph); - survivor = survivor.update({nodes: _map(joined.nodes, 'id')}); + var survivor = graph.entity(survivorID); + survivor = survivor.update({ nodes: joined.nodes.map(function(n) { return n.id; }) }); graph = graph.replace(survivor); joined.forEach(function(way) { - if (way.id === survivor.id) - return; + if (way.id === survivorID) return; graph.parentRelations(way).forEach(function(parent) { graph = graph.replace(parent.replaceMember(way, survivor)); @@ -70,10 +70,10 @@ export function actionJoin(ids) { if (joined.length > 1) return 'not_adjacent'; - var nodeIds = _map(joined[0].nodes, 'id').slice(1, -1), - relation, - tags = {}, - conflicting = false; + var nodeIds = joined[0].nodes.map(function(n) { return n.id; }).slice(1, -1); + var relation; + var tags = {}; + var conflicting = false; joined[0].forEach(function(way) { var parents = graph.parentRelations(way); diff --git a/test/spec/actions/join.js b/test/spec/actions/join.js index d591c4d550..8b13781874 100644 --- a/test/spec/actions/join.js +++ b/test/spec/actions/join.js @@ -274,25 +274,25 @@ describe('iD.actionJoin', function () { graph = iD.actionJoin(['-', '='])(graph); - expect(graph.entity('-').nodes).to.eql(['c', 'b', 'a']); + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); expect(graph.hasEntity('=')).to.be.undefined; }); it('joins a <-- b ==> c', function () { // Expected result: - // a <-- b <-- c - // tags on === reversed + // a --> b --> c + // tags on --- reversed var graph = iD.Graph([ iD.Node({id: 'a'}), iD.Node({id: 'b'}), iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['b', 'a']}), - iD.Way({id: '=', nodes: ['b', 'c'], tags: {'lanes:forward': 2}}) + iD.Way({id: '-', nodes: ['b', 'a'], tags: {'lanes:forward': 2}}), + iD.Way({id: '=', nodes: ['b', 'c']}) ]); graph = iD.actionJoin(['-', '='])(graph); - expect(graph.entity('-').nodes).to.eql(['c', 'b', 'a']); + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); expect(graph.hasEntity('=')).to.be.undefined; expect(graph.entity('-').tags).to.eql({'lanes:backward': 2}); }); diff --git a/test/spec/osm/multipolygon.js b/test/spec/osm/multipolygon.js index 1cb467d726..f3bba303b9 100644 --- a/test/spec/osm/multipolygon.js +++ b/test/spec/osm/multipolygon.js @@ -407,8 +407,8 @@ describe('iD.osmJoinWays', function() { var graph = iD.coreGraph([a, b, c, d, e, w1, w2, w3, w4, w5, r]); var result = iD.osmJoinWays(r.members, graph); - expect(result.length).to.equal(3); - expect(result.actions.length).to.equal(1); + expect(result.length).to.equal(1); + expect(result.actions.length).to.equal(3); expect(getIDs(result[0].nodes)).to.eql(['a', 'b', 'c', 'd', 'e', 'c', 'b', 'a']); expect(result[0].length).to.equal(7); From 0382ce7d4b61897ae6e9b848d8e8902a7f706db1 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 14 Jan 2018 21:45:25 -0500 Subject: [PATCH 142/206] Updated function doc --- modules/osm/multipolygon.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/modules/osm/multipolygon.js b/modules/osm/multipolygon.js index 2292f65f36..322626df8b 100644 --- a/modules/osm/multipolygon.js +++ b/modules/osm/multipolygon.js @@ -63,21 +63,26 @@ export function osmSimpleMultipolygonOuterMember(entity, graph) { } -// Join `array` into sequences of connecting ways. -// +// Join `toJoin` array into sequences of connecting ways. + // Segments which share identical start/end nodes will, as much as possible, // be connected with each other. // // The return value is a nested array. Each constituent array contains elements -// of `array` which have been determined to connect. Each consitituent array -// also has a `nodes` property whose value is an ordered array of member nodes, -// with appropriate order reversal and start/end coordinate de-duplication. +// of `toJoin` which have been determined to connect. +// +// Each consitituent array also has a `nodes` property whose value is an +// ordered array of member nodes, with appropriate order reversal and +// start/end coordinate de-duplication. // -// Members of `array` must have, at minimum, `type` and `id` properties. -// Thus either an array of `osmWay`s or a relation member array may be -// used. +// The returned sequences array also has an `actions` array property, containing +// any reversal actions that should be applied to the graph, should the calling +// code attempt to actually join the given ways. // -// If an member has a `tags` property, its tags will be reversed via +// Members of `toJoin` must have, at minimum, `type` and `id` properties. +// Thus either an array of `osmWay`s or a relation member array may be used. +// +// If an member is an `osmWay`, its tags and childnodes will be reversed via // `actionReverse` in the output. // // Incomplete members (those for which `graph.hasEntity(element.id)` returns @@ -94,7 +99,6 @@ export function osmJoinWays(toJoin, graph) { return (which instanceof osmWay) ? action(graph).entity(which.id) : which; } - // make a copy containing only the ways to join toJoin = toJoin.filter(function(member) { return member.type === 'way' && graph.hasEntity(member.id); From dd0a88c245437da9809bc70c58873075383d08ba Mon Sep 17 00:00:00 2001 From: Nicolas Decoster Date: Mon, 15 Jan 2018 11:17:51 +0100 Subject: [PATCH 143/206] fix failing npm test --- modules/svg/gpx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/svg/gpx.js b/modules/svg/gpx.js index 3d273986a3..931189aa29 100644 --- a/modules/svg/gpx.js +++ b/modules/svg/gpx.js @@ -111,7 +111,7 @@ export function svgGpx(projection, context, dispatch) { createLabels(layer, 'gpxlabel-halo', labelsData); createLabels(layer, 'gpxlabel', labelsData); - labels = layer.selectAll('text'); + var labels = layer.selectAll('text'); labels .text(function(d) { From 03fa6e7be915156bbe2a6bb5544e288a0c376df0 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 15 Jan 2018 22:02:43 -0500 Subject: [PATCH 144/206] Add tryInsert option to osmJoinWays --- modules/actions/add_member.js | 13 +++++- modules/actions/split.js | 56 ++++++++++++++----------- modules/osm/multipolygon.js | 77 +++++++++++++++++++++++------------ test/spec/actions/split.js | 61 ++++++++++++++++++++------- 4 files changed, 140 insertions(+), 67 deletions(-) diff --git a/modules/actions/add_member.js b/modules/actions/add_member.js index 195dd7df9e..3d7654d36b 100644 --- a/modules/actions/add_member.js +++ b/modules/actions/add_member.js @@ -4,12 +4,16 @@ import { osmJoinWays } from '../osm'; export function actionAddMember(relationId, member, memberIndex) { return function(graph) { var relation = graph.entity(relationId); + var numAdded = 0; + // If we weren't passed a memberIndex, + // try to perform sensible inserts based on how the ways join together if (isNaN(memberIndex) && member.type === 'way') { var members = relation.indexedMembers(); members.push(member); var joined = osmJoinWays(members, graph); + for (var i = 0; i < joined.length; i++) { var segment = joined[i]; for (var j = 0; j < segment.length && segment.length >= 2; j++) { @@ -23,10 +27,17 @@ export function actionAddMember(relationId, member, memberIndex) { } else { memberIndex = Math.min(segment[j - 1].index + 1, segment[j + 1].index + 1); } + + relation = relation.addMember(member, memberIndex + (numAdded++)); } } } - return graph.replace(relation.addMember(member, memberIndex)); + // By default, add at index (or append to end if index undefined) + if (!numAdded) { + relation = relation.addMember(member, memberIndex); + } + + return graph.replace(relation); }; } diff --git a/modules/actions/split.js b/modules/actions/split.js index 6fd73f0bc6..e0bd8b20f9 100644 --- a/modules/actions/split.js +++ b/modules/actions/split.js @@ -29,7 +29,7 @@ import { utilWrap } from '../util'; // https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as // export function actionSplit(nodeId, newWayIds) { - var wayIds; + var _wayIDs; // if the way is closed, we need to search for a partner node // to split the way at. @@ -42,11 +42,11 @@ export function actionSplit(nodeId, newWayIds) { // For example: bone-shaped areas get split across their waist // line, circles across the diameter. function splitArea(nodes, idxA, graph) { - var lengths = new Array(nodes.length), - length, - i, - best = 0, - idxB; + var lengths = new Array(nodes.length); + var length; + var i; + var best = 0; + var idxB; function wrap(index) { return utilWrap(index, nodes.length); @@ -84,16 +84,16 @@ export function actionSplit(nodeId, newWayIds) { function split(graph, wayA, newWayId) { - var wayB = osmWay({id: newWayId, tags: wayA.tags}), - nodesA, - nodesB, - isArea = wayA.isArea(), - isOuter = osmIsSimpleMultipolygonOuterMember(wayA, graph); + var wayB = osmWay({id: newWayId, tags: wayA.tags}); + var nodesA; + var nodesB; + var isArea = wayA.isArea(); + var isOuter = osmIsSimpleMultipolygonOuterMember(wayA, graph); if (wayA.isClosed()) { - var nodes = wayA.nodes.slice(0, -1), - idxA = _indexOf(nodes, nodeId), - idxB = splitArea(nodes, idxA, graph); + var nodes = wayA.nodes.slice(0, -1); + var idxA = _indexOf(nodes, nodeId); + var idxB = splitArea(nodes, idxA, graph); if (idxB < idxA) { nodesA = nodes.slice(idxA).concat(nodes.slice(0, idxB + 1)); @@ -134,7 +134,14 @@ export function actionSplit(nodeId, newWayIds) { role: relation.memberById(wayA.id).role }; - graph = actionAddMember(relation.id, member)(graph); + // how many times should this member be inserted? + // var matches = relation.members.filter(function(member) { + // return member.id === wayA.id; + // }); + + // matches.forEach(function() { + graph = actionAddMember(relation.id, member)(graph); + // }); } }); @@ -144,7 +151,8 @@ export function actionSplit(nodeId, newWayIds) { members: [ {id: wayA.id, role: 'outer', type: 'way'}, {id: wayB.id, role: 'outer', type: 'way'} - ]}); + ] + }); graph = graph.replace(multipolygon); graph = graph.replace(wayA.update({tags: {}})); @@ -165,15 +173,15 @@ export function actionSplit(nodeId, newWayIds) { action.ways = function(graph) { - var node = graph.entity(nodeId), - parents = graph.parentWays(node), - hasLines = _some(parents, function(parent) { return parent.geometry(graph) === 'line'; }); + var node = graph.entity(nodeId); + var parents = graph.parentWays(node); + var hasLines = _some(parents, function(parent) { return parent.geometry(graph) === 'line'; }); return parents.filter(function(parent) { - if (wayIds && wayIds.indexOf(parent.id) === -1) + if (_wayIDs && _wayIDs.indexOf(parent.id) === -1) return false; - if (!wayIds && hasLines && parent.geometry(graph) !== 'line') + if (!_wayIDs && hasLines && parent.geometry(graph) !== 'line') return false; if (parent.isClosed()) { @@ -193,14 +201,14 @@ export function actionSplit(nodeId, newWayIds) { action.disabled = function(graph) { var candidates = action.ways(graph); - if (candidates.length === 0 || (wayIds && wayIds.length !== candidates.length)) + if (candidates.length === 0 || (_wayIDs && _wayIDs.length !== candidates.length)) return 'not_eligible'; }; action.limitWays = function(_) { - if (!arguments.length) return wayIds; - wayIds = _; + if (!arguments.length) return _wayIDs; + _wayIDs = _; return action; }; diff --git a/modules/osm/multipolygon.js b/modules/osm/multipolygon.js index 322626df8b..c8412fecf2 100644 --- a/modules/osm/multipolygon.js +++ b/modules/osm/multipolygon.js @@ -75,31 +75,36 @@ export function osmSimpleMultipolygonOuterMember(entity, graph) { // ordered array of member nodes, with appropriate order reversal and // start/end coordinate de-duplication. // -// The returned sequences array also has an `actions` array property, containing -// any reversal actions that should be applied to the graph, should the calling -// code attempt to actually join the given ways. -// // Members of `toJoin` must have, at minimum, `type` and `id` properties. // Thus either an array of `osmWay`s or a relation member array may be used. // -// If an member is an `osmWay`, its tags and childnodes will be reversed via +// If an member is an `osmWay`, its tags and childnodes may be reversed via // `actionReverse` in the output. // +// The returned sequences array also has an `actions` array property, containing +// any reversal actions that should be applied to the graph, should the calling +// code attempt to actually join the given ways. +// // Incomplete members (those for which `graph.hasEntity(element.id)` returns // false) and non-way members are ignored. // -export function osmJoinWays(toJoin, graph) { +// `tryInsert` is an optional object. +// If supplied, insert the given way/member after an existing way/member: +// `{ item: wayOrMember, afterID: id }` +// (This feature is used by `actionSplit`) +// +export function osmJoinWays(toJoin, graph, tryInsert) { function resolve(member) { return graph.childNodes(graph.entity(member.id)); } - function reverse(which) { - var action = actionReverse(which.id, { reverseOneway: true }); + function reverse(item) { + var action = actionReverse(item.id, { reverseOneway: true }); sequences.actions.push(action); - return (which instanceof osmWay) ? action(graph).entity(which.id) : which; + return (item instanceof osmWay) ? action(graph).entity(item.id) : item; } - // make a copy containing only the ways to join + // make a copy containing only the items to join toJoin = toJoin.filter(function(member) { return member.type === 'way' && graph.hasEntity(member.id); }); @@ -109,10 +114,11 @@ export function osmJoinWays(toJoin, graph) { while (toJoin.length) { // start a new sequence - var way = toJoin.shift(); - var currWays = [way]; - var currNodes = resolve(way).slice(); + var item = toJoin.shift(); + var currWays = [item]; + var currNodes = resolve(item).slice(); var doneSequence = false; + var isInserting = false; // add to it while (toJoin.length && !doneSequence) { @@ -122,10 +128,21 @@ export function osmJoinWays(toJoin, graph) { var nodes = null; var i; - // find the next way - for (i = 0; i < toJoin.length; i++) { - way = toJoin[i]; - nodes = resolve(way); + // Find the next way/member to join. + // If it is time to attempt an insert, try that item first. + // Otherwise, search for a next item in `toJoin` + var toCheck; + if (!isInserting && tryInsert && tryInsert.afterID === currWays[currWays.length - 1].id) { + toCheck = [tryInsert.item]; + isInserting = true; + } else { + toCheck = toJoin.slice(); + isInserting = false; + } + + for (i = 0; i < toCheck.length; i++) { + item = toCheck[i]; + nodes = resolve(item); // Strongly prefer to generate a forward path that preserves the order // of the members array. For multipolygons and most relations, member @@ -148,7 +165,7 @@ export function osmJoinWays(toJoin, graph) { } else if (nodes[nodes.length - 1] === end) { fn = currNodes.push; // join to end nodes = nodes.slice(0, -1).reverse(); - way = reverse(way); + item = reverse(item); break; } else if (nodes[nodes.length - 1] === start) { fn = currNodes.unshift; // join to beginning @@ -157,25 +174,31 @@ export function osmJoinWays(toJoin, graph) { } else if (nodes[0] === start) { fn = currNodes.unshift; // join to beginning nodes = nodes.slice(1).reverse(); - way = reverse(way); + item = reverse(item); break; } else { fn = nodes = null; } } - if (!nodes) { - doneSequence = true; // couldn't find a joinable way - break; - } + if (nodes) { // we found something to join + fn.apply(currWays, [item]); + fn.apply(currNodes, nodes); - fn.apply(currWays, [way]); - fn.apply(currNodes, nodes); + if (!isInserting) { + toJoin.splice(i, 1); + } - toJoin.splice(i, 1); + } else { // couldn't find a joinable way/member + // if inserting, restart the loop and look in `toJoin` next time. + // if not inserting, there is nowhere else to look, mark as done. + if (!isInserting) { + doneSequence = true; + break; + } + } } - currWays.nodes = currNodes; sequences.push(currWays); } diff --git a/test/spec/actions/split.js b/test/spec/actions/split.js index cc5fd31c41..1ebbcf89c1 100644 --- a/test/spec/actions/split.js +++ b/test/spec/actions/split.js @@ -183,7 +183,7 @@ describe('iD.actionSplit', function () { iD.osmNode({id: 'a'}), iD.osmNode({id: 'b'}), iD.osmNode({id: 'c'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), iD.osmNode({id: '*'}), iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) @@ -202,7 +202,7 @@ describe('iD.actionSplit', function () { iD.osmNode({id: 'a'}), iD.osmNode({id: 'b'}), iD.osmNode({id: 'c'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), iD.osmNode({id: '*'}), iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) @@ -244,7 +244,7 @@ describe('iD.actionSplit', function () { iD.osmNode({id: 'a'}), iD.osmNode({id: 'b'}), iD.osmNode({id: 'c'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']}) ]); @@ -314,15 +314,15 @@ describe('iD.actionSplit', function () { describe('member ordering', function () { - it('adds the new way to parent relations (no connections)', function () { + it('adds the new way to parent relations (simple)', function () { // Situation: - // a ---- b ---- c + // a ----> b ----> c // Relation: [----] // // Split at b. // // Expected result: - // a ---- b ==== c + // a ----> b ====> c // Relation: [----, ====] // var graph = iD.coreGraph([ @@ -343,13 +343,13 @@ describe('iD.actionSplit', function () { it('adds the new way to parent relations (forward order)', function () { // Situation: - // a ---- b ---- c ~~~~ d + // a ----> b ----> c ~~~~> d // Relation: [----, ~~~~] // // Split at b. // // Expected result: - // a ---- b ==== c ~~~~ d + // a ----> b ====> c ~~~~> d // Relation: [----, ====, ~~~~] // var graph = iD.coreGraph([ @@ -370,13 +370,13 @@ describe('iD.actionSplit', function () { it('adds the new way to parent relations (reverse order)', function () { // Situation: - // a ---- b ---- c ~~~~ d + // a ----> b ----> c ~~~~> d // Relation: [~~~~, ----] // // Split at b. // // Expected result: - // a ---- b ==== c ~~~~ d + // a ----> b ====> c ~~~~> d // Relation: [~~~~, ====, ----] // var graph = iD.coreGraph([ @@ -397,13 +397,13 @@ describe('iD.actionSplit', function () { it('adds the new way to parent relations (unsplit way belongs multiple times)', function () { // Situation: - // a ---- b ---- c ~~~~ d + // a ----> b ----> c ~~~~> d // Relation: [~~~~, ----, ~~~~] // // Split at b. // // Expected result: - // a ---- b ==== c ~~~~ d + // a ----> b ====> c ~~~~> d // Relation: [~~~~, ====, ----, ====, ~~~~] // var graph = iD.coreGraph([ @@ -426,15 +426,15 @@ describe('iD.actionSplit', function () { expect(ids).to.have.ordered.members(['~', '=', '-', '=', '~']); }); - it('adds the new way to parent relations (split way belongs multiple times)', function () { + it('adds the new way to parent relations (forward split way belongs multiple times)', function () { // Situation: - // a ---- b ---- c ~~~~ d + // a ----> b ----> c ~~~~> d // Relation: [----, ~~~~, ----] // // Split at b. // // Expected result: - // a ---- b ==== c ~~~~ d + // a ----> b ====> c ~~~~> d // Relation: [----, ====, ~~~~, ====, ----] // var graph = iD.coreGraph([ @@ -456,6 +456,37 @@ describe('iD.actionSplit', function () { var ids = graph.entity('r').members.map(function(m) { return m.id; }); expect(ids).to.have.ordered.members(['-', '=', '~', '=', '-']); }); + + it('adds the new way to parent relations (reverse split way belongs multiple times)', function () { + // Situation: + // a <---- b <---- c ~~~~> d + // Relation: [----, ~~~~, ----] + // + // Split at b. + // + // Expected result: + // a <==== b <---- c ~~~~> d + // Relation: [====, ----, ~~~~, ----, ====] + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['c', 'b', 'a']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmRelation({id: 'r', members: [ + {id: '-', type: 'way'}, + {id: '~', type: 'way'}, + {id: '-', type: 'way'} + ]}) + ]); + + graph = iD.actionSplit('b', ['='])(graph); + + var ids = graph.entity('r').members.map(function(m) { return m.id; }); + expect(ids).to.have.ordered.members(['=', '-', '~', '-', '=']); + }); }); From 221158e9184731be58eb5408b3e3f431154d3be3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 15 Jan 2018 23:13:59 -0500 Subject: [PATCH 145/206] WIP: Add insertHint to actionAddMember, actionSplit --- modules/actions/add_member.js | 16 +++++++++++----- modules/actions/split.js | 14 ++++++-------- modules/osm/multipolygon.js | 12 ++++++------ test/spec/actions/add_member.js | 12 ++++++++---- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/modules/actions/add_member.js b/modules/actions/add_member.js index 3d7654d36b..d5f5b26b70 100644 --- a/modules/actions/add_member.js +++ b/modules/actions/add_member.js @@ -1,18 +1,21 @@ import { osmJoinWays } from '../osm'; -export function actionAddMember(relationId, member, memberIndex) { - return function(graph) { +export function actionAddMember(relationId, member, memberIndex, insertHint) { + + var action = function(graph) { var relation = graph.entity(relationId); var numAdded = 0; // If we weren't passed a memberIndex, // try to perform sensible inserts based on how the ways join together - if (isNaN(memberIndex) && member.type === 'way') { + if ((isNaN(memberIndex) || insertHint) && member.type === 'way') { var members = relation.indexedMembers(); - members.push(member); + if (!insertHint) { + members.push(member); // just push and let osmJoinWays sort it out + } - var joined = osmJoinWays(members, graph); + var joined = osmJoinWays(members, graph, insertHint); for (var i = 0; i < joined.length; i++) { var segment = joined[i]; @@ -40,4 +43,7 @@ export function actionAddMember(relationId, member, memberIndex) { return graph.replace(relation); }; + + + return action; } diff --git a/modules/actions/split.js b/modules/actions/split.js index e0bd8b20f9..a9d0bd77f7 100644 --- a/modules/actions/split.js +++ b/modules/actions/split.js @@ -134,14 +134,12 @@ export function actionSplit(nodeId, newWayIds) { role: relation.memberById(wayA.id).role }; - // how many times should this member be inserted? - // var matches = relation.members.filter(function(member) { - // return member.id === wayA.id; - // }); - - // matches.forEach(function() { - graph = actionAddMember(relation.id, member)(graph); - // }); + var insertHint = { + item: member, + nextTo: wayA.id + }; + + graph = actionAddMember(relation.id, member, undefined, insertHint)(graph); } }); diff --git a/modules/osm/multipolygon.js b/modules/osm/multipolygon.js index c8412fecf2..0990e3e306 100644 --- a/modules/osm/multipolygon.js +++ b/modules/osm/multipolygon.js @@ -88,12 +88,12 @@ export function osmSimpleMultipolygonOuterMember(entity, graph) { // Incomplete members (those for which `graph.hasEntity(element.id)` returns // false) and non-way members are ignored. // -// `tryInsert` is an optional object. -// If supplied, insert the given way/member after an existing way/member: -// `{ item: wayOrMember, afterID: id }` +// `insertHint` is an optional object. +// If supplied, insert the given way/member next to an existing way/member: +// `{ item: wayOrMember, nextTo: id }` // (This feature is used by `actionSplit`) // -export function osmJoinWays(toJoin, graph, tryInsert) { +export function osmJoinWays(toJoin, graph, insertHint) { function resolve(member) { return graph.childNodes(graph.entity(member.id)); } @@ -132,8 +132,8 @@ export function osmJoinWays(toJoin, graph, tryInsert) { // If it is time to attempt an insert, try that item first. // Otherwise, search for a next item in `toJoin` var toCheck; - if (!isInserting && tryInsert && tryInsert.afterID === currWays[currWays.length - 1].id) { - toCheck = [tryInsert.item]; + if (!isInserting && insertHint && insertHint.nextTo === currWays[currWays.length - 1].id) { + toCheck = [insertHint.item]; isInserting = true; } else { toCheck = toJoin.slice(); diff --git a/test/spec/actions/add_member.js b/test/spec/actions/add_member.js index 95e9d98a6b..8630458491 100644 --- a/test/spec/actions/add_member.js +++ b/test/spec/actions/add_member.js @@ -101,7 +101,7 @@ describe('iD.actionAddMember', function() { expect(members(graph)).to.eql(['-', '=', '~']); }); - it('inserts the member multiple times if the way exists multiple times (middle)', function() { + it('inserts the member multiple times if hint provided (middle)', function() { // Before: a ---> b .. c ~~~> d <~~~ c .. b <--- a // After: a ---> b ===> c ~~~> d <~~~ c <=== b <--- a var graph = iD.coreGraph([ @@ -120,11 +120,13 @@ describe('iD.actionAddMember', function() { ]}) ]); - graph = iD.actionAddMember('r', {id: '=', type: 'way'})(graph); + var member = { id: '=', type: 'way' }; + var hint = { item: member, nextTo: '-' }; + graph = iD.actionAddMember('r', member, undefined, hint)(graph); expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']); }); - it('inserts the member multiple times if the way exists multiple times (beginning/end)', function() { + it('inserts the member multiple times if hint provided (beginning/end)', function() { // Before: b ===> c ~~~> d <~~~ c <=== b // After: a ---> b ===> c ~~~> d <~~~ c <=== b <--- a var graph = iD.coreGraph([ @@ -143,7 +145,9 @@ describe('iD.actionAddMember', function() { ]}) ]); - graph = iD.actionAddMember('r', {id: '-', type: 'way'})(graph); + var member = { id: '-', type: 'way' }; + var hint = { item: member, nextTo: '=' }; + graph = iD.actionAddMember('r', member, undefined, hint)(graph); expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']); }); From be46e85ec09fa6a4f7b86225d082c80b96f68001 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 16 Jan 2018 17:41:14 -0500 Subject: [PATCH 146/206] Move insert way pairing code from osmJoinWays to actionAddMember (tests for actionAddMember now passing!) --- modules/actions/add_member.js | 114 ++++++++++++++++++++++++-------- modules/actions/split.js | 10 +-- modules/osm/multipolygon.js | 45 +++---------- test/spec/actions/add_member.js | 28 +++++--- 4 files changed, 120 insertions(+), 77 deletions(-) diff --git a/modules/actions/add_member.js b/modules/actions/add_member.js index d5f5b26b70..ab15f343ce 100644 --- a/modules/actions/add_member.js +++ b/modules/actions/add_member.js @@ -1,48 +1,104 @@ -import { osmJoinWays } from '../osm'; +import _groupBy from 'lodash-es/groupBy'; +import { osmJoinWays, osmWay } from '../osm'; + + +export function actionAddMember(relationId, member, memberIndex, insertPair) { + + // Relation.replaceMember() removes duplicates, and we don't want that.. #4696 + function replaceMemberAll(relation, needleID, replacement) { + var members = []; + for (var i = 0; i < relation.members.length; i++) { + var member = relation.members[i]; + if (member.id !== needleID) { + members.push(member); + } else { + members.push({id: replacement.id, type: replacement.type, role: member.role}); + } + } + return relation.update({members: members}); + } -export function actionAddMember(relationId, member, memberIndex, insertHint) { var action = function(graph) { var relation = graph.entity(relationId); - var numAdded = 0; - - // If we weren't passed a memberIndex, - // try to perform sensible inserts based on how the ways join together - if ((isNaN(memberIndex) || insertHint) && member.type === 'way') { - var members = relation.indexedMembers(); - if (!insertHint) { - members.push(member); // just push and let osmJoinWays sort it out - } - var joined = osmJoinWays(members, graph, insertHint); + if ((isNaN(memberIndex) || insertPair) && member.type === 'way') { + // Try to perform sensible inserts based on how the ways join together + graph = addWayMember(relation, graph); + } else { + graph = graph.replace(relation.addMember(member, memberIndex)); + } + + return graph; + }; - for (var i = 0; i < joined.length; i++) { - var segment = joined[i]; - for (var j = 0; j < segment.length && segment.length >= 2; j++) { - if (segment[j] !== member) - continue; - if (j === 0) { - memberIndex = segment[j + 1].index; - } else if (j === segment.length - 1) { - memberIndex = segment[j - 1].index + 1; + // Add a way member into the relation "wherever it makes sense". + // In this situation we were not supplied a memberIndex. + function addWayMember(relation, graph) { + var groups; + var tempWay; + var i, j; + + if (insertPair) { + // We're adding a member that must stay paired with an existing member. + // (This feature is used by `actionSplit`) + // + // This is tricky because the members may exist multiple times in the + // member list, and with different A-B/B-A ordering and different roles. + // (e.g. a bus route that loops out and back - #4589). + // + // Replace the existing member with a temporary way, + // so that `osmJoinWays` can treat the pair like a single way. + tempWay = osmWay({ id: 'wTemp', nodes: insertPair.nodes }); + graph = graph.replace(tempWay); + var tempMember = { id: tempWay.id, type: 'way', role: '' }; + var tempRelation = replaceMemberAll(relation, insertPair.originalID, tempMember); + groups = _groupBy(tempRelation.members, function(m) { return m.type; }); + groups.way = groups.way || []; + + } else { + // Add the member anywhere.. Just push and let `osmJoinWays` decide where to put it. + groups = _groupBy(relation.members, function(m) { return m.type; }); + groups.way = groups.way || []; + groups.way.push(member); + } + + var joined = osmJoinWays(groups.way, graph); + + var newWayMembers = []; + for (i = 0; i < joined.length; i++) { + var segment = joined[i]; + var nodes = segment.nodes.slice(); + + for (j = 0; j < segment.length; j++) { + var way = graph.entity(segment[j].id); + if (tempWay && segment[j].id === tempWay.id) { + if (nodes[0].id === insertPair.nodes[0]) { + newWayMembers.push({ id: insertPair.originalID, type: 'way', role: segment[j].role }); + newWayMembers.push({ id: insertPair.insertedID, type: 'way', role: segment[j].role }); } else { - memberIndex = Math.min(segment[j - 1].index + 1, segment[j + 1].index + 1); + newWayMembers.push({ id: insertPair.insertedID, type: 'way', role: segment[j].role }); + newWayMembers.push({ id: insertPair.originalID, type: 'way', role: segment[j].role }); } - - relation = relation.addMember(member, memberIndex + (numAdded++)); + } else { + newWayMembers.push(segment[j]); } + nodes.splice(0, way.nodes.length - 1); } } - // By default, add at index (or append to end if index undefined) - if (!numAdded) { - relation = relation.addMember(member, memberIndex); + if (tempWay) { + graph = graph.remove(tempWay); } - return graph.replace(relation); - }; + // Write members in the order: nodes, ways, relations + // This is reccomended for Public Transport routes: + // see https://wiki.openstreetmap.org/wiki/Public_transport#Service_routes + var newMembers = (groups.node || []).concat(newWayMembers, (groups.relation || [])); + return graph.replace(relation.update({members: newMembers})); + } return action; diff --git a/modules/actions/split.js b/modules/actions/split.js index a9d0bd77f7..1763274acd 100644 --- a/modules/actions/split.js +++ b/modules/actions/split.js @@ -85,6 +85,7 @@ export function actionSplit(nodeId, newWayIds) { function split(graph, wayA, newWayId) { var wayB = osmWay({id: newWayId, tags: wayA.tags}); + var origNodes = wayA.nodes.slice(); var nodesA; var nodesB; var isArea = wayA.isArea(); @@ -134,12 +135,13 @@ export function actionSplit(nodeId, newWayIds) { role: relation.memberById(wayA.id).role }; - var insertHint = { - item: member, - nextTo: wayA.id + var insertPair = { + originalID: wayA.id, + insertedID: wayB.id, + nodes: origNodes }; - graph = actionAddMember(relation.id, member, undefined, insertHint)(graph); + graph = actionAddMember(relation.id, member, undefined, insertPair)(graph); } }); diff --git a/modules/osm/multipolygon.js b/modules/osm/multipolygon.js index 0990e3e306..41e61a5ae3 100644 --- a/modules/osm/multipolygon.js +++ b/modules/osm/multipolygon.js @@ -88,12 +88,7 @@ export function osmSimpleMultipolygonOuterMember(entity, graph) { // Incomplete members (those for which `graph.hasEntity(element.id)` returns // false) and non-way members are ignored. // -// `insertHint` is an optional object. -// If supplied, insert the given way/member next to an existing way/member: -// `{ item: wayOrMember, nextTo: id }` -// (This feature is used by `actionSplit`) -// -export function osmJoinWays(toJoin, graph, insertHint) { +export function osmJoinWays(toJoin, graph) { function resolve(member) { return graph.childNodes(graph.entity(member.id)); } @@ -109,6 +104,7 @@ export function osmJoinWays(toJoin, graph, insertHint) { return member.type === 'way' && graph.hasEntity(member.id); }); + var sequences = []; sequences.actions = []; @@ -118,7 +114,6 @@ export function osmJoinWays(toJoin, graph, insertHint) { var currWays = [item]; var currNodes = resolve(item).slice(); var doneSequence = false; - var isInserting = false; // add to it while (toJoin.length && !doneSequence) { @@ -129,19 +124,8 @@ export function osmJoinWays(toJoin, graph, insertHint) { var i; // Find the next way/member to join. - // If it is time to attempt an insert, try that item first. - // Otherwise, search for a next item in `toJoin` - var toCheck; - if (!isInserting && insertHint && insertHint.nextTo === currWays[currWays.length - 1].id) { - toCheck = [insertHint.item]; - isInserting = true; - } else { - toCheck = toJoin.slice(); - isInserting = false; - } - - for (i = 0; i < toCheck.length; i++) { - item = toCheck[i]; + for (i = 0; i < toJoin.length; i++) { + item = toJoin[i]; nodes = resolve(item); // Strongly prefer to generate a forward path that preserves the order @@ -181,22 +165,15 @@ export function osmJoinWays(toJoin, graph, insertHint) { } } - if (nodes) { // we found something to join - fn.apply(currWays, [item]); - fn.apply(currNodes, nodes); + if (!nodes) { // couldn't find a joinable way/member + doneSequence = true; + break; + } - if (!isInserting) { - toJoin.splice(i, 1); - } + fn.apply(currWays, [item]); + fn.apply(currNodes, nodes); - } else { // couldn't find a joinable way/member - // if inserting, restart the loop and look in `toJoin` next time. - // if not inserting, there is nowhere else to look, mark as done. - if (!isInserting) { - doneSequence = true; - break; - } - } + toJoin.splice(i, 1); } currWays.nodes = currNodes; diff --git a/test/spec/actions/add_member.js b/test/spec/actions/add_member.js index 8630458491..3f78950979 100644 --- a/test/spec/actions/add_member.js +++ b/test/spec/actions/add_member.js @@ -101,7 +101,7 @@ describe('iD.actionAddMember', function() { expect(members(graph)).to.eql(['-', '=', '~']); }); - it('inserts the member multiple times if hint provided (middle)', function() { + it('inserts the member multiple times if insertPair provided (middle)', function() { // Before: a ---> b .. c ~~~> d <~~~ c .. b <--- a // After: a ---> b ===> c ~~~> d <~~~ c <=== b <--- a var graph = iD.coreGraph([ @@ -121,21 +121,25 @@ describe('iD.actionAddMember', function() { ]); var member = { id: '=', type: 'way' }; - var hint = { item: member, nextTo: '-' }; - graph = iD.actionAddMember('r', member, undefined, hint)(graph); + var insertPair = { + originalID: '-', + insertedID: '=', + nodes: ['a','b','c'] + }; + graph = iD.actionAddMember('r', member, undefined, insertPair)(graph); expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']); }); - it('inserts the member multiple times if hint provided (beginning/end)', function() { - // Before: b ===> c ~~~> d <~~~ c <=== b - // After: a ---> b ===> c ~~~> d <~~~ c <=== b <--- a + it('inserts the member multiple times if insertPair provided (beginning/end)', function() { + // Before: b <=== c ~~~> d <~~~ c ===> b + // After: a <--- b <=== c ~~~> d <~~~ c ===> b ---> a var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), iD.osmNode({id: 'b', loc: [0, 0]}), iD.osmNode({id: 'c', loc: [0, 0]}), iD.osmNode({id: 'd', loc: [0, 0]}), - iD.osmWay({id: '-', nodes: ['a', 'b']}), - iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmWay({id: '-', nodes: ['b', 'a']}), + iD.osmWay({id: '=', nodes: ['c', 'b']}), iD.osmWay({id: '~', nodes: ['c', 'd']}), iD.osmRelation({id: 'r', members: [ {id: '=', type: 'way'}, @@ -146,8 +150,12 @@ describe('iD.actionAddMember', function() { ]); var member = { id: '-', type: 'way' }; - var hint = { item: member, nextTo: '=' }; - graph = iD.actionAddMember('r', member, undefined, hint)(graph); + var insertPair = { + originalID: '=', + insertedID: '-', + nodes: ['c','b','a'] + }; + graph = iD.actionAddMember('r', member, undefined, insertPair)(graph); expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']); }); From 1b965666e26085e3c4ca4d9978f0f58e76f7d67e Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 12 Jan 2018 08:15:05 +0000 Subject: [PATCH 147/206] chore(package): update shelljs to version 0.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d1f3f47071..c4d1dfafdb 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "rollup-plugin-includepaths": "0.2.2", "rollup-plugin-json": "2.2.0", "rollup-plugin-node-resolve": "3.0.0", - "shelljs": "^0.7.5", + "shelljs": "^0.8.0", "shx": "^0.2.1", "sinon": "^4.0.0", "sinon-chai": "^2.14.0", From 4abc5ccf35bf6d7fe112fd222ce5c7544a6fcb24 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 16 Jan 2018 21:01:14 -0500 Subject: [PATCH 148/206] Update rollup dependencies and switch to patch semver --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c4d1dfafdb..7ac9f200ae 100644 --- a/package.json +++ b/package.json @@ -61,11 +61,11 @@ "npm-run-all": "^4.0.0", "phantomjs-prebuilt": "~2.1.11", "request": "^2.81.0", - "rollup": "0.53.3", - "rollup-plugin-commonjs": "8.2.6", - "rollup-plugin-includepaths": "0.2.2", - "rollup-plugin-json": "2.2.0", - "rollup-plugin-node-resolve": "3.0.0", + "rollup": "~0.54.0", + "rollup-plugin-commonjs": "~8.2.6", + "rollup-plugin-includepaths": "~0.2.2", + "rollup-plugin-json": "~2.2.0", + "rollup-plugin-node-resolve": "~3.0.0", "shelljs": "^0.8.0", "shx": "^0.2.1", "sinon": "^4.0.0", From 3be577d8db034024bcc6fb7d3a7034da58987986 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 16 Jan 2018 21:37:43 -0500 Subject: [PATCH 149/206] However we fix actionAddMember, it needs to work for incomplete relations --- test/spec/actions/add_member.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/spec/actions/add_member.js b/test/spec/actions/add_member.js index 3f78950979..40b9a63c28 100644 --- a/test/spec/actions/add_member.js +++ b/test/spec/actions/add_member.js @@ -10,6 +10,26 @@ describe('iD.actionAddMember', function() { return graph.entity('r').members.map(function (m) { return m.id; }); } + it('handles incomplete relations', function () { + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmNode({id: 'd', loc: [0, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '=', nodes: ['c','d']}), + iD.osmRelation({id: 'r', members: [ + {id: '~', type: 'way'}, + {id: '-', type: 'way'} + ]}) + ]); + + graph = iD.actionAddMember('r', {id: '=', type: 'way'})(graph); + + var ids = graph.entity('r').members.map(function(m) { return m.id; }); + expect(members(graph)).to.eql(['~', '-', '=']); + }); + it('adds the member to a relation with no members', function() { var graph = iD.coreGraph([ iD.osmNode({id: 'a', loc: [0, 0]}), From 9fee9f01d69896c1e456e6d5780d18c8022efb50 Mon Sep 17 00:00:00 2001 From: Benoit Costamagna Date: Wed, 17 Jan 2018 12:48:33 +0100 Subject: [PATCH 150/206] Added a boathouse preset --- data/presets.yaml | 4 ++++ data/presets/presets.json | 17 +++++++++++++++++ data/presets/presets/building/boathouse.json | 17 +++++++++++++++++ data/taginfo.json | 4 ++++ dist/locales/en.json | 4 ++++ 5 files changed, 46 insertions(+) create mode 100644 data/presets/presets/building/boathouse.json diff --git a/data/presets.yaml b/data/presets.yaml index 7577852d3e..1400a67377 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -2491,6 +2491,10 @@ en: # building=barn name: Barn terms: '' + building/boathouse: + # building=boathouse + name: Boathouse + terms: '' building/bungalow: # building=bungalow name: Bungalow diff --git a/data/presets/presets.json b/data/presets/presets.json index 415fc8d7c0..074a879d7f 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -4247,6 +4247,23 @@ "matchScore": 0.5, "name": "Barn" }, + "building/boathouse": { + "icon": "harbor", + "fields": [ + "name", + "levels", + "address" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "boathouse" + }, + "matchScore": 0.5, + "terms": [], + "name": "Boathouse" + }, "building/bungalow": { "icon": "home", "fields": [ diff --git a/data/presets/presets/building/boathouse.json b/data/presets/presets/building/boathouse.json new file mode 100644 index 0000000000..94d52cba03 --- /dev/null +++ b/data/presets/presets/building/boathouse.json @@ -0,0 +1,17 @@ +{ + "icon": "harbor", + "fields": [ + "name", + "levels", + "address" + ], + "geometry": [ + "area" + ], + "tags": { + "building": "boathouse" + }, + "matchScore": 0.5, + "terms": [], + "name": "Boathouse" +} diff --git a/data/taginfo.json b/data/taginfo.json index d03317a08c..e356c22287 100644 --- a/data/taginfo.json +++ b/data/taginfo.json @@ -788,6 +788,10 @@ "key": "building", "value": "barn" }, + { + "key": "building", + "value": "boathouse" + }, { "key": "building", "value": "bungalow" diff --git a/dist/locales/en.json b/dist/locales/en.json index 976862a902..a81b5609b8 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -3425,6 +3425,10 @@ "name": "Barn", "terms": "" }, + "building/boathouse": { + "name": "Boathouse", + "terms": "" + }, "building/bungalow": { "name": "Bungalow", "terms": "home,detached" From 4dc32343d7af8b05aa8f24f0385d3361b087772c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 17 Jan 2018 08:03:53 -0500 Subject: [PATCH 151/206] npm run build to rebuild preset file --- data/presets.yaml | 3 ++ data/presets/fields.json | 5 +++ data/presets/presets.json | 72 ++++++++++++++++++++++++++------------- dist/locales/en.json | 3 ++ 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/data/presets.yaml b/data/presets.yaml index 1400a67377..92d2f3da3f 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -1145,6 +1145,9 @@ en: ref: # ref=* label: Reference Code + ref/isil: + # 'ref:isil=*' + label: ISIL Code ref_aeroway_gate: # ref=* label: Gate Number diff --git a/data/presets/fields.json b/data/presets/fields.json index 90b49d4ccc..25c2fa544e 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -1571,6 +1571,11 @@ "type": "text", "label": "Reference Code" }, + "ref/isil": { + "key": "ref:isil", + "type": "text", + "label": "ISIL Code" + }, "relation": { "key": "type", "type": "combo", diff --git a/data/presets/presets.json b/data/presets/presets.json index 074a879d7f..3686b7629b 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -1882,7 +1882,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "geometry": [ "point", @@ -44431,7 +44432,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44454,7 +44456,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44477,7 +44480,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44500,7 +44504,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44523,7 +44528,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44546,7 +44552,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44569,7 +44576,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44592,7 +44600,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44615,7 +44624,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44638,7 +44648,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44661,7 +44672,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44684,7 +44696,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44707,7 +44720,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44730,7 +44744,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44753,7 +44768,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44776,7 +44792,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44799,7 +44816,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44822,7 +44840,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44845,7 +44864,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44868,7 +44888,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44891,7 +44912,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44914,7 +44936,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, @@ -44937,7 +44960,8 @@ "opening_hours", "internet_access", "internet_access/fee", - "internet_access/ssid" + "internet_access/ssid", + "ref/isil" ], "suggestion": true }, diff --git a/dist/locales/en.json b/dist/locales/en.json index a81b5609b8..6ddd7846d5 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -2249,6 +2249,9 @@ "ref": { "label": "Reference Code" }, + "ref/isil": { + "label": "ISIL Code" + }, "relation": { "label": "Type" }, From 8c9a0eb2c1fe7af7e40e3ac51a126417805488e8 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 17 Jan 2018 09:23:09 -0500 Subject: [PATCH 152/206] Fix doubleclick on line to create a point (closes #4691) --- modules/modes/select.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/modes/select.js b/modules/modes/select.js index 1c87a96ed7..ba9af0b951 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -248,13 +248,13 @@ export function modeSelect(context, selectedIDs) { var target = d3_select(d3_event.target); var datum = target.datum(); - var entity = datum && datum.id && context.hasEntity(datum.id); - if (entity) datum = entity; + var entity = datum && datum.properties && datum.properties.entity; + if (!entity) return; - if (datum instanceof osmWay && target.classed('target')) { - var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection); - var prev = datum.nodes[choice.index - 1]; - var next = datum.nodes[choice.index]; + if (entity instanceof osmWay && target.classed('target')) { + var choice = geoChooseEdge(context.childNodes(entity), context.mouse(), context.projection); + var prev = entity.nodes[choice.index - 1]; + var next = entity.nodes[choice.index]; context.perform( actionAddMidpoint({loc: choice.loc, edge: [prev, next]}, osmNode()), @@ -264,9 +264,9 @@ export function modeSelect(context, selectedIDs) { d3_event.preventDefault(); d3_event.stopPropagation(); - } else if (datum.type === 'midpoint') { + } else if (entity.type === 'midpoint') { context.perform( - actionAddMidpoint({loc: datum.loc, edge: datum.edge}, osmNode()), + actionAddMidpoint({loc: entity.loc, edge: entity.edge}, osmNode()), t('operations.add.annotation.vertex')); d3_event.preventDefault(); From baa289c6c22dec1532f7ed2eef81a2dcc4be707f Mon Sep 17 00:00:00 2001 From: JamesKingdom Date: Wed, 17 Jan 2018 17:22:58 +0000 Subject: [PATCH 153/206] Add name and elevation fields to guidepost preset (closes #4700) --- data/presets/presets.json | 2 ++ data/presets/presets/tourism/information/guidepost.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/data/presets/presets.json b/data/presets/presets.json index 3686b7629b..f369241f48 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -18514,6 +18514,8 @@ "tourism/information/guidepost": { "icon": "information", "fields": [ + "name", + "elevation", "operator", "ref" ], diff --git a/data/presets/presets/tourism/information/guidepost.json b/data/presets/presets/tourism/information/guidepost.json index 2fe4f0edef..f9e0ee043a 100644 --- a/data/presets/presets/tourism/information/guidepost.json +++ b/data/presets/presets/tourism/information/guidepost.json @@ -1,6 +1,8 @@ { "icon": "information", "fields": [ + "name", + "elevation", "operator", "ref" ], From 9bbb5db5ef1ed1bea67fe75100e305294ef2ad69 Mon Sep 17 00:00:00 2001 From: JamesKingdom Date: Wed, 17 Jan 2018 17:42:35 +0000 Subject: [PATCH 154/206] Rename garage landuse preset (closes #4697) --- data/presets.yaml | 4 ++-- data/presets/presets.json | 2 +- data/presets/presets/landuse/garages.json | 2 +- dist/locales/en.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/presets.yaml b/data/presets.yaml index 92d2f3da3f..7cf9cab58d 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -3413,8 +3413,8 @@ en: terms: '' landuse/garages: # landuse=garages - name: Garages - terms: '' + name: Garage Landuse + terms: '' landuse/grass: # landuse=grass name: Grass diff --git a/data/presets/presets.json b/data/presets/presets.json index f369241f48..7ff1bc71cd 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -8414,7 +8414,7 @@ "landuse": "garages" }, "terms": [], - "name": "Garages" + "name": "Garage Landuse" }, "landuse/grass": { "geometry": [ diff --git a/data/presets/presets/landuse/garages.json b/data/presets/presets/landuse/garages.json index ba5f253ab1..72575fdd93 100644 --- a/data/presets/presets/landuse/garages.json +++ b/data/presets/presets/landuse/garages.json @@ -9,5 +9,5 @@ "landuse": "garages" }, "terms": [], - "name": "Garages" + "name": "Garage Landuse" } diff --git a/dist/locales/en.json b/dist/locales/en.json index 6ddd7846d5..ae008afc4e 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -4257,7 +4257,7 @@ "terms": "tree" }, "landuse/garages": { - "name": "Garages", + "name": "Garage Landuse", "terms": "" }, "landuse/grass": { From dfefdf93263d9a3893b067d7680cd85b1542cb15 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 18 Jan 2018 02:37:13 +0000 Subject: [PATCH 155/206] chore(package): update mocha to version 5.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ac9f200ae..cc85b6b2e4 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "jsonschema": "^1.1.0", "mapillary-js": "2.10.1", "minimist": "^1.2.0", - "mocha": "^4.0.0", + "mocha": "^5.0.0", "mocha-phantomjs-core": "^2.1.0", "name-suggestion-index": "0.1.3", "npm-run-all": "^4.0.0", From 8f9a46b75a8ac9a9030a9e11cbd4be589867374f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 17 Jan 2018 22:59:55 -0500 Subject: [PATCH 156/206] Change actionAddMember to rearrange indexed members in place This allows it to work around issues where a relation may not be completely downloaded --- modules/actions/add_member.js | 153 ++++++++++++++++++++++++++-------- 1 file changed, 119 insertions(+), 34 deletions(-) diff --git a/modules/actions/add_member.js b/modules/actions/add_member.js index ab15f343ce..67f1d8b040 100644 --- a/modules/actions/add_member.js +++ b/modules/actions/add_member.js @@ -1,26 +1,13 @@ +import _clone from 'lodash-es/clone'; import _groupBy from 'lodash-es/groupBy'; +import _omit from 'lodash-es/omit'; import { osmJoinWays, osmWay } from '../osm'; export function actionAddMember(relationId, member, memberIndex, insertPair) { - // Relation.replaceMember() removes duplicates, and we don't want that.. #4696 - function replaceMemberAll(relation, needleID, replacement) { - var members = []; - for (var i = 0; i < relation.members.length; i++) { - var member = relation.members[i]; - if (member.id !== needleID) { - members.push(member); - } else { - members.push({id: replacement.id, type: replacement.type, role: member.role}); - } - } - return relation.update({members: members}); - } - - - var action = function(graph) { + return function action(graph) { var relation = graph.entity(relationId); if ((isNaN(memberIndex) || insertPair) && member.type === 'way') { @@ -37,9 +24,7 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) { // Add a way member into the relation "wherever it makes sense". // In this situation we were not supplied a memberIndex. function addWayMember(relation, graph) { - var groups; - var tempWay; - var i, j; + var groups, tempWay, item, i, j, k; if (insertPair) { // We're adding a member that must stay paired with an existing member. @@ -59,32 +44,56 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) { groups.way = groups.way || []; } else { - // Add the member anywhere.. Just push and let `osmJoinWays` decide where to put it. + // Add the member anywhere, one time. Just push and let `osmJoinWays` decide where to put it. groups = _groupBy(relation.members, function(m) { return m.type; }); groups.way = groups.way || []; groups.way.push(member); } - var joined = osmJoinWays(groups.way, graph); + var members = withIndex(groups.way); + var joined = osmJoinWays(members, graph); - var newWayMembers = []; + // `joined` might not contain all of the way members, + // But will contain only the completed (downloaded) members for (i = 0; i < joined.length; i++) { var segment = joined[i]; var nodes = segment.nodes.slice(); + var startIndex = segment[0].index; + + // j = array index in `members` where this segment starts + for (j = 0; j < members.length; j++) { + if (members[j].index === startIndex) { + break; + } + } + + // k = each member in segment + for (k = 0; k < segment.length; k++) { + item = segment[k]; + var way = graph.entity(item.id); - for (j = 0; j < segment.length; j++) { - var way = graph.entity(segment[j].id); - if (tempWay && segment[j].id === tempWay.id) { + // If this is a paired item, generate members in correct order and role + if (tempWay && item.id === tempWay.id) { if (nodes[0].id === insertPair.nodes[0]) { - newWayMembers.push({ id: insertPair.originalID, type: 'way', role: segment[j].role }); - newWayMembers.push({ id: insertPair.insertedID, type: 'way', role: segment[j].role }); + item.pair = [ + { id: insertPair.originalID, type: 'way', role: item.role }, + { id: insertPair.insertedID, type: 'way', role: item.role } + ]; } else { - newWayMembers.push({ id: insertPair.insertedID, type: 'way', role: segment[j].role }); - newWayMembers.push({ id: insertPair.originalID, type: 'way', role: segment[j].role }); + item.pair = [ + { id: insertPair.insertedID, type: 'way', role: item.role }, + { id: insertPair.originalID, type: 'way', role: item.role } + ]; } - } else { - newWayMembers.push(segment[j]); } + + // reorder `members` if necessary + if (k > 0) { + if (j+k >= members.length || item.index !== members[j+k].index) { + moveMember(members, item.index, j+k); + } + } + nodes.splice(0, way.nodes.length - 1); } } @@ -93,13 +102,89 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) { graph = graph.remove(tempWay); } + // Final pass: skip dead items, split pairs, remove index properties + var wayMembers = []; + for (i = 0; i < members.length; i++) { + item = members[i]; + if (item.index === -1) continue; + + if (item.pair) { + wayMembers.push(item.pair[0]); + wayMembers.push(item.pair[1]); + } else { + wayMembers.push(_omit(item, 'index')); + } + } + // Write members in the order: nodes, ways, relations // This is reccomended for Public Transport routes: // see https://wiki.openstreetmap.org/wiki/Public_transport#Service_routes - var newMembers = (groups.node || []).concat(newWayMembers, (groups.relation || [])); + var newMembers = (groups.node || []).concat(wayMembers, (groups.relation || [])); + return graph.replace(relation.update({members: newMembers})); - } - return action; + // `moveMember()` changes the `members` array in place by splicing + // the item with `.index = findIndex` to where it belongs, + // and marking the old position as "dead" with `.index = -1` + // + // j=5, k=0 jk + // segment 5 4 7 6 + // members 0 1 2 3 4 5 6 7 8 9 keep 5 in j+k + // + // j=5, k=1 j k + // segment 5 4 7 6 + // members 0 1 2 3 4 5 6 7 8 9 move 4 to j+k + // members 0 1 2 3 x 5 4 6 7 8 9 moved + // + // j=5, k=2 j k + // segment 5 4 7 6 + // members 0 1 2 3 x 5 4 6 7 8 9 move 7 to j+k + // members 0 1 2 3 x 5 4 7 6 x 8 9 moved + // + // j=5, k=3 j k + // segment 5 4 7 6 + // members 0 1 2 3 x 5 4 7 6 x 8 9 keep 6 in j+k + // + function moveMember(arr, findIndex, toIndex) { + for (var i = 0; i < arr.length; i++) { + if (arr[i].index === findIndex) { + break; + } + } + + var item = _clone(arr[i]); + arr[i].index = -1; // mark as dead + item.index = toIndex; + arr.splice(toIndex, 0, item); + } + + + // Relation.replaceMember() removes duplicates, and we don't want that.. #4696 + function replaceMemberAll(relation, needleID, replacement) { + var members = []; + for (var i = 0; i < relation.members.length; i++) { + var member = relation.members[i]; + if (member.id !== needleID) { + members.push(member); + } else { + members.push({id: replacement.id, type: replacement.type, role: member.role}); + } + } + return relation.update({members: members}); + } + + + // This is the same as `Relation.indexedMembers`, + // Except we don't want to index all the members, only the ways + function withIndex(arr) { + var result = new Array(arr.length); + for (var i = 0; i < arr.length; i++) { + result[i] = arr[i]; + result[i].index = i; + } + return result; + } + } + } From bdbc63b1c19b4d215c623734be2187b19d3d9200 Mon Sep 17 00:00:00 2001 From: Benoit Costamagna Date: Thu, 18 Jan 2018 07:48:25 +0100 Subject: [PATCH 157/206] removed the building tag from sports centre preset --- data/presets/presets.json | 21 ------------------- .../presets/leisure/sports_centre.json | 1 - 2 files changed, 22 deletions(-) diff --git a/data/presets/presets.json b/data/presets/presets.json index 074a879d7f..415a51dd64 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -10524,7 +10524,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "geometry": [ @@ -60571,7 +60570,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60591,7 +60589,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60611,7 +60608,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60631,7 +60627,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60651,7 +60646,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60671,7 +60665,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60691,7 +60684,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60711,7 +60703,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60731,7 +60722,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60751,7 +60741,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60771,7 +60760,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60791,7 +60779,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60811,7 +60798,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60831,7 +60817,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60851,7 +60836,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60871,7 +60855,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60891,7 +60874,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60911,7 +60893,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60931,7 +60912,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true @@ -60951,7 +60931,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "suggestion": true diff --git a/data/presets/presets/leisure/sports_centre.json b/data/presets/presets/leisure/sports_centre.json index 3c4eeffccd..e310be451e 100644 --- a/data/presets/presets/leisure/sports_centre.json +++ b/data/presets/presets/leisure/sports_centre.json @@ -4,7 +4,6 @@ "name", "sport", "address", - "building_area", "opening_hours" ], "geometry": [ From 189204f506b3676e7aaebd0efc46edb5cfff94d6 Mon Sep 17 00:00:00 2001 From: Benoit Costamagna Date: Thu, 18 Jan 2018 17:07:16 +0100 Subject: [PATCH 158/206] Added bus guideway preset and modified the css to render it in blue --- css/30_highways.css | 21 +++++++++++++++++++ data/presets.yaml | 4 ++++ data/presets/presets.json | 17 +++++++++++++++ .../presets/presets/highway/bus_guideway.json | 17 +++++++++++++++ data/taginfo.json | 4 ++++ dist/locales/en.json | 4 ++++ 6 files changed, 67 insertions(+) create mode 100644 data/presets/presets/highway/bus_guideway.json diff --git a/css/30_highways.css b/css/30_highways.css index 8dcb1cdab2..148e7fd364 100644 --- a/css/30_highways.css +++ b/css/30_highways.css @@ -215,6 +215,21 @@ path.casing.tag-residential { stroke:#444; } +.preset-icon .icon.highway-bus_guideway { + color: #fff; + fill: #444; +} +path.stroke.tag-highway-bus_guideway, +path.stroke.tag-bus_guideway { + stroke:#fff; + stroke-linecap: butt; + stroke-dasharray: 12, 16; +} +path.casing.tag-highway-bus_guideway, +path.casing.tag-bus_guideway { + stroke:#66a3ff; +} + .preset-icon .icon.highway-unclassified { color: #dcd9b9; fill: #444; @@ -270,6 +285,7 @@ path.shadow.tag-highway-corridor, path.shadow.tag-highway-pedestrian, path.shadow.tag-highway-steps, path.shadow.tag-path, +path.shadow.tag-highway-bus_guideway, path.shadow.tag-footway, path.shadow.tag-cycleway, path.shadow.tag-bridleway, @@ -286,6 +302,7 @@ path.casing.tag-highway-corridor, path.casing.tag-highway-pedestrian, path.casing.tag-highway-steps, path.casing.tag-path, +path.casing.tag-highway-bus_guideway, path.casing.tag-footway, path.casing.tag-cycleway, path.casing.tag-bridleway, @@ -302,6 +319,7 @@ path.stroke.tag-highway-corridor, path.stroke.tag-highway-pedestrian, path.stroke.tag-highway-steps, path.stroke.tag-path, +path.stroke.tag-highway-bus_guideway, path.stroke.tag-footway, path.stroke.tag-cycleway, path.stroke.tag-bridleway, @@ -350,6 +368,7 @@ path.stroke.tag-steps { .low-zoom path.shadow.tag-highway-pedestrian, .low-zoom path.shadow.tag-highway-steps, .low-zoom path.shadow.tag-path, +.low-zoom path.shadow.tag-highway-bus_guideway, .low-zoom path.shadow.tag-footway, .low-zoom path.shadow.tag-cycleway, .low-zoom path.shadow.tag-bridleway, @@ -366,6 +385,7 @@ path.stroke.tag-steps { .low-zoom path.casing.tag-highway-pedestrian, .low-zoom path.casing.tag-highway-steps, .low-zoom path.casing.tag-path, +.low-zoom path.casing.tag-highway-bus_guideway, .low-zoom path.casing.tag-footway, .low-zoom path.casing.tag-cycleway, .low-zoom path.casing.tag-bridleway, @@ -382,6 +402,7 @@ path.stroke.tag-steps { .low-zoom path.stroke.tag-highway-pedestrian, .low-zoom path.stroke.tag-highway-steps, .low-zoom path.stroke.tag-path, +.low-zoom path.stroke.tag-highway-bus_guideway, .low-zoom path.stroke.tag-footway, .low-zoom path.stroke.tag-cycleway, .low-zoom path.stroke.tag-bridleway, diff --git a/data/presets.yaml b/data/presets.yaml index 7cf9cab58d..74ef602ee2 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -3096,6 +3096,10 @@ en: name: Bridle Path # 'terms: bridleway,equestrian,horse' terms: '' + highway/bus_guideway: + # 'highway=bus_guideway, access=no, bus=designated' + name: Bus Guideway + terms: '' highway/bus_stop: # highway=bus_stop name: Bus Stop / Platform diff --git a/data/presets/presets.json b/data/presets/presets.json index 49f2a2c55c..64f5a00aed 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -6929,6 +6929,23 @@ ], "name": "Bridle Path" }, + "highway/bus_guideway": { + "icon": "bus", + "fields": [ + "name", + "operator" + ], + "geometry": [ + "line" + ], + "tags": { + "highway": "bus_guideway", + "access": "no", + "bus": "designated" + }, + "terms": [], + "name": "Bus Guideway" + }, "highway/corridor": { "icon": "highway-footway", "fields": [ diff --git a/data/presets/presets/highway/bus_guideway.json b/data/presets/presets/highway/bus_guideway.json new file mode 100644 index 0000000000..9d1dadc5c3 --- /dev/null +++ b/data/presets/presets/highway/bus_guideway.json @@ -0,0 +1,17 @@ +{ + "icon": "bus", + "fields": [ + "name", + "operator" + ], + "geometry": [ + "line" + ], + "tags": { + "highway": "bus_guideway", + "access": "no", + "bus": "designated" + }, + "terms": [], + "name": "Bus Guideway" +} diff --git a/data/taginfo.json b/data/taginfo.json index e356c22287..84da0f7be4 100644 --- a/data/taginfo.json +++ b/data/taginfo.json @@ -1332,6 +1332,10 @@ "key": "highway", "value": "bridleway" }, + { + "key": "bus", + "value": "designated" + }, { "key": "highway", "value": "corridor" diff --git a/dist/locales/en.json b/dist/locales/en.json index ae008afc4e..2b486058ea 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -3976,6 +3976,10 @@ "name": "Bridle Path", "terms": "bridleway,equestrian,horse" }, + "highway/bus_guideway": { + "name": "Bus Guideway", + "terms": "" + }, "highway/corridor": { "name": "Indoor Corridor", "terms": "gallery,hall,hallway,indoor,passage,passageway" From d6afd399fcedd98f26b34180e6a941c0006e0387 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 18 Jan 2018 14:34:34 -0500 Subject: [PATCH 159/206] Revised and expanded actionSplit tests to cover route splitting --- test/spec/actions/add_member.js | 2 - test/spec/actions/split.js | 715 +++++++++++++++++++++++--------- 2 files changed, 514 insertions(+), 203 deletions(-) diff --git a/test/spec/actions/add_member.js b/test/spec/actions/add_member.js index 40b9a63c28..b2a8486ec2 100644 --- a/test/spec/actions/add_member.js +++ b/test/spec/actions/add_member.js @@ -25,8 +25,6 @@ describe('iD.actionAddMember', function() { ]); graph = iD.actionAddMember('r', {id: '=', type: 'way'})(graph); - - var ids = graph.entity('r').members.map(function(m) { return m.id; }); expect(members(graph)).to.eql(['~', '-', '=']); }); diff --git a/test/spec/actions/split.js b/test/spec/actions/split.js index 1ebbcf89c1..febf47d723 100644 --- a/test/spec/actions/split.js +++ b/test/spec/actions/split.js @@ -7,85 +7,122 @@ describe('iD.actionSplit', function () { describe('#disabled', function () { it('returns falsy for a non-end node of a single way', function () { + // + // a ---> b ---> c split at 'b' not disabled + // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}) + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [2, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'] }) ]); expect(iD.actionSplit('b').disabled(graph)).not.to.be.ok; }); it('returns falsy for an intersection of two ways', function () { + // + // c + // | + // a ---> * ---> b split at '*' not disabled + // | + // d + // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: '*'}), - iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), - iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) + iD.osmNode({ id: 'a', loc: [-1, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [0, 1] }), + iD.osmNode({ id: 'd', loc: [0, -1] }), + iD.osmNode({ id: '*', loc: [0, 0] }), + iD.osmWay({ id: '-', nodes: ['a', '*', 'b'] }), + iD.osmWay({ id: '|', nodes: ['c', '*', 'd'] }) ]); expect(iD.actionSplit('*').disabled(graph)).not.to.be.ok; }); it('returns falsy for an intersection of two ways with parent way specified', function () { + // + // c + // | + // a ---> * ---> b split '-' at '*' not disabled + // | + // d + // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: '*'}), - iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), - iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) + iD.osmNode({ id: 'a', loc: [-1, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [0, 1] }), + iD.osmNode({ id: 'd', loc: [0, -1] }), + iD.osmNode({ id: '*', loc: [0, 0] }), + iD.osmWay({ id: '-', nodes: ['a', '*', 'b'] }), + iD.osmWay({ id: '|', nodes: ['c', '*', 'd'] }) ]); expect(iD.actionSplit('*').limitWays(['-']).disabled(graph)).not.to.be.ok; }); it('returns falsy for a self-intersection', function () { + // + // b -- c + // | / + // | / split '-' at 'a' not disabled + // | / + // a -- b + // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'c'}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']}) + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [0, 2] }), + iD.osmNode({ id: 'c', loc: [1, 2] }), + iD.osmNode({ id: 'd', loc: [1, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c', 'a', 'd'] }) ]); expect(iD.actionSplit('a').disabled(graph)).not.to.be.ok; }); it('returns \'not_eligible\' for the first node of a single way', function () { + // + // a ---> b split at 'a' disabled - 'not eligible' + // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmWay({id: '-', nodes: ['a', 'b']}) + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b'] }) ]); expect(iD.actionSplit('a').disabled(graph)).to.equal('not_eligible'); }); it('returns \'not_eligible\' for the last node of a single way', function () { + // + // a ---> b split at 'b' disabled - 'not eligible' + // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmWay({id: '-', nodes: ['a', 'b']}) + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b'] }) ]); expect(iD.actionSplit('b').disabled(graph)).to.equal('not_eligible'); }); it('returns \'not_eligible\' for an intersection of two ways with non-parent way specified', function () { + // + // c + // | + // a ---> * ---> b split '-' and '=' at '*' disabled - 'not eligible' + // | (there is no '=' here) + // d + // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: '*'}), - iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), - iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) + iD.osmNode({ id: 'a', loc: [-1, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [0, 1] }), + iD.osmNode({ id: 'd', loc: [0, -1] }), + iD.osmNode({ id: '*', loc: [0, 0] }), + iD.osmWay({ id: '-', nodes: ['a', '*', 'b'] }), + iD.osmWay({ id: '|', nodes: ['c', '*', 'd'] }) ]); expect(iD.actionSplit('*').limitWays(['-', '=']).disabled(graph)).to.equal('not_eligible'); @@ -96,19 +133,18 @@ describe('iD.actionSplit', function () { describe('ways', function () { it('creates a new way with the appropriate nodes', function () { - // Situation: - // a ---- b ---- c // - // Split at b. + // Situation: + // a ---> b ---> c split at 'b' // // Expected result: - // a ---- b ==== c + // a ---> b ===> c // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}) + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [2, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'] }) ]); graph = iD.actionSplit('b', ['='])(graph); @@ -118,12 +154,12 @@ describe('iD.actionSplit', function () { }); it('copies tags to the new way', function () { - var tags = {highway: 'residential'}; + var tags = { highway: 'residential' }; var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c'], tags: tags}) + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [2, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'], tags: tags }) ]); graph = iD.actionSplit('b', ['='])(graph); @@ -134,23 +170,22 @@ describe('iD.actionSplit', function () { }); it('splits a way at a T-junction', function () { + // // Situation: - // a ---- b ---- c + // a ---- b ---- c split at 'b' // | // d // - // Split at b. - // // Expected result: // a ---- b ==== c // | // d // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + iD.osmNode({ id: 'a', loc: [-1, 0] }), + iD.osmNode({ id: 'b', loc: [0, 0] }), + iD.osmNode({ id: 'c', loc: [1, 0] }), + iD.osmNode({ id: 'd', loc: [0, -1] }), iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), iD.osmWay({id: '|', nodes: ['d', 'b']}) ]); @@ -163,30 +198,29 @@ describe('iD.actionSplit', function () { }); it('splits multiple ways at an intersection', function () { - // Situation: - // c - // | - // a ---- * ---- b - // ¦ - // d // - // Split at b. + // Situation: + // c + // | + // a ---- * ---- b split at '*' + // | + // d // // Expected result: - // c - // | - // a ---- * ==== b - // ¦ - // d + // c + // | + // a ---- * ==== b + // ¦ + // d // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), - iD.osmNode({id: '*'}), - iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), - iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) + iD.osmNode({ id: 'a', loc: [-1, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [0, 1] }), + iD.osmNode({ id: 'd', loc: [0, -1] }), + iD.osmNode({ id: '*', loc: [0, 0] }), + iD.osmWay({ id: '-', nodes: ['a', '*', 'b'] }), + iD.osmWay({ id: '|', nodes: ['c', '*', 'd'] }) ]); graph = iD.actionSplit('*', ['=', '¦'])(graph); @@ -198,14 +232,21 @@ describe('iD.actionSplit', function () { }); it('splits the specified ways at an intersection', function () { + // + // c + // | + // a ---- * ---- b split at '*' + // | + // d + // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), - iD.osmNode({id: '*'}), - iD.osmWay({id: '-', nodes: ['a', '*', 'b']}), - iD.osmWay({id: '|', nodes: ['c', '*', 'd']}) + iD.osmNode({ id: 'a', loc: [-1, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [0, 1] }), + iD.osmNode({ id: 'd', loc: [0, -1] }), + iD.osmNode({ id: '*', loc: [0, 0] }), + iD.osmWay({ id: '-', nodes: ['a', '*', 'b'] }), + iD.osmWay({ id: '|', nodes: ['c', '*', 'd'] }) ]); var g1 = iD.actionSplit('*', ['=']).limitWays(['-'])(graph); @@ -226,26 +267,27 @@ describe('iD.actionSplit', function () { }); it('splits self-intersecting ways', function () { + // // Situation: - // b + // b + // /| // / | // / | - // c - a -- d - // - // Split at a. + // c - a -- d split at 'a' // // Expected result: - // b + // b + // /| // / | // / | // c - a == d // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']}) + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [0, 2] }), + iD.osmNode({ id: 'c', loc: [-1, 0] }), + iD.osmNode({ id: 'd', loc: [1, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c', 'a', 'd'] }) ]); graph = iD.actionSplit('a', ['='])(graph); @@ -255,24 +297,18 @@ describe('iD.actionSplit', function () { }); it('splits a closed way at the given point and its antipode', function () { + // // Situation: // a ---- b // | | // d ---- c // - // Split at a. - // - // Expected result: - // a ---- b - // || | - // d ==== c - // var graph = iD.coreGraph([ - iD.osmNode({id: 'a', loc: [0,1]}), - iD.osmNode({id: 'b', loc: [1,1]}), - iD.osmNode({id: 'c', loc: [1,0]}), - iD.osmNode({id: 'd', loc: [0,0]}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + iD.osmNode({ id: 'a', loc: [0, 1] }), + iD.osmNode({ id: 'b', loc: [1, 1] }), + iD.osmNode({ id: 'c', loc: [1, 0] }), + iD.osmNode({ id: 'd', loc: [0, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) ]); var g1 = iD.actionSplit('a', ['='])(graph); @@ -296,197 +332,474 @@ describe('iD.actionSplit', function () { describe('relations', function () { + function members(graph) { + return graph.entity('r').members.map(function (m) { return m.id; }); + } + + it('handles incomplete relations', function () { + // + // Situation: + // a ---> b ---> c split at 'b' + // Relation: ['~', '-'] + // + // Expected result: + // a ---> b ===> c + // Relation: ['~', '-', '='] + // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), - iD.osmRelation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [2, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'] }), + iD.osmRelation({id: 'r', members: [ + { id: '~', type: 'way' }, + { id: '-', type: 'way' } + ]}) ]); graph = iD.actionSplit('b', ['='])(graph); - - var ids = graph.entity('r').members.map(function(m) { return m.id; }); - expect(ids).to.have.ordered.members(['~', '-', '=']); + expect(members(graph)).to.eql(['~', '-', '=']); }); describe('member ordering', function () { it('adds the new way to parent relations (simple)', function () { - // Situation: - // a ----> b ----> c - // Relation: [----] // - // Split at b. + // Situation: + // a ---> b ---> c split at 'b' + // Relation: ['-'] // // Expected result: - // a ----> b ====> c - // Relation: [----, ====] + // a ---> b ===> c + // Relation: ['-', '='] // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), - iD.osmRelation({id: 'r', members: [{id: '-', type: 'way', role: 'forward'}]}) + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [2, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'] }), + iD.osmRelation({id: 'r', members: [ + { id: '-', type: 'way', role: 'forward' } + ]}) ]); graph = iD.actionSplit('b', ['='])(graph); expect(graph.entity('r').members).to.eql([ - {id: '-', type: 'way', role: 'forward'}, - {id: '=', type: 'way', role: 'forward'} + { id: '-', type: 'way', role: 'forward' }, + { id: '=', type: 'way', role: 'forward' } ]); }); it('adds the new way to parent relations (forward order)', function () { - // Situation: - // a ----> b ----> c ~~~~> d - // Relation: [----, ~~~~] // - // Split at b. + // Situation: + // a ---> b ---> c ~~~> d split at 'b' + // Relation: ['-', '~'] // // Expected result: - // a ----> b ====> c ~~~~> d - // Relation: [----, ====, ~~~~] + // a ---> b ===> c ~~~> d + // Relation: ['-', '=', '~'] // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), - iD.osmWay({id: '~', nodes: ['c', 'd']}), - iD.osmRelation({id: 'r', members: [{id: '-', type: 'way'}, {id: '~', type: 'way'}]}) + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [2, 0] }), + iD.osmNode({ id: 'd', loc: [3, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'] }), + iD.osmWay({ id: '~', nodes: ['c', 'd'] }), + iD.osmRelation({id: 'r', members: [ + { id: '-', type: 'way' }, + { id: '~', type: 'way' } + ]}) ]); graph = iD.actionSplit('b', ['='])(graph); - - var ids = graph.entity('r').members.map(function(m) { return m.id; }); - expect(ids).to.have.ordered.members(['-', '=', '~']); + expect(members(graph)).to.eql(['-', '=', '~']); }); it('adds the new way to parent relations (reverse order)', function () { + // // Situation: - // a ----> b ----> c ~~~~> d - // Relation: [~~~~, ----] + // a ---> b ---> c ~~~> d split at 'b' + // Relation: ['~', '-'] + // + // Expected result: + // a ---> b ===> c ~~~> d + // Relation: ['~', '=', '-'] // - // Split at b. + var graph = iD.coreGraph([ + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [2, 0] }), + iD.osmNode({ id: 'd', loc: [3, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'] }), + iD.osmWay({ id: '~', nodes: ['c', 'd'] }), + iD.osmRelation({id: 'r', members: [ + { id: '~', type: 'way' }, + { id: '-', type: 'way' } + ]}) + ]); + + graph = iD.actionSplit('b', ['='])(graph); + expect(members(graph)).to.eql(['~', '=', '-']); + }); + }); + + describe('splitting out-and-back routes', function () { + var a = iD.osmNode({ id: 'a', loc: [0, 0] }); + var b = iD.osmNode({ id: 'b', loc: [0, 1] }); + var c = iD.osmNode({ id: 'c', loc: [0, 2] }); + var d = iD.osmNode({ id: 'd', loc: [0, 3] }); + + it('splits out-and-back1 route at b', function () { + // + // Situation: + // a ---> b ---> c ~~~> d split at 'b' + // Relation: ['-', '~', '~', '-'] // // Expected result: - // a ----> b ====> c ~~~~> d - // Relation: [~~~~, ====, ----] + // a ---> b ===> c ~~~> d + // Relation: ['-', '=', '~', '~', '=', '-'] // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + a, b, c, d, iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), iD.osmWay({id: '~', nodes: ['c', 'd']}), - iD.osmRelation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) + iD.osmRelation({id: 'r', members: [ + {id: '-', type: 'way'}, + {id: '~', type: 'way'}, + {id: '~', type: 'way'}, + {id: '-', type: 'way'} + ]}) ]); - graph = iD.actionSplit('b', ['='])(graph); - var ids = graph.entity('r').members.map(function(m) { return m.id; }); - expect(ids).to.have.ordered.members(['~', '=', '-']); + expect(graph.entity('-').nodes).to.eql(['a', 'b']); + expect(graph.entity('=').nodes).to.eql(['b', 'c']); + expect(graph.entity('~').nodes).to.eql(['c', 'd']); + expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']); }); - it('adds the new way to parent relations (unsplit way belongs multiple times)', function () { - // Situation: - // a ----> b ----> c ~~~~> d - // Relation: [~~~~, ----, ~~~~] + it('splits out-and-back2 route at b', function () { // - // Split at b. + // Situation: + // a <--- b <--- c ~~~> d split at 'b' + // Relation: ['-', '~', '~', '-'] // // Expected result: - // a ----> b ====> c ~~~~> d - // Relation: [~~~~, ====, ----, ====, ~~~~] + // a <=== b <--- c ~~~> d + // Relation: ['=', '-', '~', '~', '-', '='] // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), - iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + a, b, c, d, + iD.osmWay({id: '-', nodes: ['c', 'b', 'a']}), iD.osmWay({id: '~', nodes: ['c', 'd']}), iD.osmRelation({id: 'r', members: [ - {id: '~', type: 'way'}, {id: '-', type: 'way'}, - {id: '~', type: 'way'} + {id: '~', type: 'way'}, + {id: '~', type: 'way'}, + {id: '-', type: 'way'} ]}) ]); - graph = iD.actionSplit('b', ['='])(graph); - var ids = graph.entity('r').members.map(function(m) { return m.id; }); - expect(ids).to.have.ordered.members(['~', '=', '-', '=', '~']); + expect(graph.entity('-').nodes).to.eql(['c', 'b']); + expect(graph.entity('=').nodes).to.eql(['b', 'a']); + expect(graph.entity('~').nodes).to.eql(['c', 'd']); + expect(members(graph)).to.eql(['=', '-', '~', '~', '-', '=']); }); - it('adds the new way to parent relations (forward split way belongs multiple times)', function () { - // Situation: - // a ----> b ----> c ~~~~> d - // Relation: [----, ~~~~, ----] + it('splits out-and-back3 route at b', function () { // - // Split at b. + // Situation: + // a ---> b ---> c <~~~ d split at 'b' + // Relation: ['-', '~', '~', '-'] // // Expected result: - // a ----> b ====> c ~~~~> d - // Relation: [----, ====, ~~~~, ====, ----] + // a ---> b ===> c <~~~ d + // Relation: ['-', '=', '~', '~', '=', '-'] // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + a, b, c, d, iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), - iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmWay({id: '~', nodes: ['d', 'c']}), iD.osmRelation({id: 'r', members: [ {id: '-', type: 'way'}, {id: '~', type: 'way'}, + {id: '~', type: 'way'}, {id: '-', type: 'way'} ]}) ]); - graph = iD.actionSplit('b', ['='])(graph); - var ids = graph.entity('r').members.map(function(m) { return m.id; }); - expect(ids).to.have.ordered.members(['-', '=', '~', '=', '-']); + expect(graph.entity('-').nodes).to.eql(['a', 'b']); + expect(graph.entity('=').nodes).to.eql(['b', 'c']); + expect(graph.entity('~').nodes).to.eql(['d', 'c']); + expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']); }); - it('adds the new way to parent relations (reverse split way belongs multiple times)', function () { - // Situation: - // a <---- b <---- c ~~~~> d - // Relation: [----, ~~~~, ----] + it('splits out-and-back4 route at b', function () { // - // Split at b. + // Situation: + // a <--- b <--- c <~~~ d split at 'b' + // Relation: ['-', '~', '~', '-'] // // Expected result: - // a <==== b <---- c ~~~~> d - // Relation: [====, ----, ~~~~, ----, ====] + // a <=== b <--- c <~~~ d + // Relation: ['=', '-', '~', '~', '-', '='] // var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + a, b, c, d, iD.osmWay({id: '-', nodes: ['c', 'b', 'a']}), - iD.osmWay({id: '~', nodes: ['c', 'd']}), + iD.osmWay({id: '~', nodes: ['d', 'c']}), iD.osmRelation({id: 'r', members: [ {id: '-', type: 'way'}, {id: '~', type: 'way'}, + {id: '~', type: 'way'}, {id: '-', type: 'way'} ]}) ]); - graph = iD.actionSplit('b', ['='])(graph); - var ids = graph.entity('r').members.map(function(m) { return m.id; }); - expect(ids).to.have.ordered.members(['=', '-', '~', '-', '=']); + expect(graph.entity('-').nodes).to.eql(['c', 'b']); + expect(graph.entity('=').nodes).to.eql(['b', 'a']); + expect(graph.entity('~').nodes).to.eql(['d', 'c']); + expect(members(graph)).to.eql(['=', '-', '~', '~', '-', '=']); + }); + }); + + + describe('splitting spoon routes', function () { + var a = iD.osmNode({ id: 'a', loc: [0, 0] }); + var b = iD.osmNode({ id: 'b', loc: [0, 1] }); + var c = iD.osmNode({ id: 'c', loc: [1, 1] }); + var d = iD.osmNode({ id: 'd', loc: [1, 0] }); + var e = iD.osmNode({ id: 'e', loc: [2, 0] }); + var f = iD.osmNode({ id: 'f', loc: [3, 0] }); + + // + // Situation: + // b --> c + // | | + // a <-- d ~~~> e ~~~> f + // + // Relation: ['~', '-', '~'] + // + var spoon1 = iD.coreGraph([ + a, b, c, d, e, f, + iD.osmWay({id: '-', nodes: ['d', 'a', 'b', 'c', 'd']}), + iD.osmWay({id: '~', nodes: ['d', 'e', 'f']}), + iD.osmRelation({id: 'r', members: [ + {id: '~', type: 'way'}, + {id: '-', type: 'way'}, + {id: '~', type: 'way'} + ]}) + ]); + + // + // Situation: + // b <-- c + // | | + // a --> d ~~~> e ~~~> f + // + // Relation: ['~', '-', '~'] + // + var spoon2 = iD.coreGraph([ + a, b, c, d, e, f, + iD.osmWay({id: '-', nodes: ['d', 'c', 'b', 'a', 'd']}), + iD.osmWay({id: '~', nodes: ['d', 'e', 'f']}), + iD.osmRelation({id: 'r', members: [ + {id: '~', type: 'way'}, + {id: '-', type: 'way'}, + {id: '~', type: 'way'} + ]}) + ]); + + // + // Situation: + // b --> c + // | | + // a <-- d <~~~ e <~~~ f + // + // Relation: ['~', '-', '~'] + // + var spoon3 = iD.coreGraph([ + a, b, c, d, e, f, + iD.osmWay({id: '-', nodes: ['d', 'a', 'b', 'c', 'd']}), + iD.osmWay({id: '~', nodes: ['f', 'e', 'd']}), + iD.osmRelation({id: 'r', members: [ + {id: '~', type: 'way'}, + {id: '-', type: 'way'}, + {id: '~', type: 'way'} + ]}) + ]); + + // + // Situation: + // b <-- c + // | | + // a --> d <~~~ e <~~~ f + // + // Relation: ['~', '-', '~'] + // + var spoon4 = iD.coreGraph([ + a, b, c, d, e, f, + iD.osmWay({id: '-', nodes: ['d', 'c', 'b', 'a', 'd']}), + iD.osmWay({id: '~', nodes: ['f', 'e', 'd']}), + iD.osmRelation({id: 'r', members: [ + {id: '~', type: 'way'}, + {id: '-', type: 'way'}, + {id: '~', type: 'way'} + ]}) + ]); + + it('splits spoon1 route at d', function () { + // + // Expected result: + // b ==> c + // | ‖ + // a <-- d ~~~> e ~~~> f + // + // Relation: ['~', '-', '=', '~'] + // + var graph = spoon1; + graph = iD.actionSplit('d', ['='])(graph); + + expect(graph.entity('-').nodes).to.eql(['d', 'a', 'b']); + expect(graph.entity('=').nodes).to.eql(['b', 'c', 'd']); + expect(graph.entity('~').nodes).to.eql(['d', 'e', 'f']); + expect(members(graph)).to.eql(['~', '-', '=', '~']); + }); + + it('splits spoon2 route at d', function () { + // + // Expected result: + // b <-- c + // ‖ | + // a ==> d ~~~> e ~~~> f + // + // Relation: ['~', '-', '=', '~'] + // + var graph = spoon2; + graph = iD.actionSplit('d', ['='])(graph); + + expect(graph.entity('-').nodes).to.eql(['d', 'c', 'b']); + expect(graph.entity('=').nodes).to.eql(['b', 'a', 'd']); + expect(graph.entity('~').nodes).to.eql(['d', 'e', 'f']); + expect(members(graph)).to.eql(['~', '-', '=', '~']); + }); + + it('splits spoon3 route at d', function () { + // + // Expected result: + // b ==> c + // | ‖ + // a <-- d <~~~ e <~~~ f + // + // Relation: ['~', '-', '=', '~'] + // + var graph = spoon3; + graph = iD.actionSplit('d', ['='])(graph); + + expect(graph.entity('-').nodes).to.eql(['d', 'a', 'b']); + expect(graph.entity('=').nodes).to.eql(['b', 'c', 'd']); + expect(graph.entity('~').nodes).to.eql(['f', 'e', 'd']); + expect(members(graph)).to.eql(['~', '-', '=', '~']); }); + + it('splits spoon4 route at d', function () { + // + // Expected result: + // b <-- c + // ‖ | + // a ==> d <~~~ e <~~~ f + // + // Relation: ['~', '-', '=', '~'] + // + var graph = spoon4; + graph = iD.actionSplit('d', ['='])(graph); + + expect(graph.entity('-').nodes).to.eql(['d', 'c', 'b']); + expect(graph.entity('=').nodes).to.eql(['b', 'a', 'd']); + expect(graph.entity('~').nodes).to.eql(['f', 'e', 'd']); + expect(members(graph)).to.eql(['~', '-', '=', '~']); + }); + + it('splits spoon1 route at e', function () { + // + // Expected result: + // b --> c + // | | + // a <-- d ~~~> e ===> f + // + // Relation: ['=', '~', '-', '~', '='] + // + var graph = spoon1; + graph = iD.actionSplit('e', ['='])(graph); + + expect(graph.entity('-').nodes).to.eql(['d', 'a', 'b', 'c', 'd']); + expect(graph.entity('~').nodes).to.eql(['d', 'e']); + expect(graph.entity('=').nodes).to.eql(['e', 'f']); + expect(members(graph)).to.eql(['=', '~', '-', '~', '=']); + }); + + it('splits spoon2 route at e', function () { + // + // Expected result: + // b <-- c + // | | + // a --> d ~~~> e ===> f + // + // Relation: ['=', '~', '-', '~', '='] + // + var graph = spoon2; + graph = iD.actionSplit('e', ['='])(graph); + + expect(graph.entity('-').nodes).to.eql(['d', 'c', 'b', 'a', 'd']); + expect(graph.entity('~').nodes).to.eql(['d', 'e']); + expect(graph.entity('=').nodes).to.eql(['e', 'f']); + expect(members(graph)).to.eql(['=', '~', '-', '~', '=']); + }); + + it('splits spoon3 route at e', function () { + // + // Expected result: + // b --> c + // | | + // a <-- d <=== e <~~~ f + // + // Relation: ['~', '=', '-', '=', '~'] + // + var graph = spoon3; + graph = iD.actionSplit('e', ['='])(graph); + + expect(graph.entity('-').nodes).to.eql(['d', 'a', 'b', 'c', 'd']); + expect(graph.entity('~').nodes).to.eql(['f', 'e']); + expect(graph.entity('=').nodes).to.eql(['e', 'd']); + expect(members(graph)).to.eql(['~', '=', '-', '=', '~']); + }); + + it('splits spoon4 route at e', function () { + // + // Expected result: + // b <-- c + // | | + // a --> d <=== e <~~~ f + // + // Relation: ['~', '=', '-', '=', '~'] + // + var graph = spoon4; + graph = iD.actionSplit('e', ['='])(graph); + + expect(graph.entity('-').nodes).to.eql(['d', 'c', 'b', 'a', 'd']); + expect(graph.entity('~').nodes).to.eql(['f', 'e']); + expect(graph.entity('=').nodes).to.eql(['e', 'd']); + expect(members(graph)).to.eql(['~', '=', '-', '=', '~']); + }); + }); From be9bbd927133aeee7254add81b541f2d9b25eb4d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 18 Jan 2018 15:21:38 -0500 Subject: [PATCH 160/206] Add tests for member ordering: node, way, relation --- modules/actions/add_member.js | 2 +- test/spec/actions/add_member.js | 24 ++++++++++++++++++++++++ test/spec/actions/split.js | 25 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/modules/actions/add_member.js b/modules/actions/add_member.js index 67f1d8b040..a9dfa7ec64 100644 --- a/modules/actions/add_member.js +++ b/modules/actions/add_member.js @@ -38,7 +38,7 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) { // so that `osmJoinWays` can treat the pair like a single way. tempWay = osmWay({ id: 'wTemp', nodes: insertPair.nodes }); graph = graph.replace(tempWay); - var tempMember = { id: tempWay.id, type: 'way', role: '' }; + var tempMember = { id: tempWay.id, type: 'way', role: member.role }; var tempRelation = replaceMemberAll(relation, insertPair.originalID, tempMember); groups = _groupBy(tempRelation.members, function(m) { return m.type; }); groups.way = groups.way || []; diff --git a/test/spec/actions/add_member.js b/test/spec/actions/add_member.js index b2a8486ec2..e27ef07e7b 100644 --- a/test/spec/actions/add_member.js +++ b/test/spec/actions/add_member.js @@ -177,6 +177,30 @@ describe('iD.actionAddMember', function() { expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']); }); + it('reorders members as node, way, relation (for Public Transport routing)', function() { + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [0, 0]}), + iD.osmNode({id: 'c', loc: [0, 0]}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmRelation({id: 'r', members: [ + { id: 'n1', type: 'node', role: 'forward' }, + { id: '-', type: 'way', role: 'forward' }, + { id: 'r1', type: 'relation', role: 'forward' }, + { id: 'n2', type: 'node', role: 'forward' } + ]}) + ]); + + graph = iD.actionAddMember('r', { id: '=', type: 'way', role: 'forward' })(graph); + expect(graph.entity('r').members).to.eql([ + { id: 'n1', type: 'node', role: 'forward' }, + { id: 'n2', type: 'node', role: 'forward' }, + { id: '-', type: 'way', role: 'forward' }, + { id: '=', type: 'way', role: 'forward' }, + { id: 'r1', type: 'relation', role: 'forward' } + ]); + }); }); }); diff --git a/test/spec/actions/split.js b/test/spec/actions/split.js index febf47d723..8e536a754a 100644 --- a/test/spec/actions/split.js +++ b/test/spec/actions/split.js @@ -446,6 +446,31 @@ describe('iD.actionSplit', function () { graph = iD.actionSplit('b', ['='])(graph); expect(members(graph)).to.eql(['~', '=', '-']); }); + + it('reorders members as node, way, relation (for Public Transport routing)', function () { + var graph = iD.coreGraph([ + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [2, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'] }), + iD.osmRelation({id: 'r', members: [ + { id: 'n1', type: 'node', role: 'forward' }, + { id: '-', type: 'way', role: 'forward' }, + { id: 'r1', type: 'relation', role: 'forward' }, + { id: 'n2', type: 'node', role: 'forward' } + ]}) + ]); + + graph = iD.actionSplit('b', ['='])(graph); + + expect(graph.entity('r').members).to.eql([ + { id: 'n1', type: 'node', role: 'forward' }, + { id: 'n2', type: 'node', role: 'forward' }, + { id: '-', type: 'way', role: 'forward' }, + { id: '=', type: 'way', role: 'forward' }, + { id: 'r1', type: 'relation', role: 'forward'} + ]); + }); }); describe('splitting out-and-back routes', function () { From 295514490b555108f8464f9838e772e5250aaa82 Mon Sep 17 00:00:00 2001 From: Benoit Costamagna Date: Thu, 18 Jan 2018 21:43:19 +0100 Subject: [PATCH 161/206] re added the building tag with no default value --- data/presets/presets.json | 21 +++++++++++++++++++ .../presets/leisure/sports_centre.json | 1 + 2 files changed, 22 insertions(+) diff --git a/data/presets/presets.json b/data/presets/presets.json index 49f2a2c55c..56c128ee1c 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -10524,6 +10524,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60595,6 +60596,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60614,6 +60616,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60633,6 +60636,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60652,6 +60656,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60671,6 +60676,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60690,6 +60696,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60709,6 +60716,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60728,6 +60736,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60747,6 +60756,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60766,6 +60776,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60785,6 +60796,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60804,6 +60816,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60823,6 +60836,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60842,6 +60856,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60861,6 +60876,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60880,6 +60896,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60899,6 +60916,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60918,6 +60936,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60937,6 +60956,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], @@ -60956,6 +60976,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], diff --git a/data/presets/presets/leisure/sports_centre.json b/data/presets/presets/leisure/sports_centre.json index e310be451e..a757a4cdac 100644 --- a/data/presets/presets/leisure/sports_centre.json +++ b/data/presets/presets/leisure/sports_centre.json @@ -3,6 +3,7 @@ "fields": [ "name", "sport", + "building", "address", "opening_hours" ], From 7c918ba16162fbdb55a8fadffc15dffad941332c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 18 Jan 2018 16:52:23 -0500 Subject: [PATCH 162/206] Allow Relation.replaceMember to optionally preserve duplicates (closes #4696) --- modules/actions/add_member.js | 17 +- modules/osm/relation.js | 8 +- test/spec/actions/join.js | 432 +++++++++++++++++++--------------- test/spec/osm/relation.js | 23 +- 4 files changed, 262 insertions(+), 218 deletions(-) diff --git a/modules/actions/add_member.js b/modules/actions/add_member.js index a9dfa7ec64..0491753290 100644 --- a/modules/actions/add_member.js +++ b/modules/actions/add_member.js @@ -39,7 +39,7 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) { tempWay = osmWay({ id: 'wTemp', nodes: insertPair.nodes }); graph = graph.replace(tempWay); var tempMember = { id: tempWay.id, type: 'way', role: member.role }; - var tempRelation = replaceMemberAll(relation, insertPair.originalID, tempMember); + var tempRelation = relation.replaceMember({id: insertPair.originalID}, tempMember, true); groups = _groupBy(tempRelation.members, function(m) { return m.type; }); groups.way = groups.way || []; @@ -160,21 +160,6 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) { } - // Relation.replaceMember() removes duplicates, and we don't want that.. #4696 - function replaceMemberAll(relation, needleID, replacement) { - var members = []; - for (var i = 0; i < relation.members.length; i++) { - var member = relation.members[i]; - if (member.id !== needleID) { - members.push(member); - } else { - members.push({id: replacement.id, type: replacement.type, role: member.role}); - } - } - return relation.update({members: members}); - } - - // This is the same as `Relation.indexedMembers`, // Except we don't want to index all the members, only the ways function withIndex(arr) { diff --git a/modules/osm/relation.js b/modules/osm/relation.js index 0526278600..b44dbd4200 100644 --- a/modules/osm/relation.js +++ b/modules/osm/relation.js @@ -161,9 +161,9 @@ _extend(osmRelation.prototype, { // Wherever a member appears with id `needle.id`, replace it with a member // with id `replacement.id`, type `replacement.type`, and the original role, - // unless a member already exists with that id and role. Return an updated - // relation. - replaceMember: function(needle, replacement) { + // By default, adding a duplicate member (by id and role) is prevented. + // Return an updated relation. + replaceMember: function(needle, replacement, keepDuplicates) { if (!this.memberById(needle.id)) return this; @@ -173,7 +173,7 @@ _extend(osmRelation.prototype, { var member = this.members[i]; if (member.id !== needle.id) { members.push(member); - } else if (!this.memberByIdAndRole(replacement.id, member.role)) { + } else if (keepDuplicates || !this.memberByIdAndRole(replacement.id, member.role)) { members.push({id: replacement.id, type: replacement.type, role: member.role}); } } diff --git a/test/spec/actions/join.js b/test/spec/actions/join.js index 8b13781874..e7de473f35 100644 --- a/test/spec/actions/join.js +++ b/test/spec/actions/join.js @@ -2,67 +2,67 @@ describe('iD.actionJoin', function () { describe('#disabled', function () { it('returns falsy for ways that share an end/start node', function () { // a --> b ==> c - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['b', 'c']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}) + ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).not.to.be.ok; }); it('returns falsy for ways that share a start/end node', function () { // a <-- b <== c - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['b', 'a']}), - iD.Way({id: '=', nodes: ['c', 'b']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['b', 'a']}), + iD.osmWay({id: '=', nodes: ['c', 'b']}) + ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).not.to.be.ok; }); it('returns falsy for ways that share a start/start node', function () { // a <-- b ==> c - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['b', 'a']}), - iD.Way({id: '=', nodes: ['b', 'c']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['b', 'a']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}) + ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).not.to.be.ok; }); it('returns falsy for ways that share an end/end node', function () { // a --> b <== c - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['c', 'b']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['c', 'b']}) + ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).not.to.be.ok; }); it('returns falsy for more than two ways when connected, regardless of order', function () { // a --> b ==> c ~~> d - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['b', 'c']}), - iD.Way({id: '~', nodes: ['c', 'd']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmWay({id: '~', nodes: ['c', 'd']}) + ]); expect(iD.actionJoin(['-', '=', '~']).disabled(graph)).not.to.be.ok; expect(iD.actionJoin(['-', '~', '=']).disabled(graph)).not.to.be.ok; @@ -73,9 +73,9 @@ describe('iD.actionJoin', function () { }); it('returns \'not_eligible\' for non-line geometries', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}) + ]); expect(iD.actionJoin(['a']).disabled(graph)).to.equal('not_eligible'); }); @@ -84,14 +84,14 @@ describe('iD.actionJoin', function () { // a -- b -- c // | // d - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - iD.Way({id: '=', nodes: ['b', 'd']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '=', nodes: ['b', 'd']}) + ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).to.equal('not_adjacent'); }); @@ -101,18 +101,18 @@ describe('iD.actionJoin', function () { // from: - // to: = // via: b - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['b', 'c']}), - iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ - {type: 'way', id: '-', role: 'from'}, - {type: 'way', id: '=', role: 'to'}, - {type: 'node', id: 'b', role: 'via'} - ]}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmRelation({id: 'r', tags: {type: 'restriction'}, members: [ + {type: 'way', id: '-', role: 'from'}, + {type: 'way', id: '=', role: 'to'}, + {type: 'node', id: 'b', role: 'via'} + ]}) + ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).to.equal('restriction'); }); @@ -124,20 +124,20 @@ describe('iD.actionJoin', function () { // from: - // to: | // via: b - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['b', 'c']}), - iD.Way({id: '|', nodes: ['b', 'd']}), - iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ - {type: 'way', id: '-', role: 'from'}, - {type: 'way', id: '|', role: 'to'}, - {type: 'node', id: 'b', role: 'via'} - ]}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmWay({id: '|', nodes: ['b', 'd']}), + iD.osmRelation({id: 'r', tags: {type: 'restriction'}, members: [ + {type: 'way', id: '-', role: 'from'}, + {type: 'way', id: '|', role: 'to'}, + {type: 'node', id: 'b', role: 'via'} + ]}) + ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).to.equal('restriction'); }); @@ -149,20 +149,20 @@ describe('iD.actionJoin', function () { // from: - // to: | // via: a - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['b', 'c']}), - iD.Way({id: '|', nodes: ['a', 'd']}), - iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ - {type: 'way', id: '-', role: 'from'}, - {type: 'way', id: '|', role: 'to'}, - {type: 'node', id: 'a', role: 'via'} - ]}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmWay({id: '|', nodes: ['a', 'd']}), + iD.osmRelation({id: 'r', tags: {type: 'restriction'}, members: [ + {type: 'way', id: '-', role: 'from'}, + {type: 'way', id: '|', role: 'to'}, + {type: 'node', id: 'a', role: 'via'} + ]}) + ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).not.to.be.ok; }); @@ -176,68 +176,68 @@ describe('iD.actionJoin', function () { // from: | // to: \ // via: b - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['b', 'c']}), - iD.Way({id: '|', nodes: ['d', 'b']}), - iD.Way({id: '\\', nodes: ['b', 'e']}), - iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ - {type: 'way', id: '|', role: 'from'}, - {type: 'way', id: '\\', role: 'to'}, - {type: 'node', id: 'b', role: 'via'} - ]}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmWay({id: '|', nodes: ['d', 'b']}), + iD.osmWay({id: '\\', nodes: ['b', 'e']}), + iD.osmRelation({id: 'r', tags: {type: 'restriction'}, members: [ + {type: 'way', id: '|', role: 'from'}, + {type: 'way', id: '\\', role: 'to'}, + {type: 'node', id: 'b', role: 'via'} + ]}) + ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).not.to.be.ok; }); it('returns \'conflicting_tags\' for two entities that have conflicting tags', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b'], tags: {highway: 'primary'}}), - iD.Way({id: '=', nodes: ['b', 'c'], tags: {highway: 'secondary'}}) + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b'], tags: {highway: 'primary'}}), + iD.osmWay({id: '=', nodes: ['b', 'c'], tags: {highway: 'secondary'}}) ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).to.equal('conflicting_tags'); }); it('takes tag reversals into account when calculating conflicts', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b'], tags: {'oneway': 'yes'}}), - iD.Way({id: '=', nodes: ['c', 'b'], tags: {'oneway': '-1'}}) + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b'], tags: {'oneway': 'yes'}}), + iD.osmWay({id: '=', nodes: ['c', 'b'], tags: {'oneway': '-1'}}) ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).not.to.be.ok; }); it('returns falsy for exceptions to tag conflicts: missing tag', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b'], tags: {highway: 'primary'}}), - iD.Way({id: '=', nodes: ['b', 'c'], tags: {}}) + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b'], tags: {highway: 'primary'}}), + iD.osmWay({id: '=', nodes: ['b', 'c'], tags: {}}) ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).not.to.be.ok; }); it('returns falsy for exceptions to tag conflicts: uninteresting tag', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b'], tags: {'tiger:cfcc': 'A41'}}), - iD.Way({id: '=', nodes: ['b', 'c'], tags: {'tiger:cfcc': 'A42'}}) + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b'], tags: {'tiger:cfcc': 'A41'}}), + iD.osmWay({id: '=', nodes: ['b', 'c'], tags: {'tiger:cfcc': 'A42'}}) ]); expect(iD.actionJoin(['-', '=']).disabled(graph)).not.to.be.ok; @@ -247,13 +247,13 @@ describe('iD.actionJoin', function () { it('joins a --> b ==> c', function () { // Expected result: // a --> b --> c - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['b', 'c']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}) + ]); graph = iD.actionJoin(['-', '='])(graph); @@ -264,13 +264,13 @@ describe('iD.actionJoin', function () { it('joins a <-- b <== c', function () { // Expected result: // a <-- b <-- c - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['b', 'a']}), - iD.Way({id: '=', nodes: ['c', 'b']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['b', 'a']}), + iD.osmWay({id: '=', nodes: ['c', 'b']}) + ]); graph = iD.actionJoin(['-', '='])(graph); @@ -282,13 +282,13 @@ describe('iD.actionJoin', function () { // Expected result: // a --> b --> c // tags on --- reversed - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['b', 'a'], tags: {'lanes:forward': 2}}), - iD.Way({id: '=', nodes: ['b', 'c']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['b', 'a'], tags: {'lanes:forward': 2}}), + iD.osmWay({id: '=', nodes: ['b', 'c']}) + ]); graph = iD.actionJoin(['-', '='])(graph); @@ -301,13 +301,13 @@ describe('iD.actionJoin', function () { // Expected result: // a --> b --> c // tags on === reversed - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['c', 'b'], tags: {'lanes:forward': 2}}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['c', 'b'], tags: {'lanes:forward': 2}}) + ]); graph = iD.actionJoin(['-', '='])(graph); @@ -320,17 +320,17 @@ describe('iD.actionJoin', function () { // Expected result: // a --> b --> c --> d --> e // tags on === reversed - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Node({id: 'e'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['c', 'b'], tags: {'lanes:forward': 2}}), - iD.Way({id: '+', nodes: ['d', 'c']}), - iD.Way({id: '*', nodes: ['d', 'e'], tags: {'lanes:backward': 2}}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmNode({id: 'e'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['c', 'b'], tags: {'lanes:forward': 2}}), + iD.osmWay({id: '+', nodes: ['d', 'c']}), + iD.osmWay({id: '*', nodes: ['d', 'e'], tags: {'lanes:backward': 2}}) + ]); graph = iD.actionJoin(['-', '=', '+', '*'])(graph); @@ -346,15 +346,15 @@ describe('iD.actionJoin', function () { // --- is new, === is existing, +++ is new // Expected result: // a ==> b ==> c ==> d - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: 'w-1', nodes: ['a', 'b']}), - iD.Way({id: 'w1', nodes: ['b', 'c']}), - iD.Way({id: 'w-2', nodes: ['c', 'd']}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: 'w-1', nodes: ['a', 'b']}), + iD.osmWay({id: 'w1', nodes: ['b', 'c']}), + iD.osmWay({id: 'w-2', nodes: ['c', 'd']}) + ]); graph = iD.actionJoin(['w-1', 'w1', 'w-2'])(graph); @@ -364,15 +364,15 @@ describe('iD.actionJoin', function () { }); it('merges tags', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: 'd'}), - iD.Way({id: '-', nodes: ['a', 'b'], tags: {a: 'a', b: '-', c: 'c'}}), - iD.Way({id: '=', nodes: ['b', 'c'], tags: {a: 'a', b: '=', d: 'd'}}), - iD.Way({id: '+', nodes: ['c', 'd'], tags: {a: 'a', b: '=', e: 'e'}}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: 'd'}), + iD.osmWay({id: '-', nodes: ['a', 'b'], tags: {a: 'a', b: '-', c: 'c'}}), + iD.osmWay({id: '=', nodes: ['b', 'c'], tags: {a: 'a', b: '=', d: 'd'}}), + iD.osmWay({id: '+', nodes: ['c', 'd'], tags: {a: 'a', b: '=', e: 'e'}}) + ]); graph = iD.actionJoin(['-', '=', '+'])(graph); @@ -380,19 +380,65 @@ describe('iD.actionJoin', function () { }); it('merges relations', function () { - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Way({id: '-', nodes: ['a', 'b']}), - iD.Way({id: '=', nodes: ['b', 'c']}), - iD.Relation({id: 'r1', members: [{id: '=', role: 'r1', type: 'way'}]}), - iD.Relation({id: 'r2', members: [{id: '=', role: 'r2', type: 'way'}, {id: '-', role: 'r2', type: 'way'}]}) - ]); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmWay({id: '-', nodes: ['a', 'b']}), + iD.osmWay({id: '=', nodes: ['b', 'c']}), + iD.osmRelation({id: 'r1', members: [ + {id: '=', role: 'r1', type: 'way'} + ]}), + iD.osmRelation({id: 'r2', members: [ + {id: '=', role: 'r2', type: 'way'}, + {id: '-', role: 'r2', type: 'way'} + ]}) + ]); graph = iD.actionJoin(['-', '='])(graph); expect(graph.entity('r1').members).to.eql([{id: '-', role: 'r1', type: 'way'}]); expect(graph.entity('r2').members).to.eql([{id: '-', role: 'r2', type: 'way'}]); }); + + it('preserves duplicate route segments in relations', function () { + // + // Situation: + // a ---> b ===> c ~~~~> d join '-' and '=' + // Relation: ['-', '=', '~', '~', '=', '-'] + // + // Expected result: + // a ---> b ---> c ~~~~> d + // Relation: ['-', '~', '~', '-'] + // + var graph = iD.coreGraph([ + iD.osmNode({ id: 'a', loc: [0, 0] }), + iD.osmNode({ id: 'b', loc: [1, 0] }), + iD.osmNode({ id: 'c', loc: [2, 0] }), + iD.osmNode({ id: 'd', loc: [3, 0] }), + iD.osmWay({ id: '-', nodes: ['a', 'b'] }), + iD.osmWay({ id: '=', nodes: ['b', 'c'] }), + iD.osmWay({ id: '~', nodes: ['c', 'd'] }), + iD.osmRelation({id: 'r', members: [ + {id: '-', role: 'forward', type: 'way'}, + {id: '=', role: 'forward', type: 'way'}, + {id: '~', role: 'forward', type: 'way'}, + {id: '~', role: 'forward', type: 'way'}, + {id: '=', role: 'forward', type: 'way'}, + {id: '-', role: 'forward', type: 'way'} + ]}) + ]); + + graph = iD.actionJoin(['-', '='])(graph); + + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(graph.entity('~').nodes).to.eql(['c', 'd']); + expect(graph.entity('r').members).to.eql([ + {id: '-', role: 'forward', type: 'way'}, + {id: '~', role: 'forward', type: 'way'}, + {id: '~', role: 'forward', type: 'way'}, + {id: '-', role: 'forward', type: 'way'} + ]); + }); + }); diff --git a/test/spec/osm/relation.js b/test/spec/osm/relation.js index c5135ab259..4d449d2265 100644 --- a/test/spec/osm/relation.js +++ b/test/spec/osm/relation.js @@ -258,24 +258,37 @@ describe('iD.osmRelation', function () { it('replaces a member which doesn\'t already exist', function () { var r = iD.Relation({members: [{id: 'a', role: 'a'}]}); - expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members).to.eql([{id: 'b', role: 'a', type: 'node'}]); + expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members) + .to.eql([{id: 'b', role: 'a', type: 'node'}]); }); it('preserves the existing role', function () { var r = iD.Relation({members: [{id: 'a', role: 'a', type: 'node'}]}); - expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members).to.eql([{id: 'b', role: 'a', type: 'node'}]); + expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members) + .to.eql([{id: 'b', role: 'a', type: 'node'}]); }); it('uses the replacement type', function () { var r = iD.Relation({members: [{id: 'a', role: 'a', type: 'node'}]}); - expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'way'}).members).to.eql([{id: 'b', role: 'a', type: 'way'}]); + expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'way'}).members) + .to.eql([{id: 'b', role: 'a', type: 'way'}]); }); it('removes members if replacing them would produce duplicates', function () { var r = iD.Relation({members: [ {id: 'a', role: 'b', type: 'node'}, - {id: 'b', role: 'b', type: 'node'}]}); - expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members).to.eql([{id: 'b', role: 'b', type: 'node'}]); + {id: 'b', role: 'b', type: 'node'} + ]}); + expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members) + .to.eql([{id: 'b', role: 'b', type: 'node'}]); + }); + it('keeps duplicate members if `keepDuplicates = true`', function () { + var r = iD.Relation({members: [ + {id: 'a', role: 'b', type: 'node'}, + {id: 'b', role: 'b', type: 'node'} + ]}); + expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}, true).members) + .to.eql([{id: 'b', role: 'b', type: 'node'}, {id: 'b', role: 'b', type: 'node'}]); }); }); From dc4461ed13016bddb995d51a58ad98d02ecbd07a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 18 Jan 2018 17:23:40 -0500 Subject: [PATCH 163/206] Fix pointer when hovering over editmenu (closes #4673) --- css/80_app.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/css/80_app.css b/css/80_app.css index e104870257..b126d5fa6b 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -3998,6 +3998,7 @@ li.hide + li.version .badge .tooltip .tooltip-arrow { .edit-menu-item rect { fill: #eee; + cursor: default; } .edit-menu-item rect:active, @@ -4016,6 +4017,7 @@ li.hide + li.version .badge .tooltip .tooltip-arrow { .edit-menu-item use { fill: #222; color: #79f; + pointer-events: none; } .edit-menu-item.disabled use { From 44234ede660988f3bbf7e70e2ab3707a83906a49 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 18 Jan 2018 17:31:26 -0500 Subject: [PATCH 164/206] Make Help pane slightly wider (re: "claustrophobia" on #4651, #4686) --- modules/ui/help.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/ui/help.js b/modules/ui/help.js index 01196b3a75..30d17833a7 100644 --- a/modules/ui/help.js +++ b/modules/ui/help.js @@ -370,17 +370,17 @@ export function uiHelp(context) { var pane = selection.append('div') - .attr('class', 'help-wrap map-overlay fillL col5 content hide'), - tooltipBehavior = tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - .html(true) - .title(uiTooltipHtml(t('help.title'), key)), - button = selection.append('button') - .attr('tabindex', -1) - .on('click', togglePane) - .call(svgIcon('#icon-help', 'light')) - .call(tooltipBehavior), - shown = false; + .attr('class', 'help-wrap map-overlay fillL col6 content hide'); + var tooltipBehavior = tooltip() + .placement((textDirection === 'rtl') ? 'right' : 'left') + .html(true) + .title(uiTooltipHtml(t('help.title'), key)); + var button = selection.append('button') + .attr('tabindex', -1) + .on('click', togglePane) + .call(svgIcon('#icon-help', 'light')) + .call(tooltipBehavior); + var shown = false; var toc = pane From efc2678610b366006a609957d9c2e4de1cc9a5fe Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 18 Jan 2018 17:55:14 -0500 Subject: [PATCH 165/206] Fix feature list / current view searching (closes #4690) --- modules/ui/feature_list.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/ui/feature_list.js b/modules/ui/feature_list.js index d5ed9330bb..4fc298731c 100644 --- a/modules/ui/feature_list.js +++ b/modules/ui/feature_list.js @@ -174,7 +174,9 @@ export function uiFeatureList(context) { var visible = context.surface().selectAll('.point, .line, .area').nodes(); for (var i = 0; i < visible.length && result.length <= 200; i++) { - addEntity(visible[i].__data__); + var datum = visible[i].__data__; + var entity = datum && datum.properties && datum.properties.entity; + if (entity) { addEntity(entity); } } (_geocodeResults || []).forEach(function(d) { From 1697c483602f4ea0f6f122868995bc5df44eca8f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 18 Jan 2018 22:02:56 -0500 Subject: [PATCH 166/206] Fix flickering and drag-node breakage in line endpoint walkthrough step --- modules/ui/intro/line.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/modules/ui/intro/line.js b/modules/ui/intro/line.js index c5d0d9f2fe..9d8a656b17 100644 --- a/modules/ui/intro/line.js +++ b/modules/ui/intro/line.js @@ -467,10 +467,6 @@ export function uiIntroLine(context, reveal) { if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) { return continueTo(updateLine); } - if (context.selectedIDs().indexOf(woodRoadId) === -1) { - context.enter(modeSelect(context, [woodRoadId])); - } - var padding = 100 * Math.pow(2, context.map().zoom() - 19); var box = pad(woodRoadDragEndpoint, padding, context); reveal(box, t('intro.lines.start_drag_endpoint')); @@ -489,16 +485,8 @@ export function uiIntroLine(context, reveal) { } }); - context.on('enter.intro', function(mode) { - if (mode.id !== 'select') { - // keep Wood Road selected so endpoint stays draggable.. - context.enter(modeSelect(context, [woodRoadId])); - } - }); - function continueTo(nextStep) { context.map().on('move.intro drawn.intro', null); - context.on('enter.intro', null); nextStep(); } } From f8c199474489990c5c15560de9350f7393536eee Mon Sep 17 00:00:00 2001 From: Arun Ganesh Date: Fri, 19 Jan 2018 12:58:39 +0530 Subject: [PATCH 167/206] Update hindu.json - Removing uncommon hindi/sanskrit synonyms - Add common English spellings of Hindu temples in various Indian languages. --- data/presets/presets/amenity/place_of_worship/hindu.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/data/presets/presets/amenity/place_of_worship/hindu.json b/data/presets/presets/amenity/place_of_worship/hindu.json index 02a5c66a8f..d54440dbf9 100644 --- a/data/presets/presets/amenity/place_of_worship/hindu.json +++ b/data/presets/presets/amenity/place_of_worship/hindu.json @@ -12,9 +12,11 @@ "area" ], "terms": [ - "garbhargriha", - "mandu", - "puja", + "kovil", + "devasthana", + "mandir", + "kshetram", + "alayam", "shrine", "temple" ], From a87456abb88b7752be0a921a0707d07aa69c1000 Mon Sep 17 00:00:00 2001 From: Benoit Costamagna Date: Fri, 19 Jan 2018 10:23:01 +0100 Subject: [PATCH 168/206] Making the guideway a bit easier on the eye by reusing the cycleway's blue color and shrinking the dashes --- css/30_highways.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/css/30_highways.css b/css/30_highways.css index 148e7fd364..82851892c5 100644 --- a/css/30_highways.css +++ b/css/30_highways.css @@ -223,11 +223,17 @@ path.stroke.tag-highway-bus_guideway, path.stroke.tag-bus_guideway { stroke:#fff; stroke-linecap: butt; - stroke-dasharray: 12, 16; + stroke-dasharray: 8, 8; + stroke-width: 1; +} + +.low-zoom path.stroke.tag-highway-bus_guideway, +.low-zoom path.stroke.tag-bus_guideway { + stroke-dasharray: 4, 4; } path.casing.tag-highway-bus_guideway, path.casing.tag-bus_guideway { - stroke:#66a3ff; + stroke:#58a9ed; } .preset-icon .icon.highway-unclassified { @@ -319,7 +325,6 @@ path.stroke.tag-highway-corridor, path.stroke.tag-highway-pedestrian, path.stroke.tag-highway-steps, path.stroke.tag-path, -path.stroke.tag-highway-bus_guideway, path.stroke.tag-footway, path.stroke.tag-cycleway, path.stroke.tag-bridleway, From 6c84312a20e36758e6d93c77d08d459ae1e07dcc Mon Sep 17 00:00:00 2001 From: Benoit Costamagna Date: Fri, 19 Jan 2018 10:37:06 +0100 Subject: [PATCH 169/206] Added oneway to suggested tags list for a bus guideway --- data/presets/presets.json | 3 ++- data/presets/presets/highway/bus_guideway.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/data/presets/presets.json b/data/presets/presets.json index 64f5a00aed..230c380b44 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -6933,7 +6933,8 @@ "icon": "bus", "fields": [ "name", - "operator" + "operator", + "oneway" ], "geometry": [ "line" diff --git a/data/presets/presets/highway/bus_guideway.json b/data/presets/presets/highway/bus_guideway.json index 9d1dadc5c3..d4f1272b94 100644 --- a/data/presets/presets/highway/bus_guideway.json +++ b/data/presets/presets/highway/bus_guideway.json @@ -2,7 +2,8 @@ "icon": "bus", "fields": [ "name", - "operator" + "operator", + "oneway" ], "geometry": [ "line" From 1aebbc5bb38f94e865f80522ff73f2cde94f966e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 19 Jan 2018 08:57:33 -0500 Subject: [PATCH 170/206] npm run build for new preset files --- data/presets.yaml | 2 +- data/presets/presets.json | 8 +++++--- dist/locales/en.json | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/data/presets.yaml b/data/presets.yaml index 7cf9cab58d..58d8e6107a 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -2070,7 +2070,7 @@ en: amenity/place_of_worship/hindu: # 'amenity=place_of_worship, religion=hindu' name: Hindu Temple - # 'terms: garbhargriha,mandu,puja,shrine,temple' + # 'terms: kovil,devasthana,mandir,kshetram,alayam,shrine,temple' terms: '' amenity/place_of_worship/jewish: # 'amenity=place_of_worship, religion=jewish' diff --git a/data/presets/presets.json b/data/presets/presets.json index 56c128ee1c..b899db6502 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -2244,9 +2244,11 @@ "area" ], "terms": [ - "garbhargriha", - "mandu", - "puja", + "kovil", + "devasthana", + "mandir", + "kshetram", + "alayam", "shrine", "temple" ], diff --git a/dist/locales/en.json b/dist/locales/en.json index ae008afc4e..a9b1e6edf4 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -3058,7 +3058,7 @@ }, "amenity/place_of_worship/hindu": { "name": "Hindu Temple", - "terms": "garbhargriha,mandu,puja,shrine,temple" + "terms": "kovil,devasthana,mandir,kshetram,alayam,shrine,temple" }, "amenity/place_of_worship/jewish": { "name": "Synagogue", From c2eeb27d1b4bdecc09987c1197fdff387f318e71 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 19 Jan 2018 10:31:01 -0500 Subject: [PATCH 171/206] Add bus_guideway icon, move weeds from highway=service to highway=track --- svg/iD-sprite.json | 3 + svg/iD-sprite.src.idraw | Bin 361221 -> 362833 bytes svg/iD-sprite.src.svg | 256 ++++++++++++++++++++-------------------- 3 files changed, 134 insertions(+), 125 deletions(-) diff --git a/svg/iD-sprite.json b/svg/iD-sprite.json index d0e9e8cd3e..601522a967 100644 --- a/svg/iD-sprite.json +++ b/svg/iD-sprite.json @@ -84,6 +84,7 @@ "highway-residential": { "viewBox": "600 20 60 60" }, "highway-unclassified": { "viewBox": "660 20 60 60" }, "highway-service": { "viewBox": "720 20 60 60" }, + "highway-bus_guideway": { "viewBox": "780 20 60 60" }, "highway-road": { "viewBox": "0 80 60 60" }, "highway-track": { "viewBox": "60 80 60 60" }, "highway-living-street": { "viewBox": "120 80 60 60" }, @@ -141,6 +142,8 @@ "highway-unclassified-stroke1": { "fill": "currentColor" }, "highway-service-casing1": { "fill": "inherit" }, "highway-service-stroke1": { "fill": "currentColor" }, + "highway-bus_guideway-casing1": { "fill": "inherit" }, + "highway-bus_guideway-stroke1": { "fill": "currentColor" }, "highway-road-casing1": { "fill": "inherit" }, "highway-road-stroke1": { "fill": "currentColor" }, "highway-track-casing1": { "fill": "inherit" }, diff --git a/svg/iD-sprite.src.idraw b/svg/iD-sprite.src.idraw index f05167f87187c9f592c80b7df0c6c6a0d849ff77..689c720f937d788087abc87fbc2fc47db998f37a 100644 GIT binary patch delta 239330 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 HL-y?QmYnXJjqc}$bRT8No>#ul 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 _#h9UyqB--od3~aeMS0 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