diff --git a/.tx/config b/.tx/config index 4f3831038..21389fb4a 100644 --- a/.tx/config +++ b/.tx/config @@ -2,6 +2,11 @@ host = https://www.transifex.com minimum_perc = 1 +[id-editor.community] +file_filter = .tx/tmp/community/.yaml +source_lang = en +type = YAML + [id-editor.core] file_filter = .tx/tmp/core/.yaml source_file = data/core.yaml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11c033760..0451a0279 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -431,7 +431,7 @@ Then change to the master branch and get everything from upstream (the main repo If you want to submit Documentation, Spelling improvements, etc. which do not need testing, you can do this with your browser in GitHub. Please don't use this to change Code and create untested Pull Requests. -You also need a GitHub account and may find this [Article about Editing](https://help.github.com/articles/editing-files-in-another-user-s-repository/) and this [Article about Pull Requests](https://help.github.com/articles/about-pull-requests/) usefull. +You also need a GitHub account and may find this [Article about Editing](https://help.github.com/articles/editing-files-in-another-user-s-repository/) and this [Article about Pull Requests](https://help.github.com/articles/about-pull-requests/) useful. ### Step by Step with Browser diff --git a/README.md b/README.md index 6a84a4131..5a8821728 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ To run the current development version of iD on your own computer: #### Cloning the repository -The repository is reasonably large, and it's unlikely that you need the full history. If you are happy to wait for it all to download, run: +The repository is reasonably large, and it's unlikely that you need the full history (~200 MB). If you are happy to wait for it all to download, run: ``` git clone https://github.com/openstreetmap/iD.git diff --git a/build_data.js b/build_data.js index 512c497c2..ad5e92033 100644 --- a/build_data.js +++ b/build_data.js @@ -533,7 +533,12 @@ function writeFaIcons(faIcons) { var prefix = key.substring(0, 3); // `fas`, `far`, `fab` var name = key.substring(4); var def = fontawesome.findIconDefinition({ prefix: prefix, iconName: name }); - writeFileProm('svg/fontawesome/' + key + '.svg', fontawesome.icon(def).html); + try { + writeFileProm('svg/fontawesome/' + key + '.svg', fontawesome.icon(def).html); + } catch (error) { + console.error('Error: No FontAwesome icon for ' + key); + throw (error); + } } } diff --git a/css/20_map.css b/css/20_map.css index 9ded521e0..6c9ed5c4b 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -31,7 +31,9 @@ /* No interactivity except what we specifically allow */ -.layer-osm * { +.data-layer.osm *, +.data-layer.notes *, +.data-layer.keepRight * { pointer-events: none; } @@ -42,6 +44,8 @@ /* `.target` objects are interactive */ /* They can be picked up, clicked, hovered, or things can connect to them */ +.kr_error.target, +.note.target, .node.target, .turn .target { pointer-events: fill; @@ -75,8 +79,11 @@ pointer-events: none !important; } +/* NOTE: when more QA layers are added, replace kr_error with generic QA layer selector */ +/* points, notes & QA */ -/* points & notes */ +/* points, notes, markers */ +g.kr_error .stroke, g.note .stroke { stroke: #222; stroke-width: 1; @@ -84,6 +91,7 @@ g.note .stroke { opacity: 0.6; } +g.kr_error.active .stroke, g.note.active .stroke { stroke: #222; stroke-width: 1; @@ -97,6 +105,7 @@ g.point .stroke { fill: #fff; } +g.kr_error .shadow, g.point .shadow, g.note .shadow { fill: none; @@ -105,13 +114,14 @@ g.note .shadow { stroke-opacity: 0; } -g.note.related:not(.selected) .shadow, +g.kr_error.hover:not(.selected) .shadow, g.note.hover:not(.selected) .shadow, g.point.related:not(.selected) .shadow, g.point.hover:not(.selected) .shadow { stroke-opacity: 0.5; } +g.kr_error.selected .shadow, g.note.selected .shadow, g.point.selected .shadow { stroke-opacity: 0.7; diff --git a/css/55_cursors.css b/css/55_cursors.css index 466e21ae2..f473301c4 100644 --- a/css/55_cursors.css +++ b/css/55_cursors.css @@ -96,7 +96,12 @@ cursor: url(img/cursor-draw.png) 9 9, crosshair; /* FF */ } +.mode-browse .note, +.mode-browse .kr_error, +.mode-select .note, +.mode-select .kr_error, .turn rect, .turn circle { cursor: pointer; } + diff --git a/css/65_data.css b/css/65_data.css index 24af906c4..71aafab9c 100644 --- a/css/65_data.css +++ b/css/65_data.css @@ -1,39 +1,27 @@ -/* OSM Notes Layer */ -.layer-notes { - pointer-events: none; -} -.layer-notes .note * { - pointer-events: none; -} -.mode-browse .layer-notes .note .note-fill, -.mode-select .layer-notes .note .note-fill, -.mode-select-data .layer-notes .note .note-fill, -.mode-select-note .layer-notes .note .note-fill { - pointer-events: visible; - cursor: pointer; /* Opera */ - cursor: url(img/cursor-select-point.png), pointer; /* FF */ +/* OSM Notes and KeepRight Layers */ + +.kr_error-header-icon .kr_error-fill, +.layer-keepRight .kr_error .kr_error-fill { + stroke: #333; + stroke-width: 1.3px; /* NOTE: likely a better way to scale the icon stroke */ } -.note-header-icon .note-shadow, -.layer-notes .note .note-shadow { - color: #000; -} .note-header-icon .note-fill, .layer-notes .note .note-fill { - color: #ff3300; + color: #f30; stroke: #333; stroke-width: 40px; } .note-header-icon.new .note-fill, .layer-notes .note.new .note-fill { - color: #ffee00; + color: #fe0; stroke: #333; stroke-width: 40px; } .note-header-icon.closed .note-fill, .layer-notes .note.closed .note-fill { - color: #55dd00; + color: #5d0; stroke: #333; stroke-width: 40px; } @@ -54,8 +42,81 @@ } -/* Custom Map Data (geojson, gpx, kml, vector tile) */ +/* Keep Right Errors +------------------------------------------------------- */ +.kr_error_type_20, /* multiple nodes on same spot */ +.kr_error_type_40, /* impossible oneways */ +.kr_error_type_210, /* self intersecting ways */ +.kr_error_type_270, /* unusual motorway connection */ +.kr_error_type_310, /* roundabout issues */ +.kr_error_type_320, /* improper _link */ +.kr_error_type_350 { /* improper bridge tag */ + color: #ff9; +} +.kr_error_type_50 { /* almost junctions */ + color: #88f; +} + +.kr_error_type_60, /* deprecated tags */ +.kr_error_type_70, /* tagging issues */ +.kr_error_type_90, /* motorway without ref */ +.kr_error_type_100, /* place of worship without religion */ +.kr_error_type_110, /* poi without name */ +.kr_error_type_150, /* railway crossing without tag */ +.kr_error_type_220, /* misspelled tag */ +.kr_error_type_380 { /* non-physical sport tag */ + color: #5d0; +} + +.kr_error_type_130 { /* disconnected ways */ + color: #fa3; +} + +.kr_error_type_170 { /* FIXME tag */ + color: #ff0; +} + +.kr_error_type_190 { /* intersection without junction */ + color: #f33; +} + +.kr_error_type_200 { /* overlapping ways */ + color: #fdbf6f; +} + +.kr_error_type_160, /* railway layer conflict */ +.kr_error_type_230 { /* layer conflict */ + color: #b60; +} + +.kr_error_type_280 { /* boundary issues */ + color: #5f47a0; +} + +.kr_error_type_180, /* relation without type */ +.kr_error_type_290 { /* turn restriction issues */ + color: #ace; +} + +.kr_error_type_300, /* missing maxspeed */ +.kr_error_type_390 { /* missing tracktype */ + color: #090; +} + +.kr_error_type_360, /* language unknown */ +.kr_error_type_370, /* doubled places */ +.kr_error_type_410 { /* website issues */ + color: #f9b; +} + +.kr_error_type_120, /* way without nodes */ +.kr_error_type_400 { /* geometry / turn angles */ + color: #c35; +} + + +/* Custom Map Data (geojson, gpx, kml, vector tile) */ .layer-mapdata { pointer-events: none; } diff --git a/css/80_app.css b/css/80_app.css index b6a9fb82a..9cc9ea7fa 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -586,6 +586,7 @@ button.add-note svg.icon { .field-help-title button.close, .sidebar-component .header button.data-editor-close, .sidebar-component .header button.note-editor-close, +.sidebar-component .header button.keepRight-editor-close, .entity-editor-pane .header button.preset-close, .preset-list-pane .header button.preset-choose { position: absolute; @@ -595,6 +596,7 @@ button.add-note svg.icon { [dir='rtl'] .field-help-title button.close, [dir='rtl'] .sidebar-component .header button.data-editor-close, [dir='rtl'] .sidebar-component .header button.note-editor-close, +[dir='rtl'] .sidebar-component .header button.keepRight-editor-close, [dir='rtl'] .entity-editor-pane .header button.preset-close, [dir='rtl'] .preset-list-pane .header button.preset-choose { left: 0; @@ -782,10 +784,6 @@ a.hide-toggle { bottom: 0; } -.inspector-border { - border-bottom: 1px solid #ccc -} - .feature-list-pane .inspector-body, .preset-list-pane .inspector-body { top: 120px; @@ -796,7 +794,7 @@ a.hide-toggle { } .inspector-inner { - padding: 20px; + padding: 20px 20px 5px 20px; position: relative; } @@ -849,6 +847,10 @@ a.hide-toggle { border-radius: 2px; } +[dir='rtl'] .geocode-item { + left: -25%; +} + .geocode-item:hover { background-color: #aaa; } @@ -1155,11 +1157,30 @@ img.tag-reference-wiki-image { } +/* Quick links +------------------------------------------------------- */ +.quick-links { + display: flex; + flex-flow: row wrap; + justify-content: flex-end; + padding: 0 20px; +} +.quick-link { + margin: 0 5px; +} + +.data-editor .quick-links, +.keepRight-editor .quick-links, +.note-editor .quick-links { + padding: 5px 0 0 0; +} + + /* Entity/Preset Editor ------------------------------------------------------- */ .preset-editor { overflow: hidden; - padding-bottom: 10px; + padding: 10px 0px 5px 0px; } .preset-editor a.hide-toggle { margin: 0 20px 5px 20px; @@ -2254,6 +2275,12 @@ div.combobox { padding-left: 10px; } +[dir='rtl'] .raw-member-editor .member-row .member-entity-name, +[dir='rtl'] .raw-membership-editor .member-row .member-entity-name { + padding-left:0; + padding-right: 10px; +} + .form-field-input-member > input.member-role { border-radius: 0 0 0 4px; } @@ -2354,6 +2381,7 @@ input.key-trap { /* hide but preserve in layout */ .inspector-hover .combobox-caret, .inspector-hover .header button, +.inspector-hover .quick-links, .inspector-hover .form-field-input-multicombo .chips .remove, .inspector-hover .hide-toggle:before, .inspector-hover .more-fields, @@ -2430,9 +2458,10 @@ input.key-trap { } -/* OSM Note Editor +/* OSM Note / KeepRight Editors ------------------------------------------------------- */ -.note-header { +.note-header, +.kr_error-header { background-color: #f6f6f6; border-radius: 5px; border: 1px solid #ccc; @@ -2441,7 +2470,8 @@ input.key-trap { align-items: center; } -.note-header-icon { +.note-header-icon, +.kr_error-header-icon { background-color: #fff; padding: 10px; flex: 0 0 62px; @@ -2451,18 +2481,21 @@ input.key-trap { border-right: 1px solid #ccc; border-radius: 5px 0 0 5px; } -[dir='rtl'] .note-header-icon { +[dir='rtl'] .note-header-icon, +[dir='rtl'] .kr_error-header-icon { border-right: unset; border-left: 1px solid #ccc; border-radius: 0 5px 5px 0; } -.note-header-icon .icon-wrap { +.note-header-icon .icon-wrap, +.kr_error-header-icon .icon-wrap { position: absolute; top: 0px; } -.note-header-label { +.note-header-label, +.kr_error-header-label { background-color: #f6f6f6; padding: 0 15px; flex: 1 1 100%; @@ -2470,7 +2503,8 @@ input.key-trap { font-weight: bold; border-radius: 0 5px 5px 0; } -[dir='rtl'] .note-header-label { +[dir='rtl'] .note-header-label, +[dir='rtl'] .kr_error-header-label { border-radius: 5px 0 0 5px; } @@ -2536,17 +2570,24 @@ input.key-trap { border-left: none; } -.note-save { +.note-save, +.keepRight-save { + padding-top: 20px; +} +.kr_error-details, +.kr_error-comment-container { padding: 10px; } -.note-save #new-comment-input { +.keepRight-save .new-comment-input, +.note-save .new-comment-input { width: 100%; height: 100px; max-height: 300px; min-height: 100px; } +.keepRight-save .detail-section, .note-save .detail-section { margin: 10px 0; } @@ -2555,6 +2596,21 @@ input.key-trap { float: right; } +.kr_error-details-container { + background: #ececec; + padding: 10px; + margin-top: 20px; + border-radius: 4px; + border: 1px solid #ccc; +} + +.kr_error-details-description { + margin-bottom: 10px; +} +.kr_error-details-description-text::first-letter { + text-transform: capitalize; +} + /* Custom Data Editor ------------------------------------------------------- */ @@ -2864,7 +2920,6 @@ div.full-screen > button:hover { .entity-issues { padding: 0 20px 20px 20px; - margin-bottom: 20px; } .entity-issues .issue { border-radius: 4px; @@ -4380,29 +4435,24 @@ svg.mouseclick use.right { font-size: 12px; white-space: initial; } - .tooltip.in { opacity: 0.9; z-index: 1030; height: auto; display: block; } - .tooltip.top { margin-top: -20px; text-align: center; } - .tooltip.right { margin-left: 20px; text-align: left; } - .tooltip.bottom { margin-top: 20px; text-align: center; } - .tooltip.left { margin-left: -20px; text-align: right; @@ -4427,7 +4477,6 @@ svg.mouseclick use.right { position: absolute; background: transparent; } - .tail::after { content: ""; position: absolute; @@ -4474,7 +4523,6 @@ svg.mouseclick use.right { border-color: transparent; border-style: solid; } - .tooltip.top .tooltip-arrow { bottom: -5px; left: 50%; @@ -4482,7 +4530,6 @@ svg.mouseclick use.right { border-top-color: #fff; border-width: 5px 5px 0; } - .tooltip.right .tooltip-arrow { top: 50%; left: -5px; @@ -4490,7 +4537,6 @@ svg.mouseclick use.right { border-right-color: #fff; border-width: 5px 5px 5px 0; } - .tooltip.left .tooltip-arrow { top: 50%; right: -5px; @@ -4498,7 +4544,6 @@ svg.mouseclick use.right { border-left-color: #fff; border-width: 5px 0 5px 5px; } - .tooltip.bottom .tooltip-arrow { top: -5px; left: 50%; @@ -4509,7 +4554,7 @@ svg.mouseclick use.right { .tooltip-heading { font-weight: bold; - background: #F6F6F6; + background: #f6f6f6; padding: 10px; margin: -10px -10px 10px -10px; border-radius: 3px 3px 0 0; @@ -4517,52 +4562,50 @@ svg.mouseclick use.right { } .keyhint-wrap { - background: #F6F6F6; + background: #f6f6f6; padding: 10px; margin: 10px -10px -10px -10px; border-radius: 0 0 3px 3px; } - .tooltip-inner .keyhint { font-weight: bold; margin-left: 5px; } -/* Exceptions for tooltip layouts */ +[dir='rtl'] .tooltip-inner .keyhint { + margin-left: 0; + margin-right: 5px; +} -/* make tooltips in panels dark */ +/* dark tooltips for sidebar / panels */ .map-pane .tooltip.top .tooltip-arrow, -.entity-editor-pane .tooltip.top .tooltip-arrow, -.warning-section .tooltip.top .tooltip-arrow { +#sidebar .tooltip.top .tooltip-arrow { border-top-color: #000; } - .map-pane .tooltip.bottom .tooltip-arrow, -.entity-editor-pane .tooltip.bottom .tooltip-arrow, -.warning-section .tooltip.bottom .tooltip-arrow { +#sidebar .tooltip.bottom .tooltip-arrow { border-bottom-color: #000; } - .map-pane .tooltip.left .tooltip-arrow, -.entity-editor-pane .tooltip.left .tooltip-arrow, -.warning-section .tooltip.left .tooltip-arrow { +#sidebar .tooltip.left .tooltip-arrow { border-left-color: #000; } - .map-pane .tooltip.right .tooltip-arrow, -.entity-editor-pane .tooltip.right .tooltip-arrow, -.warning-section .tooltip.right .tooltip-arrow { +#sidebar .tooltip.right .tooltip-arrow { border-right-color: #000; } - .map-pane .tooltip-inner, .map-pane .tooltip-heading, .map-pane .keyhint-wrap, -.entity-editor-pane .tooltip-inner, -.warning-section .tooltip-inner { +#sidebar .tooltip-inner, +#sidebar .tooltip-heading, +#sidebar .keyhint-wrap { background: #000; color: #ccc; } + +/* Exceptions for tooltip layouts */ + /* commit warning tooltips need to be closer */ .warning-section .tooltip.top { margin-top: -5px; @@ -4578,11 +4621,11 @@ svg.mouseclick use.right { /* Move over tooltips that are near the edge of screen */ button.sidebar-toggle .tooltip .tooltip-arrow { - left: 32px; + left: 36px; } [dir='rtl'] button.sidebar-toggle .tooltip .tooltip-arrow { left: auto; - right: 32px; + right: 36px; } li:first-of-type .badge .tooltip, diff --git a/data/core.yaml b/data/core.yaml index d81019a74..647d47eff 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -403,11 +403,18 @@ en: title: Show My Location locating: "Locating, please wait..." inspector: + zoom_to: + key: Z + title: Zoom to this + tooltip_feature: "Center and zoom the map to focus on this feature." + tooltip_note: "Center and zoom the map to focus on this note." + tooltip_data: "Center and zoom the map to focus on this data." + tooltip_issue: "Center and zoom the map to focus on this issue." no_documentation_combination: There is no documentation available for this tag combination no_documentation_key: There is no documentation available for this key - documentation_redirect: This documentation has been redirected to a new page show_more: Show More view_on_osm: View on openstreetmap.org + view_on_keepRight: View on keepright.at all_fields: All fields all_tags: All tags all_members: All members @@ -418,6 +425,7 @@ en: choose: Select feature type results: "{n} results for {search}" reference: View on OpenStreetMap Wiki + edit_reference: Edit or translate on OSM Wiki back_tooltip: Change feature remove: Remove search: Search @@ -479,6 +487,9 @@ en: notes: tooltip: Note data from OpenStreetMap title: OpenStreetMap notes + keepRight: + tooltip: Automatically detected map issues from keepright.at + title: KeepRight Issues custom: tooltip: "Drag and drop a data file onto the page, or click the button to setup" title: Custom Map Data @@ -627,6 +638,246 @@ en: out: Zoom out cannot_zoom: "Cannot zoom out further in current mode." full_screen: Toggle Full Screen + QA: + keepRight: + title: KeepRight Error + detail_title: Error + detail_description: Description + comment: Comment + comment_placeholder: Enter a comment to share with other users. + close: Close (Error Fixed) + ignore: Ignore (Not an Error) + save_comment: Save Comment + close_comment: Close and Comment + ignore_comment: Ignore and Comment + error_parts: + node: 'this node' + way: 'this way' + relation: 'this relation' + oneway: 'this oneway' + highway: 'this highway' + railway: 'this railway' + waterway: 'this waterway' + cycleway: 'this cycleway' + cycleway_footpath: 'this cycleway/footpath' + riverbank: 'this riverbank' + crossing: 'this crossing' + railway_crossing: 'this railway crossing' + bridge: 'this bridge' + tunnel: 'this tunnel' + boundary: 'this boundary' + turn_restriction: 'this turn restriction' + roundabout: 'this roundabout' + mini_roundabout: 'this mini-roundabout' + track: 'this track' + feature: 'this feature' + place_of_worship: 'place of worship' + pub: 'pub' + restaurant: 'restaurant' + school: 'school' + university: 'university' + hospital: 'hospital' + library: 'library' + theatre: 'theatre' + courthouse: 'courthouse' + bank: 'bank' + cinema: 'cinema' + pharmacy: 'pharmacy' + cafe: 'cafe' + fast_food: 'fast food' + fuel: 'fuel' + from: 'from' + to: 'to' + left_hand: 'left-hand' + right_hand: 'right-hand' + errorTypes: + 20: + title: 'Multiple nodes on the same spot' + description: 'There is more than one node in this spot. Node IDs: {var1}.' + 30: + title: 'Non-closed area' + description: '{var1} is tagged with "{var2}" and should be a closed loop.' + 40: + title: 'Impossible oneway' + description: 'The first node {var1} of {var2} is not connected to any other way.' + 41: + description: 'The last node {var1} of {var2} is not connected to any other way.' + 42: + description: 'You cannot reach {var1} because all ways leading from it are oneway.' + 43: + description: 'You cannot escape from {var1} because all ways leading to it are oneway.' + 50: + title: 'Almost junction' + description: '{var1} is very close but not connected to way {var2}.' + 60: + title: 'Deprecated tag' + description: '{var1} uses deprecated tag "{var2}". Please use "{var3}" instead.' + 70: + title: 'Missing tag' + description: '{var1} has an empty tag: "{var2}".' + 71: + description: '{var1} has no tags.' + 72: + description: '{var1} is not member of any way and doesn''t have any tags.' + 73: + description: '{var1} has a "{var2}" tag but no "highway" tag.' + 74: + description: '{var1} has an empty tag: "{var2}".' + 75: + description: '{var1} has a name "{var2}" but no other tags.' + 90: + title: 'Motorway without ref tag' + description: '{var1} is tagged as a motorway and therefore needs a "ref", "nat_ref", or "int_ref" tag.' + 100: + title: 'Place of worship without religion' + description: '{var1} is tagged as a place of worship and therefore needs a religion tag.' + 110: + title: 'Point of interest without name' + description: '{var1} is tagged as a "{var2}" and therefore needs a name tag.' + 120: + title: 'Way without nodes' + description: '{var1} has just one single node.' + 130: + title: 'Disconnected way' + description: '{var1} is not connected to the rest of the map.' + 150: + title: 'Railway crossing without tag' + description: '{var1} of a highway and a railway needs to be tagged as "railway=crossing" or "railway=level_crossing".' + 160: + title: 'Railway layer conflict' + description: 'There are ways in different layers (e.g. tunnel or bridge) meeting at {var1}.' + 170: + title: 'FIXME tagged item' + description: '{var1} has a FIXME tag: {var2}' + 180: + title: 'Relation without type' + description: '{var1} is missing a "type" tag.' + 190: + title: 'Intersection without junction' + description: '{var1} intersects the {var2} {var3} but there is no junction node, bridge, or tunnel.' + 200: + title: 'Overlapping ways' + description: '{var1} overlaps the {var2} {var3}.' + 210: + title: 'Self-intersecting way' + description: 'There is an unspecified issue with self intersecting ways.' + 211: + description: '{var1} contains more than one node multiple times. Nodes are {var2}. This may or may not be an error.' + 212: + description: '{var1} has only two different nodes and contains one of them more than once.' + 220: + title: 'Misspelled tag' + description: '{var1} is tagged "{var2}" where "{var3}" looks like "{var4}".' + 221: + description: '{var1} has a suspicious tag "{var2}".' + 230: + title: 'Layer conflict' + description: '{var1} is a junction of ways on different layers.' + 231: + description: '{var1} is a junction of ways on different layers: {var2}.' + layer: '(layer: {layer})' + 232: + description: '{var1} is tagged with "layer={var2}". This need not be an error but it looks strange.' + 270: + title: 'Unusual motorway connection' + description: '{var1} is a junction of a motorway and a highway other than "motorway", "motorway_link", "trunk", "rest_area", or "construction". Connection to "service" or "unclassified" is only valid if it has "access=no/private", or it leads to a motorway service area, or if it is a "service=parking_aisle".' + 280: + title: 'Boundary issue' + description: 'There is an unspecified issue with this boundary.' + 281: + title: 'Boundary missing name' + description: '{var1} has no name.' + 282: + title: 'Boundary missing admin level' + description: 'The boundary of {var1} has no valid numeric admin_level. Please do not mix admin levels (e.g. "6;7"). Always tag the lowest admin_level of all boundaries.' + 283: + title: 'Boundary not a closed loop' + description: 'The boundary of {var1} is not a closed loop.' + 284: + title: 'Boundary is split' + description: 'The boundary of {var1} splits here.' + 285: + title: 'Boundary admin_level too high' + description: '{var1} has "admin_level={var2}" but belongs to a relation with lower "admin_level" (e.g. higher priority); it should have the lowest "admin_level" of all relations.' + 290: + title: 'Restriction issue' + description: 'There is an unspecified issue with this restriction.' + 291: + title: 'Restriction missing type' + description: '{var1} has an unrecognized restriction type.' + 292: + title: 'Restriction missing "from" way' + description: '{var1} has {var2} "from" members, but it should have 1.' + 293: + title: 'Restriction missing "to" way' + description: '{var1} has {var2} "to" members, but it should have 1.' + 294: + title: 'Restriction "from" or "to" is not a way' + description: '{var1} has "from" or "to" members which should be ways. {var2}.' + 295: + title: 'Restriction "via" is not an endpoint' + description: '{var1} has a "via" (node {var2}) which is not the first or the last member of "{var3}" (way {var4}).' + 296: + title: 'Unusual restriction angle' + description: '{var1} has a restriction type "{var2}" but the angle is {var3} degrees. Maybe the restriction type is not appropriate?' + 297: + title: 'Wrong direction of to member' + description: '{var1} does not match the direction of "to" way {var2}.' + 298: + title: 'Redundant restriction - oneway' + description: '{var1} may be redundant. Entry already prohibited by "oneway" tag on {var2}.' + 300: + title: 'Missing maxspeed' + description: '{var1} is missing a "maxspeed" tag and is tagged as motorway, trunk, primary, or secondary.' + 310: + title: 'Roundabout issue' + description: 'There is an unspecified issue with this roundabout.' + 311: + title: 'Roundabout not closed loop' + description: '{var1} is part of a roundabout but is not closed-loop. (Split carriageways approaching a roundabout should not be tagged as roundabout).' + 312: + title: 'Roundabout wrong direction' + description: 'If {var1} is in a country with {var2} traffic then its orientation goes the wrong way around.' + 313: + title: 'Roundabout weakly connected' + description: '{var1} has only {var2} other road(s) connected. Roundabouts typically have 3 or more.' + 320: + title: 'Improper link connection' + description: '{var1} is tagged as "{var2}" but doesn''t have a connection to any other "{var3}" or "{var4}".' + 350: + title: 'Improper bridge tag' + description: '{var1} doesn''t have a tag in common with its surrounding ways that shows the purpose of this bridge. There should be one of these tags: {var2}.' + 360: + title: 'Missing local name tag' + description: 'It would be nice if {var1} had a local name tag "name:XX={var2}" where XX shows the language of its common name "{var2}".' + 370: + title: 'Doubled places' + description: '{var1} has tags in common with the surrounding way {var2} {var3} and seems to be redundant.' + including_the_name: "(including the name {name})" + 380: + title: 'Non-physical use of sport tag' + description: '{var1} is tagged "{var2}" but has no physical tag (e.g. "leisure", "building", "amenity", or "highway").' + 390: + title: 'Missing tracktype' + description: '{var1} doesn''t have a "tracktype" tag.' + 400: + title: 'Geometry issue' + description: 'There is an unspecified issue with the geometry here.' + 401: + title: 'Missing turn restriction' + description: 'Ways {var1} and {var2} join in a very sharp angle here and there is no oneway tag or turn restriction that prevents turning.' + 402: + title: 'Impossible angle' + description: '{var1} bends in a very sharp angle here.' + 410: + title: 'Website issue' + description: 'There is an unspecified issue with a contact website or URL.' + 411: + description: '{var1} may have an outdated URL: {var2} returned HTTP status code {var3}.' + 412: + description: '{var1} may have an outdated URL: {var2} contained suspicious text "{var3}".' + 413: + description: '{var1} may have an outdated URL: {var2} did not contain keywords "{var3}".' streetside: tooltip: "Streetside photos from Microsoft" title: "Photo Overlay (Bing Streetside)" @@ -834,6 +1085,13 @@ en: using: "To use a GPS trace for mapping, drag and drop the data file onto the map editor. If it's recognized, it will be drawn on the map as a bright purple line. Click the {data} **Map data** panel on the side of the map to enable, disable, or zoom to your GPS data." tracing: "The GPS track isn't sent to OpenStreetMap - the best way to use it is to draw on the map, using it as a guide for the new features that you add." upload: "You can also [upload your GPS data to OpenStreetMap](https://www.openstreetmap.org/trace/create) for other users to use." + qa: + title: Quality Assurance + intro: "*Quality Assurance* (Q/A) 3rd party tools help lead to better quality of OSM data. They list automatically deteted bugs, conflics, and issues with the data, which mappers can then go and fix. To view existing Q/A issues, click the {data} **Map data** panel to enable a specific Q/A layer." + tools_h: "Tools" + tools: "The following tools are currently supported: [KeepRight](https://www.keepright.at/). Expect iD to support [Osmose](https://osmose.openstreetmap.fr/), [ImproveOSM](https://improveosm.org/en/), and more Q/A tools in the future." + issues_h: "Handling Issues" + issues: "Handling Q/A issues is similar to handling notes. Clicking an existing Q/A issue populates the sidebar with details on the issue type and related features. Each tool has its own capabilities, but generally you can comment and/or close an issue. Expect iD to support a 'fix me' button to automatically fix simple issues in the future." field: restrictions: title: Turn Restrictions Help @@ -1238,6 +1496,7 @@ en: with_selected: title: "With feature selected" edit_menu: "Toggle edit menu" + zoom_to: "Zoom to selected feature" vertex_selected: title: "With node selected" previous: "Jump to previous node" diff --git a/data/keepRight.json b/data/keepRight.json new file mode 100644 index 000000000..52967eb7a --- /dev/null +++ b/data/keepRight.json @@ -0,0 +1,497 @@ +{ + "localizeStrings": { + "this node": "node", + "this way": "way", + "this relation": "relation", + "this one-way": "oneway", + "this highway": "highway", + "this railway": "railway", + "this waterway": "waterway", + "this cycleway": "cycleway", + "this footpath": "footpath", + "this cycleway/footpath": "cycleway_footpath", + "this riverbank": "riverbank", + "this crossing": "crossing", + "this railway crossing": "railway_crossing", + "this bridge": "bridge", + "this tunnel": "tunnel", + "this boundary": "boundary", + "this turn-restriction": "turn_restriction", + "this roundabout": "roundabout", + "this mini-roundabout": "mini_roundabout", + "this track": "track", + "this feature": "feature", + "place_of_worship": "place_of_worship", + "pub": "pub", + "restaurant": "restaurant", + "school": "school", + "university": "university", + "hospital": "hospital", + "library": "library", + "theatre": "theatre", + "courthouse": "courthouse", + "bank": "bank", + "cinema": "cinema", + "pharmacy": "pharmacy", + "cafe": "cafe", + "fast_food": "fast_food", + "fuel": "fuel", + "from": "from", + "to": "to", + "left-hand": "left_hand", + "right-hand": "right_hand" + }, + "errorTypes": { + "20": { + "title": "multiple nodes on the same spot", + "severity": "warning", + "description": "There is more than one node in this spot. Offending node IDs: $1", + "IDs": ["20"], + "regex": "IDs: ((?:#\\d+,?)+)" + }, + "30": { + "title": "non-closed_areas", + "severity": "error", + "description": "This way is tagged with '$1' and should be closed-loop.", + "IDs": ["this", ""], + "regex": "(this way) is tagged with '(.+)'" + }, + "40": { + "title": "dead-ended one-ways", + "severity": "error", + "description": "The first node (id $1) of this one-way is not connected to any other way", + "IDs": ["n", "this"], + "regex": "\\(id (\\d+)\\) of (this one-way)" + }, + "41": { + "title": "", + "severity": "error", + "description": "The last node (id $1) of this one-way is not connected to any other way", + "IDs": ["n", "this"], + "regex": "\\(id (\\d+)\\) of (this one-way)" + }, + "42": { + "title": "", + "severity": "error", + "description": "This node cannot be reached because one-ways only lead away from here", + "IDs": ["this"], + "regex": "(this node)" + }, + "43": { + "title": "", + "severity": "error", + "description": "You cannot escape from this node because one-ways only lead to here", + "IDs": ["this"], + "regex": "(this node)" + }, + "50": { + "title": "almost-junctions", + "severity": "error", + "description": "This node is very close but not connected to way #$1", + "IDs": ["this", "w"], + "regex": "(this node) is very close but not connected to way #(\\d+)" + }, + "60": { + "title": "deprecated tags", + "severity": "warning", + "description": "This $1 uses deprecated tag $2. Please use $3 instead!", + "IDs": ["this", "", ""], + "regex": "(this (?:node|way|relation)) uses deprecated tag '(.+)'\\. Please use "(.+)"" + }, + "70": { + "title": "missing tags", + "severity": "error", + "description": "This $1 has an empty tag: $2", + "IDs": ["this", ""], + "regex": "(this (?:node|way|relation)) has an empty tag: "(.+)="" + }, + "71": { + "title": "", + "severity": "error", + "description": "This way has no tags", + "IDs": ["this"], + "regex": "(this way)" + }, + "72": { + "title": "", + "severity": "error", + "description": "This node is not member of any way and does not have any tags", + "IDs": ["this"], + "regex": "(this node)" + }, + "73": { + "title": "", + "severity": "error", + "description": "This way has a $1 tag but no highway tag", + "IDs": ["this", ""], + "regex": "(this way) has a (.+) tag" + }, + "74": { + "title": "missing tags", + "severity": "error", + "description": "This $1 has an empty tag: $2", + "IDs": ["this", ""], + "regex": "(this (?:node|way|relation)) has an empty tag: "(.+)="" + }, + "75": { + "description": "This (node|way|relation) has a name \\((.+)\\) but no other tag", + "IDs": ["this", ""], + "regex": "(this (?:node|way|relation)) has a name \\((.+)\\)" + }, + "90": { + "title": "motorways without ref", + "severity": "error", + "description": "This way is tagged as motorway and therefore needs a ref nat_ref or int_ref tag", + "IDs": ["this"], + "regex": "(this way)" + }, + "100": { + "title": "places of worship without religion", + "severity": "error", + "description": "This $1 is tagged as place of worship and therefore needs a religion tag", + "IDs": ["this"], + "regex": "(this (?:node|way|relation))" + }, + "110": { + "title": "point of interest without name", + "severity": "error", + "description": "This node is tagged as $1 and therefore needs a name tag", + "IDs": ["this", ""], + "regex": "(this (?:node|way|relation)) is tagged as (.+) and" + }, + "120": { + "title": "ways without nodes", + "severity": "error", + "description": "This way has just one single node", + "IDs": ["this"], + "regex": "(this way)" + }, + "130": { + "title": "floating islands", + "severity": "error", + "description": "This way is not connected to the rest of the map", + "IDs": ["this"], + "regex": "(this way)" + }, + "150": { + "title": "railway crossing without tag", + "severity": "error", + "description": "This crossing of a highway and a railway needs to be tagged as railway=crossing or railway=level_crossing", + "IDs": ["this"], + "regex": "(this crossing)" + }, + "160": { + "title": "wrongly used railway tag", + "severity": "error", + "description": "There are ways in different layers coming together in this railway crossing. There are ways tagged as tunnel or bridge coming together in this railway crossing", + "IDs": ["this"], + "regex": "(this railway crossing)" + }, + "170": { + "title": "FIXME tagged items", + "severity": "error", + "description": "This feature has a FIXME tag: (.*)", + "IDs": ["this"], + "regex": "(this feature) has a FIXME tag: (.*)" + }, + "180": { + "title": "relations without type", + "severity": "error", + "description": "This relation has no type tag which is mandatory for relations", + "IDs": ["this"], + "regex": "(this relation)" + }, + "190": { + "title": "intersections without junctions", + "severity": "error", + "description": "This $1 intersects the $2 #$3 but there is no junction node", + "IDs": ["this", "", "w"], + "regex": "(this .+) intersects the (.+) #(\\d+)" + }, + "200": { + "title": "overlapping ways", + "severity": "error", + "description": "This $1 overlaps the $2 #$3", + "IDs": ["this", "", "w"], + "regex": "(this .+) overlaps the (.+) #(\\d+)" + }, + "210": { + "title": "loopings", + "severity": "error", + "description": "These errors contain self intersecting ways" + }, + "211": { + "title": "", + "severity": "error", + "description": "This way contains more than one node at least twice. Nodes are $1.", + "IDs": ["this", "211"], + "regex": "(this way) contains more than one node at least twice. Nodes are ((?:#\\d+(?:, )?)+)\\." + }, + "212": { + "title": "", + "severity": "error", + "description": "This way has only two different nodes and contains one of them more than once", + "IDs": ["this"], + "regex": "(this way)" + }, + "220": { + "title": "misspelled tags", + "severity": "error", + "description": "This $1 is tagged '$2' where $3 looks like $4", + "regex": "(this (?:node|way|relation)) is tagged '(.+)' where "(.+)" looks like "(.+)"" + }, + "221": { + "title": "", + "severity": "error", + "description": "The key of this $1's tag is 'key': $2", + "regex": "(this (?:node|way|relation))\\'s tag is \\'key\\': (.+)" + }, + "230": { + "title": "layer conflicts", + "severity": "error", + "description": "This node is a junction of ways on different layers.", + "IDs": ["this"], + "regex": "(this node)" + }, + "231": { + "title": "mixed layers intersection", + "severity": "error", + "description": "This node is a junction of ways on different layers: $1", + "IDs": ["this", "231"], + "regex": "(this node) is a junction of ways on different layers: (.+)" + }, + "232": { + "title": "strange layers", + "severity": "error", + "description": "This $1 is tagged with layer $2. This need not be an error, but it looks strange", + "IDs": ["this", ""], + "regex": "(this (?:bridge|tunnel)) is tagged with layer (-?\\d+)\\." + }, + "270": { + "title": "motorways connected directly", + "severity": "error", + "description": "This node is a junction of a motorway and a highway other than motorway, motorway_link, trunk, rest_area or construction. Service or unclassified is only valid if it has access=no/private or it leads to a motorway service area or if it is a service=parking_aisle.", + "IDs": ["this"], + "regex": "(this node)" + }, + "280": { + "title": "boundaries", + "severity": "error", + "description": "Administrative Boundaries can be expressed either by tagging ways or by adding them to a relation. They should be closed-loop sequences of ways, they must not self-intersect or split and they must have a name and an admin_level." + }, + "281": { + "title": "missing name", + "severity": "error", + "description": "This boundary has no name", + "IDs": ["this"], + "regex": "(this boundary)" + }, + "282": { + "title": "missing admin level", + "severity": "error", + "description": "The boundary of $1 has no (?:valid numeric)?admin_level", + "regex": "of (.+) has" + }, + "283": { + "title": "no closed loop", + "severity": "error", + "description": "The boundary of $1 is not closed-loop", + "regex": "boundary of (.+) is" + }, + "284": { + "title": "splitting boundary", + "severity": "error", + "description": "The boundary of $1 splits here", + "regex": "boundary of (.+) splits" + }, + "285": { + "title": "admin_level too high", + "severity": "error", + "description": "This boundary-way has admin_level $1 but belongs to a relation with lower admin_level (higher priority); it should have the lowest admin_level of all relations", + "IDs": ["this", ""], + "regex": "(this boundary)-way has admin_level (-?\\d+) but" + }, + "290": { + "title": "restrictions", + "severity": "error", + "description": "Analyses all relations tagged type=restriction or following variations type=restriction:hgv type=restriction:caravan type=restriction:motorcar type=restriction:bus type=restriction:agricultural type=restriction:motorcycle type=restriction:bicycle and type=restriction:hazmat" + }, + "291": { + "title": "missing type", + "severity": "error", + "description": "This turn-restriction has no (?:known )?restriction type", + "IDs": ["this"], + "regex": "(this turn-restriction) has" + }, + "292": { + "title": "missing from way", + "severity": "error", + "description": "This turn-restriction needs exactly one from member. This one has $1", + "IDs": ["this", ""], + "regex": "(this turn-restriction) needs.+has (\\d+)" + }, + "293": { + "title": "missing to way", + "severity": "error", + "description": "This turn-restriction needs exactly one to member. This one has $1", + "IDs": ["this", ""], + "regex": "(this turn-restriction) needs.+has (\\d+)" + }, + "294": { + "title": "from or to not a way", + "severity": "error", + "description": "From- and To-members of this turn-restriction need to be ways. $1", + "IDs": ["this", "294"], + "regex": "(this turn-restriction)~.+ways\\. ((?:(?:from|to) (?:node|relation) #\\d+,?)+)" + }, + "295": { + "title": "via is not on the way ends", + "severity": "error", + "description": "via (node #$1) is not the first or the last member of (from|to) (way #$3)", + "IDs": ["this", "n", "", "w"], + "regex": "(this turn-restriction)~via \\(node #(\\d+)\\).+ of (from|to) \\(way #(\\d+)\\)" + }, + "296": { + "title": "wrong restriction angle", + "severity": "error", + "description": "This turn-restriction type is $1, but angle is $2 degrees. Maybe the restriction type is not appropriate?", + "IDs": ["this", "", ""], + "regex": "(this turn-restriction)~.+is (\\w+), but angle is (-?\\d+)" + }, + "297": { + "title": "wrong direction of to member", + "severity": "error", + "description": "wrong direction of to way $1", + "IDs": ["this", "w"], + "regex": "(this turn-restriction)~.+to way (\\d+)" + }, + "298": { + "title": "already restricted by oneway", + "severity": "error", + "description": "entry already prohibited by oneway tag on $1", + "IDs": ["this", "w"], + "regex": "(this turn-restriction)~.+tag on (\\d+)" + }, + "300": { + "title": "missing maxspeed", + "severity": "warning", + "description": "This highway is missing a maxspeed tag", + "IDs": ["this"], + "regex": "(this highway)" + }, + "310": { + "title": "roundabouts", + "severity": "error", + "description": "Analyses ways with tag junction=roundabout. More then one way can form a roundabout. It supports tag oneway=-1" + }, + "311": { + "title": "not closed loop", + "severity": "error", + "description": "This way is part of a roundabout but is not closed-loop. (split carriageways approaching a roundabout should not be tagged as roundabout)", + "IDs": ["this"], + "regex": "(this way)" + }, + "312": { + "title": "wrong direction", + "severity": "error", + "description": "If this ((?:mini_)?roundabout) is in a country with (left|right)-hand traffic then its orientation goes the wrong way around", + "IDs": ["this", ""], + "regex": "(this (?:mini_)?roundabout) is in a country with ((?:left|right)-hand)" + }, + "313": { + "title": "faintly connected", + "severity": "error", + "description": "This roundabout has only $1 other roads connected. Roundabouts typically have three", + "IDs": ["this", ""], + "regex": "(this roundabout) has only (\\d) other" + }, + "320": { + "title": "*_link connections", + "severity": "error", + "description": "This way is tagged as highway=$1_link but doesn't have a connection to any other $1 or $1_link", + "IDs": ["this", "", "", "", ""], + "regex": "(this way) is tagged as (highway=.+) but doesn't have a connection to any other (.+) or (.+)" + }, + "350": { + "title": "bridge-tags", + "severity": "error", + "description": "This bridge does not have a tag in common with its surrounding ways that shows the purpose of this bridge. There should be one of these tags: (.+)", + "NOTE": "Group can be arbitrary list of form: key=value,key=value,key=value...", + "IDs": ["this", ""], + "regex": "(this bridge).*tags: (.+)" + }, + "360": { + "title": "language unknown", + "severity": "warning", + "description": "It would be nice if this (node|way|relation) had an additional tag 'name:XX=(.+)' where XX shows the language of its name '\\2'", + "IDs": ["this", ""], + "regex": "(this (?:node|way|relation)) had an additional tag 'name:XX=(.+)' where" + }, + "370": { + "title": "doubled places", + "severity": "error", + "description": "This node has tags in common with the surrounding way #$1 ((?:\\(including the name '.+'\\) )?)and seems to be redundand", + "IDs": ["this", "w", "370"], + "regex": "(this node) has tags in common with the surrounding way #(\\d+) ((?:\\(including the name '.+'\\) )?)and" + }, + "380": { + "title": "non-physical use of sport-tag", + "severity": "error", + "description": "This way is tagged sport=$1 but has no physical tag like e.g. leisure, building, amenity or highway", + "IDs": ["this", ""], + "regex": "(this way) is tagged (sport=.+) but" + }, + "390": { + "title": "missing tracktype", + "severity": "warning", + "description": "This track doesn''t have a tracktype", + "IDs": ["this"], + "regex": "(this track)" + }, + "400": { + "title": "geometry glitches", + "severity": "error", + "description": "" + }, + "401": { + "title": "missing turn restriction", + "severity": "error", + "description": "ways $1 and $2 join in a very sharp angle here and there is no oneway tag or turn restriction that prevents turning( from way (\\1|\\2) to (\\1|\\2))?", + "IDs": ["w", "w"], + "regex": "ways (\\d+) and (\\d+) join" + }, + "402": { + "title": "impossible angles", + "severity": "error", + "description": "this way bends in a very sharp angle here", + "IDs": ["this"], + "regex": "(this way)" + }, + "410": { + "title": "website", + "severity": "error", + "description": "Web pages are analyzed. Web page is defined by any of the following tags website=* url=* website:mobile=* contact:website=* contact:url=* image=* source:website=* or source:url=*" + }, + "411": { + "title": "http error", + "severity": "error", + "description": "The URL ($1) cannot be opened (HTTP status code $2)", + "IDs": ["this", "url", ""], + "regex": "(this feature)~.+ href=([^>]+)>.+ code (\\d+)\\)" + }, + "412": { + "title": "domain hijacking", + "severity": "error", + "description": "Possible domain squatting: $1. Suspicious text is: \"$2\"", + "IDs": ["this", "url", ""], + "regex": "(this feature)~.+ href=([^>]+)>.+ is: "(.+)"" + }, + "413": { + "title": "non-match", + "severity": "error", + "description": "Content of the URL ($1) did not contain these keywords: ($2)", + "IDs": ["this", "url", ""], + "regex": "(this feature)~.+ href=([^>]+)>.+ keywords: \\((.+)\\)" + } + } +} diff --git a/data/presets.yaml b/data/presets.yaml index f6b2138d0..ef4ce6615 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -2553,9 +2553,9 @@ en: terms: '' amenity/pharmacy: # amenity=pharmacy - name: Pharmacy - # 'terms: apothecary,drug*,med*,prescription' - terms: '' + name: Pharmacy Counter + # 'terms: apothecary,drug store,drugstore,med*,prescription' + terms: '' amenity/place_of_worship: # amenity=place_of_worship name: Place of Worship @@ -2568,9 +2568,9 @@ en: terms: '' amenity/place_of_worship/christian: # 'amenity=place_of_worship, religion=christian' - name: Church + name: Christian Church # 'terms: christian,abbey,basilica,bethel,cathedral,chancel,chantry,chapel,fold,house of God,house of prayer,house of worship,minster,mission,oratory,parish,sacellum,sanctuary,shrine,tabernacle,temple' - terms: '' + terms: '' amenity/place_of_worship/hindu: # 'amenity=place_of_worship, religion=hindu' name: Hindu Temple @@ -2578,14 +2578,14 @@ en: terms: '' amenity/place_of_worship/jewish: # 'amenity=place_of_worship, religion=jewish' - name: Synagogue + name: Jewish Synagogue # 'terms: jewish' - terms: '' + terms: '' amenity/place_of_worship/muslim: # 'amenity=place_of_worship, religion=muslim' - name: Mosque + name: Muslim Mosque # 'terms: muslim' - terms: '' + terms: '' amenity/place_of_worship/shinto: # 'amenity=place_of_worship, religion=shinto' name: Shinto Shrine @@ -4674,11 +4674,6 @@ en: # leisure=resort name: Resort terms: '' - leisure/running_track: - # 'leisure=track, sport=running' - name: Racetrack (Running) - # 'terms: race*,running,sprint,track' - terms: '' leisure/sauna: # leisure=sauna name: Sauna @@ -4711,6 +4706,16 @@ en: name: Racetrack (Non-Motorsport) # 'terms: cycle,dog,greyhound,horse,race*,track' terms: '' + leisure/track/horse_racing: + # 'leisure=track, sport=horse_racing' + name: Racetrack (Horse Racing) + # 'terms: race*,horse,track' + terms: '' + leisure/track/running: + # 'leisure=track, sport=running' + name: Racetrack (Running) + # 'terms: race*,running,sprint,track' + terms: '' leisure/water_park: # leisure=water_park name: Water Park @@ -5249,6 +5254,11 @@ en: name: Downhill Piste/Ski Run # 'terms: ski,alpine,snowboard,downhill,piste' terms: '' + piste/downhill/halfpipe: + # 'piste:type=downhill, man_made=piste:halfpipe' + name: Halfpipe + # 'terms: ski,alpine,halfpipe,half pipe,snowboard,downhill,piste' + terms: '' piste/hike: # 'piste:type=hike' name: Snowshoeing or Winter Hiking Trail @@ -5951,6 +5961,11 @@ en: name: Car Repair Shop # 'terms: auto,garage,service' terms: '' + shop/caravan: + # shop=caravan + name: RV Dealership + # 'terms: auto' + terms: '' shop/carpet: # shop=carpet name: Carpet Store @@ -5972,7 +5987,7 @@ en: shop/chemist: # shop=chemist name: Drugstore - # 'terms: apothecary,med*,drug*,gift' + # 'terms: apothecary,beauty,drug store,drugstore,gift,hair,med*,pharmacy,prescription,tooth' terms: '' shop/chocolate: # shop=chocolate @@ -6072,6 +6087,15 @@ en: # shop=fashion name: Fashion Store terms: '' + shop/fireplace: + # shop=fireplace + name: Fireplace Store + # 'terms: fireplace,stove,masonry heater' + terms: '' + shop/fishing: + # shop=fishing + name: Fishing Shop + terms: '' shop/fishmonger: # shop=fishmonger name: Fishmonger @@ -6085,6 +6109,10 @@ en: name: Framing Shop # 'terms: art*,paint*,photo*,frame' terms: '' + shop/fuel: + # shop=fuel + name: Fuel Shop + terms: '' shop/funeral_directors: # shop=funeral_directors name: Funeral Home @@ -6155,6 +6183,10 @@ en: name: Houseware Store # 'terms: home,household' terms: '' + shop/hunting: + # shop=hunting + name: Hunting Shop + terms: '' shop/interior_decoration: # shop=interior_decoration name: Interior Decoration Store @@ -6408,6 +6440,10 @@ en: # shop=watches name: Watches Shop terms: '' + shop/water: + # shop=water + name: Drinking Water Shop + terms: '' shop/water_sports: # shop=water_sports name: Watersport/Swim Shop diff --git a/data/presets/presets.json b/data/presets/presets.json index a46eff7b5..67fa6891b 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -117,16 +117,16 @@ "amenity/parking/multi-storey": {"icon": "maki-car", "fields": ["name", "operator", "building", "levels", "height", "address", "capacity", "fee", "access_simple"], "geometry": ["area"], "tags": {"amenity": "parking", "parking": "multi-storey"}, "addTags": {"building": "parking", "amenity": "parking", "parking": "multi-storey"}, "removeTags": {"building": "parking", "amenity": "parking", "parking": "multi-storey"}, "reference": {"key": "parking", "value": "multi-storey"}, "terms": ["car", "indoor parking", "multistorey car park", "parkade", "parking building", "parking deck", "parking garage", "parking ramp", "parking structure"], "name": "Multilevel Parking Garage"}, "amenity/payment_centre": {"icon": "maki-bank", "fields": ["name", "brand", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["check", "tax pay", "bill pay", "currency", "finance", "cash", "money"], "tags": {"amenity": "payment_centre"}, "name": "Payment Center"}, "amenity/payment_terminal": {"icon": "maki-bank", "fields": ["name", "brand", "address", "opening_hours", "payment_multi"], "geometry": ["point"], "terms": ["interactive kiosk", "ekiosk", "atm", "bill pay", "tax pay", "phone pay", "finance", "cash", "money transfer", "card"], "tags": {"amenity": "payment_terminal"}, "name": "Payment Terminal"}, - "amenity/pharmacy": {"icon": "maki-pharmacy", "fields": ["name", "operator", "address", "building_area", "drive_through", "opening_hours", "payment_multi", "dispensing"], "geometry": ["point", "area"], "tags": {"amenity": "pharmacy"}, "addTags": {"amenity": "pharmacy", "healthcare": "pharmacy"}, "removeTags": {"amenity": "pharmacy", "healthcare": "pharmacy"}, "reference": {"key": "amenity", "value": "pharmacy"}, "terms": ["apothecary", "drug*", "med*", "prescription"], "name": "Pharmacy"}, + "amenity/pharmacy": {"icon": "maki-pharmacy", "fields": ["name", "operator", "address", "building_area", "drive_through", "opening_hours", "payment_multi", "dispensing"], "geometry": ["point", "area"], "tags": {"amenity": "pharmacy"}, "addTags": {"amenity": "pharmacy", "healthcare": "pharmacy"}, "removeTags": {"amenity": "pharmacy", "healthcare": "pharmacy"}, "reference": {"key": "amenity", "value": "pharmacy"}, "terms": ["apothecary", "drug store", "drugstore", "med*", "prescription"], "name": "Pharmacy Counter"}, "amenity/place_of_worship": {"icon": "maki-place-of-worship", "fields": ["name", "religion", "denomination", "address", "building_area", "service_times"], "geometry": ["point", "area"], "terms": ["abbey", "basilica", "bethel", "cathedral", "chancel", "chantry", "chapel", "church", "fold", "house of God", "house of prayer", "house of worship", "minster", "mission", "mosque", "oratory", "parish", "sacellum", "sanctuary", "shrine", "synagogue", "tabernacle", "temple"], "tags": {"amenity": "place_of_worship"}, "name": "Place of Worship"}, - "amenity/place_of_worship/buddhist": {"icon": "maki-religious-buddhist", "fields": ["name", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["stupa", "vihara", "monastery", "temple", "pagoda", "zendo", "dojo"], "tags": {"amenity": "place_of_worship", "religion": "buddhist"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Buddhist Temple"}, - "amenity/place_of_worship/christian": {"icon": "maki-religious-christian", "fields": ["name", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["christian", "abbey", "basilica", "bethel", "cathedral", "chancel", "chantry", "chapel", "fold", "house of God", "house of prayer", "house of worship", "minster", "mission", "oratory", "parish", "sacellum", "sanctuary", "shrine", "tabernacle", "temple"], "tags": {"amenity": "place_of_worship", "religion": "christian"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Church"}, - "amenity/place_of_worship/hindu": {"icon": "temaki-hinduism", "fields": ["name", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["kovil", "devasthana", "mandir", "kshetram", "alayam", "shrine", "temple"], "tags": {"amenity": "place_of_worship", "religion": "hindu"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Hindu Temple"}, - "amenity/place_of_worship/jewish": {"icon": "maki-religious-jewish", "fields": ["name", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["jewish"], "tags": {"amenity": "place_of_worship", "religion": "jewish"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Synagogue"}, - "amenity/place_of_worship/muslim": {"icon": "maki-religious-muslim", "fields": ["name", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["muslim"], "tags": {"amenity": "place_of_worship", "religion": "muslim"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Mosque"}, - "amenity/place_of_worship/shinto": {"icon": "temaki-shinto", "fields": ["name", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["kami", "torii"], "tags": {"amenity": "place_of_worship", "religion": "shinto"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Shinto Shrine"}, - "amenity/place_of_worship/sikh": {"icon": "temaki-sikhism", "fields": ["name", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["gurudwara", "temple"], "tags": {"amenity": "place_of_worship", "religion": "sikh"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Sikh Temple"}, - "amenity/place_of_worship/taoist": {"icon": "temaki-taoism", "fields": ["name", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["daoist", "monastery", "temple"], "tags": {"amenity": "place_of_worship", "religion": "taoist"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Taoist Temple"}, + "amenity/place_of_worship/buddhist": {"icon": "maki-religious-buddhist", "fields": ["name", "religion", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["stupa", "vihara", "monastery", "temple", "pagoda", "zendo", "dojo"], "tags": {"amenity": "place_of_worship", "religion": "buddhist"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Buddhist Temple"}, + "amenity/place_of_worship/christian": {"icon": "maki-religious-christian", "fields": ["name", "religion", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["christian", "abbey", "basilica", "bethel", "cathedral", "chancel", "chantry", "chapel", "fold", "house of God", "house of prayer", "house of worship", "minster", "mission", "oratory", "parish", "sacellum", "sanctuary", "shrine", "tabernacle", "temple"], "tags": {"amenity": "place_of_worship", "religion": "christian"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Christian Church"}, + "amenity/place_of_worship/hindu": {"icon": "temaki-hinduism", "fields": ["name", "religion", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["kovil", "devasthana", "mandir", "kshetram", "alayam", "shrine", "temple"], "tags": {"amenity": "place_of_worship", "religion": "hindu"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Hindu Temple"}, + "amenity/place_of_worship/jewish": {"icon": "maki-religious-jewish", "fields": ["name", "religion", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["jewish"], "tags": {"amenity": "place_of_worship", "religion": "jewish"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Jewish Synagogue"}, + "amenity/place_of_worship/muslim": {"icon": "maki-religious-muslim", "fields": ["name", "religion", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["muslim"], "tags": {"amenity": "place_of_worship", "religion": "muslim"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Muslim Mosque"}, + "amenity/place_of_worship/shinto": {"icon": "temaki-shinto", "fields": ["name", "religion", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["kami", "torii"], "tags": {"amenity": "place_of_worship", "religion": "shinto"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Shinto Shrine"}, + "amenity/place_of_worship/sikh": {"icon": "temaki-sikhism", "fields": ["name", "religion", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["gurudwara", "temple"], "tags": {"amenity": "place_of_worship", "religion": "sikh"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Sikh Temple"}, + "amenity/place_of_worship/taoist": {"icon": "temaki-taoism", "fields": ["name", "religion", "denomination", "building_area", "address", "service_times"], "geometry": ["point", "area"], "terms": ["daoist", "monastery", "temple"], "tags": {"amenity": "place_of_worship", "religion": "taoist"}, "reference": {"key": "amenity", "value": "place_of_worship"}, "name": "Taoist Temple"}, "amenity/planetarium": {"icon": "maki-museum", "fields": ["name", "operator", "address", "building_area", "opening_hours"], "geometry": ["point", "area"], "terms": ["museum", "astronomy", "observatory"], "tags": {"amenity": "planetarium"}, "name": "Planetarium"}, "amenity/police": {"icon": "maki-police", "fields": ["name", "operator", "address", "building_area", "opening_hours"], "geometry": ["point", "area"], "terms": ["badge", "constable", "constabulary", "cop", "detective", "fed", "law", "enforcement", "officer", "patrol"], "tags": {"amenity": "police"}, "name": "Police"}, "amenity/post_box": {"icon": "maki-post", "fields": ["operator", "collection_times", "drive_through", "ref"], "geometry": ["point", "vertex"], "tags": {"amenity": "post_box"}, "terms": ["letter", "post"], "name": "Mailbox"}, @@ -191,7 +191,7 @@ "amenity/vending_machine/excrement_bags": {"icon": "temaki-vending_machine", "fields": ["vending", "operator", "fee", "payment_multi", "currency_multi"], "geometry": ["point"], "terms": ["excrement bags", "poop", "dog", "animal"], "tags": {"amenity": "vending_machine", "vending": "excrement_bags"}, "reference": {"key": "vending", "value": "excrement_bags"}, "name": "Excrement Bag Vending Machine"}, "amenity/vending_machine/feminine_hygiene": {"icon": "temaki-vending_machine", "fields": ["vending", "operator", "payment_multi", "currency_multi"], "geometry": ["point"], "terms": ["condom", "tampon", "pad", "woman", "women", "menstrual hygiene products", "personal care"], "tags": {"amenity": "vending_machine", "vending": "feminine_hygiene"}, "reference": {"key": "vending", "value": "feminine_hygiene"}, "name": "Feminine Hygiene Vending Machine"}, "amenity/vending_machine/food": {"icon": "temaki-vending_machine", "fields": ["vending", "operator", "payment_multi", "currency_multi"], "geometry": ["point"], "terms": ["food"], "tags": {"amenity": "vending_machine", "vending": "food"}, "reference": {"key": "vending", "value": "food"}, "name": "Food Vending Machine"}, - "amenity/vending_machine/fuel": {"icon": "maki-fuel", "fields": ["vending", "operator", "payment_multi", "currency_multi"], "geometry": ["point"], "terms": ["petrol", "fuel", "gasoline", "propane", "diesel", "lng", "cng", "biodiesel"], "tags": {"amenity": "vending_machine", "vending": "fuel"}, "reference": {"key": "vending", "value": "fuel"}, "name": "Gas Pump"}, + "amenity/vending_machine/fuel": {"icon": "maki-fuel", "fields": ["vending", "operator", "payment_multi", "currency_multi"], "geometry": ["point"], "terms": ["petrol", "fuel", "gasoline", "propane", "diesel", "lng", "cng", "biodiesel"], "tags": {"amenity": "vending_machine", "vending": "fuel"}, "reference": {"key": "vending", "value": "fuel"}, "name": "Gas Pump", "matchScore": 0.5}, "amenity/vending_machine/ice_cream": {"icon": "temaki-vending_machine", "fields": ["vending", "operator", "payment_multi", "currency_multi"], "geometry": ["point"], "terms": ["chocolate", "ice cream", "frozen", "popsicle", "vanilla"], "tags": {"amenity": "vending_machine", "vending": "ice_cream"}, "reference": {"key": "vending", "value": "ice_cream"}, "name": "Ice Cream Vending Machine"}, "amenity/vending_machine/newspapers": {"icon": "temaki-vending_machine", "fields": ["vending", "operator", "fee", "payment_multi", "currency_multi"], "geometry": ["point"], "terms": ["newspaper"], "tags": {"amenity": "vending_machine", "vending": "newspapers"}, "reference": {"key": "vending", "value": "newspapers"}, "name": "Newspaper Vending Machine"}, "amenity/vending_machine/parcel_pickup_dropoff": {"icon": "temaki-vending_machine", "fields": ["vending", "operator", "payment_multi", "currency_multi"], "geometry": ["point"], "terms": ["mail", "parcel", "pickup"], "tags": {"amenity": "vending_machine", "vending": "parcel_pickup;parcel_mail_in"}, "reference": {"key": "vending", "value": "parcel_pickup;parcel_mail_in"}, "name": "Parcel Pickup/Dropoff Locker"}, @@ -549,7 +549,7 @@ "leisure/golf_course": {"icon": "maki-golf", "fields": ["name", "operator", "address", "opening_hours"], "geometry": ["point", "area"], "terms": ["links"], "tags": {"leisure": "golf_course"}, "name": "Golf Course"}, "leisure/hackerspace": {"icon": "maki-commercial", "fields": ["name", "address", "building_area", "opening_hours", "website"], "geometry": ["point", "area"], "terms": ["makerspace", "hackspace", "hacklab"], "tags": {"leisure": "hackerspace"}, "name": "Hackerspace"}, "leisure/horse_riding": {"icon": "maki-horse-riding", "fields": ["name", "access_simple", "operator", "address", "building"], "geometry": ["point", "area"], "terms": ["equestrian", "stable"], "tags": {"leisure": "horse_riding"}, "name": "Horseback Riding Facility"}, - "leisure/ice_rink": {"icon": "maki-pitch", "fields": ["name", "seasonal", "sport_ice", "operator", "address", "building", "opening_hours"], "geometry": ["point", "area"], "terms": ["hockey", "skating", "curling"], "tags": {"leisure": "ice_rink"}, "name": "Ice Rink"}, + "leisure/ice_rink": {"icon": "fas-skating", "fields": ["name", "seasonal", "sport_ice", "operator", "address", "building", "opening_hours"], "geometry": ["point", "area"], "terms": ["hockey", "skating", "curling"], "tags": {"leisure": "ice_rink"}, "name": "Ice Rink"}, "leisure/marina": {"icon": "maki-harbor", "fields": ["name", "operator", "address", "capacity", "fee", "sanitary_dump_station", "power_supply", "internet_access", "internet_access/fee"], "moreFields": ["internet_access/ssid"], "geometry": ["point", "vertex", "area"], "terms": ["boat"], "tags": {"leisure": "marina"}, "name": "Marina"}, "leisure/miniature_golf": {"icon": "maki-golf", "fields": ["name", "operator", "address", "opening_hours"], "geometry": ["point", "area"], "terms": ["crazy golf", "mini golf", "putt-putt"], "tags": {"leisure": "miniature_golf"}, "name": "Miniature Golf"}, "leisure/nature_reserve": {"icon": "maki-park", "geometry": ["point", "area"], "fields": ["name"], "tags": {"leisure": "nature_reserve"}, "terms": ["protected", "wildlife"], "name": "Nature Reserve"}, @@ -578,7 +578,6 @@ "leisure/pitch/volleyball": {"icon": "maki-volleyball", "fields": ["name", "surface", "lit"], "geometry": ["point", "area"], "tags": {"leisure": "pitch", "sport": "volleyball"}, "reference": {"key": "sport", "value": "volleyball"}, "terms": [], "name": "Volleyball Court"}, "leisure/playground": {"icon": "maki-playground", "fields": ["name", "operator", "surface", "playground/max_age", "playground/min_age", "access_simple"], "geometry": ["point", "area"], "terms": ["jungle gym", "play area"], "tags": {"leisure": "playground"}, "name": "Playground"}, "leisure/resort": {"icon": "maki-lodging", "fields": ["name", "operator", "address", "opening_hours"], "geometry": ["point", "area"], "tags": {"leisure": "resort"}, "name": "Resort"}, - "leisure/running_track": {"icon": "maki-pitch", "fields": ["surface", "sport_racing_nonmotor", "lit", "width", "lanes"], "geometry": ["point", "line", "area"], "tags": {"leisure": "track", "sport": "running"}, "terms": ["race*", "running", "sprint", "track"], "name": "Racetrack (Running)"}, "leisure/sauna": {"icon": "fas-thermometer-three-quarters", "fields": ["name", "operator", "address", "opening_hours", "access_simple", "fee"], "geometry": ["point", "area"], "tags": {"leisure": "sauna"}, "name": "Sauna"}, "leisure/slipway": {"icon": "maki-slipway", "fields": ["name", "access_simple", "fee"], "geometry": ["point", "vertex", "line"], "terms": ["boat launch", "boat ramp", "boat landing"], "tags": {"leisure": "slipway"}, "name": "Slipway"}, "leisure/sports_centre": {"icon": "maki-pitch", "fields": ["name", "sport", "building", "address", "opening_hours"], "geometry": ["point", "area"], "tags": {"leisure": "sports_centre"}, "terms": [], "name": "Sports Center / Complex"}, @@ -586,6 +585,8 @@ "leisure/stadium": {"icon": "maki-pitch", "fields": ["name", "sport", "address"], "geometry": ["point", "area"], "tags": {"leisure": "stadium"}, "name": "Stadium"}, "leisure/swimming_pool": {"icon": "maki-swimming", "fields": ["name", "access_simple", "operator", "address", "lit", "location_pool", "length", "swimming_pool"], "geometry": ["point", "area"], "terms": ["dive", "water", "aquatics"], "tags": {"leisure": "swimming_pool"}, "name": "Swimming Pool"}, "leisure/track": {"icon": "iD-highway-road", "fields": ["surface", "sport_racing_nonmotor", "lit", "width", "lanes"], "geometry": ["point", "line", "area"], "tags": {"leisure": "track"}, "terms": ["cycle", "dog", "greyhound", "horse", "race*", "track"], "name": "Racetrack (Non-Motorsport)"}, + "leisure/track/horse_racing": {"icon": "maki-horse-riding", "fields": ["surface", "sport_racing_nonmotor", "lit", "width", "lanes"], "geometry": ["point", "line", "area"], "tags": {"leisure": "track", "sport": "horse_racing"}, "terms": ["race*", "horse", "track"], "name": "Racetrack (Horse Racing)"}, + "leisure/track/running": {"icon": "maki-pitch", "fields": ["surface", "sport_racing_nonmotor", "lit", "width", "lanes"], "geometry": ["point", "line", "area"], "tags": {"leisure": "track", "sport": "running"}, "terms": ["race*", "running", "sprint", "track"], "name": "Racetrack (Running)"}, "leisure/water_park": {"icon": "maki-swimming", "fields": ["name", "operator", "address"], "geometry": ["point", "area"], "terms": ["swim", "pool", "dive"], "tags": {"leisure": "water_park"}, "name": "Water Park"}, "line": {"fields": ["name"], "geometry": ["line"], "tags": {}, "name": "Line", "matchScore": 0.1}, "man_made": {"icon": "temaki-storage_tank", "fields": ["name", "man_made"], "geometry": ["point", "vertex", "line", "area"], "tags": {"man_made": "*"}, "name": "Man Made"}, @@ -703,14 +704,15 @@ "office/telecommunication": {"icon": "maki-telephone", "fields": ["name", "address", "building_area", "opening_hours"], "geometry": ["point", "area"], "tags": {"office": "telecommunication"}, "terms": ["communication", "internet", "phone", "voice"], "name": "Telecom Office"}, "office/therapist": {"icon": "maki-suitcase", "fields": ["name", "address", "building_area", "opening_hours"], "geometry": ["point", "area"], "tags": {"office": "therapist"}, "terms": ["therapy"], "name": "Therapist Office"}, "office/water_utility": {"icon": "maki-suitcase", "fields": ["name", "address", "building_area", "opening_hours", "operator"], "geometry": ["point", "area"], "tags": {"office": "water_utility"}, "terms": ["water board", "utility"], "name": "Water Utility Office"}, - "piste/downhill": {"icon": "maki-skiing", "fields": ["name", "piste/type", "piste/difficulty_downhill", "piste/grooming_downhill", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "alpine", "snowboard", "downhill", "piste"], "tags": {"piste:type": "downhill"}, "name": "Downhill Piste/Ski Run"}, - "piste/hike": {"icon": "fas-snowflake", "fields": ["name", "piste/type", "piste/difficulty", "piste/grooming_hike", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["hike", "winter hiking", "snowshoe", "snowshoeing", "piste", "ski"], "tags": {"piste:type": "hike"}, "name": "Snowshoeing or Winter Hiking Trail"}, - "piste/ice_skate": {"icon": "fas-snowflake", "fields": ["name", "piste/type", "sport_ice", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ice", "skating", "ski", "piste"], "tags": {"piste:type": "ice_skate"}, "name": "Ice Skating Piste"}, - "piste/nordic": {"icon": "maki-skiing", "fields": ["name", "piste/type", "piste/difficulty_nordic", "piste/grooming_nordic", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "nordic", "crosscountry", "skating", "piste"], "tags": {"piste:type": "nordic"}, "name": "Nordic or Crosscountry Piste/Ski Trail"}, - "piste/piste": {"icon": "maki-skiing", "fields": ["name", "piste/type", "piste/difficulty", "piste/grooming", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "nordic", "crosscountry", "downhill", "alpine", "snowboard", "skitour", "ski touring", "sled", "luge", "sleigh", "sledge", "ski-joring", "husky", "horse", "winter hiking", "snowshoe", "snowshoeing", "ice", "skating"], "tags": {"piste:type": "*"}, "name": "Winter Sport Trails"}, - "piste/skitour": {"icon": "maki-skiing", "fields": ["name", "piste/type", "piste/difficulty_skitour", "piste/grooming", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "skitour", "crosscountry", "ski touring", "piste"], "tags": {"piste:type": "skitour"}, "name": "Ski Touring Trail"}, + "piste/downhill": {"icon": "fas-skiing", "fields": ["name", "piste/type", "piste/difficulty_downhill", "piste/grooming_downhill", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "alpine", "snowboard", "downhill", "piste"], "tags": {"piste:type": "downhill"}, "name": "Downhill Piste/Ski Run"}, + "piste/downhill/halfpipe": {"icon": "fas-snowboarding", "fields": ["name", "piste/type", "piste/difficulty_downhill", "piste/grooming_downhill", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "alpine", "halfpipe", "half pipe", "snowboard", "downhill", "piste"], "tags": {"piste:type": "downhill", "man_made": "piste:halfpipe"}, "name": "Halfpipe"}, + "piste/hike": {"icon": "fas-skating", "fields": ["name", "piste/type", "piste/difficulty", "piste/grooming_hike", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["hike", "winter hiking", "snowshoe", "snowshoeing", "piste", "ski"], "tags": {"piste:type": "hike"}, "name": "Snowshoeing or Winter Hiking Trail"}, + "piste/ice_skate": {"icon": "fas-skating", "fields": ["name", "piste/type", "sport_ice", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ice", "skating", "ski", "piste"], "tags": {"piste:type": "ice_skate"}, "name": "Ice Skating Piste"}, + "piste/nordic": {"icon": "fas-skiing-nordic", "fields": ["name", "piste/type", "piste/difficulty_nordic", "piste/grooming_nordic", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "nordic", "crosscountry", "skating", "piste"], "tags": {"piste:type": "nordic"}, "name": "Nordic or Crosscountry Piste/Ski Trail"}, + "piste/piste": {"icon": "fas-skiing", "fields": ["name", "piste/type", "piste/difficulty", "piste/grooming", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "nordic", "crosscountry", "downhill", "alpine", "snowboard", "skitour", "ski touring", "sled", "luge", "sleigh", "sledge", "ski-joring", "husky", "horse", "winter hiking", "snowshoe", "snowshoeing", "ice", "skating"], "tags": {"piste:type": "*"}, "name": "Winter Sport Trails"}, + "piste/skitour": {"icon": "fas-skiing-nordic", "fields": ["name", "piste/type", "piste/difficulty_skitour", "piste/grooming", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "skitour", "crosscountry", "ski touring", "piste"], "tags": {"piste:type": "skitour"}, "name": "Ski Touring Trail"}, "piste/sled": {"icon": "fas-snowflake", "fields": ["name", "piste/type", "piste/difficulty", "piste/grooming", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "sled", "luge", "sleigh", "sledge", "piste"], "tags": {"piste:type": "sled"}, "name": "Sled Piste"}, - "piste/sleigh": {"icon": "fas-snowflake", "fields": ["name", "piste/type", "piste/difficulty", "piste/grooming", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "piste", "sled", "luge", "sleigh", "sledge", "ski-joring", "husky", "horse"], "tags": {"piste:type": "sleigh"}, "name": "Sleigh Piste"}, + "piste/sleigh": {"icon": "fas-sleigh", "fields": ["name", "piste/type", "piste/difficulty", "piste/grooming", "oneway", "lit"], "geometry": ["line", "area"], "terms": ["ski", "piste", "sled", "luge", "sleigh", "sledge", "ski-joring", "husky", "horse"], "tags": {"piste:type": "sleigh"}, "name": "Sleigh Piste"}, "place/farm": {"icon": "maki-farm", "geometry": ["point", "area"], "fields": ["name"], "tags": {"place": "farm"}, "name": "Farm", "searchable": false}, "place/city_block": {"icon": "maki-triangle-stroked", "fields": ["name"], "geometry": ["point", "area"], "tags": {"place": "city_block"}, "name": "City Block"}, "place/city": {"icon": "maki-city", "fields": ["name", "population"], "geometry": ["point", "area"], "tags": {"place": "city"}, "name": "City"}, @@ -856,11 +858,12 @@ "shop/car_parts": {"icon": "maki-car", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["auto"], "tags": {"shop": "car_parts"}, "name": "Car Parts Store"}, "shop/car_repair": {"icon": "maki-car-repair", "fields": ["name", "operator", "address", "building_area", "service/vehicle", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["auto", "garage", "service"], "tags": {"shop": "car_repair"}, "name": "Car Repair Shop"}, "shop/car": {"icon": "maki-car", "fields": ["name", "brand", "operator", "address", "building_area", "second_hand", "service/vehicle", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["auto"], "tags": {"shop": "car"}, "name": "Car Dealership"}, + "shop/caravan": {"icon": "maki-car", "fields": ["name", "brand", "operator", "address", "building_area", "second_hand", "service/vehicle", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["auto"], "tags": {"shop": "caravan"}, "name": "RV Dealership"}, "shop/carpet": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["rug"], "tags": {"shop": "carpet"}, "name": "Carpet Store"}, "shop/catalogue": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "catalogue"}, "name": "Catalog Shop"}, "shop/charity": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "second_hand", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["thrift", "op shop", "nonprofit"], "tags": {"shop": "charity"}, "name": "Charity Store"}, "shop/cheese": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "cheese"}, "name": "Cheese Store"}, - "shop/chemist": {"icon": "maki-grocery", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "chemist"}, "terms": ["apothecary", "med*", "drug*", "gift"], "name": "Drugstore"}, + "shop/chemist": {"icon": "maki-grocery", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "chemist"}, "terms": ["apothecary", "beauty", "drug store", "drugstore", "gift", "hair", "med*", "pharmacy", "prescription", "tooth"], "name": "Drugstore"}, "shop/chocolate": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "chocolate"}, "name": "Chocolate Store"}, "shop/clothes": {"icon": "maki-clothing-store", "fields": ["name", "clothes", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "clothes"}, "name": "Clothing Store"}, "shop/coffee": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "coffee"}, "name": "Coffee Store"}, @@ -883,8 +886,11 @@ "shop/fabric": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["sew"], "tags": {"shop": "fabric"}, "name": "Fabric Store"}, "shop/farm": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["farm shop", "farm stand"], "tags": {"shop": "farm"}, "name": "Produce Stand"}, "shop/fashion": {"icon": "maki-shop", "fields": ["name", "clothes", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "fashion"}, "name": "Fashion Store"}, + "shop/fireplace": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["fireplace", "stove", "masonry heater"], "tags": {"shop": "fireplace"}, "name": "Fireplace Store"}, + "shop/fishing": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "fishing"}, "name": "Fishing Shop"}, "shop/florist": {"icon": "maki-florist", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["flower"], "tags": {"shop": "florist"}, "name": "Florist"}, "shop/frame": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "frame"}, "terms": ["art*", "paint*", "photo*", "frame"], "name": "Framing Shop"}, + "shop/fuel": {"icon": "maki-shop", "fields": ["name", "operator", "address", "fuel_multi", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "fuel"}, "name": "Fuel Shop", "matchScore": 0.5}, "shop/funeral_directors": {"icon": "maki-cemetery", "fields": ["name", "operator", "address", "building_area", "religion", "denomination"], "geometry": ["point", "area"], "terms": ["undertaker", "memorial home"], "tags": {"shop": "funeral_directors"}, "name": "Funeral Home"}, "shop/furniture": {"icon": "fas-couch", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["chair", "sofa", "table"], "tags": {"shop": "furniture"}, "name": "Furniture Store"}, "shop/garden_centre": {"icon": "maki-garden-centre", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["landscape", "mulch", "shrub", "tree"], "tags": {"shop": "garden_centre"}, "name": "Garden Center"}, @@ -899,6 +905,7 @@ "shop/herbalist": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "herbalist"}, "name": "Herbalist"}, "shop/hifi": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["stereo", "video"], "tags": {"shop": "hifi"}, "name": "Hifi Store"}, "shop/houseware": {"icon": "fas-blender", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["home", "household"], "tags": {"shop": "houseware"}, "name": "Houseware Store"}, + "shop/hunting": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "hunting"}, "name": "Hunting Shop"}, "shop/interior_decoration": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "interior_decoration"}, "name": "Interior Decoration Store"}, "shop/jewelry": {"icon": "maki-jewelry-store", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["diamond", "gem", "ring"], "tags": {"shop": "jewelry"}, "name": "Jeweler"}, "shop/kiosk": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi", "levels"], "geometry": ["point", "area"], "tags": {"shop": "kiosk"}, "name": "Kiosk"}, @@ -957,6 +964,7 @@ "shop/video": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["DVD"], "tags": {"shop": "video"}, "name": "Video Store"}, "shop/watches": {"icon": "maki-watch", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "watches"}, "name": "Watches Shop"}, "shop/water_sports": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "water_sports"}, "name": "Watersport/Swim Shop"}, + "shop/water": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "water"}, "name": "Drinking Water Shop"}, "shop/weapons": {"icon": "maki-shop", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["ammo", "gun", "knife", "knives"], "tags": {"shop": "weapons"}, "name": "Weapon Shop"}, "shop/wholesale": {"icon": "maki-warehouse", "fields": ["name", "operator", "wholesale", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "terms": ["warehouse club", "cash and carry"], "tags": {"shop": "wholesale"}, "name": "Wholesale Store"}, "shop/window_blind": {"icon": "temaki-window", "fields": ["name", "operator", "address", "building_area", "opening_hours", "payment_multi"], "geometry": ["point", "area"], "tags": {"shop": "window_blind"}, "name": "Window Blind Store"}, diff --git a/data/presets/presets/amenity/pharmacy.json b/data/presets/presets/amenity/pharmacy.json index 69f0a5d9e..495097150 100644 --- a/data/presets/presets/amenity/pharmacy.json +++ b/data/presets/presets/amenity/pharmacy.json @@ -31,9 +31,10 @@ }, "terms": [ "apothecary", - "drug*", + "drug store", + "drugstore", "med*", "prescription" ], - "name": "Pharmacy" + "name": "Pharmacy Counter" } diff --git a/data/presets/presets/amenity/place_of_worship/buddhist.json b/data/presets/presets/amenity/place_of_worship/buddhist.json index f2745d213..ace4cf645 100644 --- a/data/presets/presets/amenity/place_of_worship/buddhist.json +++ b/data/presets/presets/amenity/place_of_worship/buddhist.json @@ -2,6 +2,7 @@ "icon": "maki-religious-buddhist", "fields": [ "name", + "religion", "denomination", "building_area", "address", diff --git a/data/presets/presets/amenity/place_of_worship/christian.json b/data/presets/presets/amenity/place_of_worship/christian.json index 4824d4b1e..25f0fdb18 100644 --- a/data/presets/presets/amenity/place_of_worship/christian.json +++ b/data/presets/presets/amenity/place_of_worship/christian.json @@ -2,6 +2,7 @@ "icon": "maki-religious-christian", "fields": [ "name", + "religion", "denomination", "building_area", "address", @@ -42,5 +43,5 @@ "key": "amenity", "value": "place_of_worship" }, - "name": "Church" + "name": "Christian Church" } diff --git a/data/presets/presets/amenity/place_of_worship/hindu.json b/data/presets/presets/amenity/place_of_worship/hindu.json index d6dba5466..2a5b53567 100644 --- a/data/presets/presets/amenity/place_of_worship/hindu.json +++ b/data/presets/presets/amenity/place_of_worship/hindu.json @@ -2,6 +2,7 @@ "icon": "temaki-hinduism", "fields": [ "name", + "religion", "denomination", "building_area", "address", diff --git a/data/presets/presets/amenity/place_of_worship/jewish.json b/data/presets/presets/amenity/place_of_worship/jewish.json index 1c626cb0f..c0b406055 100644 --- a/data/presets/presets/amenity/place_of_worship/jewish.json +++ b/data/presets/presets/amenity/place_of_worship/jewish.json @@ -2,6 +2,7 @@ "icon": "maki-religious-jewish", "fields": [ "name", + "religion", "denomination", "building_area", "address", @@ -22,5 +23,5 @@ "key": "amenity", "value": "place_of_worship" }, - "name": "Synagogue" + "name": "Jewish Synagogue" } diff --git a/data/presets/presets/amenity/place_of_worship/muslim.json b/data/presets/presets/amenity/place_of_worship/muslim.json index c802138b7..bc7d80459 100644 --- a/data/presets/presets/amenity/place_of_worship/muslim.json +++ b/data/presets/presets/amenity/place_of_worship/muslim.json @@ -2,6 +2,7 @@ "icon": "maki-religious-muslim", "fields": [ "name", + "religion", "denomination", "building_area", "address", @@ -22,5 +23,5 @@ "key": "amenity", "value": "place_of_worship" }, - "name": "Mosque" + "name": "Muslim Mosque" } diff --git a/data/presets/presets/amenity/place_of_worship/shinto.json b/data/presets/presets/amenity/place_of_worship/shinto.json index 7a940b181..1bfe24aeb 100644 --- a/data/presets/presets/amenity/place_of_worship/shinto.json +++ b/data/presets/presets/amenity/place_of_worship/shinto.json @@ -2,6 +2,7 @@ "icon": "temaki-shinto", "fields": [ "name", + "religion", "denomination", "building_area", "address", diff --git a/data/presets/presets/amenity/place_of_worship/sikh.json b/data/presets/presets/amenity/place_of_worship/sikh.json index fb2aba2fc..83196bc56 100644 --- a/data/presets/presets/amenity/place_of_worship/sikh.json +++ b/data/presets/presets/amenity/place_of_worship/sikh.json @@ -2,6 +2,7 @@ "icon": "temaki-sikhism", "fields": [ "name", + "religion", "denomination", "building_area", "address", diff --git a/data/presets/presets/amenity/place_of_worship/taoist.json b/data/presets/presets/amenity/place_of_worship/taoist.json index a4fbcde3d..5c01f1f3b 100644 --- a/data/presets/presets/amenity/place_of_worship/taoist.json +++ b/data/presets/presets/amenity/place_of_worship/taoist.json @@ -2,6 +2,7 @@ "icon": "temaki-taoism", "fields": [ "name", + "religion", "denomination", "building_area", "address", diff --git a/data/presets/presets/amenity/vending_machine/fuel.json b/data/presets/presets/amenity/vending_machine/fuel.json index 0c6952f25..febabeec8 100644 --- a/data/presets/presets/amenity/vending_machine/fuel.json +++ b/data/presets/presets/amenity/vending_machine/fuel.json @@ -27,5 +27,6 @@ "key": "vending", "value": "fuel" }, - "name": "Gas Pump" + "name": "Gas Pump", + "matchScore": 0.5 } diff --git a/data/presets/presets/leisure/ice_rink.json b/data/presets/presets/leisure/ice_rink.json index 84a715e64..567dd8ec0 100644 --- a/data/presets/presets/leisure/ice_rink.json +++ b/data/presets/presets/leisure/ice_rink.json @@ -1,5 +1,5 @@ { - "icon": "maki-pitch", + "icon": "fas-skating", "fields": [ "name", "seasonal", diff --git a/data/presets/presets/leisure/track/horse_racing.json b/data/presets/presets/leisure/track/horse_racing.json new file mode 100644 index 000000000..6801993c8 --- /dev/null +++ b/data/presets/presets/leisure/track/horse_racing.json @@ -0,0 +1,25 @@ +{ + "icon": "maki-horse-riding", + "fields": [ + "surface", + "sport_racing_nonmotor", + "lit", + "width", + "lanes" + ], + "geometry": [ + "point", + "line", + "area" + ], + "tags": { + "leisure": "track", + "sport": "horse_racing" + }, + "terms": [ + "race*", + "horse", + "track" + ], + "name": "Racetrack (Horse Racing)" +} diff --git a/data/presets/presets/leisure/running_track.json b/data/presets/presets/leisure/track/running.json similarity index 100% rename from data/presets/presets/leisure/running_track.json rename to data/presets/presets/leisure/track/running.json diff --git a/data/presets/presets/piste/downhill.json b/data/presets/presets/piste/downhill.json index c8e453def..454724ee5 100644 --- a/data/presets/presets/piste/downhill.json +++ b/data/presets/presets/piste/downhill.json @@ -1,5 +1,5 @@ { - "icon": "maki-skiing", + "icon": "fas-skiing", "fields": [ "name", "piste/type", diff --git a/data/presets/presets/piste/downhill/halfpipe.json b/data/presets/presets/piste/downhill/halfpipe.json new file mode 100644 index 000000000..da347d44d --- /dev/null +++ b/data/presets/presets/piste/downhill/halfpipe.json @@ -0,0 +1,29 @@ +{ + "icon": "fas-snowboarding", + "fields": [ + "name", + "piste/type", + "piste/difficulty_downhill", + "piste/grooming_downhill", + "oneway", + "lit" + ], + "geometry": [ + "line", + "area" + ], + "terms": [ + "ski", + "alpine", + "halfpipe", + "half pipe", + "snowboard", + "downhill", + "piste" + ], + "tags": { + "piste:type": "downhill", + "man_made": "piste:halfpipe" + }, + "name": "Halfpipe" +} diff --git a/data/presets/presets/piste/hike.json b/data/presets/presets/piste/hike.json index 79aaa6183..0801dec6f 100644 --- a/data/presets/presets/piste/hike.json +++ b/data/presets/presets/piste/hike.json @@ -1,5 +1,5 @@ { - "icon": "fas-snowflake", + "icon": "fas-skating", "fields": [ "name", "piste/type", diff --git a/data/presets/presets/piste/ice_skate.json b/data/presets/presets/piste/ice_skate.json index a64e4854c..c5f8650d7 100644 --- a/data/presets/presets/piste/ice_skate.json +++ b/data/presets/presets/piste/ice_skate.json @@ -1,5 +1,5 @@ { - "icon": "fas-snowflake", + "icon": "fas-skating", "fields": [ "name", "piste/type", diff --git a/data/presets/presets/piste/nordic.json b/data/presets/presets/piste/nordic.json index 99d4d17f2..5774c79c3 100644 --- a/data/presets/presets/piste/nordic.json +++ b/data/presets/presets/piste/nordic.json @@ -1,5 +1,5 @@ { - "icon": "maki-skiing", + "icon": "fas-skiing-nordic", "fields": [ "name", "piste/type", diff --git a/data/presets/presets/piste/piste.json b/data/presets/presets/piste/piste.json index c6c38f060..72b40659d 100644 --- a/data/presets/presets/piste/piste.json +++ b/data/presets/presets/piste/piste.json @@ -1,5 +1,5 @@ { - "icon": "maki-skiing", + "icon": "fas-skiing", "fields": [ "name", "piste/type", diff --git a/data/presets/presets/piste/skitour.json b/data/presets/presets/piste/skitour.json index 831b84fe9..aaaf5c7b4 100644 --- a/data/presets/presets/piste/skitour.json +++ b/data/presets/presets/piste/skitour.json @@ -1,5 +1,5 @@ { - "icon": "maki-skiing", + "icon": "fas-skiing-nordic", "fields": [ "name", "piste/type", diff --git a/data/presets/presets/piste/sleigh.json b/data/presets/presets/piste/sleigh.json index dc7c7cd28..061b4e412 100644 --- a/data/presets/presets/piste/sleigh.json +++ b/data/presets/presets/piste/sleigh.json @@ -1,5 +1,5 @@ { - "icon": "fas-snowflake", + "icon": "fas-sleigh", "fields": [ "name", "piste/type", diff --git a/data/presets/presets/shop/caravan.json b/data/presets/presets/shop/caravan.json new file mode 100644 index 000000000..4912aa4fa --- /dev/null +++ b/data/presets/presets/shop/caravan.json @@ -0,0 +1,25 @@ +{ + "icon": "maki-car", + "fields": [ + "name", + "brand", + "operator", + "address", + "building_area", + "second_hand", + "service/vehicle", + "opening_hours", + "payment_multi" + ], + "geometry": [ + "point", + "area" + ], + "terms": [ + "auto" + ], + "tags": { + "shop": "caravan" + }, + "name": "RV Dealership" +} diff --git a/data/presets/presets/shop/chemist.json b/data/presets/presets/shop/chemist.json index f093f4610..5dd15f2e0 100644 --- a/data/presets/presets/shop/chemist.json +++ b/data/presets/presets/shop/chemist.json @@ -17,9 +17,15 @@ }, "terms": [ "apothecary", + "beauty", + "drug store", + "drugstore", + "gift", + "hair", "med*", - "drug*", - "gift" + "pharmacy", + "prescription", + "tooth" ], "name": "Drugstore" } diff --git a/data/presets/presets/shop/fireplace.json b/data/presets/presets/shop/fireplace.json new file mode 100644 index 000000000..e393c9307 --- /dev/null +++ b/data/presets/presets/shop/fireplace.json @@ -0,0 +1,24 @@ +{ + "icon": "maki-shop", + "fields": [ + "name", + "operator", + "address", + "building_area", + "opening_hours", + "payment_multi" + ], + "geometry": [ + "point", + "area" + ], + "terms": [ + "fireplace", + "stove", + "masonry heater" + ], + "tags": { + "shop": "fireplace" + }, + "name": "Fireplace Store" +} diff --git a/data/presets/presets/shop/fishing.json b/data/presets/presets/shop/fishing.json new file mode 100644 index 000000000..ccbba74ce --- /dev/null +++ b/data/presets/presets/shop/fishing.json @@ -0,0 +1,19 @@ +{ + "icon": "maki-shop", + "fields": [ + "name", + "operator", + "address", + "building_area", + "opening_hours", + "payment_multi" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "shop": "fishing" + }, + "name": "Fishing Shop" +} diff --git a/data/presets/presets/shop/fuel.json b/data/presets/presets/shop/fuel.json new file mode 100644 index 000000000..5a1690d04 --- /dev/null +++ b/data/presets/presets/shop/fuel.json @@ -0,0 +1,21 @@ +{ + "icon": "maki-shop", + "fields": [ + "name", + "operator", + "address", + "fuel_multi", + "building_area", + "opening_hours", + "payment_multi" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "shop": "fuel" + }, + "name": "Fuel Shop", + "matchScore": 0.5 +} diff --git a/data/presets/presets/shop/hunting.json b/data/presets/presets/shop/hunting.json new file mode 100644 index 000000000..e92b28612 --- /dev/null +++ b/data/presets/presets/shop/hunting.json @@ -0,0 +1,19 @@ +{ + "icon": "maki-shop", + "fields": [ + "name", + "operator", + "address", + "building_area", + "opening_hours", + "payment_multi" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "shop": "hunting" + }, + "name": "Hunting Shop" +} diff --git a/data/presets/presets/shop/water.json b/data/presets/presets/shop/water.json new file mode 100644 index 000000000..3e66135c9 --- /dev/null +++ b/data/presets/presets/shop/water.json @@ -0,0 +1,19 @@ +{ + "icon": "maki-shop", + "fields": [ + "name", + "operator", + "address", + "building_area", + "opening_hours", + "payment_multi" + ], + "geometry": [ + "point", + "area" + ], + "tags": { + "shop": "water" + }, + "name": "Drinking Water Shop" +} diff --git a/data/shortcuts.json b/data/shortcuts.json index 39a81889f..376cb9a94 100644 --- a/data/shortcuts.json +++ b/data/shortcuts.json @@ -41,7 +41,7 @@ "text": "shortcuts.browsing.help.help" }, { - "shortcuts": ["shortcuts.toggle.key"], + "shortcuts": ["shortcuts.toggle.key", "?"], "text": "shortcuts.browsing.help.keyboard" }, { @@ -62,12 +62,12 @@ "text": "shortcuts.browsing.display_options.map_data" }, { - "modifiers": ["⌃","⌘"], + "modifiers": ["⌃", "⌘"], "shortcuts": ["F", "F11"], "text": "shortcuts.browsing.display_options.fullscreen" }, { - "shortcuts": ["sidebar.key"], + "shortcuts": ["sidebar.key", "`", "²"], "text": "shortcuts.browsing.display_options.sidebar" }, { @@ -115,8 +115,8 @@ "text": "shortcuts.browsing.with_selected.edit_menu" }, { - "shortcuts": [], - "text": "" + "shortcuts": ["inspector.zoom_to.key"], + "text": "shortcuts.browsing.with_selected.zoom_to" }, { "section": "vertex_selected", @@ -127,19 +127,19 @@ "text": "shortcuts.browsing.vertex_selected.previous" }, { - "shortcuts": ["]","↘"], + "shortcuts": ["]", "↘"], "text": "shortcuts.browsing.vertex_selected.next" }, { - "shortcuts": ["{","⇞"], + "shortcuts": ["{", "⇞"], "text": "shortcuts.browsing.vertex_selected.first" }, { - "shortcuts": ["}","⇟"], + "shortcuts": ["}", "⇟"], "text": "shortcuts.browsing.vertex_selected.last" }, { - "shortcuts": ["\\","shortcuts.key.pause"], + "shortcuts": ["\\", "shortcuts.key.pause"], "text": "shortcuts.browsing.vertex_selected.change_parent" } ] @@ -173,7 +173,7 @@ "text": "shortcuts.editing.drawing.add_note" }, { - "shortcuts": ["Left-click","shortcuts.key.space"], + "shortcuts": ["Left-click", "shortcuts.key.space"], "text": "shortcuts.editing.drawing.place_point" }, { @@ -181,7 +181,7 @@ "text": "shortcuts.editing.drawing.disable_snap" }, { - "shortcuts": ["↵","⎋"], + "shortcuts": ["↵", "⎋"], "text": "shortcuts.editing.drawing.stop_line" }, { @@ -204,7 +204,7 @@ "text": "shortcuts.editing.commands.undo" }, { - "modifiers": ["⌘","⇧"], + "modifiers": ["⌘", "⇧"], "shortcuts": ["Z"], "text": "shortcuts.editing.commands.redo" }, @@ -294,22 +294,22 @@ "text": "shortcuts.tools.info.all" }, { - "modifiers": ["⌘","⇧"], + "modifiers": ["⌘", "⇧"], "shortcuts": ["info_panels.background.key"], "text": "shortcuts.tools.info.background" }, { - "modifiers": ["⌘","⇧"], + "modifiers": ["⌘", "⇧"], "shortcuts": ["info_panels.history.key"], "text": "shortcuts.tools.info.history" }, { - "modifiers": ["⌘","⇧"], + "modifiers": ["⌘", "⇧"], "shortcuts": ["info_panels.location.key"], "text": "shortcuts.tools.info.location" }, { - "modifiers": ["⌘","⇧"], + "modifiers": ["⌘", "⇧"], "shortcuts": ["info_panels.measurement.key"], "text": "shortcuts.tools.info.measurement" } diff --git a/data/taginfo.json b/data/taginfo.json index c76bf847f..3b8ff842c 100644 --- a/data/taginfo.json +++ b/data/taginfo.json @@ -794,7 +794,7 @@ { "key": "amenity", "value": "pharmacy", - "description": "🄿 Pharmacy", + "description": "🄿 Pharmacy Counter", "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/pharmacy-15.svg?sanitize=true" }, @@ -815,7 +815,7 @@ { "key": "religion", "value": "christian", - "description": "🄿 Church", + "description": "🄿 Christian Church", "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/religious-christian-15.svg?sanitize=true" }, @@ -829,14 +829,14 @@ { "key": "religion", "value": "jewish", - "description": "🄿 Synagogue", + "description": "🄿 Jewish Synagogue", "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/religious-jewish-15.svg?sanitize=true" }, { "key": "religion", "value": "muslim", - "description": "🄿 Mosque", + "description": "🄿 Muslim Mosque", "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/religious-muslim-15.svg?sanitize=true" }, @@ -3678,7 +3678,7 @@ "value": "ice_rink", "description": "🄿 Ice Rink", "object_types": ["node", "area"], - "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/pitch-15.svg?sanitize=true" + "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-skating.svg?sanitize=true" }, { "key": "leisure", @@ -3876,13 +3876,6 @@ "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/lodging-15.svg?sanitize=true" }, - { - "key": "sport", - "value": "running", - "description": "🄿 Racetrack (Running)", - "object_types": ["node", "way", "area"], - "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/pitch-15.svg?sanitize=true" - }, { "key": "leisure", "value": "sauna", @@ -3932,6 +3925,20 @@ "object_types": ["node", "way", "area"], "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/iD-sprite/presets/highway-road.svg?sanitize=true" }, + { + "key": "sport", + "value": "horse_racing", + "description": "🄿 Racetrack (Horse Racing)", + "object_types": ["node", "way", "area"], + "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/horse-riding-15.svg?sanitize=true" + }, + { + "key": "sport", + "value": "running", + "description": "🄿 Racetrack (Running)", + "object_types": ["node", "way", "area"], + "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/pitch-15.svg?sanitize=true" + }, { "key": "leisure", "value": "water_park", @@ -4710,41 +4717,48 @@ "value": "downhill", "description": "🄿 Downhill Piste/Ski Run, 🄵 Type", "object_types": ["way", "area"], - "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/skiing-15.svg?sanitize=true" + "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-skiing.svg?sanitize=true" + }, + { + "key": "man_made", + "value": "piste:halfpipe", + "description": "🄿 Halfpipe", + "object_types": ["way", "area"], + "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-snowboarding.svg?sanitize=true" }, { "key": "piste:type", "value": "hike", "description": "🄿 Snowshoeing or Winter Hiking Trail, 🄵 Type", "object_types": ["way", "area"], - "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-snowflake.svg?sanitize=true" + "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-skating.svg?sanitize=true" }, { "key": "piste:type", "value": "ice_skate", "description": "🄿 Ice Skating Piste, 🄵 Type", "object_types": ["way", "area"], - "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-snowflake.svg?sanitize=true" + "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-skating.svg?sanitize=true" }, { "key": "piste:type", "value": "nordic", "description": "🄿 Nordic or Crosscountry Piste/Ski Trail, 🄵 Type", "object_types": ["way", "area"], - "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/skiing-15.svg?sanitize=true" + "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-skiing-nordic.svg?sanitize=true" }, { "key": "piste:type", "description": "🄿 Winter Sport Trails", "object_types": ["way", "area"], - "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/skiing-15.svg?sanitize=true" + "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-skiing.svg?sanitize=true" }, { "key": "piste:type", "value": "skitour", "description": "🄿 Ski Touring Trail, 🄵 Type", "object_types": ["way", "area"], - "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/skiing-15.svg?sanitize=true" + "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-skiing-nordic.svg?sanitize=true" }, { "key": "piste:type", @@ -4758,7 +4772,7 @@ "value": "sleigh", "description": "🄿 Sleigh Piste, 🄵 Type", "object_types": ["way", "area"], - "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-snowflake.svg?sanitize=true" + "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-sleigh.svg?sanitize=true" }, { "key": "place", @@ -5547,6 +5561,13 @@ "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/car-15.svg?sanitize=true" }, + { + "key": "shop", + "value": "caravan", + "description": "🄿 RV Dealership", + "object_types": ["node", "area"], + "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/car-15.svg?sanitize=true" + }, { "key": "shop", "value": "carpet", @@ -5736,6 +5757,20 @@ "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/shop-15.svg?sanitize=true" }, + { + "key": "shop", + "value": "fireplace", + "description": "🄿 Fireplace Store", + "object_types": ["node", "area"], + "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/shop-15.svg?sanitize=true" + }, + { + "key": "shop", + "value": "fishing", + "description": "🄿 Fishing Shop", + "object_types": ["node", "area"], + "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/shop-15.svg?sanitize=true" + }, { "key": "shop", "value": "florist", @@ -5750,6 +5785,13 @@ "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/shop-15.svg?sanitize=true" }, + { + "key": "shop", + "value": "fuel", + "description": "🄿 Fuel Shop", + "object_types": ["node", "area"], + "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/shop-15.svg?sanitize=true" + }, { "key": "shop", "value": "funeral_directors", @@ -5848,6 +5890,13 @@ "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/openstreetmap/iD/master/svg/fontawesome/fas-blender.svg?sanitize=true" }, + { + "key": "shop", + "value": "hunting", + "description": "🄿 Hunting Shop", + "object_types": ["node", "area"], + "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/shop-15.svg?sanitize=true" + }, { "key": "shop", "value": "interior_decoration", @@ -6254,6 +6303,13 @@ "object_types": ["node", "area"], "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/shop-15.svg?sanitize=true" }, + { + "key": "shop", + "value": "water", + "description": "🄿 Drinking Water Shop", + "object_types": ["node", "area"], + "icon_url": "https://raw.githubusercontent.com/mapbox/maki/master/icons/shop-15.svg?sanitize=true" + }, { "key": "shop", "value": "weapons", diff --git a/dist/locales/en.json b/dist/locales/en.json index b75419b59..9122c51ef 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -497,11 +497,19 @@ "locating": "Locating, please wait..." }, "inspector": { + "zoom_to": { + "key": "Z", + "title": "Zoom to this", + "tooltip_feature": "Center and zoom the map to focus on this feature.", + "tooltip_note": "Center and zoom the map to focus on this note.", + "tooltip_data": "Center and zoom the map to focus on this data.", + "tooltip_issue": "Center and zoom the map to focus on this issue." + }, "no_documentation_combination": "There is no documentation available for this tag combination", "no_documentation_key": "There is no documentation available for this key", - "documentation_redirect": "This documentation has been redirected to a new page", "show_more": "Show More", "view_on_osm": "View on openstreetmap.org", + "view_on_keepRight": "View on keepright.at", "all_fields": "All fields", "all_tags": "All tags", "all_members": "All members", @@ -512,6 +520,7 @@ "choose": "Select feature type", "results": "{n} results for {search}", "reference": "View on OpenStreetMap Wiki", + "edit_reference": "Edit or translate on OSM Wiki", "back_tooltip": "Change feature", "remove": "Remove", "search": "Search", @@ -582,6 +591,10 @@ "tooltip": "Note data from OpenStreetMap", "title": "OpenStreetMap notes" }, + "keepRight": { + "tooltip": "Automatically detected map issues from keepright.at", + "title": "KeepRight Issues" + }, "custom": { "tooltip": "Drag and drop a data file onto the page, or click the button to setup", "title": "Custom Map Data", @@ -765,6 +778,317 @@ }, "cannot_zoom": "Cannot zoom out further in current mode.", "full_screen": "Toggle Full Screen", + "QA": { + "keepRight": { + "title": "KeepRight Error", + "detail_title": "Error", + "detail_description": "Description", + "comment": "Comment", + "comment_placeholder": "Enter a comment to share with other users.", + "close": "Close (Error Fixed)", + "ignore": "Ignore (Not an Error)", + "save_comment": "Save Comment", + "close_comment": "Close and Comment", + "ignore_comment": "Ignore and Comment", + "error_parts": { + "node": "this node", + "way": "this way", + "relation": "this relation", + "oneway": "this oneway", + "highway": "this highway", + "railway": "this railway", + "waterway": "this waterway", + "cycleway": "this cycleway", + "cycleway_footpath": "this cycleway/footpath", + "riverbank": "this riverbank", + "crossing": "this crossing", + "railway_crossing": "this railway crossing", + "bridge": "this bridge", + "tunnel": "this tunnel", + "boundary": "this boundary", + "turn_restriction": "this turn restriction", + "roundabout": "this roundabout", + "mini_roundabout": "this mini-roundabout", + "track": "this track", + "feature": "this feature", + "place_of_worship": "place of worship", + "pub": "pub", + "restaurant": "restaurant", + "school": "school", + "university": "university", + "hospital": "hospital", + "library": "library", + "theatre": "theatre", + "courthouse": "courthouse", + "bank": "bank", + "cinema": "cinema", + "pharmacy": "pharmacy", + "cafe": "cafe", + "fast_food": "fast food", + "fuel": "fuel", + "from": "from", + "to": "to", + "left_hand": "left-hand", + "right_hand": "right-hand" + }, + "errorTypes": { + "20": { + "title": "Multiple nodes on the same spot", + "description": "There is more than one node in this spot. Node IDs: {var1}." + }, + "30": { + "title": "Non-closed area", + "description": "{var1} is tagged with \"{var2}\" and should be a closed loop." + }, + "40": { + "title": "Impossible oneway", + "description": "The first node {var1} of {var2} is not connected to any other way." + }, + "41": { + "description": "The last node {var1} of {var2} is not connected to any other way." + }, + "42": { + "description": "You cannot reach {var1} because all ways leading from it are oneway." + }, + "43": { + "description": "You cannot escape from {var1} because all ways leading to it are oneway." + }, + "50": { + "title": "Almost junction", + "description": "{var1} is very close but not connected to way {var2}." + }, + "60": { + "title": "Deprecated tag", + "description": "{var1} uses deprecated tag \"{var2}\". Please use \"{var3}\" instead." + }, + "70": { + "title": "Missing tag", + "description": "{var1} has an empty tag: \"{var2}\"." + }, + "71": { + "description": "{var1} has no tags." + }, + "72": { + "description": "{var1} is not member of any way and doesn't have any tags." + }, + "73": { + "description": "{var1} has a \"{var2}\" tag but no \"highway\" tag." + }, + "74": { + "description": "{var1} has an empty tag: \"{var2}\"." + }, + "75": { + "description": "{var1} has a name \"{var2}\" but no other tags." + }, + "90": { + "title": "Motorway without ref tag", + "description": "{var1} is tagged as a motorway and therefore needs a \"ref\", \"nat_ref\", or \"int_ref\" tag." + }, + "100": { + "title": "Place of worship without religion", + "description": "{var1} is tagged as a place of worship and therefore needs a religion tag." + }, + "110": { + "title": "Point of interest without name", + "description": "{var1} is tagged as a \"{var2}\" and therefore needs a name tag." + }, + "120": { + "title": "Way without nodes", + "description": "{var1} has just one single node." + }, + "130": { + "title": "Disconnected way", + "description": "{var1} is not connected to the rest of the map." + }, + "150": { + "title": "Railway crossing without tag", + "description": "{var1} of a highway and a railway needs to be tagged as \"railway=crossing\" or \"railway=level_crossing\"." + }, + "160": { + "title": "Railway layer conflict", + "description": "There are ways in different layers (e.g. tunnel or bridge) meeting at {var1}." + }, + "170": { + "title": "FIXME tagged item", + "description": "{var1} has a FIXME tag: {var2}" + }, + "180": { + "title": "Relation without type", + "description": "{var1} is missing a \"type\" tag." + }, + "190": { + "title": "Intersection without junction", + "description": "{var1} intersects the {var2} {var3} but there is no junction node, bridge, or tunnel." + }, + "200": { + "title": "Overlapping ways", + "description": "{var1} overlaps the {var2} {var3}." + }, + "210": { + "title": "Self-intersecting way", + "description": "There is an unspecified issue with self intersecting ways." + }, + "211": { + "description": "{var1} contains more than one node multiple times. Nodes are {var2}. This may or may not be an error." + }, + "212": { + "description": "{var1} has only two different nodes and contains one of them more than once." + }, + "220": { + "title": "Misspelled tag", + "description": "{var1} is tagged \"{var2}\" where \"{var3}\" looks like \"{var4}\"." + }, + "221": { + "description": "{var1} has a suspicious tag \"{var2}\"." + }, + "230": { + "title": "Layer conflict", + "description": "{var1} is a junction of ways on different layers." + }, + "231": { + "description": "{var1} is a junction of ways on different layers: {var2}.", + "layer": "(layer: {layer})" + }, + "232": { + "description": "{var1} is tagged with \"layer={var2}\". This need not be an error but it looks strange." + }, + "270": { + "title": "Unusual motorway connection", + "description": "{var1} is a junction of a motorway and a highway other than \"motorway\", \"motorway_link\", \"trunk\", \"rest_area\", or \"construction\". Connection to \"service\" or \"unclassified\" is only valid if it has \"access=no/private\", or it leads to a motorway service area, or if it is a \"service=parking_aisle\"." + }, + "280": { + "title": "Boundary issue", + "description": "There is an unspecified issue with this boundary." + }, + "281": { + "title": "Boundary missing name", + "description": "{var1} has no name." + }, + "282": { + "title": "Boundary missing admin level", + "description": "The boundary of {var1} has no valid numeric admin_level. Please do not mix admin levels (e.g. \"6;7\"). Always tag the lowest admin_level of all boundaries." + }, + "283": { + "title": "Boundary not a closed loop", + "description": "The boundary of {var1} is not a closed loop." + }, + "284": { + "title": "Boundary is split", + "description": "The boundary of {var1} splits here." + }, + "285": { + "title": "Boundary admin_level too high", + "description": "{var1} has \"admin_level={var2}\" but belongs to a relation with lower \"admin_level\" (e.g. higher priority); it should have the lowest \"admin_level\" of all relations." + }, + "290": { + "title": "Restriction issue", + "description": "There is an unspecified issue with this restriction." + }, + "291": { + "title": "Restriction missing type", + "description": "{var1} has an unrecognized restriction type." + }, + "292": { + "title": "Restriction missing \"from\" way", + "description": "{var1} has {var2} \"from\" members, but it should have 1." + }, + "293": { + "title": "Restriction missing \"to\" way", + "description": "{var1} has {var2} \"to\" members, but it should have 1." + }, + "294": { + "title": "Restriction \"from\" or \"to\" is not a way", + "description": "{var1} has \"from\" or \"to\" members which should be ways. {var2}." + }, + "295": { + "title": "Restriction \"via\" is not an endpoint", + "description": "{var1} has a \"via\" (node {var2}) which is not the first or the last member of \"{var3}\" (way {var4})." + }, + "296": { + "title": "Unusual restriction angle", + "description": "{var1} has a restriction type \"{var2}\" but the angle is {var3} degrees. Maybe the restriction type is not appropriate?" + }, + "297": { + "title": "Wrong direction of to member", + "description": "{var1} does not match the direction of \"to\" way {var2}." + }, + "298": { + "title": "Redundant restriction - oneway", + "description": "{var1} may be redundant. Entry already prohibited by \"oneway\" tag on {var2}." + }, + "300": { + "title": "Missing maxspeed", + "description": "{var1} is missing a \"maxspeed\" tag and is tagged as motorway, trunk, primary, or secondary." + }, + "310": { + "title": "Roundabout issue", + "description": "There is an unspecified issue with this roundabout." + }, + "311": { + "title": "Roundabout not closed loop", + "description": "{var1} is part of a roundabout but is not closed-loop. (Split carriageways approaching a roundabout should not be tagged as roundabout)." + }, + "312": { + "title": "Roundabout wrong direction", + "description": "If {var1} is in a country with {var2} traffic then its orientation goes the wrong way around." + }, + "313": { + "title": "Roundabout weakly connected", + "description": "{var1} has only {var2} other road(s) connected. Roundabouts typically have 3 or more." + }, + "320": { + "title": "Improper link connection", + "description": "{var1} is tagged as \"{var2}\" but doesn't have a connection to any other \"{var3}\" or \"{var4}\"." + }, + "350": { + "title": "Improper bridge tag", + "description": "{var1} doesn't have a tag in common with its surrounding ways that shows the purpose of this bridge. There should be one of these tags: {var2}." + }, + "360": { + "title": "Missing local name tag", + "description": "It would be nice if {var1} had a local name tag \"name:XX={var2}\" where XX shows the language of its common name \"{var2}\"." + }, + "370": { + "title": "Doubled places", + "description": "{var1} has tags in common with the surrounding way {var2} {var3} and seems to be redundant.", + "including_the_name": "(including the name {name})" + }, + "380": { + "title": "Non-physical use of sport tag", + "description": "{var1} is tagged \"{var2}\" but has no physical tag (e.g. \"leisure\", \"building\", \"amenity\", or \"highway\")." + }, + "390": { + "title": "Missing tracktype", + "description": "{var1} doesn't have a \"tracktype\" tag." + }, + "400": { + "title": "Geometry issue", + "description": "There is an unspecified issue with the geometry here." + }, + "401": { + "title": "Missing turn restriction", + "description": "Ways {var1} and {var2} join in a very sharp angle here and there is no oneway tag or turn restriction that prevents turning." + }, + "402": { + "title": "Impossible angle", + "description": "{var1} bends in a very sharp angle here." + }, + "410": { + "title": "Website issue", + "description": "There is an unspecified issue with a contact website or URL." + }, + "411": { + "description": "{var1} may have an outdated URL: {var2} returned HTTP status code {var3}." + }, + "412": { + "description": "{var1} may have an outdated URL: {var2} contained suspicious text \"{var3}\"." + }, + "413": { + "description": "{var1} may have an outdated URL: {var2} did not contain keywords \"{var3}\"." + } + } + } + }, "streetside": { "tooltip": "Streetside photos from Microsoft", "title": "Photo Overlay (Bing Streetside)", @@ -992,6 +1316,14 @@ "tracing": "The GPS track isn't sent to OpenStreetMap - the best way to use it is to draw on the map, using it as a guide for the new features that you add.", "upload": "You can also [upload your GPS data to OpenStreetMap](https://www.openstreetmap.org/trace/create) for other users to use." }, + "qa": { + "title": "Quality Assurance", + "intro": "*Quality Assurance* (Q/A) 3rd party tools help lead to better quality of OSM data. They list automatically deteted bugs, conflics, and issues with the data, which mappers can then go and fix. To view existing Q/A issues, click the {data} **Map data** panel to enable a specific Q/A layer.", + "tools_h": "Tools", + "tools": "The following tools are currently supported: [KeepRight](https://www.keepright.at/). Expect iD to support [Osmose](https://osmose.openstreetmap.fr/), [ImproveOSM](https://improveosm.org/en/), and more Q/A tools in the future.", + "issues_h": "Handling Issues", + "issues": "Handling Q/A issues is similar to handling notes. Clicking an existing Q/A issue populates the sidebar with details on the issue type and related features. Each tool has its own capabilities, but generally you can comment and/or close an issue. Expect iD to support a 'fix me' button to automatically fix simple issues in the future." + }, "field": { "restrictions": { "title": "Turn Restrictions Help", @@ -1448,7 +1780,8 @@ }, "with_selected": { "title": "With feature selected", - "edit_menu": "Toggle edit menu" + "edit_menu": "Toggle edit menu", + "zoom_to": "Zoom to selected feature" }, "vertex_selected": { "title": "With node selected", @@ -3777,8 +4110,8 @@ "terms": "interactive kiosk,ekiosk,atm,bill pay,tax pay,phone pay,finance,cash,money transfer,card" }, "amenity/pharmacy": { - "name": "Pharmacy", - "terms": "apothecary,drug*,med*,prescription" + "name": "Pharmacy Counter", + "terms": "apothecary,drug store,drugstore,med*,prescription" }, "amenity/place_of_worship": { "name": "Place of Worship", @@ -3789,7 +4122,7 @@ "terms": "stupa,vihara,monastery,temple,pagoda,zendo,dojo" }, "amenity/place_of_worship/christian": { - "name": "Church", + "name": "Christian Church", "terms": "christian,abbey,basilica,bethel,cathedral,chancel,chantry,chapel,fold,house of God,house of prayer,house of worship,minster,mission,oratory,parish,sacellum,sanctuary,shrine,tabernacle,temple" }, "amenity/place_of_worship/hindu": { @@ -3797,11 +4130,11 @@ "terms": "kovil,devasthana,mandir,kshetram,alayam,shrine,temple" }, "amenity/place_of_worship/jewish": { - "name": "Synagogue", + "name": "Jewish Synagogue", "terms": "jewish" }, "amenity/place_of_worship/muslim": { - "name": "Mosque", + "name": "Muslim Mosque", "terms": "muslim" }, "amenity/place_of_worship/shinto": { @@ -5620,10 +5953,6 @@ "name": "Resort", "terms": "" }, - "leisure/running_track": { - "name": "Racetrack (Running)", - "terms": "race*,running,sprint,track" - }, "leisure/sauna": { "name": "Sauna", "terms": "" @@ -5652,6 +5981,14 @@ "name": "Racetrack (Non-Motorsport)", "terms": "cycle,dog,greyhound,horse,race*,track" }, + "leisure/track/horse_racing": { + "name": "Racetrack (Horse Racing)", + "terms": "race*,horse,track" + }, + "leisure/track/running": { + "name": "Racetrack (Running)", + "terms": "race*,running,sprint,track" + }, "leisure/water_park": { "name": "Water Park", "terms": "swim,pool,dive" @@ -6124,6 +6461,10 @@ "name": "Downhill Piste/Ski Run", "terms": "ski,alpine,snowboard,downhill,piste" }, + "piste/downhill/halfpipe": { + "name": "Halfpipe", + "terms": "ski,alpine,halfpipe,half pipe,snowboard,downhill,piste" + }, "piste/hike": { "name": "Snowshoeing or Winter Hiking Trail", "terms": "hike,winter hiking,snowshoe,snowshoeing,piste,ski" @@ -6732,6 +7073,10 @@ "name": "Car Dealership", "terms": "auto" }, + "shop/caravan": { + "name": "RV Dealership", + "terms": "auto" + }, "shop/carpet": { "name": "Carpet Store", "terms": "rug" @@ -6750,7 +7095,7 @@ }, "shop/chemist": { "name": "Drugstore", - "terms": "apothecary,med*,drug*,gift" + "terms": "apothecary,beauty,drug store,drugstore,gift,hair,med*,pharmacy,prescription,tooth" }, "shop/chocolate": { "name": "Chocolate Store", @@ -6840,6 +7185,14 @@ "name": "Fashion Store", "terms": "" }, + "shop/fireplace": { + "name": "Fireplace Store", + "terms": "fireplace,stove,masonry heater" + }, + "shop/fishing": { + "name": "Fishing Shop", + "terms": "" + }, "shop/florist": { "name": "Florist", "terms": "flower" @@ -6848,6 +7201,10 @@ "name": "Framing Shop", "terms": "art*,paint*,photo*,frame" }, + "shop/fuel": { + "name": "Fuel Shop", + "terms": "" + }, "shop/funeral_directors": { "name": "Funeral Home", "terms": "undertaker,memorial home" @@ -6904,6 +7261,10 @@ "name": "Houseware Store", "terms": "home,household" }, + "shop/hunting": { + "name": "Hunting Shop", + "terms": "" + }, "shop/interior_decoration": { "name": "Interior Decoration Store", "terms": "" @@ -7136,6 +7497,10 @@ "name": "Watersport/Swim Shop", "terms": "" }, + "shop/water": { + "name": "Drinking Water Shop", + "terms": "" + }, "shop/weapons": { "name": "Weapon Shop", "terms": "ammo,gun,knife,knives" diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index 0221149ec..5dd432975 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -160,8 +160,8 @@ export function behaviorDrag() { for (; target && target !== root; target = target.parentNode) { var datum = target.__data__; - var entity = datum instanceof osmNote ? - datum : datum && datum.properties && datum.properties.entity; + var entity = datum instanceof osmNote ? datum + : datum && datum.properties && datum.properties.entity; if (entity && target[matchesSelector](_selector)) { return dragstart.call(target, entity); diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index b7bd503de..58b9187a9 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -5,7 +5,7 @@ import { select as d3_select } from 'd3-selection'; -import { osmEntity, osmNote } from '../osm'; +import { osmEntity, osmNote, krError } from '../osm'; import { utilKeybinding, utilRebind } from '../util'; @@ -112,6 +112,10 @@ export function behaviorHover(context) { entity = datum; selector = '.data' + datum.__featurehash__; + } else if (datum instanceof krError) { + entity = datum; + selector = '.kr_error-' + datum.id; + } else if (datum instanceof osmNote) { entity = datum; selector = '.note-' + datum.id; diff --git a/modules/behavior/select.js b/modules/behavior/select.js index bbb6df526..8c445930d 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -12,12 +12,14 @@ import { modeBrowse, modeSelect, modeSelectData, - modeSelectNote + modeSelectNote, + modeSelectError } from '../modes'; import { osmEntity, - osmNote + osmNote, + krError } from '../osm'; @@ -130,6 +132,7 @@ export function behaviorSelect(context) { if (datum instanceof osmEntity) { // clicked an entity.. var selectedIDs = context.selectedIDs(); context.selectedNoteID(null); + context.selectedErrorID(null); if (!isMultiselect) { if (selectedIDs.length > 1 && (!suppressMenu && !isShowAlways)) { @@ -167,9 +170,13 @@ export function behaviorSelect(context) { context .selectedNoteID(datum.id) .enter(modeSelectNote(context, datum.id)); - + } else if (datum instanceof krError & !isMultiselect) { // clicked a krError error + context + .selectedErrorID(datum.id) + .enter(modeSelectError(context, datum.id)); } else { // clicked nothing.. context.selectedNoteID(null); + context.selectedErrorID(null); if (!isMultiselect && mode.id !== 'browse') { context.enter(modeBrowse(context)); } diff --git a/modules/core/context.js b/modules/core/context.js index e6c0e37c6..8846a31d1 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -146,7 +146,9 @@ export function coreContext() { this.loadEntity(entityID, function(err, result) { if (err) return; var entity = _find(result.data, function(e) { return e.id === entityID; }); - if (entity) { map.zoomTo(entity); } + if (entity) { + map.zoomTo(entity); + } }); } @@ -264,6 +266,13 @@ export function coreContext() { return context; }; + var _selectedErrorID; + context.selectedErrorID = function(errorID) { + if (!arguments.length) return _selectedErrorID; + _selectedErrorID = errorID; + return context; + }; + /* Behaviors */ context.install = function(behavior) { diff --git a/modules/core/history.js b/modules/core/history.js index 9df59db4b..1c7c66d28 100644 --- a/modules/core/history.js +++ b/modules/core/history.js @@ -52,9 +52,6 @@ export function coreHistory(context) { annotation = actions.pop(); } - _stack[_index].transform = context.projection.transform(); - _stack[_index].selectedIDs = context.selectedIDs(); - var graph = _stack[_index].graph; for (var i = 0; i < actions.length; i++) { graph = actions[i](graph, t); @@ -63,7 +60,9 @@ export function coreHistory(context) { return { graph: graph, annotation: annotation, - imageryUsed: _imageryUsed + imageryUsed: _imageryUsed, + transform: context.projection.transform(), + selectedIDs: context.selectedIDs() }; } @@ -410,7 +409,8 @@ export function coreHistory(context) { var base = _stack[0]; var s = _stack.map(function(i) { - var modified = [], deleted = []; + var modified = []; + var deleted = []; _forEach(i.graph.entities, function(entity, id) { if (entity) { @@ -440,6 +440,8 @@ export function coreHistory(context) { if (deleted.length) x.deleted = deleted; if (i.imageryUsed) x.imageryUsed = i.imageryUsed; if (i.annotation) x.annotation = i.annotation; + if (i.transform) x.transform = i.transform; + if (i.selectedIDs) x.selectedIDs = i.selectedIDs; return x; }); @@ -537,7 +539,9 @@ export function coreHistory(context) { return { graph: coreGraph(_stack[0].graph).load(entities), annotation: d.annotation, - imageryUsed: d.imageryUsed + imageryUsed: d.imageryUsed, + transform: d.transform, + selectedIDs: d.selectedIDs }; }); @@ -555,6 +559,11 @@ export function coreHistory(context) { }); } + var transform = _stack[_index].transform; + if (transform) { + context.map().transformEase(transform, 0); // 0 = immediate, no easing + } + if (loadComplete) { dispatch.call('change'); } diff --git a/modules/modes/drag_note.js b/modules/modes/drag_note.js index 7f834cb10..5c9714fc6 100644 --- a/modules/modes/drag_note.js +++ b/modules/modes/drag_note.js @@ -20,13 +20,14 @@ export function modeDragNote(context) { var _nudgeInterval; var _lastLoc; + var _note; // most current note.. dragged note may have stale datum. - function startNudge(note, nudge) { + function startNudge(nudge) { if (_nudgeInterval) window.clearInterval(_nudgeInterval); _nudgeInterval = window.setInterval(function() { context.pan(nudge); - doMove(note, nudge); + doMove(nudge); }, 50); } @@ -45,58 +46,66 @@ export function modeDragNote(context) { function start(note) { - context.surface().selectAll('.note-' + note.id) + _note = note; + var osm = services.osm; + if (osm) { + // Get latest note from cache.. The marker may have a stale datum bound to it + // and dragging it around can sometimes delete the users note comment. + _note = osm.getNote(_note.id); + } + + context.surface().selectAll('.note-' + _note.id) .classed('active', true); context.perform(actionNoop()); context.enter(mode); - context.selectedNoteID(note.id); + context.selectedNoteID(_note.id); } - function move(note) { + function move() { d3_event.sourceEvent.stopPropagation(); _lastLoc = context.projection.invert(d3_event.point); - doMove(note); + doMove(); var nudge = geoViewportEdge(d3_event.point, context.map().dimensions()); if (nudge) { - startNudge(note, nudge); + startNudge(nudge); } else { stopNudge(); } } - function doMove(note, nudge) { + function doMove(nudge) { nudge = nudge || [0, 0]; var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); - note = note.move(loc); + _note = _note.move(loc); var osm = services.osm; if (osm) { - osm.replaceNote(note); // update note cache + osm.replaceNote(_note); // update note cache } context.replace(actionNoop()); // trigger redraw } - function end(note) { + function end() { context.replace(actionNoop()); // trigger redraw context - .selectedNoteID(note.id) - .enter(modeSelectNote(context, note.id)); + .selectedNoteID(_note.id) + .enter(modeSelectNote(context, _note.id)); } var drag = behaviorDrag() - .selector('.layer-notes .new') + .selector('.layer-touch.markers .target.note.new') .surface(d3_select('#map').node()) .origin(origin) .on('start', start) diff --git a/modules/modes/index.js b/modules/modes/index.js index af440c4c2..f7f4d985c 100644 --- a/modules/modes/index.js +++ b/modules/modes/index.js @@ -12,4 +12,5 @@ export { modeRotate } from './rotate'; export { modeSave } from './save'; export { modeSelect } from './select'; export { modeSelectData } from './select_data'; +export { modeSelectError} from './select_error'; export { modeSelectNote } from './select_note'; diff --git a/modules/modes/select.js b/modules/modes/select.js index ff6e93191..0b4c8e4e9 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -192,6 +192,14 @@ export function modeSelect(context, selectedIDs) { }; + mode.zoomToSelected = function() { + var entity = singular(); + if (entity) { + context.map().zoomTo(entity); + } + }; + + mode.reselect = function() { if (!checkSelectedIDs()) return; @@ -229,6 +237,91 @@ export function modeSelect(context, selectedIDs) { mode.enter = function() { + if (!checkSelectedIDs()) return; + + var operations = _without(_values(Operations), Operations.operationDelete) + .map(function(o) { return o(selectedIDs, context); }) + .filter(function(o) { return o.available(); }); + + // deprecation warning - Radial Menu to be removed in iD v3 + var isRadialMenu = context.storage('edit-menu-style') === 'radial'; + if (isRadialMenu) { + operations = operations.slice(0,7); + operations.unshift(Operations.operationDelete(selectedIDs, context)); + } else { + operations.push(Operations.operationDelete(selectedIDs, context)); + } + + operations.forEach(function(operation) { + if (operation.behavior) { + behaviors.push(operation.behavior); + } + }); + + behaviors.forEach(context.install); + + keybinding + .on(t('inspector.zoom_to.key'), mode.zoomToSelected) + .on(['[', 'pgup'], previousVertex) + .on([']', 'pgdown'], nextVertex) + .on(['{', uiCmd('⌘['), 'home'], firstVertex) + .on(['}', uiCmd('⌘]'), 'end'], lastVertex) + .on(['\\', 'pause'], nextParent) + .on('⎋', esc, true) + .on('space', toggleMenu); + + d3_select(document) + .call(keybinding); + + + // deprecation warning - Radial Menu to be removed in iD v3 + editMenu = isRadialMenu + ? uiRadialMenu(context, operations) + : uiEditMenu(context, operations); + + context.ui().sidebar + .select(singular() ? singular().id : null, newFeature); + + context.history() + .on('undone.select', update) + .on('redone.select', update); + + context.map() + .on('move.select', closeMenu) + .on('drawn.select', selectElements); + + context.surface() + .on('dblclick.select', dblclick); + + + selectElements(); + + if (selectedIDs.length > 1) { + var entities = uiSelectionList(context, selectedIDs); + context.ui().sidebar.show(entities); + } + + if (follow) { + var extent = geoExtent(); + var graph = context.graph(); + selectedIDs.forEach(function(id) { + var entity = context.entity(id); + extent._extend(entity.extent(graph)); + }); + + var loc = extent.center(); + context.map().centerEase(loc); + } else if (singular() && singular().type === 'way') { + context.map().pan([0,0]); // full redraw, to adjust z-sorting #2914 + } + + timeout = window.setTimeout(function() { + positionMenu(); + if (!suppressMenu) { + showMenu(); + } + }, 270); /* after any centerEase completes */ + function update() { closeMenu(); @@ -419,91 +512,6 @@ export function modeSelect(context, selectedIDs) { .classed('related', true); } } - - - if (!checkSelectedIDs()) return; - - var operations = _without(_values(Operations), Operations.operationDelete) - .map(function(o) { return o(selectedIDs, context); }) - .filter(function(o) { return o.available(); }); - - // deprecation warning - Radial Menu to be removed in iD v3 - var isRadialMenu = context.storage('edit-menu-style') === 'radial'; - if (isRadialMenu) { - operations = operations.slice(0,7); - operations.unshift(Operations.operationDelete(selectedIDs, context)); - } else { - operations.push(Operations.operationDelete(selectedIDs, context)); - } - - operations.forEach(function(operation) { - if (operation.behavior) { - behaviors.push(operation.behavior); - } - }); - - behaviors.forEach(context.install); - - keybinding - .on(['[', 'pgup'], previousVertex) - .on([']', 'pgdown'], nextVertex) - .on(['{', uiCmd('⌘['), 'home'], firstVertex) - .on(['}', uiCmd('⌘]'), 'end'], lastVertex) - .on(['\\', 'pause'], nextParent) - .on('⎋', esc, true) - .on('space', toggleMenu); - - d3_select(document) - .call(keybinding); - - - // deprecation warning - Radial Menu to be removed in iD v3 - editMenu = isRadialMenu - ? uiRadialMenu(context, operations) - : uiEditMenu(context, operations); - - context.ui().sidebar - .select(singular() ? singular().id : null, newFeature); - - context.history() - .on('undone.select', update) - .on('redone.select', update); - - context.map() - .on('move.select', closeMenu) - .on('drawn.select', selectElements); - - context.surface() - .on('dblclick.select', dblclick); - - - selectElements(); - - if (selectedIDs.length > 1) { - var entities = uiSelectionList(context, selectedIDs); - context.ui().sidebar.show(entities); - } - - if (follow) { - var extent = geoExtent(); - var graph = context.graph(); - selectedIDs.forEach(function(id) { - var entity = context.entity(id); - extent._extend(entity.extent(graph)); - }); - - var loc = extent.center(); - context.map().centerEase(loc); - } else if (singular() && singular().type === 'way') { - context.map().pan([0,0]); // full redraw, to adjust z-sorting #2914 - } - - timeout = window.setTimeout(function() { - positionMenu(); - if (!suppressMenu) { - showMenu(); - } - }, 270); /* after any centerEase completes */ }; diff --git a/modules/modes/select_data.js b/modules/modes/select_data.js index de9b6513e..744800654 100644 --- a/modules/modes/select_data.js +++ b/modules/modes/select_data.js @@ -1,4 +1,3 @@ - import { geoBounds as d3_geoBounds } from 'd3-geo'; import { @@ -13,6 +12,8 @@ import { behaviorSelect } from '../behavior'; +import { t } from '../util/locale'; + import { geoExtent } from '../geo'; import { modeBrowse, modeDragNode, modeDragNote } from '../modes'; import { uiDataEditor } from '../ui'; @@ -61,9 +62,18 @@ export function modeSelectData(context, selectedDatum) { } + mode.zoomToSelected = function() { + var extent = geoExtent(d3_geoBounds(selectedDatum)); + context.map().centerZoom(extent.center(), context.map().trimmedExtentZoom(extent)); + }; + + mode.enter = function() { behaviors.forEach(context.install); - keybinding.on('⎋', esc, true); + + keybinding + .on(t('inspector.zoom_to.key'), mode.zoomToSelected) + .on('⎋', esc, true); d3_select(document) .call(keybinding); diff --git a/modules/modes/select_error.js b/modules/modes/select_error.js new file mode 100644 index 000000000..aa0c8ef3b --- /dev/null +++ b/modules/modes/select_error.js @@ -0,0 +1,138 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { + behaviorBreathe, + behaviorHover, + behaviorLasso, + behaviorSelect +} from '../behavior'; + +import { t } from '../util/locale'; +import { services } from '../services'; +import { modeBrowse, modeDragNode, modeDragNote } from '../modes'; +import { uiKeepRightEditor } from '../ui'; +import { utilKeybinding } from '../util'; + + +export function modeSelectError(context, selectedErrorID) { + var mode = { + id: 'select-error', + button: 'browse' + }; + + var keepRight = services.keepRight; + var keybinding = utilKeybinding('select-error'); + var keepRightEditor = uiKeepRightEditor(context) + .on('change', function() { + context.map().pan([0,0]); // trigger a redraw + var error = checkSelectedID(); + if (!error) return; + context.ui().sidebar + .show(keepRightEditor.error(error)); + }); + + var behaviors = [ + behaviorBreathe(context), + behaviorHover(context), + behaviorSelect(context), + behaviorLasso(context), + modeDragNode(context).behavior, + modeDragNote(context).behavior + ]; + + + function checkSelectedID() { + if (!keepRight) return; + var error = keepRight.getError(selectedErrorID); + if (!error) { + context.enter(modeBrowse(context)); + } + return error; + } + + + mode.zoomToSelected = function() { + if (!keepRight) return; + var error = keepRight.getError(selectedErrorID); + if (error) { + context.map().centerZoom(error.loc, 20); + } + }; + + + mode.enter = function() { + var error = checkSelectedID(); + if (!error) return; + + behaviors.forEach(context.install); + keybinding + .on(t('inspector.zoom_to.key'), mode.zoomToSelected) + .on('⎋', esc, true); + + d3_select(document) + .call(keybinding); + + selectError(); + + var sidebar = context.ui().sidebar; + sidebar.show(keepRightEditor.error(error)); + + context.map() + .on('drawn.select-error', selectError); + + + // class the error as selected, or return to browse mode if the error is gone + function selectError(drawn) { + if (!checkSelectedID()) return; + + var selection = context.surface() + .selectAll('.kr_error-' + selectedErrorID); + + if (selection.empty()) { + // Return to browse mode if selected DOM elements have + // disappeared because the user moved them out of view.. + var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent; + if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) { + context.enter(modeBrowse(context)); + } + + } else { + selection + .classed('selected', true); + + context.selectedErrorID(selectedErrorID); + } + } + + function esc() { + if (d3_select('.combobox').size()) return; + context.enter(modeBrowse(context)); + } + }; + + + mode.exit = function() { + behaviors.forEach(context.uninstall); + + d3_select(document) + .call(keybinding.unbind); + + context.surface() + .selectAll('.kr_error.selected') + .classed('selected hover', false); + + context.map() + .on('drawn.select-error', null); + + context.ui().sidebar + .hide(); + + context.selectedErrorID(null); + }; + + + return mode; +} diff --git a/modules/modes/select_note.js b/modules/modes/select_note.js index 4e5fd5d15..099d97a15 100644 --- a/modules/modes/select_note.js +++ b/modules/modes/select_note.js @@ -10,6 +10,8 @@ import { behaviorSelect } from '../behavior'; +import { t } from '../util/locale'; + import { modeBrowse, modeDragNode, modeDragNote } from '../modes'; import { services } from '../services'; import { uiNoteEditor } from '../ui'; @@ -72,6 +74,7 @@ export function modeSelectNote(context, selectedNoteID) { } else { selection .classed('selected', true); + context.selectedNoteID(selectedNoteID); } } @@ -83,9 +86,18 @@ export function modeSelectNote(context, selectedNoteID) { } - mode.newFeature = function(_) { + mode.zoomToSelected = function() { + if (!osm) return; + var note = osm.getNote(selectedNoteID); + if (note) { + context.map().centerZoom(note.loc, 20); + } + }; + + + mode.newFeature = function(val) { if (!arguments.length) return newFeature; - newFeature = _; + newFeature = val; return mode; }; @@ -95,7 +107,10 @@ export function modeSelectNote(context, selectedNoteID) { if (!note) return; behaviors.forEach(context.install); - keybinding.on('⎋', esc, true); + + keybinding + .on(t('inspector.zoom_to.key'), mode.zoomToSelected) + .on('⎋', esc, true); d3_select(document) .call(keybinding); diff --git a/modules/osm/index.js b/modules/osm/index.js index bbbb4835a..f8d7addc1 100644 --- a/modules/osm/index.js +++ b/modules/osm/index.js @@ -1,5 +1,6 @@ export { osmChangeset } from './changeset'; export { osmEntity } from './entity'; +export { krError } from './keepRight'; export { osmNode } from './node'; export { osmNote } from './note'; export { osmRelation } from './relation'; diff --git a/modules/osm/keepRight.js b/modules/osm/keepRight.js new file mode 100644 index 000000000..1ea2d4b25 --- /dev/null +++ b/modules/osm/keepRight.js @@ -0,0 +1,49 @@ +import _extend from 'lodash-es/extend'; + + +export function krError() { + if (!(this instanceof krError)) { + return (new krError()).initialize(arguments); + } else if (arguments.length) { + this.initialize(arguments); + } +} + + +krError.id = function() { + return krError.id.next--; +}; + + +krError.id.next = -1; + + +_extend(krError.prototype, { + + type: 'krError', + + initialize: function(sources) { + for (var i = 0; i < sources.length; ++i) { + var source = sources[i]; + for (var prop in source) { + if (Object.prototype.hasOwnProperty.call(source, prop)) { + if (source[prop] === undefined) { + delete this[prop]; + } else { + this[prop] = source[prop]; + } + } + } + } + + if (!this.id) { + this.id = krError.id() + ''; // as string + } + + return this; + }, + + update: function(attrs) { + return krError(this, attrs); // {v: 1 + (this.v || 0)} + } +}); \ No newline at end of file diff --git a/modules/renderer/map.js b/modules/renderer/map.js index ce737f891..e5ba177ec 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -351,10 +351,11 @@ export function rendererMap(context) { function editOff() { context.features().resetStats(); surface.selectAll('.layer-osm *').remove(); - surface.selectAll('.layer-touch *').remove(); + surface.selectAll('.layer-touch:not(.markers) *').remove(); var mode = context.mode(); - if (mode && mode.id !== 'save' && mode.id !== 'select-note' && mode.id !== 'select-data') { + if (mode && mode.id !== 'save' && mode.id !== 'select-note' && + mode.id !== 'select-data' && mode.id !== 'select-error') { context.enter(modeBrowse(context)); } @@ -857,7 +858,7 @@ export function rendererMap(context) { if (!isFinite(extent.area())) return; var z2 = map.trimmedExtentZoom(extent); - zoomLimits = zoomLimits || [context.minEditableZoom(), 19]; + zoomLimits = zoomLimits || [context.minEditableZoom(), 20]; map.centerZoom(extent.center(), Math.min(Math.max(z2, zoomLimits[0]), zoomLimits[1])); }; diff --git a/modules/services/index.js b/modules/services/index.js index 59c9d9524..8b75d7418 100644 --- a/modules/services/index.js +++ b/modules/services/index.js @@ -1,8 +1,10 @@ +import serviceKeepRight from './keepRight'; import serviceMapillary from './mapillary'; import serviceMapRules from './maprules'; import serviceNominatim from './nominatim'; import serviceOpenstreetcam from './openstreetcam'; import serviceOsm from './osm'; +import serviceOsmWikibase from './osm_wikibase'; import serviceStreetside from './streetside'; import serviceTaginfo from './taginfo'; import serviceVectorTile from './vector_tile'; @@ -12,9 +14,11 @@ import serviceWikipedia from './wikipedia'; export var services = { geocoder: serviceNominatim, + keepRight: serviceKeepRight, mapillary: serviceMapillary, openstreetcam: serviceOpenstreetcam, osm: serviceOsm, + osmWikibase: serviceOsmWikibase, maprules: serviceMapRules, streetside: serviceStreetside, taginfo: serviceTaginfo, @@ -24,11 +28,13 @@ export var services = { }; export { + serviceKeepRight, serviceMapillary, serviceMapRules, serviceNominatim, serviceOpenstreetcam, serviceOsm, + serviceOsmWikibase, serviceStreetside, serviceTaginfo, serviceVectorTile, diff --git a/modules/services/keepRight.js b/modules/services/keepRight.js new file mode 100644 index 000000000..cf3b70ef9 --- /dev/null +++ b/modules/services/keepRight.js @@ -0,0 +1,498 @@ +import _extend from 'lodash-es/extend'; +import _find from 'lodash-es/find'; +import _forEach from 'lodash-es/forEach'; + +import rbush from 'rbush'; + +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { json as d3_json } from 'd3-request'; +import { request as d3_request } from 'd3-request'; + +import { geoExtent, geoVecAdd } from '../geo'; +import { krError } from '../osm'; +import { t } from '../util/locale'; +import { utilRebind, utilTiler, utilQsString } from '../util'; + +import { errorTypes, localizeStrings } from '../../data/keepRight.json'; + + +var tiler = utilTiler(); +var dispatch = d3_dispatch('loaded'); + +var _krCache; +var _krZoom = 14; +var _krUrlRoot = 'https://www.keepright.at/'; + +var _krRuleset = [ + // no 20 - multiple node on same spot - these are mostly boundaries overlapping roads + 30, 40, 50, 60, 70, 90, 100, 110, 120, 130, 150, 160, 170, 180, + 190, 191, 192, 193, 194, 195, 196, 197, 198, + 200, 201, 202, 203, 204, 205, 206, 207, 208, 210, 220, + 230, 231, 232, 270, 280, 281, 282, 283, 284, 285, + 290, 291, 292, 293, 294, 295, 296, 297, 298, 300, 310, 311, 312, 313, + 320, 350, 360, 370, 380, 390, 400, 401, 402, 410, 411, 412, 413 +]; + + +function abortRequest(i) { + if (i) { + i.abort(); + } +} + +function abortUnwantedRequests(cache, tiles) { + _forEach(cache.inflight, function(v, k) { + var wanted = _find(tiles, function(tile) { + return k === tile.id; + }); + if (!wanted) { + abortRequest(v); + delete cache.inflight[k]; + } + }); +} + + +function encodeErrorRtree(d) { + return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d }; +} + + +// replace or remove error from rtree +function updateRtree(item, replace) { + _krCache.rtree.remove(item, function isEql(a, b) { + return a.data.id === b.data.id; + }); + + if (replace) { + _krCache.rtree.insert(item); + } +} + + +function tokenReplacements(d) { + if (!(d instanceof krError)) return; + + var htmlRegex = new RegExp(/<\/[a-z][\s\S]*>/); + var replacements = {}; + + var errorTemplate = errorTypes[d.which_type]; + if (!errorTemplate) { + /* eslint-disable no-console */ + console.log('No Template: ', d.which_type); + console.log(' ', d.description); + /* eslint-enable no-console */ + return; + } + + // some descriptions are just fixed text + if (!errorTemplate.regex) return; + + // regex pattern should match description with variable details captured + var errorRegex = new RegExp(errorTemplate.regex, 'i'); + var errorMatch = errorRegex.exec(d.description); + if (!errorMatch) { + /* eslint-disable no-console */ + console.log('Unmatched: ', d.which_type); + console.log(' ', d.description); + console.log(' ', errorRegex); + /* eslint-enable no-console */ + return; + } + + for (var i = 1; i < errorMatch.length; i++) { // skip first + var capture = errorMatch[i]; + var idType; + + idType = 'IDs' in errorTemplate ? errorTemplate.IDs[i-1] : ''; + if (idType && capture) { // link IDs if present in the capture + capture = parseError(capture, idType); + } else if (htmlRegex.test(capture)) { // escape any html in non-IDs + capture = '\\' + capture + '\\'; + } else { + var compare = capture.toLowerCase(); + if (localizeStrings[compare]) { // some replacement strings can be localized + capture = t('QA.keepRight.error_parts.' + localizeStrings[compare]); + } + } + + replacements['var' + i] = capture; + } + + return replacements; +} + + +function parseError(capture, idType) { + var compare = capture.toLowerCase(); + if (localizeStrings[compare]) { // some replacement strings can be localized + capture = t('QA.keepRight.error_parts.' + localizeStrings[compare]); + } + + switch (idType) { + // link a string like "this node" + case 'this': + capture = linkErrorObject(capture); + break; + + case 'url': + capture = linkURL(capture); + break; + + // link an entity ID + case 'n': + case 'w': + case 'r': + capture = linkEntity(idType + capture); + break; + + // some errors have more complex ID lists/variance + case '20': + capture = parse20(capture); + break; + case '211': + capture = parse211(capture); + break; + case '231': + capture = parse231(capture); + break; + case '294': + capture = parse294(capture); + break; + case '370': + capture = parse370(capture); + break; + } + + return capture; + + + function linkErrorObject(d) { + return '' + d + ''; + } + + function linkEntity(d) { + return '' + d + ''; + } + + function linkURL(d) { + return '' + d + ''; + } + + // arbitrary node list of form: #ID, #ID, #ID... + function parse211(capture) { + var newList = []; + var items = capture.split(', '); + + items.forEach(function(item) { + // ID has # at the front + var id = linkEntity('n' + item.slice(1)); + newList.push(id); + }); + + return newList.join(', '); + } + + // arbitrary way list of form: #ID(layer),#ID(layer),#ID(layer)... + function parse231(capture) { + var newList = []; + // unfortunately 'layer' can itself contain commas, so we split on '),' + var items = capture.split('),'); + + items.forEach(function(item) { + var match = item.match(/\#(\d+)\((.+)\)?/); + if (match !== null && match.length > 2) { + newList.push(linkEntity('w' + match[1]) + ' ' + + t('QA.keepRight.errorTypes.231.layer', { layer: match[2] }) + ); + } + }); + + return newList.join(', '); + } + + // arbitrary node/relation list of form: from node #ID,to relation #ID,to node #ID... + function parse294(capture) { + var newList = []; + var items = capture.split(','); + + items.forEach(function(item) { + var role; + var idType; + var id; + + // item of form "from/to node/relation #ID" + item = item.split(' '); + + // to/from role is more clear in quotes + role = '"' + item[0] + '"'; + + // first letter of node/relation provides the type + idType = item[1].slice(0,1); + + // ID has # at the front + id = item[2].slice(1); + id = linkEntity(idType + id); + + item = [role, item[1], id].join(' '); + newList.push(item); + }); + + return newList.join(', '); + } + + // may or may not include the string "(including the name 'name')" + function parse370(capture) { + if (!capture) return ''; + + var match = capture.match(/\(including the name (\'.+\')\)/); + if (match !== null && match.length) { + return t('QA.keepRight.errorTypes.370.including_the_name', { name: match[1] }); + } + return ''; + } + + // arbitrary node list of form: #ID,#ID,#ID... + function parse20(capture) { + var newList = []; + var items = capture.split(','); + + items.forEach(function(item) { + // ID has # at the front + var id = linkEntity('n' + item.slice(1)); + newList.push(id); + }); + + return newList.join(', '); + } +} + + +export default { + init: function() { + if (!_krCache) { + this.reset(); + } + + this.event = utilRebind(this, dispatch, 'on'); + }, + + reset: function() { + if (_krCache) { + _forEach(_krCache.inflight, abortRequest); + } + _krCache = { + data: {}, + loaded: {}, + inflight: {}, + closed: {}, + rtree: rbush() + }; + }, + + + // KeepRight API: http://osm.mueschelsoft.de/keepright/interfacing.php + loadErrors: function(projection) { + var options = { format: 'geojson' }; + var rules = _krRuleset.join(); + + // determine the needed tiles to cover the view + var tiles = tiler + .zoomExtent([_krZoom, _krZoom]) + .getTiles(projection); + + // abort inflight requests that are no longer needed + abortUnwantedRequests(_krCache, tiles); + + // issue new requests.. + tiles.forEach(function(tile) { + if (_krCache.loaded[tile.id] || _krCache.inflight[tile.id]) return; + + var rect = tile.extent.rectangle(); + var params = _extend({}, options, { left: rect[0], bottom: rect[3], right: rect[2], top: rect[1] }); + var url = _krUrlRoot + 'export.php?' + utilQsString(params) + '&ch=' + rules; + + _krCache.inflight[tile.id] = d3_json(url, + function(err, data) { + delete _krCache.inflight[tile.id]; + + if (err) return; + _krCache.loaded[tile.id] = true; + + if (!data.features || !data.features.length) return; + + data.features.forEach(function(feature) { + var loc = feature.geometry.coordinates; + var props = feature.properties; + + // if there is a parent, save its error type e.g.: + // Error 191 = "highway-highway" + // Error 190 = "intersections without junctions" (parent) + var errorType = props.error_type; + var errorTemplate = errorTypes[errorType]; + var parentErrorType = (Math.floor(errorType / 10) * 10).toString(); + + // try to handle error type directly, fallback to parent error type. + var whichType = errorTemplate ? errorType : parentErrorType; + var whichTemplate = errorTypes[whichType]; + + // Rewrite a few of the errors at this point.. + // This is done to make them easier to linkify and translate. + switch (whichType) { + case '170': + props.description = 'This feature has a FIXME tag: ' + props.description; + break; + case '292': + case '293': + props.description = props.description.replace('A turn-', 'This turn-'); + break; + case '294': + case '295': + case '296': + case '297': + case '298': + props.description = 'This turn-restriction~' + props.description; + break; + case '300': + props.description = 'This highway is missing a maxspeed tag'; + break; + case '411': + case '412': + case '413': + props.description = 'This feature~' + props.description; + break; + } + + // - move markers slightly so it doesn't obscure the geometry, + // - then move markers away from other coincident markers + var coincident = false; + do { + // first time, move marker up. after that, move marker right. + var delta = coincident ? [0.00001, 0] : [0, 0.00001]; + loc = geoVecAdd(loc, delta); + var bbox = geoExtent(loc).bbox(); + coincident = _krCache.rtree.search(bbox).length; + } while (coincident); + + var d = new krError({ + loc: loc, + id: props.error_id, + comment: props.comment || null, + description: props.description || '', + error_id: props.error_id, + which_type: whichType, + error_type: errorType, + parent_error_type: parentErrorType, + severity: whichTemplate.severity || 'error', + object_id: props.object_id, + object_type: props.object_type, + schema: props.schema, + title: props.title + }); + + d.replacements = tokenReplacements(d); + + _krCache.data[d.id] = d; + _krCache.rtree.insert(encodeErrorRtree(d)); + }); + + dispatch.call('loaded'); + } + ); + }); + }, + + + postKeepRightUpdate: function(d, callback) { + if (_krCache.inflight[d.id]) { + return callback({ message: 'Error update already inflight', status: -2 }, d); + } + + var that = this; + var params = { schema: d.schema, id: d.error_id }; + + if (d.state) { + params.st = d.state; + } + if (d.newComment !== undefined) { + params.co = d.newComment; + } + + // NOTE: This throws a CORS err, but it seems successful. + // We don't care too much about the response, so this is fine. + var url = _krUrlRoot + 'comment.php?' + utilQsString(params); + _krCache.inflight[d.id] = d3_request(url) + .post(function(err) { + delete _krCache.inflight[d.id]; + + if (d.state === 'ignore') { // ignore permanently (false positive) + that.removeError(d); + + } else if (d.state === 'ignore_t') { // ignore temporarily (error fixed) + that.removeError(d); + _krCache.closed[d.schema + ':' + d.error_id] = true; + + } else { + d = that.replaceError(d.update({ + comment: d.newComment, + newComment: undefined, + state: undefined + })); + } + + return callback(err, d); + }); + + }, + + + // get all cached errors covering the viewport + getErrors: function(projection) { + var viewport = projection.clipExtent(); + var min = [viewport[0][0], viewport[1][1]]; + var max = [viewport[1][0], viewport[0][1]]; + var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox(); + + return _krCache.rtree.search(bbox).map(function(d) { + return d.data; + }); + }, + + + // get a single error from the cache + getError: function(id) { + return _krCache.data[id]; + }, + + + // replace a single error in the cache + replaceError: function(error) { + if (!(error instanceof krError) || !error.id) return; + + _krCache.data[error.id] = error; + updateRtree(encodeErrorRtree(error), true); // true = replace + return error; + }, + + + // remove a single error from the cache + removeError: function(error) { + if (!(error instanceof krError) || !error.id) return; + + delete _krCache.data[error.id]; + updateRtree(encodeErrorRtree(error), false); // false = remove + }, + + + errorURL: function(error) { + return _krUrlRoot + 'report_map.php?schema=' + error.schema + '&error=' + error.id; + }, + + + // Get an array of errors closed during this session. + // Used to populate `closed:keepright` changeset tag + getClosedIDs: function() { + return Object.keys(_krCache.closed).sort(); + } + +}; diff --git a/modules/services/osm.js b/modules/services/osm.js index 9e4f355a8..9b5166859 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -47,7 +47,7 @@ var oauth = osmAuth({ var _blacklists = ['.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*']; var _tileCache = { loaded: {}, inflight: {}, seen: {} }; -var _noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, rtree: rbush() }; +var _noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: rbush() }; var _userCache = { toLoad: {}, user: {} }; var _changeset = {}; @@ -385,7 +385,7 @@ export default { if (_changeset.inflight) abortRequest(_changeset.inflight); _tileCache = { loaded: {}, inflight: {}, seen: {} }; - _noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, rtree: rbush() }; + _noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: rbush() }; _userCache = { toLoad: {}, user: {} }; _changeset = {}; @@ -432,6 +432,11 @@ export default { }, + noteReportURL: function(note) { + return urlroot + '/reports/new?reportable_type=Note&reportable_id=' + note.id; + }, + + // Generic method to load data from the OSM API // Can handle either auth or unauth calls. loadFromAPI: function(path, callback, options) { @@ -951,6 +956,13 @@ export default { // we get the updated note back, remove from caches and reparse.. this.removeNote(note); + // update closed note cache - used to populate `closed:note` changeset tag + if (action === 'close') { + _noteCache.closed[note.id] = true; + } else if (action === 'reopen') { + delete _noteCache.closed[note.id]; + } + var options = { skipSeen: false }; return parseXML(xml, function(err, results) { if (err) { @@ -1113,6 +1125,13 @@ export default { _noteCache.note[note.id] = note; updateRtree(encodeNoteRtree(note), true); // true = replace return note; + }, + + + // Get an array of note IDs closed during this session. + // Used to populate `closed:note` changeset tag + getClosedIDs: function() { + return Object.keys(_noteCache.closed).sort(); } }; diff --git a/modules/services/osm_wikibase.js b/modules/services/osm_wikibase.js new file mode 100644 index 000000000..1ef3637d7 --- /dev/null +++ b/modules/services/osm_wikibase.js @@ -0,0 +1,201 @@ +import _debounce from 'lodash-es/debounce'; +import _forEach from 'lodash-es/forEach'; + +import { json as d3_json } from 'd3-request'; + +import { utilQsString } from '../util'; + + +var apibase = 'https://wiki.openstreetmap.org/w/api.php'; +var _inflight = {}; +var _wikibaseCache = {}; +var _localeIDs = { en: false }; + + +var debouncedRequest = _debounce(request, 500, { leading: false }); + +function request(url, callback) { + if (_inflight[url]) return; + + _inflight[url] = d3_json(url, function (err, data) { + delete _inflight[url]; + callback(err, data); + }); +} + + +/** + * Get the best string value from the descriptions/labels result + * Note that if mediawiki doesn't recognize language code, it will return all values. + * In that case, fallback to use English. + * @param values object - either descriptions or labels + * @param langCode String + * @returns localized string + */ +function localizedToString(values, langCode) { + if (values) { + values = values[langCode] || values.en; + } + return values ? values.value : ''; +} + + +export default { + + init: function() { + _inflight = {}; + _wikibaseCache = {}; + _localeIDs = {}; + }, + + + reset: function() { + _forEach(_inflight, function(req) { req.abort(); }); + _inflight = {}; + }, + + + /** + * Get the best value for the property, or undefined if not found + * @param entity object from wikibase + * @param property string e.g. 'P4' for image + * @param langCode string e.g. 'fr' for French + */ + claimToValue: function(entity, property, langCode) { + if (!entity.claims[property]) return undefined; + var locale = _localeIDs[langCode]; + var preferredPick, localePick; + _forEach(entity.claims[property], function(stmt) { + // If exists, use value limited to the needed language (has a qualifier P26 = locale) + // Or if not found, use the first value with the "preferred" rank + if (!preferredPick && stmt.rank === 'preferred') { + preferredPick = stmt; + } + if (locale && stmt.qualifiers && stmt.qualifiers.P26 && + stmt.qualifiers.P26[0].datavalue.value.id === locale + ) { + localePick = stmt; + } + }); + var result = localePick || preferredPick; + + if (result) { + var datavalue = result.mainsnak.datavalue; + return datavalue.type === 'wikibase-entityid' ? datavalue.value.id : datavalue.value; + } else { + return undefined; + } + }, + + + toSitelink: function(key, value) { + var result = value ? 'Tag:' + key + '=' + value : 'Key:' + key; + return result.replace(/_/g, ' ').trim(); + }, + + + getEntity: function(params, callback) { + var doRequest = params.debounce ? debouncedRequest : request; + var self = this; + var titles = []; + var result = {}; + var keySitelink = this.toSitelink(params.key); + var tagSitelink = params.value ? this.toSitelink(params.key, params.value) : false; + var localeSitelink; + + if (params.langCode && _localeIDs[params.langCode] === undefined) { + // If this is the first time we are asking about this locale, + // fetch corresponding entity (if it exists), and cache it. + // If there is no such entry, cache `false` value to avoid re-requesting it. + localeSitelink = ('Locale:' + params.langCode).replace(/_/g, ' ').trim(); + titles.push(localeSitelink); + } + + if (_wikibaseCache[keySitelink]) { + result.key = _wikibaseCache[keySitelink]; + } else { + titles.push(keySitelink); + } + + if (tagSitelink) { + if (_wikibaseCache[tagSitelink]) { + result.tag = _wikibaseCache[tagSitelink]; + } else { + titles.push(tagSitelink); + } + } + + if (!titles.length) { + // Nothing to do, we already had everything in the cache + return callback(null, result); + } + + // Requesting just the user language code + // If backend recognizes the code, it will perform proper fallbacks, + // and the result will contain the requested code. If not, all values are returned: + // {"zh-tw":{"value":"...","language":"zh-tw","source-language":"zh-hant"} + // {"pt-br":{"value":"...","language":"pt","for-language":"pt-br"}} + var obj = { + action: 'wbgetentities', + sites: 'wiki', + titles: titles.join('|'), + languages: params.langCode, + languagefallback: 1, + origin: '*', + format: 'json', + // There is an MW Wikibase API bug https://phabricator.wikimedia.org/T212069 + // We shouldn't use v1 until it gets fixed, but should switch to it afterwards + // formatversion: 2, + }; + + var url = apibase + '?' + utilQsString(obj); + doRequest(url, function(err, d) { + if (err) { + callback(err); + } else if (!d.success || d.error) { + callback(d.error.messages.map(function(v) { return v.html['*']; }).join('
')); + } else { + var localeID = false; + _forEach(d.entities, function(res) { + if (res.missing !== '') { + var title = res.sitelinks.wiki.title; + // Simplify access to the localized values + res.description = localizedToString(res.descriptions, params.langCode); + res.label = localizedToString(res.labels, params.langCode); + if (title === keySitelink) { + _wikibaseCache[keySitelink] = res; + result.key = res; + } else if (title === tagSitelink) { + _wikibaseCache[tagSitelink] = res; + result.tag = res; + } else if (title === localeSitelink) { + localeID = res.id; + } else { + console.log('Unexpected title ' + title); // eslint-disable-line no-console + } + } + }); + + if (localeSitelink) { + // If locale ID is not found, store false to prevent repeated queries + self.addLocale(params.langCode, localeID); + } + + callback(null, result); + } + }); + }, + + + addLocale: function(langCode, qid) { + // Makes it easier to unit test + _localeIDs[langCode] = qid; + }, + + apibase: function(val) { + if (!arguments.length) return apibase; + apibase = val; + return this; + } + +}; diff --git a/modules/svg/geolocate.js b/modules/svg/geolocate.js index f5ca10f97..9b025e586 100644 --- a/modules/svg/geolocate.js +++ b/modules/svg/geolocate.js @@ -2,9 +2,9 @@ import { select as d3_select } from 'd3-selection'; import { svgPointTransform } from './helpers'; import { geoMetersToLat } from '../geo'; -import _throttle from 'lodash-es/throttle'; -export function svgGeolocate(projection, context, dispatch) { + +export function svgGeolocate(projection) { var layer = d3_select(null); var _position; diff --git a/modules/svg/index.js b/modules/svg/index.js index b444bd7a0..82d6bfe9a 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -2,6 +2,7 @@ export { svgAreas } from './areas.js'; export { svgData } from './data.js'; export { svgDebug } from './debug.js'; export { svgDefs } from './defs.js'; +export { svgKeepRight } from './keepRight'; export { svgIcon } from './icon.js'; export { svgGeolocate } from './geolocate'; export { svgLabels } from './labels.js'; diff --git a/modules/svg/keepRight.js b/modules/svg/keepRight.js new file mode 100644 index 000000000..c4b8807ab --- /dev/null +++ b/modules/svg/keepRight.js @@ -0,0 +1,245 @@ +import _throttle from 'lodash-es/throttle'; +import { select as d3_select } from 'd3-selection'; + +import { modeBrowse } from '../modes'; +import { svgPointTransform } from './index'; +import { services } from '../services'; + +var _keepRightEnabled = false; +var _keepRightService; + + +export function svgKeepRight(projection, context, dispatch) { + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var touchLayer = d3_select(null); + var drawLayer = d3_select(null); + var _keepRightVisible = false; + + + function markerPath(selection, klass) { + selection + .attr('class', klass) + .attr('transform', 'translate(-4, -24)') + .attr('d', 'M11.6,6.2H7.1l1.4-5.1C8.6,0.6,8.1,0,7.5,0H2.2C1.7,0,1.3,0.3,1.3,0.8L0,10.2c-0.1,0.6,0.4,1.1,0.9,1.1h4.6l-1.8,7.6C3.6,19.4,4.1,20,4.7,20c0.3,0,0.6-0.2,0.8-0.5l6.9-11.9C12.7,7,12.3,6.2,11.6,6.2z'); + } + + + // Loosely-coupled keepRight service for fetching errors. + function getService() { + if (services.keepRight && !_keepRightService) { + _keepRightService = services.keepRight; + _keepRightService.on('loaded', throttledRedraw); + } else if (!services.keepRight && _keepRightService) { + _keepRightService = null; + } + + return _keepRightService; + } + + + // Show the errors + function editOn() { + if (!_keepRightVisible) { + _keepRightVisible = true; + drawLayer + .style('display', 'block'); + } + } + + + // Immediately remove the errors and their touch targets + function editOff() { + if (_keepRightVisible) { + _keepRightVisible = false; + drawLayer + .style('display', 'none'); + drawLayer.selectAll('.kr_error') + .remove(); + touchLayer.selectAll('.kr_error') + .remove(); + } + } + + + // Enable the layer. This shows the errors and transitions them to visible. + function layerOn() { + editOn(); + + drawLayer + .style('opacity', 0) + .transition() + .duration(250) + .style('opacity', 1) + .on('end interrupt', function () { + dispatch.call('change'); + }); + } + + + // Disable the layer. This transitions the layer invisible and then hides the errors. + function layerOff() { + throttledRedraw.cancel(); + drawLayer.interrupt(); + touchLayer.selectAll('.kr_error') + .remove(); + + drawLayer + .transition() + .duration(250) + .style('opacity', 0) + .on('end interrupt', function () { + editOff(); + dispatch.call('change'); + }); + } + + + // Update the error markers + function updateMarkers() { + if (!_keepRightVisible || !_keepRightEnabled) return; + + var service = getService(); + var selectedID = context.selectedErrorID(); + var data = (service ? service.getErrors(projection) : []); + var getTransform = svgPointTransform(projection); + + // Draw markers.. + var markers = drawLayer.selectAll('.kr_error') + .data(data, function(d) { return d.id; }); + + // exit + markers.exit() + .remove(); + + // enter + var markersEnter = markers.enter() + .append('g') + .attr('class', function(d) { + return 'kr_error kr_error-' + d.id + ' kr_error_type_' + d.parent_error_type; } + ); + + markersEnter + .append('ellipse') + .attr('cx', 0.5) + .attr('cy', 1) + .attr('rx', 6.5) + .attr('ry', 3) + .attr('class', 'stroke'); + + markersEnter + .append('path') + .call(markerPath, 'shadow'); + + markersEnter + .append('use') + .attr('class', 'kr_error-fill') + .attr('width', '20px') + .attr('height', '20px') + .attr('x', '-8px') + .attr('y', '-22px') + .attr('xlink:href', '#iD-icon-bolt'); + + // update + markers + .merge(markersEnter) + .sort(sortY) + .classed('selected', function(d) { return d.id === selectedID; }) + .attr('transform', getTransform); + + + // Draw targets.. + if (touchLayer.empty()) return; + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + + var targets = touchLayer.selectAll('.kr_error') + .data(data, function(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('rect') + .attr('width', '20px') + .attr('height', '20px') + .attr('x', '-8px') + .attr('y', '-22px') + .merge(targets) + .sort(sortY) + .attr('class', function(d) { + return 'kr_error target kr_error-' + d.id + ' ' + fillClass; + }) + .attr('transform', getTransform); + + + function sortY(a, b) { + return (a.id === selectedID) ? 1 + : (b.id === selectedID) ? -1 + : (a.severity === 'error' && b.severity !== 'error') ? 1 + : (b.severity === 'error' && a.severity !== 'error') ? -1 + : b.loc[1] - a.loc[1]; + } + } + + + // Draw the keepRight layer and schedule loading errors and updating markers. + function drawKeepRight(selection) { + var service = getService(); + + var surface = context.surface(); + if (surface && !surface.empty()) { + touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers'); + } + + drawLayer = selection.selectAll('.layer-keepRight') + .data(service ? [0] : []); + + drawLayer.exit() + .remove(); + + drawLayer = drawLayer.enter() + .append('g') + .attr('class', 'layer-keepRight') + .style('display', _keepRightEnabled ? 'block' : 'none') + .merge(drawLayer); + + if (_keepRightEnabled) { + if (service && ~~context.map().zoom() >= minZoom) { + editOn(); + service.loadErrors(projection); + updateMarkers(); + } else { + editOff(); + } + } + } + + + // Toggles the layer on and off + drawKeepRight.enabled = function(val) { + if (!arguments.length) return _keepRightEnabled; + + _keepRightEnabled = val; + if (_keepRightEnabled) { + layerOn(); + } else { + layerOff(); + if (context.selectedErrorID()) { + context.enter(modeBrowse(context)); + } + } + + dispatch.call('change'); + return this; + }; + + + drawKeepRight.supported = function() { + return !!getService(); + }; + + + return drawKeepRight; +} diff --git a/modules/svg/layers.js b/modules/svg/layers.js index 4d0b9f7d2..59c9ab22c 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -10,6 +10,7 @@ import { select as d3_select } from 'd3-selection'; import { svgData } from './data'; import { svgDebug } from './debug'; import { svgGeolocate } from './geolocate'; +import { svgKeepRight } from './keepRight'; import { svgStreetside } from './streetside'; import { svgMapillaryImages } from './mapillary_images'; import { svgMapillarySigns } from './mapillary_signs'; @@ -28,6 +29,7 @@ export function svgLayers(projection, context) { { id: 'osm', layer: svgOsm(projection, context, dispatch) }, { id: 'notes', layer: svgNotes(projection, context, dispatch) }, { id: 'data', layer: svgData(projection, context, dispatch) }, + { id: 'keepRight', layer: svgKeepRight(projection, context, dispatch) }, { id: 'streetside', layer: svgStreetside(projection, context, dispatch)}, { id: 'mapillary-images', layer: svgMapillaryImages(projection, context, dispatch) }, { id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) }, diff --git a/modules/svg/notes.js b/modules/svg/notes.js index bc97adb55..d49fb3223 100644 --- a/modules/svg/notes.js +++ b/modules/svg/notes.js @@ -8,12 +8,18 @@ import { svgPointTransform } from './index'; import { services } from '../services'; +var _notesEnabled = false; +var _osmService; + + export function svgNotes(projection, context, dispatch) { if (!dispatch) { dispatch = d3_dispatch('change'); } var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); var minZoom = 12; - var layer = d3_select(null); - var _notes; + var touchLayer = d3_select(null); + var drawLayer = d3_select(null); + var _notesVisible = false; + function markerPath(selection, klass) { selection @@ -22,40 +28,49 @@ export function svgNotes(projection, context, dispatch) { .attr('d', 'm17.5,0l-15,0c-1.37,0 -2.5,1.12 -2.5,2.5l0,11.25c0,1.37 1.12,2.5 2.5,2.5l3.75,0l0,3.28c0,0.38 0.43,0.6 0.75,0.37l4.87,-3.65l5.62,0c1.37,0 2.5,-1.12 2.5,-2.5l0,-11.25c0,-1.37 -1.12,-2.5 -2.5,-2.5z'); } - function init() { - if (svgNotes.initialized) return; // run once - svgNotes.enabled = false; - svgNotes.initialized = true; - } - - function editOn() { - layer.style('display', 'block'); - } - - - function editOff() { - layer.selectAll('.note').remove(); - layer.style('display', 'none'); - } - + // Loosely-coupled osm service for fetching notes. function getService() { - if (services.osm && !_notes) { - _notes = services.osm; - _notes.on('loadedNotes', throttledRedraw); - } else if (!services.osm && _notes) { - _notes = null; + if (services.osm && !_osmService) { + _osmService = services.osm; + _osmService.on('loadedNotes', throttledRedraw); + } else if (!services.osm && _osmService) { + _osmService = null; } - return _notes; + return _osmService; } - function showLayer() { + // Show the notes + function editOn() { + if (!_notesVisible) { + _notesVisible = true; + drawLayer + .style('display', 'block'); + } + } + + + // Immediately remove the notes and their touch targets + function editOff() { + if (_notesVisible) { + _notesVisible = false; + drawLayer + .style('display', 'none'); + drawLayer.selectAll('.note') + .remove(); + touchLayer.selectAll('.note') + .remove(); + } + } + + + // Enable the layer. This shows the notes and transitions them to visible. + function layerOn() { editOn(); - layer - .classed('disabled', false) + drawLayer .style('opacity', 0) .transition() .duration(250) @@ -66,30 +81,35 @@ export function svgNotes(projection, context, dispatch) { } - function hideLayer() { - editOff(); - + // Disable the layer. This transitions the layer invisible and then hides the notes. + function layerOff() { throttledRedraw.cancel(); - layer.interrupt(); + drawLayer.interrupt(); + touchLayer.selectAll('.note') + .remove(); - layer + drawLayer .transition() .duration(250) .style('opacity', 0) .on('end interrupt', function () { - layer.classed('disabled', true); + editOff(); dispatch.call('change'); }); - } - function update() { + // Update the note markers + function updateMarkers() { + if (!_notesVisible || !_notesEnabled) return; + var service = getService(); var selectedID = context.selectedNoteID(); var data = (service ? service.notes(projection) : []); - var transform = svgPointTransform(projection); - var notes = layer.selectAll('.note') + var getTransform = svgPointTransform(projection); + + // Draw markers.. + var notes = drawLayer.selectAll('.note') .data(data, function(d) { return d.status + d.id; }); // exit @@ -139,55 +159,90 @@ export function svgNotes(projection, context, dispatch) { // update notes .merge(notesEnter) - .sort(function(a, b) { - return (a.id === selectedID) ? 1 - : (b.id === selectedID) ? -1 - : b.loc[1] - a.loc[1]; // sort Y + .sort(sortY) + .classed('selected', function(d) { + var mode = context.mode(); + var isMoving = mode && mode.id === 'drag-note'; // no shadows when dragging + return !isMoving && d.id === selectedID; }) - .classed('selected', function(d) { return d.id === selectedID; }) - .attr('transform', transform); + .attr('transform', getTransform); + + + // Draw targets.. + if (touchLayer.empty()) return; + var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + + var targets = touchLayer.selectAll('.note') + .data(data, function(d) { return d.id; }); + + // exit + targets.exit() + .remove(); + + // enter/update + targets.enter() + .append('rect') + .attr('width', '20px') + .attr('height', '20px') + .attr('x', '-8px') + .attr('y', '-22px') + .merge(targets) + .sort(sortY) + .attr('class', function(d) { + var newClass = (d.id < 0 ? 'new' : ''); + return 'note target note-' + d.id + ' ' + fillClass + newClass; + }) + .attr('transform', getTransform); + + + function sortY(a, b) { + return (a.id === selectedID) ? 1 : (b.id === selectedID) ? -1 : b.loc[1] - a.loc[1]; + } } + // Draw the notes layer and schedule loading notes and updating markers. function drawNotes(selection) { - var enabled = svgNotes.enabled; var service = getService(); - layer = selection.selectAll('.layer-notes') - .data(service ? [0] : []); - - layer.exit() - .remove(); - - layer.enter() - .append('g') - .attr('class', 'layer-notes') - .style('display', enabled ? 'block' : 'none') - .merge(layer); - - function dimensions() { - return [window.innerWidth, window.innerHeight]; + var surface = context.surface(); + if (surface && !surface.empty()) { + touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers'); } - if (enabled) { + drawLayer = selection.selectAll('.layer-notes') + .data(service ? [0] : []); + + drawLayer.exit() + .remove(); + + drawLayer = drawLayer.enter() + .append('g') + .attr('class', 'layer-notes') + .style('display', _notesEnabled ? 'block' : 'none') + .merge(drawLayer); + + if (_notesEnabled) { if (service && ~~context.map().zoom() >= minZoom) { editOn(); - service.loadNotes(projection, dimensions()); - update(); + service.loadNotes(projection); + updateMarkers(); } else { editOff(); } } } - drawNotes.enabled = function(val) { - if (!arguments.length) return svgNotes.enabled; - svgNotes.enabled = val; - if (svgNotes.enabled) { - showLayer(); + // Toggles the layer on and off + drawNotes.enabled = function(val) { + if (!arguments.length) return _notesEnabled; + + _notesEnabled = val; + if (_notesEnabled) { + layerOn(); } else { - hideLayer(); + layerOff(); if (context.selectedNoteID()) { context.enter(modeBrowse(context)); } @@ -197,6 +252,6 @@ export function svgNotes(projection, context, dispatch) { return this; }; - init(); + return drawNotes; } diff --git a/modules/svg/touch.js b/modules/svg/touch.js index 96bb1c871..860f95bd1 100644 --- a/modules/svg/touch.js +++ b/modules/svg/touch.js @@ -2,7 +2,7 @@ export function svgTouch() { function drawTouch(selection) { selection.selectAll('.layer-touch') - .data(['areas', 'lines', 'points', 'turns', 'notes']) + .data(['areas', 'lines', 'points', 'turns', 'markers']) .enter() .append('g') .attr('class', function(d) { return 'layer-touch ' + d; }); diff --git a/modules/ui/combobox.js b/modules/ui/combobox.js index 6a2e0174a..961b9adb6 100644 --- a/modules/ui/combobox.js +++ b/modules/ui/combobox.js @@ -56,15 +56,12 @@ export function uiCombobox(context, klass) { var parent = this.parentNode; var sibling = this.nextSibling; - var caret = d3_select(parent).selectAll('.combobox-caret') + d3_select(parent).selectAll('.combobox-caret') .filter(function(d) { return d === input.node(); }) - .data([input.node()]); - - caret = caret.enter() + .data([input.node()]) + .enter() .insert('div', function() { return sibling; }) - .attr('class', 'combobox-caret') - .merge(caret); - + .attr('class', 'combobox-caret'); } @@ -341,7 +338,7 @@ export function uiCombobox(context, klass) { // Dispatches an 'accept' event if an option has been chosen. // Then hides the combobox. function accept(d) { - d = d || _choice; + d = d || _choice || value(); if (d) { utilGetSetValue(input, d.value); utilTriggerEvent(input, 'change'); diff --git a/modules/ui/commit.js b/modules/ui/commit.js index 5d818a152..46201e30c 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -8,6 +8,7 @@ import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; import { osmChangeset } from '../osm'; +import { services } from '../services'; import { uiChangesetEditor } from './changeset_editor'; import { uiCommitChanges } from './commit_changes'; import { uiCommitWarnings } from './commit_warnings'; @@ -63,6 +64,8 @@ export function uiCommit(context) { } var tags; + // Initialize changeset if one does not exist yet. + // Also pull values from local storage. if (!_changeset) { var detected = utilDetect(); tags = { @@ -81,13 +84,9 @@ export function uiCommit(context) { tags.hashtags = hashtags; } - // iD 2.8.1 could write a literal 'undefined' here.. see #5021 - // (old source values expire after 2 days, so 'undefined' checks can go away in v2.9) var source = context.storage('source'); - if (source && source !== 'undefined') { + if (source) { tags.source = source; - } else if (source === 'undefined') { - context.storage('source', null); } _changeset = new osmChangeset({ tags: tags }); @@ -95,8 +94,22 @@ export function uiCommit(context) { tags = _clone(_changeset.tags); + // assign tags for imagery used var imageryUsed = context.history().imageryUsed().join(';').substr(0, 255); tags.imagery_used = imageryUsed || 'None'; + + // assign tags for closed issues and notes + var osmClosed = osm.getClosedIDs(); + if (osmClosed.length) { + tags['closed:note'] = osmClosed.join(';').substr(0, 255); + } + if (services.keepRight) { + var krClosed = services.keepRight.getClosedIDs(); + if (krClosed.length) { + tags['closed:keepright'] = krClosed.join(';').substr(0, 255); + } + } + _changeset = _changeset.update({ tags: tags }); var header = selection.selectAll('.header') @@ -109,17 +122,17 @@ export function uiCommit(context) { headerTitle .append('div') .attr('class', 'header-block header-block-outer'); - + headerTitle .append('div') .attr('class', 'header-block') .append('h3') .text(t('commit.title')); - + headerTitle .append('div') .attr('class', 'header-block header-block-outer header-block-close') - .append('button') + .append('button') .attr('class', 'close') .on('click', function() { context.enter(modeBrowse(context)); }) .call(svgIcon('#iD-icon-close')); diff --git a/modules/ui/commit_warnings.js b/modules/ui/commit_warnings.js index c9f508530..a022e3b6c 100644 --- a/modules/ui/commit_warnings.js +++ b/modules/ui/commit_warnings.js @@ -26,7 +26,9 @@ export function uiCommitWarnings(context) { }, {}); _forEach(issues, function(instances, severity) { - instances = _uniqBy(instances, function(val) { return val.id + '_' + val.message.replace(/\s+/g,''); }); + instances = _uniqBy(instances, function(val) { + return val.entity || (val.id + '_' + val.message.replace(/\s+/g,'')); + }); var section = severity + '-section'; var instanceItem = severity + '-item'; diff --git a/modules/ui/data_editor.js b/modules/ui/data_editor.js index 593517de6..59d25bb3f 100644 --- a/modules/ui/data_editor.js +++ b/modules/ui/data_editor.js @@ -4,17 +4,33 @@ import { svgIcon } from '../svg'; import { uiDataHeader, - uiRawTagEditor + uiQuickLinks, + uiRawTagEditor, + uiTooltipHtml } from './index'; export function uiDataEditor(context) { var dataHeader = uiDataHeader(); + var quickLinks = uiQuickLinks(); var rawTagEditor = uiRawTagEditor(context); var _datum; function dataEditor(selection) { + // quick links + var choices = [{ + id: 'zoom_to', + label: 'inspector.zoom_to.title', + tooltip: function() { + return uiTooltipHtml(t('inspector.zoom_to.tooltip_data'), t('inspector.zoom_to.key')); + }, + click: function zoomTo() { + context.mode().zoomToSelected(); + } + }]; + + var header = selection.selectAll('.header') .data([0]); @@ -50,14 +66,15 @@ export function uiDataEditor(context) { .append('div') .attr('class', 'modal-section data-editor') .merge(editor) - .call(dataHeader.datum(_datum)); + .call(dataHeader.datum(_datum)) + .call(quickLinks.choices(choices)); var rte = body.selectAll('.raw-tag-editor') .data([0]); rte.enter() .append('div') - .attr('class', 'inspector-border raw-tag-editor inspector-inner data-editor') + .attr('class', 'raw-tag-editor inspector-inner data-editor') .merge(rte) .call(rawTagEditor .expanded(true) diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index 8c99a45b2..f74272964 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -104,7 +104,7 @@ export function uiEditMenu(context, operations) { .attr('transform', function () { return 'translate(' + [2 * p, 5] + ')'; }) .attr('xlink:href', function (d) { return '#iD-operation-' + d.id; }); - tooltip = d3_select(document.body) + tooltip = d3_select('#id-container') .append('div') .attr('class', 'tooltip-inner edit-menu-tooltip'); diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index eda779b25..880b92142 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -15,12 +15,14 @@ import { actionChangeTags } from '../actions'; import { modeBrowse } from '../modes'; import { svgIcon } from '../svg'; import { uiPresetIcon } from './preset_icon'; +import { uiQuickLinks } from './quick_links'; import { uiRawMemberEditor } from './raw_member_editor'; import { uiRawMembershipEditor } from './raw_membership_editor'; import { uiRawTagEditor } from './raw_tag_editor'; import { uiTagReference } from './tag_reference'; import { uiPresetEditor } from './preset_editor'; import { uiEntityIssues } from './entity_issues'; +import { uiTooltipHtml } from './tooltipHtml'; import { utilCleanTags, utilRebind } from '../util'; @@ -35,6 +37,7 @@ export function uiEntityEditor(context) { var _tagReference; var entityIssues = uiEntityIssues(context); + var quickLinks = uiQuickLinks(); var presetEditor = uiPresetEditor(context).on('change', changeTags); var rawTagEditor = uiRawTagEditor(context).on('change', changeTags); var rawMemberEditor = uiRawMemberEditor(context); @@ -49,28 +52,28 @@ export function uiEntityEditor(context) { .data([0]); // Enter - var enter = header.enter() + var headerEnter = header.enter() .append('div') .attr('class', 'header fillL cf'); - enter + headerEnter .append('button') .attr('class', 'fl preset-reset preset-choose') .call(svgIcon((textDirection === 'rtl') ? '#iD-icon-forward' : '#iD-icon-backward')); - enter + headerEnter .append('button') .attr('class', 'fr preset-close') .on('click', function() { context.enter(modeBrowse(context)); }) .call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close')); - enter + headerEnter .append('h3') .text(t('inspector.edit')); // Update header = header - .merge(enter); + .merge(headerEnter); header.selectAll('.preset-reset') .on('click', function() { @@ -83,11 +86,11 @@ export function uiEntityEditor(context) { .data([0]); // Enter - enter = body.enter() + var bodyEnter = body.enter() .append('div') .attr('class', 'inspector-body'); - enter + bodyEnter .append('div') .attr('class', 'preset-list-item inspector-inner') .append('div') @@ -100,27 +103,31 @@ export function uiEntityEditor(context) { .append('div') .attr('class', 'label-inner'); - enter + bodyEnter .append('div') - .attr('class', 'inspector-border entity-issues'); + .attr('class', 'preset-quick-links'); - enter + bodyEnter .append('div') - .attr('class', 'inspector-border preset-editor'); + .attr('class', 'entity-issues'); - enter + bodyEnter .append('div') - .attr('class', 'inspector-border raw-tag-editor inspector-inner'); + .attr('class', 'preset-editor'); - enter + bodyEnter .append('div') - .attr('class', 'inspector-border raw-member-editor inspector-inner'); + .attr('class', 'raw-tag-editor inspector-inner'); - enter + bodyEnter + .append('div') + .attr('class', 'raw-member-editor inspector-inner'); + + bodyEnter .append('div') .attr('class', 'raw-membership-editor inspector-inner'); - enter + bodyEnter .append('input') .attr('type', 'text') .attr('class', 'key-trap'); @@ -128,8 +135,9 @@ export function uiEntityEditor(context) { // Update body = body - .merge(enter); + .merge(bodyEnter); + // update header if (_tagReference) { body.selectAll('.preset-list-button-wrap') .call(_tagReference.button); @@ -149,7 +157,6 @@ export function uiEntityEditor(context) { .preset(_activePreset) ); - var label = body.select('.label-inner'); var nameparts = label.selectAll('.namepart') .data(_activePreset.name().split(' - '), function(d) { return d; }); @@ -168,6 +175,23 @@ export function uiEntityEditor(context) { .entityID(_entityID) ); + // update quick links + var choices = [{ + id: 'zoom_to', + label: 'inspector.zoom_to.title', + tooltip: function() { + return uiTooltipHtml(t('inspector.zoom_to.tooltip_feature'), t('inspector.zoom_to.key')); + }, + click: function zoomTo() { + context.mode().zoomToSelected(); + } + }]; + + body.select('.preset-quick-links') + .call(quickLinks.choices(choices)); + + + // update editor sections body.select('.preset-editor') .call(presetEditor .preset(_activePreset) @@ -274,25 +298,25 @@ export function uiEntityEditor(context) { } - entityEditor.modified = function(_) { + entityEditor.modified = function(val) { if (!arguments.length) return _modified; - _modified = _; + _modified = val; d3_selectAll('button.preset-close use') .attr('xlink:href', (_modified ? '#iD-icon-apply' : '#iD-icon-close')); return entityEditor; }; - entityEditor.state = function(_) { + entityEditor.state = function(val) { if (!arguments.length) return _state; - _state = _; + _state = val; return entityEditor; }; - entityEditor.entityID = function(_) { + entityEditor.entityID = function(val) { if (!arguments.length) return _entityID; - _entityID = _; + _entityID = val; _base = context.graph(); _coalesceChanges = false; @@ -310,10 +334,10 @@ export function uiEntityEditor(context) { }; - entityEditor.preset = function(_) { + entityEditor.preset = function(val) { if (!arguments.length) return _activePreset; - if (_ !== _activePreset) { - _activePreset = _; + if (val !== _activePreset) { + _activePreset = val; _tagReference = uiTagReference(_activePreset.reference(context.geometry(_entityID)), context) .showing(false); } diff --git a/modules/ui/field.js b/modules/ui/field.js index ccccb66eb..856b9e41a 100644 --- a/modules/ui/field.js +++ b/modules/ui/field.js @@ -231,38 +231,38 @@ export function uiField(context, presetField, entity, options) { } }; + // A shown field has a visible UI, a non-shown field is in the 'Add field' dropdown field.isShown = function() { return _show || isPresent(); }; + // An allowed field can appear in the UI or in the 'Add field' dropdown. // A non-allowed field is hidden from the user altogether field.isAllowed = function() { + if (!entity || isPresent()) return true; // a field with a value should always display - if (isPresent()) { - // always allow a field with a value to display - return true; - } + var latest = context.hasEntity(entity.id); // check the most current copy of the entity + if (!latest) return true; - var prerequisiteTag = field.prerequisiteTag; - if (prerequisiteTag && prerequisiteTag.key && field.entityID && context.hasEntity(field.entityID)) { - var value = context.entity(field.entityID).tags[prerequisiteTag.key]; - if (value) { - if (prerequisiteTag.valueNot) { - return prerequisiteTag.valueNot !== value; - } - if (prerequisiteTag.value) { - return prerequisiteTag.value === value; - } - return true; - } else { - return false; + var require = field.prerequisiteTag; + if (require && require.key) { + var value = latest.tags[require.key]; + if (!value) return false; + + if (require.valueNot) { + return require.valueNot !== value; } + if (require.value) { + return require.value === value; + } + return true; } return true; }; + field.focus = function() { if (field.impl) { field.impl.focus(); diff --git a/modules/ui/field_help.js b/modules/ui/field_help.js index 6289180dd..fcc54f42d 100644 --- a/modules/ui/field_help.js +++ b/modules/ui/field_help.js @@ -4,7 +4,7 @@ import { } from 'd3-selection'; import marked from 'marked'; -import { t } from '../util/locale'; +import { t, textDirection } from '../util/locale'; import { svgIcon } from '../svg'; import { icon } from './intro/helper'; @@ -197,7 +197,7 @@ export function uiFieldHelp(context, fieldName) { titleEnter .append('h2') - .attr('class', 'fl') + .attr('class', ((textDirection === 'rtl') ? 'fr' : 'fl')) .text(t('help.field.' + fieldName + '.title')); titleEnter diff --git a/modules/ui/form_fields.js b/modules/ui/form_fields.js index 08a7e2afb..a4a248174 100644 --- a/modules/ui/form_fields.js +++ b/modules/ui/form_fields.js @@ -7,25 +7,13 @@ import { utilGetSetValue, utilNoAuto } from '../util'; export function uiFormFields(context) { var moreCombo = uiCombobox(context, 'more-fields').minItems(1); + var _fieldsArr = []; var _state = ''; - var _fieldsArr; + var _klass = ''; - function formFields(selection, klass) { - render(selection, klass); - } - - - formFields.tagsChanged = function() {}; - - function render(selection, klass) { - - formFields.tagsChanged = function() { - render(selection, klass); - }; - + function formFields(selection) { var allowedFields = _fieldsArr.filter(function(field) { return field.isAllowed(); }); - var shown = allowedFields.filter(function(field) { return field.isShown(); }); var notShown = allowedFields.filter(function(field) { return !field.isShown(); }); @@ -34,7 +22,7 @@ export function uiFormFields(context) { container = container.enter() .append('div') - .attr('class', 'form-fields-container ' + (klass || '')) + .attr('class', 'form-fields-container ' + (_klass || '')) .merge(container); @@ -111,7 +99,7 @@ export function uiFormFields(context) { .on('accept', function (d) { var field = d.field; field.show(); - render(selection); + selection.call(formFields); // rerender if (field.type !== 'semiCombo' && field.type !== 'multiCombo') { field.focus(); } @@ -122,7 +110,7 @@ export function uiFormFields(context) { formFields.fieldsArr = function(val) { if (!arguments.length) return _fieldsArr; - _fieldsArr = val; + _fieldsArr = val || []; return formFields; }; @@ -132,6 +120,12 @@ export function uiFormFields(context) { return formFields; }; + formFields.klass = function(val) { + if (!arguments.length) return _klass; + _klass = val; + return formFields; + }; + return formFields; } diff --git a/modules/ui/help.js b/modules/ui/help.js index 20883e66c..973ed96cc 100644 --- a/modules/ui/help.js +++ b/modules/ui/help.js @@ -181,6 +181,13 @@ export function uiHelp(context) { 'using', 'tracing', 'upload' + ]], + ['qa', [ + 'intro', + 'tools_h', + 'tools', + 'issues_h', + 'issues' ]] ]; @@ -228,6 +235,8 @@ export function uiHelp(context) { 'help.imagery.offsets_h': 3, 'help.streetlevel.using_h': 3, 'help.gps.using_h': 3, + 'help.qa.tools_h': 3, + 'help.qa.issues_h': 3 }; var replacements = { diff --git a/modules/ui/index.js b/modules/ui/index.js index 3f65819cc..de4ff07c7 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -30,6 +30,9 @@ export { uiGeolocate } from './geolocate'; export { uiHelp } from './help'; export { uiInfo } from './info'; export { uiInspector } from './inspector'; +export { uiKeepRightDetails } from './keepRight_details'; +export { uiKeepRightEditor } from './keepRight_editor'; +export { uiKeepRightHeader } from './keepRight_header'; export { uiLasso } from './lasso'; export { uiLoading } from './loading'; export { uiMapData } from './map_data'; @@ -44,6 +47,7 @@ export { uiNoteReport } from './note_report'; export { uiPresetEditor } from './preset_editor'; export { uiPresetIcon } from './preset_icon'; export { uiPresetList } from './preset_list'; +export { uiQuickLinks } from './quick_links'; export { uiRadialMenu } from './radial_menu'; export { uiRawMemberEditor } from './raw_member_editor'; export { uiRawMembershipEditor } from './raw_membership_editor'; @@ -64,4 +68,5 @@ export { uiTooltipHtml } from './tooltipHtml'; export { uiUndoRedo } from './undo_redo'; export { uiVersion } from './version'; export { uiViewOnOSM } from './view_on_osm'; +export { uiViewOnKeepRight } from './view_on_keepRight'; export { uiZoom } from './zoom'; diff --git a/modules/ui/init.js b/modules/ui/init.js index 737b4bec7..366530afc 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -306,7 +306,7 @@ export function uiInit(context) { var panPixels = 80; context.keybinding() .on('⌫', function() { d3_event.preventDefault(); }) - .on(t('sidebar.key'), ui.sidebar.toggle) + .on([t('sidebar.key'), '`', '²'], ui.sidebar.toggle) // #5663 - common QWERTY, AZERTY .on('←', pan([panPixels, 0])) .on('↑', pan([0, panPixels])) .on('→', pan([-panPixels, 0])) diff --git a/modules/ui/inspector.js b/modules/ui/inspector.js index d2b701877..c6fb25b09 100644 --- a/modules/ui/inspector.js +++ b/modules/ui/inspector.js @@ -92,9 +92,9 @@ export function uiInspector(context) { inspector.showList = function() {}; inspector.setPreset = function() {}; - inspector.state = function(_) { + inspector.state = function(val) { if (!arguments.length) return _state; - _state = _; + _state = val; entityEditor.state(_state); // remove any old field help overlay that might have gotten attached to the inspector @@ -104,16 +104,16 @@ export function uiInspector(context) { }; - inspector.entityID = function(_) { + inspector.entityID = function(val) { if (!arguments.length) return _entityID; - _entityID = _; + _entityID = val; return inspector; }; - inspector.newFeature = function(_) { + inspector.newFeature = function(val) { if (!arguments.length) return _newFeature; - _newFeature = _; + _newFeature = val; return inspector; }; diff --git a/modules/ui/keepRight_details.js b/modules/ui/keepRight_details.js new file mode 100644 index 000000000..ee164ac58 --- /dev/null +++ b/modules/ui/keepRight_details.js @@ -0,0 +1,132 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { dataEn } from '../../data'; +import { modeSelect } from '../modes'; +import { t } from '../util/locale'; +import { utilDisplayName, utilEntityOrMemberSelector, utilEntityRoot } from '../util'; + + +export function uiKeepRightDetails(context) { + var _error; + + + function errorDetail(d) { + var unknown = t('inspector.unknown'); + + if (!d) return unknown; + var errorType = d.error_type; + var parentErrorType = d.parent_error_type; + + var et = dataEn.QA.keepRight.errorTypes[errorType]; + var pt = dataEn.QA.keepRight.errorTypes[parentErrorType]; + + var detail; + if (et && et.description) { + detail = t('QA.keepRight.errorTypes.' + errorType + '.description', d.replacements); + } else if (pt && pt.description) { + detail = t('QA.keepRight.errorTypes.' + parentErrorType + '.description', d.replacements); + } else { + detail = unknown; + } + + return detail; + } + + + function keepRightDetails(selection) { + var details = selection.selectAll('.kr_error-details') + .data( + (_error ? [_error] : []), + function(d) { return d.id + '-' + (d.status || 0); } + ); + + details.exit() + .remove(); + + var detailsEnter = details.enter() + .append('div') + .attr('class', 'kr_error-details kr_error-details-container'); + + + // description + var descriptionEnter = detailsEnter + .append('div') + .attr('class', 'kr_error-details-description'); + + descriptionEnter + .append('h4') + .text(function() { return t('QA.keepRight.detail_description'); }); + + descriptionEnter + .append('div') + .attr('class', 'kr_error-details-description-text') + .html(errorDetail); + + // If there are entity links in the error message.. + descriptionEnter.selectAll('.kr_error_entity_link, .kr_error_object_link') + .each(function() { + var link = d3_select(this); + var isObjectLink = link.classed('kr_error_object_link'); + var entityID = isObjectLink ? + (utilEntityRoot(_error.object_type) + _error.object_id) + : this.textContent; + var entity = context.hasEntity(entityID); + + // Add click handler + link + .on('mouseover', function() { + context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph())) + .classed('hover', true); + }) + .on('mouseout', function() { + context.surface().selectAll('.hover') + .classed('hover', false); + }) + .on('click', function() { + d3_event.preventDefault(); + var osmlayer = context.layers().layer('osm'); + if (!osmlayer.enabled()) { + osmlayer.enabled(true); + } + + context.map().centerZoom(_error.loc, 20); + + if (entity) { + context.enter(modeSelect(context, [entityID])); + } else { + context.loadEntity(entityID, function() { + context.enter(modeSelect(context, [entityID])); + }); + } + }); + + // Replace with friendly name if possible + // (The entity may not yet be loaded into the graph) + if (entity) { + var name = utilDisplayName(entity); // try to use common name + + if (!name && !isObjectLink) { + var preset = context.presets().match(entity, context.graph()); + name = preset && !preset.isFallback() && preset.name(); // fallback to preset name + } + + if (name) { + this.innerText = name; + } + } + }); + } + + + keepRightDetails.error = function(val) { + if (!arguments.length) return _error; + _error = val; + return keepRightDetails; + }; + + + return keepRightDetails; +} diff --git a/modules/ui/keepRight_editor.js b/modules/ui/keepRight_editor.js new file mode 100644 index 000000000..ca502b1e1 --- /dev/null +++ b/modules/ui/keepRight_editor.js @@ -0,0 +1,244 @@ +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { select as d3_select } from 'd3-selection'; + +import { t } from '../util/locale'; +import { services } from '../services'; +import { modeBrowse } from '../modes'; +import { svgIcon } from '../svg'; + +import { + uiKeepRightDetails, + uiKeepRightHeader, + uiQuickLinks, + uiTooltipHtml, + uiViewOnKeepRight +} from './index'; + +import { utilNoAuto, utilRebind } from '../util'; + + +export function uiKeepRightEditor(context) { + var dispatch = d3_dispatch('change'); + var keepRightDetails = uiKeepRightDetails(context); + var keepRightHeader = uiKeepRightHeader(context); + var quickLinks = uiQuickLinks(); + + var _error; + + + function keepRightEditor(selection) { + // quick links + var choices = [{ + id: 'zoom_to', + label: 'inspector.zoom_to.title', + tooltip: function() { + return uiTooltipHtml(t('inspector.zoom_to.tooltip_issue'), t('inspector.zoom_to.key')); + }, + click: function zoomTo() { + context.mode().zoomToSelected(); + } + }]; + + + var header = selection.selectAll('.header') + .data([0]); + + var headerEnter = header.enter() + .append('div') + .attr('class', 'header fillL'); + + headerEnter + .append('button') + .attr('class', 'fr keepRight-editor-close') + .on('click', function() { + context.enter(modeBrowse(context)); + }) + .call(svgIcon('#iD-icon-close')); + + headerEnter + .append('h3') + .text(t('QA.keepRight.title')); + + + var body = selection.selectAll('.body') + .data([0]); + + body = body.enter() + .append('div') + .attr('class', 'body') + .merge(body); + + var editor = body.selectAll('.keepRight-editor') + .data([0]); + + editor.enter() + .append('div') + .attr('class', 'modal-section keepRight-editor') + .merge(editor) + .call(keepRightHeader.error(_error)) + .call(quickLinks.choices(choices)) + .call(keepRightDetails.error(_error)) + .call(keepRightSaveSection); + + + var footer = selection.selectAll('.footer') + .data([0]); + + footer.enter() + .append('div') + .attr('class', 'footer') + .merge(footer) + .call(uiViewOnKeepRight(context).what(_error)); + } + + + function keepRightSaveSection(selection) { + var isSelected = (_error && _error.id === context.selectedErrorID()); + var isShown = (_error && (isSelected || _error.newComment || _error.comment)); + var saveSection = selection.selectAll('.error-save') + .data( + (isShown ? [_error] : []), + function(d) { return d.id + '-' + (d.status || 0); } + ); + + // exit + saveSection.exit() + .remove(); + + // enter + var saveSectionEnter = saveSection.enter() + .append('div') + .attr('class', 'keepRight-save save-section cf'); + + saveSectionEnter + .append('h4') + .attr('class', '.error-save-header') + .text(t('QA.keepRight.comment')); + + saveSectionEnter + .append('textarea') + .attr('class', 'new-comment-input') + .attr('placeholder', t('QA.keepRight.comment_placeholder')) + .attr('maxlength', 1000) + .property('value', function(d) { return d.newComment || d.comment; }) + .call(utilNoAuto) + .on('input', changeInput) + .on('blur', changeInput); + + // update + saveSection = saveSectionEnter + .merge(saveSection) + .call(keepRightSaveButtons); + + + function changeInput() { + var input = d3_select(this); + var val = input.property('value').trim(); + + if (val === _error.comment) { + val = undefined; + } + + // store the unsaved comment with the error itself + _error = _error.update({ newComment: val }); + + var keepRight = services.keepRight; + if (keepRight) { + keepRight.replaceError(_error); // update keepright cache + } + + saveSection + .call(keepRightSaveButtons); + } + } + + + function keepRightSaveButtons(selection) { + var isSelected = (_error && _error.id === context.selectedErrorID()); + var buttonSection = selection.selectAll('.buttons') + .data((isSelected ? [_error] : []), function(d) { return d.status + d.id; }); + + // exit + buttonSection.exit() + .remove(); + + // enter + var buttonEnter = buttonSection.enter() + .append('div') + .attr('class', 'buttons'); + + buttonEnter + .append('button') + .attr('class', 'button comment-button action') + .text(t('QA.keepRight.save_comment')); + + buttonEnter + .append('button') + .attr('class', 'button close-button action'); + + buttonEnter + .append('button') + .attr('class', 'button ignore-button action'); + + + // update + buttonSection = buttonSection + .merge(buttonEnter); + + buttonSection.select('.comment-button') // select and propagate data + .attr('disabled', function(d) { + return d.newComment === undefined ? true : null; + }) + .on('click.comment', function(d) { + this.blur(); // avoid keeping focus on the button - #4641 + var keepRight = services.keepRight; + if (keepRight) { + keepRight.postKeepRightUpdate(d, function(err, error) { + dispatch.call('change', error); + }); + } + }); + + buttonSection.select('.close-button') // select and propagate data + .text(function(d) { + var andComment = (d.newComment !== undefined ? '_comment' : ''); + return t('QA.keepRight.close' + andComment); + }) + .on('click.close', function(d) { + this.blur(); // avoid keeping focus on the button - #4641 + var keepRight = services.keepRight; + if (keepRight) { + d.state = 'ignore_t'; // ignore temporarily (error fixed) + keepRight.postKeepRightUpdate(d, function(err, error) { + dispatch.call('change', error); + }); + } + }); + + buttonSection.select('.ignore-button') // select and propagate data + .text(function(d) { + var andComment = (d.newComment !== undefined ? '_comment' : ''); + return t('QA.keepRight.ignore' + andComment); + }) + .on('click.ignore', function(d) { + this.blur(); // avoid keeping focus on the button - #4641 + var keepRight = services.keepRight; + if (keepRight) { + d.state = 'ignore'; // ignore permanently (false positive) + keepRight.postKeepRightUpdate(d, function(err, error) { + dispatch.call('change', error); + }); + } + }); + } + + + keepRightEditor.error = function(val) { + if (!arguments.length) return _error; + _error = val; + return keepRightEditor; + }; + + + return utilRebind(keepRightEditor, dispatch, 'on'); +} diff --git a/modules/ui/keepRight_header.js b/modules/ui/keepRight_header.js new file mode 100644 index 000000000..1c8d9533c --- /dev/null +++ b/modules/ui/keepRight_header.js @@ -0,0 +1,71 @@ +import { dataEn } from '../../data'; +import { svgIcon } from '../svg'; +import { t } from '../util/locale'; + + +export function uiKeepRightHeader() { + var _error; + + + function errorTitle(d) { + var unknown = t('inspector.unknown'); + + if (!d) return unknown; + var errorType = d.error_type; + var parentErrorType = d.parent_error_type; + + var et = dataEn.QA.keepRight.errorTypes[errorType]; + var pt = dataEn.QA.keepRight.errorTypes[parentErrorType]; + + if (et && et.title) { + return t('QA.keepRight.errorTypes.' + errorType + '.title'); + } else if (pt && pt.title) { + return t('QA.keepRight.errorTypes.' + parentErrorType + '.title'); + } else { + return unknown; + } + } + + + function keepRightHeader(selection) { + var header = selection.selectAll('.kr_error-header') + .data( + (_error ? [_error] : []), + function(d) { return d.id + '-' + (d.status || 0); } + ); + + header.exit() + .remove(); + + var headerEnter = header.enter() + .append('div') + .attr('class', 'kr_error-header'); + + var iconEnter = headerEnter + .append('div') + .attr('class', 'kr_error-header-icon') + .classed('new', function(d) { return d.id < 0; }); + + iconEnter + .append('div') + .attr('class', function(d) { + return 'preset-icon-28 kr_error kr_error-' + d.id + ' kr_error_type_' + d.parent_error_type; + }) + .call(svgIcon('#iD-icon-bolt', 'kr_error-fill')); + + headerEnter + .append('div') + .attr('class', 'kr_error-header-label') + .text(errorTitle); + } + + + keepRightHeader.error = function(val) { + if (!arguments.length) return _error; + _error = val; + return keepRightHeader; + }; + + + return keepRightHeader; +} diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index fc07eb7e7..07b5dfcef 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -30,6 +30,7 @@ export function uiMapData(context) { var _dataLayerContainer = d3_select(null); var _fillList = d3_select(null); var _featureList = d3_select(null); + var _QAList = d3_select(null); function showsFeature(d) { @@ -38,6 +39,7 @@ export function uiMapData(context) { function autoHiddenFeature(d) { + if (d.type === 'kr_error') return context.errors().autoHidden(d); return context.features().autoHidden(d); } @@ -48,6 +50,22 @@ export function uiMapData(context) { } + function showsQA(d) { + var QAKeys = [d]; + var QALayers = layers.all().filter(function(obj) { return QAKeys.indexOf(obj.id) !== -1; }); + var data = QALayers.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(); + } + + return layerEnabled(data[0]); + } + + function showsFill(d) { return _fillSelected === d; } @@ -207,6 +225,58 @@ export function uiMapData(context) { } + function drawQAItems(selection) { + var qaKeys = ['keepRight']; + var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; }); + + var ul = selection + .selectAll('.layer-list-qa') + .data([0]); + + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-qa') + .merge(ul); + + var li = ul.selectAll('.list-item') + .data(qaLayers); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { return 'list-item list-item-' + d.id; }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + d3_select(this) + .call(tooltip() + .title(t('map_data.layers.' + d.id + '.tooltip')) + .placement('bottom') + ); + }); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function(d) { toggleLayer(d.id); }); + + labelEnter + .append('span') + .text(function(d) { return t('map_data.layers.' + d.id + '.title'); }); + + + // Update + li + .merge(liEnter) + .classed('active', function (d) { return d.layer.enabled(); }) + .selectAll('input') + .property('checked', function (d) { return d.layer.enabled(); }); + } + + // Beta feature - sample vector layers to support Detroit Mapping Challenge // https://github.com/osmus/detroit-mapping-challenge function drawVectorItems(selection) { @@ -427,10 +497,9 @@ export function uiMapData(context) { .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 tip = t(name + '.' + d + '.tooltip'); + var key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null); + if ((name === 'feature' || name === 'keepRight') && autoHiddenFeature(d)) { var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden'); tip += '
' + msg + '
'; } @@ -461,7 +530,7 @@ export function uiMapData(context) { .selectAll('input') .property('checked', active) .property('indeterminate', function(d) { - return (name === 'feature' && autoHiddenFeature(d)); + return ((name === 'feature' || name === 'keepRight') && autoHiddenFeature(d)); }); } @@ -502,6 +571,7 @@ export function uiMapData(context) { function update() { _dataLayerContainer .call(drawOsmItems) + .call(drawQAItems) .call(drawPhotoItems) .call(drawCustomDataItems) .call(drawVectorItems); // Beta - Detroit mapping challenge @@ -511,6 +581,9 @@ export function uiMapData(context) { _featureList .call(drawListItems, features, 'checkbox', 'feature', clickFeature, showsFeature); + + _QAList + .call(drawListItems, ['keep-right'], 'checkbox', 'QA', function(d) { toggleLayer(d); }, showsQA); } @@ -611,6 +684,7 @@ export function uiMapData(context) { .append('div') .attr('class', 'pane-content'); + // data layers content .append('div') diff --git a/modules/ui/note_comments.js b/modules/ui/note_comments.js index 4534ea10b..cbb004095 100644 --- a/modules/ui/note_comments.js +++ b/modules/ui/note_comments.js @@ -110,9 +110,9 @@ export function uiNoteComments() { } - noteComments.note = function(_) { + noteComments.note = function(val) { if (!arguments.length) return _note; - _note = _; + _note = val; return noteComments; }; diff --git a/modules/ui/note_editor.js b/modules/ui/note_editor.js index 4ebc6e89d..7cd4b2511 100644 --- a/modules/ui/note_editor.js +++ b/modules/ui/note_editor.js @@ -16,6 +16,8 @@ import { uiNoteComments, uiNoteHeader, uiNoteReport, + uiQuickLinks, + uiTooltipHtml, uiViewOnOSM, } from './index'; @@ -27,6 +29,7 @@ import { export function uiNoteEditor(context) { var dispatch = d3_dispatch('change'); + var quickLinks = uiQuickLinks(); var noteComments = uiNoteComments(); var noteHeader = uiNoteHeader(); @@ -37,6 +40,19 @@ export function uiNoteEditor(context) { function noteEditor(selection) { + // quick links + var choices = [{ + id: 'zoom_to', + label: 'inspector.zoom_to.title', + tooltip: function() { + return uiTooltipHtml(t('inspector.zoom_to.tooltip_note'), t('inspector.zoom_to.key')); + }, + click: function zoomTo() { + context.mode().zoomToSelected(); + } + }]; + + var header = selection.selectAll('.header') .data([0]); @@ -73,6 +89,7 @@ export function uiNoteEditor(context) { .attr('class', 'modal-section note-editor') .merge(editor) .call(noteHeader.note(_note)) + .call(quickLinks.choices(choices)) .call(noteComments.note(_note)) .call(noteSaveSection); @@ -155,7 +172,7 @@ export function uiNoteEditor(context) { noteSaveEnter .append('textarea') - .attr('id', 'new-comment-input') + .attr('class', 'new-comment-input') .attr('placeholder', t('note.inputPlaceholder')) .attr('maxlength', 1000) .property('value', function(d) { return d.newComment; }) @@ -425,9 +442,9 @@ export function uiNoteEditor(context) { } - noteEditor.note = function(_) { + noteEditor.note = function(val) { if (!arguments.length) return _note; - _note = _; + _note = val; return noteEditor; }; diff --git a/modules/ui/note_header.js b/modules/ui/note_header.js index 8fc2a0e95..c19338c49 100644 --- a/modules/ui/note_header.js +++ b/modules/ui/note_header.js @@ -49,9 +49,9 @@ export function uiNoteHeader() { } - noteHeader.note = function(_) { + noteHeader.note = function(val) { if (!arguments.length) return _note; - _note = _; + _note = val; return noteHeader; }; diff --git a/modules/ui/note_report.js b/modules/ui/note_report.js index 759f9ba13..09b78a4e4 100644 --- a/modules/ui/note_report.js +++ b/modules/ui/note_report.js @@ -1,23 +1,20 @@ import { t } from '../util/locale'; +import { osmNote } from '../osm'; +import { services } from '../services'; import { svgIcon } from '../svg'; -import { - osmNote -} from '../osm'; export function uiNoteReport() { var _note; - var url = 'https://www.openstreetmap.org/reports/new?reportable_id='; function noteReport(selection) { + var url; + if (services.osm && (_note instanceof osmNote) && (!_note.isNew())) { + url = services.osm.noteReportURL(_note); + } - if (!(_note instanceof osmNote)) return; - - url += _note.id + '&reportable_type=Note'; - - var data = ((!_note || _note.isNew()) ? [] : [_note]); var link = selection.selectAll('.note-report') - .data(data, function(d) { return d.id; }); + .data(url ? [url] : []); // exit link.exit() @@ -28,7 +25,7 @@ export function uiNoteReport() { .append('a') .attr('class', 'note-report') .attr('target', '_blank') - .attr('href', url) + .attr('href', function(d) { return d; }) .call(svgIcon('#iD-icon-out-link', 'inline')); linkEnter @@ -37,9 +34,9 @@ export function uiNoteReport() { } - noteReport.note = function(_) { + noteReport.note = function(val) { if (!arguments.length) return _note; - _note = _; + _note = val; return noteReport; }; diff --git a/modules/ui/preset_editor.js b/modules/ui/preset_editor.js index 0c3d46ce6..2ccbabd07 100644 --- a/modules/ui/preset_editor.js +++ b/modules/ui/preset_editor.js @@ -87,8 +87,9 @@ export function uiPresetEditor(context) { selection .call(formFields .fieldsArr(_fieldsArr) - .state(_state), - 'inspector-inner fillL3'); + .state(_state) + .klass('inspector-inner fillL3') + ); selection.selectAll('.wrap-form-field input') @@ -120,7 +121,6 @@ export function uiPresetEditor(context) { presetEditor.tags = function(val) { if (!arguments.length) return _tags; _tags = val; - formFields.tagsChanged(); // Don't reset _fieldsArr here. return presetEditor; }; diff --git a/modules/ui/quick_links.js b/modules/ui/quick_links.js new file mode 100644 index 000000000..9b1a7149d --- /dev/null +++ b/modules/ui/quick_links.js @@ -0,0 +1,62 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { t } from '../util/locale'; +import { tooltip } from '../util/tooltip'; + + +export function uiQuickLinks() { + var _choices = []; + + + function quickLinks(selection) { + var container = selection.selectAll('.quick-links') + .data([0]); + + container = container.enter() + .append('div') + .attr('class', 'quick-links') + .merge(container); + + var items = container.selectAll('.quick-link') + .data(_choices, function(d) { return d.id; }); + + items.exit() + .remove(); + + items.enter() + .append('a') + .attr('class', function(d) { return 'quick-link quick-link-' + d.id; }) + .attr('href', '#') + .text(function(d) { return t(d.label); }) + .each(function(d) { + if (typeof d.tooltip !== 'function') return; + d3_select(this) + .call(tooltip().html(true).title(d.tooltip).placement('bottom')); + }) + .on('click', function(d) { + if (typeof d.click !== 'function') return; + d3_event.preventDefault(); + d.click(d); + }); + } + + + // val should be an array of choices like: + // [{ + // id: 'link-id', + // label: 'translation.key', + // tooltip: function(d), + // click: function(d) + // }, ..] + quickLinks.choices = function(val) { + if (!arguments.length) return _choices; + _choices = val; + return quickLinks; + }; + + + return quickLinks; +} diff --git a/modules/ui/shortcuts.js b/modules/ui/shortcuts.js index 43ee2b0b5..252375c4b 100644 --- a/modules/ui/shortcuts.js +++ b/modules/ui/shortcuts.js @@ -1,3 +1,5 @@ +import _uniq from 'lodash-es/uniq'; + import { select as d3_select, selectAll as d3_selectAll @@ -19,7 +21,7 @@ export function uiShortcuts(context) { context.keybinding() - .on(t('shortcuts.toggle.key'), function () { + .on([t('shortcuts.toggle.key'), '?'], function () { if (d3_selectAll('.modal-shortcuts').size()) { // already showing if (_modalSelection) { _modalSelection.close(); @@ -179,7 +181,12 @@ export function uiShortcuts(context) { arr = ['F11']; } - return arr.map(function(s) { + // replace translations + arr = arr.map(function(s) { + return uiCmd.display(s.indexOf('.') !== -1 ? t(s) : s); + }); + + return _uniq(arr).map(function(s) { return { shortcut: s, separator: d.separator @@ -191,17 +198,14 @@ export function uiShortcuts(context) { var selection = d3_select(this); var click = d.shortcut.toLowerCase().match(/(.*).click/); - if (click && click[1]) { + if (click && click[1]) { // replace "left_click", "right_click" with mouse icon selection .call(svgIcon('#iD-walkthrough-mouse', 'mouseclick', click[1])); } else { selection .append('kbd') .attr('class', 'shortcut') - .text(function (d) { - var key = d.shortcut; - return key.indexOf('.') !== -1 ? uiCmd.display(t(key)) : uiCmd.display(key); - }); + .text(function (d) { return d.shortcut; }); } if (i < nodes.length - 1) { diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index 31107290b..278c466b0 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -9,18 +9,9 @@ import { selectAll as d3_selectAll } from 'd3-selection'; -import { - osmEntity, - osmNote -} from '../osm'; - -import { - uiDataEditor, - uiFeatureList, - uiInspector, - uiNoteEditor -} from './index'; - +import { osmEntity, osmNote, krError } from '../osm'; +import { services } from '../services'; +import { uiDataEditor, uiFeatureList, uiInspector, uiNoteEditor, uiKeepRightEditor } from './index'; import { textDirection } from '../util/locale'; @@ -28,9 +19,11 @@ export function uiSidebar(context) { var inspector = uiInspector(context); var dataEditor = uiDataEditor(context); var noteEditor = uiNoteEditor(context); + var keepRightEditor = uiKeepRightEditor(context); var _current; var _wasData = false; var _wasNote = false; + var _wasKRError = false; function sidebar(selection) { @@ -127,12 +120,34 @@ export function uiSidebar(context) { if (context.mode().id === 'drag-note') return; _wasNote = true; + var osm = services.osm; + if (osm) { + datum = osm.getNote(datum.id); // marker may contain stale data - get latest + } + sidebar .show(noteEditor.note(datum)); selection.selectAll('.sidebar-component') .classed('inspector-hover', true); + } else if (datum instanceof krError) { + _wasKRError = true; + + var keepRight = services.keepRight; + if (keepRight) { + datum = keepRight.getError(datum.id); // marker may contain stale data - get latest + } + + d3_selectAll('.kr_error') + .classed('hover', function(d) { return d.id === datum.id; }); + + sidebar + .show(keepRightEditor.error(datum)); + + selection.selectAll('.sidebar-component') + .classed('inspector-hover', true); + } else if (!_current && (datum instanceof osmEntity)) { featureListWrap .classed('inspector-hidden', true); @@ -158,10 +173,12 @@ export function uiSidebar(context) { inspector .state('hide'); - } else if (_wasData || _wasNote) { + } else if (_wasData || _wasNote || _wasKRError) { _wasNote = false; _wasData = false; + _wasKRError = false; d3_selectAll('.note').classed('hover', false); + d3_selectAll('.kr_error').classed('hover', false); sidebar.hide(); } } diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js index 03f625019..6215250b9 100644 --- a/modules/ui/tag_reference.js +++ b/modules/ui/tag_reference.js @@ -1,6 +1,3 @@ -import _find from 'lodash-es/find'; -import _omit from 'lodash-es/omit'; - import { event as d3_event, select as d3_select @@ -10,10 +7,11 @@ import { t } from '../util/locale'; import { utilDetect } from '../util/detect'; import { services } from '../services'; import { svgIcon } from '../svg'; +import { utilQsString } from '../util'; export function uiTagReference(tag) { - var taginfo = services.taginfo; + var wikibase = services.osmWikibase; var tagReference = {}; var _button = d3_select(null); @@ -21,42 +19,49 @@ export function uiTagReference(tag) { var _loaded; var _showing; - + /** + * @returns {{itemTitle: String, description: String, image: String|null}|null} + **/ function findLocal(data) { - var locale = utilDetect().locale.toLowerCase(); - var localized; + var entity = data.tag || data.key; + if (!entity) return null; - if (locale !== 'pt-br') { // see #3776, prefer 'pt' over 'pt-br' - localized = _find(data, function(d) { - return d.lang.toLowerCase() === locale; - }); - if (localized) return localized; + var result = { + title: entity.title, + description: entity.description, + }; + + if (entity.claims) { + var langCode = utilDetect().locale.toLowerCase(); + var url; + var image = wikibase.claimToValue(entity, 'P4', langCode); + if (image) { + url = 'https://commons.wikimedia.org/w/index.php'; + } else { + image = wikibase.claimToValue(entity, 'P28', langCode); + if (image) { + url = 'https://wiki.openstreetmap.org/w/index.php'; + } + } + if (image) { + result.image = { + url: url, + title: 'Special:Redirect/file/' + image + }; + } } - // try the non-regional version of a language, like - // 'en' if the language is 'en-US' - if (locale.indexOf('-') !== -1) { - var first = locale.split('-')[0]; - localized = _find(data, function(d) { - return d.lang.toLowerCase() === first; - }); - if (localized) return localized; - } - - // finally fall back to english - return _find(data, function(d) { - return d.lang.toLowerCase() === 'en'; - }); + return result; } function load(param) { - if (!taginfo) return; + if (!wikibase) return; _button .classed('tag-reference-loading', true); - taginfo.docs(param, function show(err, data) { + wikibase.getEntity(param, function show(err, data) { var docs; if (!err && data) { docs = findLocal(data); @@ -65,23 +70,25 @@ export function uiTagReference(tag) { _body.html(''); if (!docs || !docs.title) { - if (param.hasOwnProperty('value')) { - load(_omit(param, 'value')); // retry with key only - } else { - _body - .append('p') - .attr('class', 'tag-reference-description') - .text(t('inspector.no_documentation_key')); - done(); - } + _body + .append('p') + .attr('class', 'tag-reference-description') + .text(t('inspector.no_documentation_key')); + done(); return; } - if (docs.image && docs.image.thumb_url_prefix) { + if (docs.image) { + var imageUrl = docs.image.url + '?' + utilQsString({ + title: docs.image.title, + width: 100, + height: 100, + }); + _body .append('img') .attr('class', 'tag-reference-wiki-image') - .attr('src', docs.image.thumb_url_prefix + '100' + docs.image.thumb_url_suffix) + .attr('src', imageUrl) .on('load', function() { done(); }) .on('error', function() { d3_select(this).remove(); done(); }); } else { @@ -91,7 +98,7 @@ export function uiTagReference(tag) { _body .append('p') .attr('class', 'tag-reference-description') - .text(docs.description || t('inspector.documentation_redirect')); + .text(docs.description || t('inspector.no_documentation_key')); _body .append('a') @@ -101,7 +108,7 @@ export function uiTagReference(tag) { .attr('href', 'https://wiki.openstreetmap.org/wiki/' + docs.title) .call(svgIcon('#iD-icon-out-link', 'inline')) .append('span') - .text(t('inspector.reference')); + .text(t('inspector.edit_reference')); // Add link to info about "good changeset comments" - #2923 if (param.key === 'comment') { @@ -171,6 +178,7 @@ export function uiTagReference(tag) { } else if (_loaded) { done(); } else { + tag.langCode = utilDetect().locale.toLowerCase(); load(tag); } }); diff --git a/modules/ui/view_on_keepRight.js b/modules/ui/view_on_keepRight.js new file mode 100644 index 000000000..d0b28a4cb --- /dev/null +++ b/modules/ui/view_on_keepRight.js @@ -0,0 +1,45 @@ +import { t } from '../util/locale'; +import { services } from '../services'; +import { svgIcon } from '../svg'; +import { krError } from '../osm'; + + +export function uiViewOnKeepRight() { + var _error; // a keepright error + + + function viewOnKeepRight(selection) { + var url; + if (services.keepRight && (_error instanceof krError)) { + url = services.keepRight.errorURL(_error); + } + + var link = selection.selectAll('.view-on-keepRight') + .data(url ? [url] : []); + + // exit + link.exit() + .remove(); + + // enter + var linkEnter = link.enter() + .append('a') + .attr('class', 'view-on-keepRight') + .attr('target', '_blank') + .attr('href', function(d) { return d; }) + .call(svgIcon('#iD-icon-out-link', 'inline')); + + linkEnter + .append('span') + .text(t('inspector.view_on_keepRight')); + } + + + viewOnKeepRight.what = function(val) { + if (!arguments.length) return _error; + _error = val; + return viewOnKeepRight; + }; + + return viewOnKeepRight; +} diff --git a/modules/ui/view_on_osm.js b/modules/ui/view_on_osm.js index b013f158b..7bf265275 100644 --- a/modules/ui/view_on_osm.js +++ b/modules/ui/view_on_osm.js @@ -1,9 +1,6 @@ import { t } from '../util/locale'; +import { osmEntity, osmNote } from '../osm'; import { svgIcon } from '../svg'; -import { - osmEntity, - osmNote -} from '../osm'; export function uiViewOnOSM(context) { diff --git a/modules/util/detect.js b/modules/util/detect.js index 19af93b5d..c3a98c185 100644 --- a/modules/util/detect.js +++ b/modules/util/detect.js @@ -9,7 +9,8 @@ export function utilDetect(force) { detected = {}; var ua = navigator.userAgent, - m = null; + m = null, + q = utilStringQs(window.location.hash.substring(1)); m = ua.match(/(edge)\/?\s*(\.?\d+(\.\d+)*)/i); // Edge if (m !== null) { @@ -59,24 +60,30 @@ export function utilDetect(force) { // Added due to incomplete svg style support. See #715 detected.opera = (detected.browser.toLowerCase() === 'opera' && parseFloat(detected.version) < 15 ); - detected.locale = (navigator.language || navigator.userLanguage || 'en-US'); - detected.language = detected.locale.split('-')[0]; + // Set locale based on url param (format 'en-US') or browser lang (default) + if (q.hasOwnProperty('locale')) { + detected.locale = q.locale; + detected.language = q.locale.split('-')[0]; + } else { + detected.locale = (navigator.language || navigator.userLanguage || 'en-US'); + detected.language = detected.locale.split('-')[0]; - // Search `navigator.languages` for a better locale.. Prefer the first language, - // unless the second language is a culture-specific version of the first one, see #3842 - if (navigator.languages && navigator.languages.length > 0) { - var code0 = navigator.languages[0], - parts0 = code0.split('-'); + // Search `navigator.languages` for a better locale. Prefer the first language, + // unless the second language is a culture-specific version of the first one, see #3842 + if (navigator.languages && navigator.languages.length > 0) { + var code0 = navigator.languages[0], + parts0 = code0.split('-'); - detected.locale = code0; - detected.language = parts0[0]; + detected.locale = code0; + detected.language = parts0[0]; - if (navigator.languages.length > 1 && parts0.length === 1) { - var code1 = navigator.languages[1], - parts1 = code1.split('-'); + if (navigator.languages.length > 1 && parts0.length === 1) { + var code1 = navigator.languages[1], + parts1 = code1.split('-'); - if (parts1[0] === parts0[0]) { - detected.locale = code1; + if (parts1[0] === parts0[0]) { + detected.locale = code1; + } } } } @@ -90,7 +97,6 @@ export function utilDetect(force) { } // detect text direction - var q = utilStringQs(window.location.hash.substring(1)); var lang = dataLocales[detected.locale]; if ((lang && lang.rtl) || (q.rtl === 'true')) { detected.textDirection = 'rtl'; diff --git a/modules/util/index.js b/modules/util/index.js index 9a0ad1251..7c31ec202 100644 --- a/modules/util/index.js +++ b/modules/util/index.js @@ -5,6 +5,7 @@ export { utilDisplayName } from './util'; export { utilDisplayNameForPath } from './util'; export { utilDisplayType } from './util'; export { utilDisplayLabel } from './util'; +export { utilEntityRoot } from './util'; export { utilEditDistance } from './util'; export { utilEntitySelector } from './util'; export { utilEntityOrMemberSelector } from './util'; @@ -28,7 +29,6 @@ export { utilRebind } from './rebind'; export { utilSetTransform } from './util'; export { utilSessionMutex } from './session_mutex'; export { utilStringQs } from './util'; -// export { utilSuggestNames } from './suggest_names'; export { utilTagText } from './util'; export { utilTiler } from './tiler'; export { utilTriggerEvent } from './trigger_event'; diff --git a/modules/util/keybinding.js b/modules/util/keybinding.js index ec643b09c..6ca9ab1db 100644 --- a/modules/util/keybinding.js +++ b/modules/util/keybinding.js @@ -1,4 +1,5 @@ import _isFunction from 'lodash-es/isFunction'; +import _uniq from 'lodash-es/uniq'; import { event as d3_event, @@ -125,7 +126,7 @@ export function utilKeybinding(namespace) { // Remove one or more keycode bindings. keybinding.off = function(codes, capture) { - var arr = [].concat(codes); + var arr = _uniq([].concat(codes)); for (var i = 0; i < arr.length; i++) { var id = arr[i] + (capture ? '-capture' : '-bubble'); @@ -141,7 +142,7 @@ export function utilKeybinding(namespace) { return keybinding.off(codes, capture); } - var arr = [].concat(codes); + var arr = _uniq([].concat(codes)); for (var i = 0; i < arr.length; i++) { var id = arr[i] + (capture ? '-capture' : '-bubble'); diff --git a/modules/util/locale.js b/modules/util/locale.js index 7396b7549..1ed2c2b7e 100644 --- a/modules/util/locale.js +++ b/modules/util/locale.js @@ -42,7 +42,9 @@ export function t(s, o, loc) { if (rep !== undefined) { if (o) { for (var k in o) { - rep = rep.replace('{' + k + '}', o[k]); + var variable = '{' + k + '}'; + var re = new RegExp(variable, 'g'); // check globally for variables + rep = rep.replace(re, o[k]); } } return rep; diff --git a/modules/util/util.js b/modules/util/util.js index 216a207ae..b790f9505 100644 --- a/modules/util/util.js +++ b/modules/util/util.js @@ -37,6 +37,7 @@ export function utilEntityOrMemberSelector(ids, graph) { export function utilEntityOrDeepMemberSelector(ids, graph) { var seen = {}; var allIDs = []; + function addEntityAndMembersIfNotYetSeen(id) { // avoid infinite recursion for circular relations by skipping seen entities if (seen[id]) return; @@ -53,6 +54,7 @@ export function utilEntityOrDeepMemberSelector(ids, graph) { } } } + ids.forEach(function(id) { addEntityAndMembersIfNotYetSeen(id); }); @@ -85,9 +87,9 @@ export function utilGetAllNodes(ids, graph) { export function utilDisplayName(entity) { - var localizedNameKey = 'name:' + utilDetect().locale.toLowerCase().split('-')[0], - name = entity.tags[localizedNameKey] || entity.tags.name || '', - network = entity.tags.cycle_network || entity.tags.network; + var localizedNameKey = 'name:' + utilDetect().locale.toLowerCase().split('-')[0]; + var name = entity.tags[localizedNameKey] || entity.tags.name || ''; + var network = entity.tags.cycle_network || entity.tags.network; if (!name && entity.tags.ref) { name = entity.tags.ref; @@ -137,6 +139,15 @@ export function utilDisplayLabel(entity, context) { } +export function utilEntityRoot(entityType) { + return { + node: 'n', + way: 'w', + relation: 'r' + }[entityType]; +} + + export function utilStringQs(str) { return str.split('&').reduce(function(obj, pair){ var parts = pair.split('='); @@ -152,11 +163,12 @@ export function utilStringQs(str) { export function utilQsString(obj, noencode) { + // encode everything except special characters used in certain hash parameters: + // "/" in map states, ":", ",", {" and "}" in background function softEncode(s) { - // encode everything except special characters used in certain hash parameters: - // "/" in map states, ":", ",", {" and "}" in background - return encodeURIComponent(s).replace(/(%2F|%3A|%2C|%7B|%7D)/g, decodeURIComponent); + return encodeURIComponent(s).replace(/(%2F|%3A|%2C|%7B|%7D)/g, decodeURIComponent); } + return Object.keys(obj).sort().map(function(key) { return encodeURIComponent(key) + '=' + ( noencode ? softEncode(obj[key]) : encodeURIComponent(obj[key])); @@ -165,36 +177,41 @@ export function utilQsString(obj, noencode) { export function utilPrefixDOMProperty(property) { - var prefixes = ['webkit', 'ms', 'moz', 'o'], - i = -1, - n = prefixes.length, - s = document.body; + var prefixes = ['webkit', 'ms', 'moz', 'o']; + var i = -1; + var n = prefixes.length; + var s = document.body; if (property in s) return property; property = property.substr(0, 1).toUpperCase() + property.substr(1); - while (++i < n) - if (prefixes[i] + property in s) + while (++i < n) { + if (prefixes[i] + property in s) { return prefixes[i] + property; + } + } return false; } export function utilPrefixCSSProperty(property) { - var prefixes = ['webkit', 'ms', 'Moz', 'O'], - i = -1, - n = prefixes.length, - s = document.body.style; + var prefixes = ['webkit', 'ms', 'Moz', 'O']; + var i = -1; + var n = prefixes.length; + var s = document.body.style; - if (property.toLowerCase() in s) + if (property.toLowerCase() in s) { return property.toLowerCase(); + } - while (++i < n) - if (prefixes[i] + property in s) + while (++i < n) { + if (prefixes[i] + property in s) { return '-' + prefixes[i].toLowerCase() + property.replace(/([A-Z])/g, '-$1').toLowerCase(); + } + } return false; } @@ -202,10 +219,9 @@ export function utilPrefixCSSProperty(property) { var transformProperty; export function utilSetTransform(el, x, y, scale) { - var prop = transformProperty = transformProperty || utilPrefixCSSProperty('Transform'), - translate = utilDetect().opera ? - 'translate(' + x + 'px,' + y + 'px)' : - 'translate3d(' + x + 'px,' + y + 'px,0)'; + var prop = transformProperty = transformProperty || utilPrefixCSSProperty('Transform'); + var translate = utilDetect().opera ? 'translate(' + x + 'px,' + y + 'px)' + : 'translate3d(' + x + 'px,' + y + 'px,0)'; return el.style(prop, translate + (scale ? ' scale(' + scale + ')' : '')); } @@ -240,11 +256,12 @@ export function utilEditDistance(a, b) { // 1. Only works on HTML elements, not SVG // 2. Does not cause style recalculation export function utilFastMouse(container) { - var rect = container.getBoundingClientRect(), - rectLeft = rect.left, - rectTop = rect.top, - clientLeft = +container.clientLeft, - clientTop = +container.clientTop; + var rect = container.getBoundingClientRect(); + var rectLeft = rect.left; + var rectTop = rect.top; + var clientLeft = +container.clientLeft; + var clientTop = +container.clientTop; + if (textDirection === 'rtl') { rectLeft = 0; } @@ -262,9 +279,9 @@ export var utilGetPrototypeOf = Object.getPrototypeOf || function(obj) { return export function utilAsyncMap(inputs, func, callback) { - var remaining = inputs.length, - results = [], - errors = []; + var remaining = inputs.length; + var results = []; + var errors = []; inputs.forEach(function(d, i) { func(d, function done(err, data) { @@ -279,8 +296,9 @@ export function utilAsyncMap(inputs, func, callback) { // wraps an index to an interval [0..length-1] export function utilWrap(index, length) { - if (index < 0) + if (index < 0) { index += Math.ceil(-index/length)*length; + } return index % length; } diff --git a/modules/validations/deprecated_tag.js b/modules/validations/deprecated_tag.js index 64ca4c644..eeda2a574 100644 --- a/modules/validations/deprecated_tag.js +++ b/modules/validations/deprecated_tag.js @@ -14,8 +14,8 @@ export function validationDeprecatedTag() { var validation = function(changes) { var issues = []; for (var i = 0; i < changes.created.length; i++) { - var change = changes.created[i], - deprecatedTags = change.deprecatedTags(); + var change = changes.created[i]; + var deprecatedTags = change.deprecatedTags(); if (!_isEmpty(deprecatedTags)) { var tags = utilTagText({ tags: deprecatedTags }); diff --git a/modules/validations/generic_name.js b/modules/validations/generic_name.js new file mode 100644 index 000000000..7cc27834a --- /dev/null +++ b/modules/validations/generic_name.js @@ -0,0 +1,51 @@ +import { t } from '../util/locale'; +import { discardNames } from '../../node_modules/name-suggestion-index/config/filters.json'; + +export function validationGenericName() { + + function isGenericName(entity) { + var name = entity.tags.name; + if (!name) return false; + + var i, re; + + // test if the name is just the tag value (e.g. "park") + var keys = ['amenity', 'leisure', 'shop', 'man_made', 'tourism']; + for (i = 0; i < keys.length; i++) { + var val = entity.tags[keys[i]]; + if (val && val.replace(/\_/g, ' ').toLowerCase() === name.toLowerCase()) { + return name; + } + } + + // test if the name is a generic name (e.g. "pizzaria") + for (i = 0; i < discardNames.length; i++) { + re = new RegExp(discardNames[i], 'i'); + if (re.test(name)) { + return name; + } + } + + return false; + } + + + return function validation(changes) { + var warnings = []; + + for (var i = 0; i < changes.created.length; i++) { + var change = changes.created[i]; + var generic = isGenericName(change); + if (generic) { + warnings.push({ + id: 'generic_name', + message: t('validations.generic_name'), + tooltip: t('validations.generic_name_tooltip', { name: generic }), + entity: change + }); + } + } + + return warnings; + }; +} diff --git a/modules/validations/index.js b/modules/validations/index.js index ed2dc4af3..c8e8a0b91 100644 --- a/modules/validations/index.js +++ b/modules/validations/index.js @@ -3,6 +3,7 @@ export { validationDisconnectedHighway } from './disconnected_highway'; export { validationHighwayCrossingOtherWays } from './crossing_ways'; export { validationHighwayAlmostJunction } from './highway_almost_junction'; export { ValidationIssueType, ValidationIssueSeverity } from './validation_issue'; +export { validationGenericName } from './generic_name.js'; export { validationManyDeletions } from './many_deletions'; export { validationMapCSSChecks } from './mapcss_checks'; export { validationMissingTag } from './missing_tag'; diff --git a/modules/validations/many_deletions.js b/modules/validations/many_deletions.js index dc0545ae6..e60c03e66 100644 --- a/modules/validations/many_deletions.js +++ b/modules/validations/many_deletions.js @@ -10,13 +10,13 @@ export function validationManyDeletions() { var validation = function(changes, graph) { var issues = []; - var nodes=0, ways=0, areas=0, relations=0; + var nodes = 0, ways = 0, areas = 0, relations = 0; changes.deleted.forEach(function(c) { - if (c.type === 'node') {nodes++;} - else if (c.type === 'way' && c.geometry(graph) === 'line') {ways++;} - else if (c.type === 'way' && c.geometry(graph) === 'area') {areas++;} - else if (c.type === 'relation') {relations++;} + if (c.type === 'node') { nodes++; } + else if (c.type === 'way' && c.geometry(graph) === 'line') { ways++; } + else if (c.type === 'way' && c.geometry(graph) === 'area') { areas++; } + else if (c.type === 'relation') { relations++; } }); if (changes.deleted.length > threshold) { issues.push(new validationIssue({ diff --git a/modules/validations/mapcss_checks.js b/modules/validations/mapcss_checks.js index 6804279da..9832c48f8 100644 --- a/modules/validations/mapcss_checks.js +++ b/modules/validations/mapcss_checks.js @@ -21,5 +21,7 @@ export function validationMapCSSChecks() { return issues; }; + + return validation; } diff --git a/modules/validations/missing_tag.js b/modules/validations/missing_tag.js index 4818dfa52..269412057 100644 --- a/modules/validations/missing_tag.js +++ b/modules/validations/missing_tag.js @@ -20,12 +20,12 @@ export function validationMissingTag(context) { } var validation = function(changes, graph) { - var types = ['point', 'line', 'area', 'relation'], - issues = []; + var types = ['point', 'line', 'area', 'relation']; + var issues = []; for (var i = 0; i < changes.created.length; i++) { - var change = changes.created[i], - geometry = change.geometry(graph); + var change = changes.created[i]; + var geometry = change.geometry(graph); if (types.indexOf(geometry) !== -1 && !hasTags(change, graph)) { var entityLabel = utilDisplayLabel(change, context); diff --git a/modules/validations/tag_suggests_area.js b/modules/validations/tag_suggests_area.js index bed1f9a63..1990ebe38 100644 --- a/modules/validations/tag_suggests_area.js +++ b/modules/validations/tag_suggests_area.js @@ -32,9 +32,9 @@ export function validationTagSuggestsArea() { var validation = function(changes, graph) { var issues = []; for (var i = 0; i < changes.created.length; i++) { - var change = changes.created[i], - geometry = change.geometry(graph), - suggestion = (geometry === 'line' ? tagSuggestsArea(change.tags) : undefined); + var change = changes.created[i]; + var geometry = change.geometry(graph); + var suggestion = (geometry === 'line' ? tagSuggestsArea(change.tags) : undefined); if (suggestion) { issues.push(new validationIssue({ diff --git a/package.json b/package.json index 927a8033f..0e2e8996c 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "diacritics": "1.3.0", "fast-json-stable-stringify": "2.0.0", "lodash-es": "4.17.11", - "marked": "0.5.2", + "marked": "0.6.0", "martinez-polygon-clipping": "0.5.0", "node-diff3": "1.0.0", "osm-auth": "1.0.2", @@ -49,10 +49,10 @@ "wmf-sitematrix": "0.1.4" }, "devDependencies": { - "@fortawesome/fontawesome-svg-core": "~1.2.9", - "@fortawesome/free-brands-svg-icons": "~5.6.0", - "@fortawesome/free-regular-svg-icons": "~5.6.0", - "@fortawesome/free-solid-svg-icons": "~5.6.0", + "@fortawesome/fontawesome-svg-core": "~1.2.12", + "@fortawesome/free-brands-svg-icons": "~5.6.3", + "@fortawesome/free-regular-svg-icons": "~5.6.3", + "@fortawesome/free-solid-svg-icons": "~5.6.3", "@mapbox/maki": "^6.0.0", "chai": "^4.1.0", "colors": "^1.1.2", @@ -78,7 +78,7 @@ "osm-community-index": "0.5.0", "phantomjs-prebuilt": "~2.1.11", "request": "^2.88.0", - "rollup": "~0.68.0", + "rollup": "~1.1.0", "rollup-plugin-commonjs": "^9.0.0", "rollup-plugin-includepaths": "~0.2.3", "rollup-plugin-json": "^3.0.0", diff --git a/svg/fontawesome/fas-skating.svg b/svg/fontawesome/fas-skating.svg new file mode 100644 index 000000000..aa80f7c2f --- /dev/null +++ b/svg/fontawesome/fas-skating.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svg/fontawesome/fas-skiing-nordic.svg b/svg/fontawesome/fas-skiing-nordic.svg new file mode 100644 index 000000000..efe3a9d59 --- /dev/null +++ b/svg/fontawesome/fas-skiing-nordic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svg/fontawesome/fas-skiing.svg b/svg/fontawesome/fas-skiing.svg new file mode 100644 index 000000000..75422fa8f --- /dev/null +++ b/svg/fontawesome/fas-skiing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svg/fontawesome/fas-sleigh.svg b/svg/fontawesome/fas-sleigh.svg new file mode 100644 index 000000000..91ffea4e7 --- /dev/null +++ b/svg/fontawesome/fas-sleigh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svg/fontawesome/fas-snowboarding.svg b/svg/fontawesome/fas-snowboarding.svg new file mode 100644 index 000000000..721620e03 --- /dev/null +++ b/svg/fontawesome/fas-snowboarding.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svg/iD-sprite/icons/icon-bolt.svg b/svg/iD-sprite/icons/icon-bolt.svg new file mode 100644 index 000000000..8078987b2 --- /dev/null +++ b/svg/iD-sprite/icons/icon-bolt.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/svg/iD-sprite/operations/operation-circularize.svg b/svg/iD-sprite/operations/operation-circularize.svg index 547af94e9..a691303fb 100644 --- a/svg/iD-sprite/operations/operation-circularize.svg +++ b/svg/iD-sprite/operations/operation-circularize.svg @@ -1,6 +1,6 @@ - + diff --git a/test/index.html b/test/index.html index 718d42921..12864e8a9 100644 --- a/test/index.html +++ b/test/index.html @@ -110,6 +110,7 @@ + diff --git a/test/spec/services/osm_wikibase.js b/test/spec/services/osm_wikibase.js new file mode 100644 index 000000000..61951abba --- /dev/null +++ b/test/spec/services/osm_wikibase.js @@ -0,0 +1,322 @@ +describe('iD.serviceOsmWikibase', function () { + var server, wikibase; + + before(function () { + iD.services.osmWikibase = iD.serviceOsmWikibase; + }); + + after(function () { + delete iD.services.osmWikibase; + }); + + beforeEach(function () { + wikibase = iD.services.osmWikibase; + wikibase.init(); + server = sinon.fakeServer.create(); + }); + + afterEach(function () { + server.restore(); + }); + + + function query(url) { + return iD.utilStringQs(url.substring(url.indexOf('?') + 1)); + } + + function adjust(params, data) { + if (params) { + if (params.norm) { + data.description = data.descriptions.fr.value; + data.label = data.labels.fr.value; + } + } + return data; + } + + function keyData(params) { + return adjust(params, { + pageid: 205725, + ns: 120, + title: 'Item:Q42', + lastrevid: 1721242, + modified: '2018-12-18T07:00:43Z', + type: 'item', + id: 'Q42', + labels: { + fr: {language: 'en', value: 'amenity', 'for-language': 'fr'} + }, + descriptions: { + fr: {language: 'en', value: 'English description', 'for-language': 'fr'} + }, + aliases: {}, + claims: { + P2: [ // instance of + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q7'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P16: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'string', + datavalue: {value: 'amenity', type: 'string'} + }, + type: 'statement', + rank: 'normal' + } + ], + P25: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q4679'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P9: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q8'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P6: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q15'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'preferred' + }, + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q14'}, type: 'wikibase-entityid'} + }, + type: 'statement', + qualifiers: { + P26: [ + { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q6994'}, type: 'wikibase-entityid'} + } + ] + }, + rank: 'normal' + } + ], + P28: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'string', + datavalue: {value: 'Mapping-Features-Parking-Lot.png', type: 'string'} + }, + type: 'statement', + rank: 'normal' + } + ] + }, + sitelinks: { + wiki: { + site: 'wiki', + title: 'Key:amenity', + badges: [] + } + } + }); + } + + function tagData(params) { + return adjust(params, { + pageid: 210934, + ns: 120, + title: 'Item:Q13', + lastrevid: 1718041, + modified: '2018-12-18T03:51:05Z', + type: 'item', + id: 'Q13', + labels: { + fr: {language: 'en', value: 'amenity=parking', 'for-language': 'fr'} + }, + descriptions: { + fr: {language: 'fr', value: 'French description'} + }, + aliases: {}, + claims: { + P2: [ // instance of = Q2 (tag) + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q2'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P19: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'string', + datavalue: {value: 'amenity=parking', type: 'string'} + }, + type: 'statement', + rank: 'normal' + } + ], + P10: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q42'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P4: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'commonsMedia', + datavalue: {value: 'Primary image.jpg', type: 'string'} + }, + type: 'statement', + rank: 'preferred' + } + ], + P6: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q14'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'preferred' + }, + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q13'}, type: 'wikibase-entityid'} + }, + type: 'statement', + qualifiers: { + P26: [ + { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q6994'}, type: 'wikibase-entityid'} + } + ] + }, + rank: 'normal' + } + ], + P25: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q4679'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ] + }, + sitelinks: { + wiki: { + site: 'wiki', + title: 'Tag:amenity=parking', + badges: [] + } + } + }); + } + + + var localeData = { + id: 'Q7792', + sitelinks: {wiki: {site: 'wiki', title: 'Locale:fr'}} + }; + + describe('#getEntity', function () { + it('calls the given callback with the results of the getEntity data item query', function () { + var callback = sinon.spy(); + wikibase.getEntity({key: 'amenity', value: 'parking', langCode: 'fr'}, callback); + + server.respondWith('GET', /action=wbgetentities/, + [200, {'Content-Type': 'application/json'}, JSON.stringify({ + entities: { + Q42: keyData(), + Q13: tagData(), + Q7792: localeData, + }, + success: 1 + })] + ); + server.respond(); + + expect(query(server.requests[0].url)).to.eql( + { + action: 'wbgetentities', + sites: 'wiki', + titles: 'Locale:fr|Key:amenity|Tag:amenity=parking', + languages: 'fr', + languagefallback: '1', + origin: '*', + format: 'json', + } + ); + expect(callback).to.have.been.calledWith(null, { + key: keyData({norm: true}), + tag: tagData({norm: true}) + }); + }); + }); + + + it('creates correct sitelinks', function () { + expect(wikibase.toSitelink('amenity')).to.eql('Key:amenity'); + expect(wikibase.toSitelink('amenity_')).to.eql('Key:amenity'); + expect(wikibase.toSitelink('_amenity_')).to.eql('Key: amenity'); + expect(wikibase.toSitelink('amenity or_not_')).to.eql('Key:amenity or not'); + expect(wikibase.toSitelink('amenity', 'parking')).to.eql('Tag:amenity=parking'); + expect(wikibase.toSitelink(' amenity_', '_parking_')).to.eql('Tag: amenity = parking'); + expect(wikibase.toSitelink('amenity or_not', '_park ing_')).to.eql('Tag:amenity or not= park ing'); + }); + + it('gets correct value from entity', function () { + wikibase.addLocale('de', 'Q6994'); + wikibase.addLocale('fr', 'Q7792'); + expect(wikibase.claimToValue(tagData(), 'P4', 'en')).to.eql('Primary image.jpg'); + expect(wikibase.claimToValue(keyData(), 'P6', 'en')).to.eql('Q15'); + expect(wikibase.claimToValue(keyData(), 'P6', 'fr')).to.eql('Q15'); + expect(wikibase.claimToValue(keyData(), 'P6', 'de')).to.eql('Q14'); + }); + +}); diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index f2277229c..5a40332d6 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -26,17 +26,18 @@ describe('iD.svgLayers', function () { it('creates default data layers', function () { container.call(iD.svgLayers(projection, context)); var nodes = container.selectAll('svg .data-layer').nodes(); - expect(nodes.length).to.eql(10); + expect(nodes.length).to.eql(11); expect(d3.select(nodes[0]).classed('osm')).to.be.true; expect(d3.select(nodes[1]).classed('notes')).to.be.true; expect(d3.select(nodes[2]).classed('data')).to.be.true; - expect(d3.select(nodes[3]).classed('streetside')).to.be.true; - expect(d3.select(nodes[4]).classed('mapillary-images')).to.be.true; - expect(d3.select(nodes[5]).classed('mapillary-signs')).to.be.true; - expect(d3.select(nodes[6]).classed('openstreetcam-images')).to.be.true; - expect(d3.select(nodes[7]).classed('debug')).to.be.true; - expect(d3.select(nodes[8]).classed('geolocate')).to.be.true; - expect(d3.select(nodes[9]).classed('touch')).to.be.true; + expect(d3.select(nodes[3]).classed('keepRight')).to.be.true; + expect(d3.select(nodes[4]).classed('streetside')).to.be.true; + expect(d3.select(nodes[5]).classed('mapillary-images')).to.be.true; + expect(d3.select(nodes[6]).classed('mapillary-signs')).to.be.true; + expect(d3.select(nodes[7]).classed('openstreetcam-images')).to.be.true; + expect(d3.select(nodes[8]).classed('debug')).to.be.true; + expect(d3.select(nodes[9]).classed('geolocate')).to.be.true; + expect(d3.select(nodes[10]).classed('touch')).to.be.true; }); }); diff --git a/test/spec/ui/fields/wikipedia.js b/test/spec/ui/fields/wikipedia.js index 8803e10fe..a1a804bc1 100644 --- a/test/spec/ui/fields/wikipedia.js +++ b/test/spec/ui/fields/wikipedia.js @@ -110,7 +110,7 @@ describe('iD.uiFieldWikipedia', function() { expect(iD.utilGetSetValue(selection.selectAll('.wiki-lang'))).to.equal('Deutsch'); }); - it('does not set delayed wikidata tag if graph has changed', function(done) { + it.skip('does not set delayed wikidata tag if graph has changed', function(done) { var wikipedia = iD.uiFieldWikipedia(field, context).entity(entity); wikipedia.on('change', changeTags); selection.call(wikipedia);