diff --git a/css/55_cursors.css b/css/55_cursors.css index 1a4c8cf7f..0a5adabbe 100644 --- a/css/55_cursors.css +++ b/css/55_cursors.css @@ -54,6 +54,7 @@ cursor: url(img/cursor-select-remove.png), pointer; /* FF */ } +.mode-add-preset #map, .mode-draw-line #map, .mode-draw-area #map, .mode-add-line #map, @@ -104,4 +105,3 @@ .turn circle { cursor: pointer; } - diff --git a/css/80_app.css b/css/80_app.css index 9948bd407..64246010c 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -472,8 +472,33 @@ button[disabled].action:hover { padding: 0 10px; min-width: 30px; white-space: nowrap; + display: flex; } -.tool-group button .icon { +[dir='ltr'] .tool-group button.add-preset.add-point, +[dir='ltr'] .tool-group button.add-preset.add-point .label { + padding-left: 0px; +} +[dir='ltr'] .tool-group button.add-preset:not(.add-point) { + padding-left: 5px; +} +[dir='ltr'] .tool-group button.add-preset:not(.add-point) .label { + padding-left: 3px; +} +[dir='rtl'] .tool-group button.add-preset.add-point, +[dir='rtl'] .tool-group button.add-preset.add-point .label { + padding-right: 0px; +} +[dir='rtl'] .tool-group button.add-preset:not(.add-point) { + padding-right: 5px; +} +[dir='rtl'] .tool-group button.add-preset:not(.add-point) .label { + padding-right: 3px; +} +.narrow .tool-group button.add-preset { + padding-right: 0 !important; + padding-left: 0 !important; +} +.tool-group button > .icon { flex: 0 0 20px; } .tool-group button .label { @@ -970,6 +995,10 @@ a.hide-toggle { height: 60px; text-align: center; } +#bar .preset-icon-container { + width: 40px; + height: 40px; +} .preset-icon-line { margin: auto; @@ -1111,28 +1140,47 @@ a.hide-toggle { background-color: #ececec; } +.preset-list-item button.preset-favorite-button, .preset-list-item button.tag-reference-button { height: 100%; border: 1px solid #ccc; flex: 32px; background: #f6f6f6; } +[dir='ltr'] .preset-list-item button.preset-favorite-button, [dir='ltr'] .preset-list-item button.tag-reference-button { border-left: none; - border-radius: 0 4px 4px 0; + border-radius: 0; } +[dir='rtl'] .preset-list-item button.preset-favorite-button, [dir='rtl'] .preset-list-item button.tag-reference-button { border-right: none; + border-radius: 0; +} +[dir='ltr'] .preset-list-item button:last-child { + border-radius: 0 4px 4px 0; +} +[dir='rtl'] .preset-list-item button:last-child { border-radius: 4px 0 0 4px; } +.preset-list-item button.preset-favorite-button:hover, .preset-list-item button.tag-reference-button:hover { background: #f1f1f1; } +.preset-list-item button.preset-favorite-button .icon, .preset-list-item button.tag-reference-button .icon { opacity: .5; } +.preset-list-item button.preset-favorite-button .icon { + fill-opacity: 0; + stroke-width: 1.6; +} +.preset-list-item button.preset-favorite-button.active .icon { + fill-opacity: inherit; +} + img.tag-reference-wiki-image { float: right; width: 33.3333%; @@ -2435,6 +2483,7 @@ input.key-trap { /* hide and remove from layout */ .inspector-hidden, +.inspector-hover .preset-list-button-wrap .preset-favorite-button, .inspector-hover .preset-list-button-wrap .tag-reference-button, .inspector-hover label input[type="checkbox"], .inspector-hover label input[type="radio"], diff --git a/data/core.yaml b/data/core.yaml index 1b868b3e8..ca8a5340b 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -7,6 +7,7 @@ en: zoom_to: zoom to copy: copy open_wikidata: open on wikidata.org + favorite: favorite modes: add_area: title: Area @@ -24,6 +25,14 @@ en: title: Note description: "Spotted an issue? Let other mappers know." tail: Click on the map to add a note. + add_preset: + title: "Add {feature}" + point: + title: "Add {feature} as a point" + line: + title: "Add {feature} as a line" + area: + title: "Add {feature} as an area" browse: title: Browse description: Pan and zoom the map. diff --git a/dist/locales/en.json b/dist/locales/en.json index 77b832aef..b41a90abf 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -7,7 +7,8 @@ "undo": "undo", "zoom_to": "zoom to", "copy": "copy", - "open_wikidata": "open on wikidata.org" + "open_wikidata": "open on wikidata.org", + "favorite": "favorite" }, "modes": { "add_area": { @@ -30,6 +31,18 @@ "description": "Spotted an issue? Let other mappers know.", "tail": "Click on the map to add a note." }, + "add_preset": { + "title": "Add {feature}", + "point": { + "title": "Add {feature} as a point" + }, + "line": { + "title": "Add {feature} as a line" + }, + "area": { + "title": "Add {feature} as an area" + } + }, "browse": { "title": "Browse", "description": "Pan and zoom the map." diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 7e4a8933f..f017d03e5 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -330,7 +330,7 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph, baselin window.setTimeout(function() { context.map().dblclickEnable(true); }, 1000); - var isNewFeature = !mode.isContinuing; + var isNewFeature = !mode.isContinuing && mode.button.indexOf('add-preset-') === -1; context.enter(modeSelect(context, [wayID]).newFeature(isNewFeature)); if (isNewFeature) { context.validator().validate(); diff --git a/modules/core/context.js b/modules/core/context.js index 7162904f1..2efa2c4f1 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -57,7 +57,7 @@ export function coreContext() { addTranslation('en', dataEn); setLocale('en'); - var dispatch = d3_dispatch('enter', 'exit', 'change'); + var dispatch = d3_dispatch('enter', 'exit', 'change', 'favoritePreset'); // https://github.com/openstreetmap/iD/issues/772 // http://mathiasbynens.be/notes/localstorage-pattern#comment-9 @@ -312,7 +312,39 @@ export function coreContext() { /* Presets */ var presets; context.presets = function() { return presets; }; + //get favorites from local storage + context.getFavoritePresets = function() { + return JSON.parse(context.storage('favorite_presets')) || []; + }; + context.favoritePreset = function(preset, geom) { + var favs = context.getFavoritePresets(); + //add/remove favorites from local storage + if (context.isFavoritePreset(preset, geom)) { + favs = favs.filter(function(d) { + return !(d.id === preset.id && d.geom === geom); + }); + } else { + // only allow 3 favorites + if (favs.length === 3) { + // remove the last favorite (first in, first out) + favs.pop(); + } + // prepend array + favs.unshift({id: preset.id, geom: geom}); + } + + context.storage('favorite_presets', JSON.stringify(favs)); + + //and call update on modes + dispatch.call('favoritePreset'); + }; + context.isFavoritePreset = function(preset, geom) { + var favs = context.getFavoritePresets(); + return favs.some(function(d) { + return d.id === preset.id && d.geom === geom; + }); + }; /* Map */ var map; diff --git a/modules/modes/add_area.js b/modules/modes/add_area.js index ec2097a3e..55b344943 100644 --- a/modules/modes/add_area.js +++ b/modules/modes/add_area.js @@ -10,8 +10,8 @@ import { modeDrawArea } from './index'; import { osmNode, osmWay } from '../osm'; -export function modeAddArea(context) { - var mode = { +export function modeAddArea(context, customMode) { + var mode = customMode || { id: 'add-area', button: 'area', title: t('modes.add_area.title'), @@ -26,6 +26,7 @@ export function modeAddArea(context) { .on('startFromNode', startFromNode); var defaultTags = { area: 'yes' }; + if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'area'); function actionClose(wayId) { @@ -47,7 +48,7 @@ export function modeAddArea(context) { actionClose(way.id) ); - context.enter(modeDrawArea(context, way.id, startGraph, context.graph())); + context.enter(modeDrawArea(context, way.id, startGraph, context.graph(), mode.button)); } @@ -64,7 +65,7 @@ export function modeAddArea(context) { actionAddMidpoint({ loc: loc, edge: edge }, node) ); - context.enter(modeDrawArea(context, way.id, startGraph, context.graph())); + context.enter(modeDrawArea(context, way.id, startGraph, context.graph(), mode.button)); } @@ -78,7 +79,7 @@ export function modeAddArea(context) { actionClose(way.id) ); - context.enter(modeDrawArea(context, way.id, startGraph, context.graph())); + context.enter(modeDrawArea(context, way.id, startGraph, context.graph(), mode.button)); } diff --git a/modules/modes/add_line.js b/modules/modes/add_line.js index 364adbcce..82f4578ae 100644 --- a/modules/modes/add_line.js +++ b/modules/modes/add_line.js @@ -10,8 +10,8 @@ import { modeDrawLine } from './index'; import { osmNode, osmWay } from '../osm'; -export function modeAddLine(context) { - var mode = { +export function modeAddLine(context, customMode) { + var mode = customMode || { id: 'add-line', button: 'line', title: t('modes.add_line.title'), @@ -25,11 +25,14 @@ export function modeAddLine(context) { .on('startFromWay', startFromWay) .on('startFromNode', startFromNode); + var defaultTags = {}; + if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'line'); + function start(loc) { var startGraph = context.graph(); var node = osmNode({ loc: loc }); - var way = osmWay(); + var way = osmWay({ tags: defaultTags }); context.perform( actionAddEntity(node), @@ -37,14 +40,14 @@ export function modeAddLine(context) { actionAddVertex(way.id, node.id) ); - context.enter(modeDrawLine(context, way.id, startGraph, context.graph())); + context.enter(modeDrawLine(context, way.id, startGraph, context.graph(), mode.button)); } function startFromWay(loc, edge) { var startGraph = context.graph(); var node = osmNode({ loc: loc }); - var way = osmWay(); + var way = osmWay({ tags: defaultTags }); context.perform( actionAddEntity(node), @@ -53,20 +56,20 @@ export function modeAddLine(context) { actionAddMidpoint({ loc: loc, edge: edge }, node) ); - context.enter(modeDrawLine(context, way.id, startGraph, context.graph())); + context.enter(modeDrawLine(context, way.id, startGraph, context.graph(), mode.button)); } function startFromNode(node) { var startGraph = context.graph(); - var way = osmWay(); + var way = osmWay({ tags: defaultTags }); context.perform( actionAddEntity(way), actionAddVertex(way.id, node.id) ); - context.enter(modeDrawLine(context, way.id, startGraph, context.graph())); + context.enter(modeDrawLine(context, way.id, startGraph, context.graph(), mode.button)); } diff --git a/modules/modes/add_point.js b/modules/modes/add_point.js index 89623a012..3b79f86b6 100644 --- a/modules/modes/add_point.js +++ b/modules/modes/add_point.js @@ -6,8 +6,8 @@ import { osmNode } from '../osm'; import { actionAddMidpoint } from '../actions'; -export function modeAddPoint(context) { - var mode = { +export function modeAddPoint(context, customMode) { + var mode = customMode || { id: 'add-point', button: 'point', title: t('modes.add_point.title'), @@ -23,9 +23,12 @@ export function modeAddPoint(context) { .on('cancel', cancel) .on('finish', cancel); + var defaultTags = {}; + if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'point'); + function add(loc) { - var node = osmNode({ loc: loc }); + var node = osmNode({ loc: loc, tags: defaultTags }); context.perform( actionAddEntity(node), @@ -33,13 +36,13 @@ export function modeAddPoint(context) { ); context.enter( - modeSelect(context, [node.id]).newFeature(true) + modeSelect(context, [node.id]).newFeature(!mode.preset) ); } function addWay(loc, edge) { - var node = osmNode(); + var node = osmNode({ tags: defaultTags }); context.perform( actionAddMidpoint({loc: loc, edge: edge}, node), @@ -47,7 +50,7 @@ export function modeAddPoint(context) { ); context.enter( - modeSelect(context, [node.id]).newFeature(true) + modeSelect(context, [node.id]).newFeature(!mode.preset) ); } diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index c365be4a8..7496dd574 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -2,9 +2,9 @@ import { t } from '../util/locale'; import { behaviorDrawWay } from '../behavior'; -export function modeDrawArea(context, wayID, startGraph, baselineGraph) { +export function modeDrawArea(context, wayID, startGraph, baselineGraph, button) { var mode = { - button: 'area', + button: button, id: 'draw-area' }; diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index f37910500..bc63806db 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -2,9 +2,9 @@ import { t } from '../util/locale'; import { behaviorDrawWay } from '../behavior'; -export function modeDrawLine(context, wayID, startGraph, baselineGraph, affix, continuing) { +export function modeDrawLine(context, wayID, startGraph, baselineGraph, button, affix, continuing) { var mode = { - button: 'line', + button: button, id: 'draw-line' }; diff --git a/modules/operations/continue.js b/modules/operations/continue.js index 088bd29ab..173965485 100644 --- a/modules/operations/continue.js +++ b/modules/operations/continue.js @@ -27,7 +27,7 @@ export function operationContinue(selectedIDs, context) { var operation = function() { var candidate = candidateWays()[0]; context.enter( - modeDrawLine(context, candidate.id, context.graph(), context.graph(), candidate.affix(vertex.id), true) + modeDrawLine(context, candidate.id, context.graph(), context.graph(), 'line', candidate.affix(vertex.id), true) ); }; diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index 845ad37e9..eba317e19 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -14,6 +14,7 @@ import { tooltip } from '../util/tooltip'; import { actionChangeTags } from '../actions'; import { modeBrowse } from '../modes'; import { svgIcon } from '../svg'; +import { uiPresetFavorite } from './preset_favorite'; import { uiPresetIcon } from './preset_icon'; import { uiQuickLinks } from './quick_links'; import { uiRawMemberEditor } from './raw_member_editor'; @@ -35,6 +36,7 @@ export function uiEntityEditor(context) { var _entityID; var _activePreset; var _tagReference; + var _presetFavorite; var entityIssues = uiEntityIssues(context); var quickLinks = uiQuickLinks(); @@ -137,6 +139,11 @@ export function uiEntityEditor(context) { body = body .merge(bodyEnter); + if (_presetFavorite) { + body.selectAll('.preset-list-button-wrap') + .call(_presetFavorite.button); + } + // update header if (_tagReference) { body.selectAll('.preset-list-button-wrap') @@ -334,6 +341,7 @@ export function uiEntityEditor(context) { _tagReference = uiTagReference(_activePreset.reference(context.geometry(_entityID)), context) .showing(false); } + _presetFavorite = uiPresetFavorite(_activePreset, context.geometry(_entityID), context); return entityEditor; }; diff --git a/modules/ui/modes.js b/modules/ui/modes.js index 35dfcc347..ea15f1199 100644 --- a/modules/ui/modes.js +++ b/modules/ui/modes.js @@ -11,7 +11,9 @@ import { } from '../modes'; import { svgIcon } from '../svg'; +import { t } from '../util/locale'; import { tooltip } from '../util/tooltip'; +import { uiPresetIcon } from './preset_icon'; import { uiTooltipHtml } from './tooltipHtml'; export function uiModes(context) { @@ -82,7 +84,8 @@ export function uiModes(context) { .on('drawn.modes', debouncedUpdate); context - .on('enter.modes', update); + .on('enter.modes', update) + .on('favoritePreset.modes', update); update(); @@ -91,6 +94,46 @@ export function uiModes(context) { var showNotes = notesEnabled(); var data = showNotes ? modes : modes.slice(0, 3); + // add favorite presets to modes + var favoritePresets = context.getFavoritePresets(); + var favoriteModes = favoritePresets.map(function(d) { + var preset = context.presets().item(d.id); + var isMaki = /^maki-/.test(preset.icon); + var icon = '#' + preset.icon + (isMaki ? '-11' : ''); + var markerClass = 'add-preset add-' + d.geom + ' add-preset-' + preset.name() + .replace(/\s+/g, '_') + + '-' + d.geom; //replace spaces with underscores to avoid css interpretation + var presetName = t('presets.presets.' + preset.id + '.name'); + var relevantMatchingGeometry = preset.geometry.filter(function(geometry) { + return ['point', 'line', 'area'].indexOf(geometry) !== -1; + }); + var tooltipTitleID = 'modes.add_preset.title'; + if (relevantMatchingGeometry.length !== 1) { + tooltipTitleID = 'modes.add_preset.' + d.geom + '.title'; + } + var favoriteMode = { + id: markerClass, + button: markerClass, + title: presetName, + description: t(tooltipTitleID, { feature: presetName }), + key: '', + icon: icon, + preset: preset, + geometry: d.geom + }; + switch (d.geom) { + case 'point': + case 'vertex': + return modeAddPoint(context, favoriteMode); + case 'line': + return modeAddLine(context, favoriteMode); + case 'area': + return modeAddArea(context, favoriteMode); + } + }); + + data = data.concat(favoriteModes); + var buttons = selection.selectAll('button.add-button') .data(data, function(d) { return d.id; }); @@ -124,8 +167,17 @@ export function uiModes(context) { buttonsEnter .each(function(d) { - d3_select(this) - .call(svgIcon('#iD-icon-' + d.button)); + if (d.preset) { + d3_select(this) + .call(uiPresetIcon() + .geometry(d.geometry) + .preset(d.preset) + .sizeClass('small') + ); + } else { + d3_select(this) + .call(svgIcon(d.icon || '#iD-icon-' + d.button)); + } }); buttonsEnter diff --git a/modules/ui/preset_favorite.js b/modules/ui/preset_favorite.js new file mode 100644 index 000000000..ef43ee338 --- /dev/null +++ b/modules/ui/preset_favorite.js @@ -0,0 +1,54 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { t } from '../util/locale'; +import { svgIcon } from '../svg'; + +export function uiPresetFavorite(preset, geom, context) { + + var presetFavorite = {}; + + var _button = d3_select(null); + + + presetFavorite.button = function(selection) { + + var canFavorite = geom !== 'vertex' && geom !== 'relation' && !preset.isFallback(); + + _button = selection.selectAll('.preset-favorite-button') + .data(canFavorite ? [0] : []); + + _button.exit().remove(); + + _button = _button.enter() + .insert('button', '.tag-reference-button') + .attr('class', 'preset-favorite-button') + .attr('title', t('icons.favorite')) + .attr('tabindex', -1) + .call(svgIcon('#iD-icon-favorite')) + .merge(_button); + + _button + .classed('active', function() { + return context.isFavoritePreset(preset, geom); + }) + .on('click', function () { + d3_event.stopPropagation(); + d3_event.preventDefault(); + + //update state of favorite icon + d3_select(this) + .classed('active', function() { + return !d3_select(this).classed('active'); + }); + + context.favoritePreset(preset, geom); + + }); + + }; + + return presetFavorite; +} diff --git a/modules/ui/preset_icon.js b/modules/ui/preset_icon.js index c421561d4..f699272e2 100644 --- a/modules/ui/preset_icon.js +++ b/modules/ui/preset_icon.js @@ -4,7 +4,11 @@ import { svgIcon, svgTagClasses } from '../svg'; import { utilFunctor } from '../util'; export function uiPresetIcon() { - var preset, geometry; + var preset, geometry, sizeClass = 'medium'; + + function isSmall() { + return sizeClass === 'small'; + } function presetIcon(selection) { @@ -39,7 +43,8 @@ export function uiPresetIcon() { } function renderSquareFill(fillEnter) { - var w = 60, h = 60, l = 40, c1 = (w-l)/2, c2 = c1 + l; + var d = isSmall() ? 40 : 60; + var w = d, h = d, l = d*2/3, c1 = (w-l)/2, c2 = c1 + l; fillEnter = fillEnter .append('svg') .attr('class', 'preset-icon-fill preset-icon-fill-area') @@ -69,20 +74,24 @@ export function uiPresetIcon() { } } - var midCoordinates = [[c1, w/2], [c2, w/2], [h/2, c1], [h/2, c2]]; - for (var index in midCoordinates) { - var loc = midCoordinates[index]; - fillEnter.append('circle') - .attr('class', 'midpoint') - .attr('cx', loc[0]) - .attr('cy', loc[1]) - .attr('r', 1.25); + if (!isSmall()) { + var midCoordinates = [[c1, w/2], [c2, w/2], [h/2, c1], [h/2, c2]]; + for (var index in midCoordinates) { + var loc = midCoordinates[index]; + fillEnter.append('circle') + .attr('class', 'midpoint') + .attr('cx', loc[0]) + .attr('cy', loc[1]) + .attr('r', 1.25); + } } + } function renderLine(lineEnter) { + var d = isSmall() ? 40 : 60; // draw the line parametrically - var w = 60, h = 60, y = 43, l = 36, r = 2.5; + var w = d, h = d, y = Math.round(d*0.72), l = Math.round(d*0.6), r = 2.5; var x1 = (w - l)/2, x2 = x1 + l; lineEnter = lineEnter @@ -224,5 +233,12 @@ export function uiPresetIcon() { return presetIcon; }; + + presetIcon.sizeClass = function(val) { + if (!arguments.length) return sizeClass; + sizeClass = val; + return presetIcon; + }; + return presetIcon; } diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index 25d8abd62..b33943003 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -215,8 +215,10 @@ export function uiSidebar(context) { var entity = context.entity(id); // uncollapse the sidebar if (selection.classed('collapsed')) { - var extent = entity.extent(context.graph()); - sidebar.expand(sidebar.intersects(extent)); + if (newFeature) { + var extent = entity.extent(context.graph()); + sidebar.expand(sidebar.intersects(extent)); + } } featureListWrap diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index 298bec323..8da908a11 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -145,6 +145,7 @@ export function validationAlmostJunction() { var validation = function(endHighway, context) { if (!isHighway(endHighway)) return []; + if (endHighway.isDegenerate()) return []; var graph = context.graph(); var tree = context.history().tree(); diff --git a/svg/iD-sprite/icons/icon-favorite.svg b/svg/iD-sprite/icons/icon-favorite.svg new file mode 100644 index 000000000..cc81bef58 --- /dev/null +++ b/svg/iD-sprite/icons/icon-favorite.svg @@ -0,0 +1,4 @@ + + + +