From 433df494b620159047cc903554ae3c8a45e25634 Mon Sep 17 00:00:00 2001 From: Sikandar Date: Sun, 22 Feb 2026 23:30:44 +0500 Subject: [PATCH 1/4] Integrate KyFromAbove oblique imagery service and COG viewer (#11853) --- css/60_photos.css | 111 +++++-- data/core.yaml | 356 ++++++++++---------- modules/services/index.js | 7 +- modules/services/kyfromabove.js | 165 ++++++++++ modules/svg/kyfromabove.js | 207 ++++++++++++ modules/svg/layers.js | 2 + modules/ui/ky_oblique_viewer.js | 130 ++++++++ modules/ui/sections/photo_overlays.js | 2 +- package-lock.json | 446 +++++++++++++++++++++++++- package.json | 1 + 10 files changed, 1208 insertions(+), 219 deletions(-) create mode 100644 modules/services/kyfromabove.js create mode 100644 modules/svg/kyfromabove.js create mode 100644 modules/ui/ky_oblique_viewer.js diff --git a/css/60_photos.css b/css/60_photos.css index c4efb412a16..a3e70f9b722 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -7,7 +7,7 @@ li.list-item-photos.active:after { right: 0; width: 8px; } -.ideditor[dir='rtl'] li.list-item-photos.active:after { +.ideditor[dir="rtl"] li.list-item-photos.active:after { right: auto; left: 0; } @@ -27,11 +27,11 @@ li.list-item-photos.active:after { padding: 5px; background-color: var(--bg-color); } -.ideditor[dir='ltr'] .photoviewer { +.ideditor[dir="ltr"] .photoviewer { margin-left: 10px; margin-right: 2px; } -.ideditor[dir='rtl'] .photoviewer { +.ideditor[dir="rtl"] .photoviewer { margin-right: 10px; margin-left: 2px; } @@ -95,7 +95,6 @@ li.list-item-photos.active:after { width: 100%; } - .photo-wrapper { width: 100%; height: 100%; @@ -187,9 +186,8 @@ li.list-item-photos.active:after { fill-opacity: 0.9; } - .viewfield-group.currentView .viewfield-scale { - transform: scale(2,2); + transform: scale(2, 2); } .sequence { @@ -203,7 +201,6 @@ li.list-item-photos.active:after { stroke-opacity: 1; } - /* Streetside Image Layer */ li.list-item-photos.list-item-streetside.active:after { background-color: #0fffc4; @@ -216,7 +213,7 @@ li.list-item-photos.list-item-streetside.active:after { } .layer-streetside-images .sequence { stroke: #0fffc4; - stroke-opacity: 0.85; /* bump opacity - only one per road */ + stroke-opacity: 0.85; /* bump opacity - only one per road */ } /* Vegbilder Image Layer */ @@ -231,10 +228,9 @@ li.list-item-photos.list-item-vegbilder.active:after { } .layer-vegbilder .sequence { stroke: #ed1c2e; - stroke-opacity: 0.85; /* bump opacity - only one per road */ + stroke-opacity: 0.85; /* bump opacity - only one per road */ } - /* Mapillary Image Layer */ li.list-item-photos.list-item-mapillary.active:after { background-color: #55ff22; @@ -249,7 +245,6 @@ li.list-item-photos.list-item-mapillary.active:after { stroke: #55ff22; } - /* Mapillary Traffic Signs and Map Features Layers */ .layer-mapillary-detections { pointer-events: none; @@ -284,7 +279,6 @@ li.list-item-photos.list-item-mapillary.active:after { outline: 3px solid rgba(255, 238, 0, 1); } - /* KartaView Image Layer */ li.list-item-photos.list-item-kartaview.active:after { background-color: #20c4ff; @@ -299,7 +293,6 @@ li.list-item-photos.list-item-kartaview.active:after { stroke: #20c4ff; } - /* Mapilio Image Layer */ li.list-item-photos.list-item-mapilio.active:after { background-color: #0056f1; @@ -312,8 +305,8 @@ li.list-item-photos.list-item-mapilio.active:after { } .layer-mapilio .viewfield-group:not(.currentView):not(.hovered) * { stroke: #ffffff; - stroke-opacity: .6; - fill-opacity: .6; + stroke-opacity: 0.6; + fill-opacity: 0.6; } .layer-mapilio .sequence { stroke: #0056f1; @@ -325,7 +318,7 @@ li.list-item-photos.list-item-mapilio.active:after { gap: 4px; } .photo-controls-mapilio button { - padding:0 6px; + padding: 0 6px; pointer-events: initial; } .ideditor .mapilio-wrapper { @@ -360,8 +353,8 @@ li.list-item-photos.list-item-panoramax.active:after { .layer-panoramax .viewfield-group * { fill: #ff6f00; stroke: #ffffff; - stroke-opacity: .6; - fill-opacity: .6; + stroke-opacity: 0.6; + fill-opacity: 0.6; } .layer-panoramax .sequence { stroke: #ff6f00; @@ -373,7 +366,7 @@ li.list-item-photos.list-item-panoramax.active:after { gap: 4px; } .photo-controls-panoramax button { - padding:0 6px; + padding: 0 6px; pointer-events: initial; } @@ -404,12 +397,11 @@ label.panoramax-hd { width: 100%; } -.yearSliderSpan{ +.yearSliderSpan { padding: 2px; } - -.list-item-date-slider label{ +.list-item-date-slider label { display: block !important; } @@ -446,8 +438,8 @@ label.panoramax-hd { } .ms-wrapper .pnlm-compass.pnlm-control, -.vegbilder-wrapper .pnlm-compass.pnlm-control, -.panoramax-wrapper .pnlm-compass.pnlm-control { +.vegbilder-wrapper .pnlm-compass.pnlm-control, +.panoramax-wrapper .pnlm-compass.pnlm-control { width: 26px; height: 26px; left: 4px; @@ -473,11 +465,10 @@ label.streetside-hires { margin-top: 6px; } - /* Mapillary viewer */ #ideditor-mly .domRenderer .TagSymbol { font-size: 10px; - background-color: rgba(0,0,0,0.4); + background-color: rgba(0, 0, 0, 0.4); padding: 0 4px; border-radius: 4px; top: -25px; @@ -488,13 +479,15 @@ label.streetside-hires { align-items: center; } -.mly-wrapper .mapillary-attribution-container .mapillary-attribution-icon-container { +.mly-wrapper + .mapillary-attribution-container + .mapillary-attribution-icon-container { display: flex; align-items: center; } .mapillary-attribution-image-container { - height: auto; + height: auto; } .mly-wrapper .mapillary-attribution-container .mapillary-attribution-username { @@ -598,7 +591,7 @@ label.streetside-hires { height: 18px; width: 18px; line-height: 18px; - background: rgba(0,0,0,0.65); + background: rgba(0, 0, 0, 0.65); color: #eee; border-radius: 0; } @@ -609,12 +602,12 @@ label.streetside-hires { border-radius: 0 3px 3px 0; } .photo-controls button:active { - background: rgba(0,0,0,0.85); + background: rgba(0, 0, 0, 0.85); color: #fff; } @media (hover: hover) { .photo-controls button:hover { - background: rgba(0,0,0,0.85); + background: rgba(0, 0, 0, 0.85); color: #fff; } } @@ -724,11 +717,63 @@ label.streetside-hires { gap: 4px; } .photo-controls-local button { - padding:0 6px; + padding: 0 6px; pointer-events: initial; } .photo-controls-local button:disabled { - background: rgba(255,255,255,.25); + background: rgba(255, 255, 255, 0.25); +} + +/* KyFromAbove */ +.ky-oblique-wrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10; + background: #000; +} + +.ky-oblique-image-wrap { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.ky-oblique-canvas { + max-width: 100%; + max-height: 100%; + object-fit: contain; } +.ky-oblique-wrapper .photo-controls-wrap { + z-index: 20; +} + +.ky-oblique-wrapper .angle-button { + background: rgba(0, 0, 0, 0.5); + color: #fff; + border: 1px solid #666; + margin: 0 2px; + padding: 2px 6px; + border-radius: 4px; + cursor: pointer; +} + +.ky-oblique-wrapper .angle-button:hover { + background: rgba(0, 0, 0, 0.8); +} + +.ky-oblique-wrapper .angle-button.active { + background: #ffbc00; + color: #000; + border-color: #ffbc00; +} diff --git a/data/core.yaml b/data/core.yaml index dcbbc2e45c4..7c9ca7d95dd 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -59,8 +59,7 @@ en: drag_node: connected_to_hidden: This can't be edited because it is connected to a hidden feature. operations: - _unavailable: - Cannot perform “{operation}” on currently selected features. + _unavailable: Cannot perform “{operation}” on currently selected features. add: annotation: point: Added a point. @@ -278,7 +277,7 @@ en: to_line: Moved a point to a line. to_area: Moved a point to an area. relation: These features can't be connected because they have conflicting relation roles. - restriction: "These features can't be connected because it would damage a \"{relation}\" relation." + restriction: 'These features can''t be connected because it would damage a "{relation}" relation.' disconnect: title: Disconnect description: @@ -321,7 +320,7 @@ en: other: "Merged {n} features." not_eligible: These features can't be merged. not_adjacent: These features can't be merged because their endpoints aren't connected. - damage_relation: "These features can't be merged because it would damage a \"{relation}\" relation." + damage_relation: 'These features can''t be merged because it would damage a "{relation}" relation.' relation: These features can't be merged because they have conflicting relation roles. incomplete_relation: These features can't be merged because at least one hasn't been fully downloaded. conflicting_tags: These features can't be merged because some of their tags have conflicting values. @@ -564,7 +563,7 @@ en: via_names: "{via} {viaNames}" select_from: "Click to select a {from} segment" select_from_name: "Click to select {from} {fromName}" - toggle: "Click for \"{turn}\"" + toggle: 'Click for "{turn}"' undo: title: Undo tooltip: "Undo: {action}" @@ -587,7 +586,7 @@ en: report_a_bug: Report a bug help_translate: Help translate sidebar: - key: '`' + key: "`" tooltip: Toggle the sidebar. feature_info: hidden_warning: @@ -840,7 +839,7 @@ en: minimap: description: Show Minimap tooltip: Show a zoomed out map to help locate the area currently displayed. - key: '/' + key: "/" panel: description: Show Background Panel tooltip: Show advanced background information. @@ -1043,9 +1042,9 @@ en: unsaved_changes: You have unsaved changes conflict: header: Resolve conflicting edits - count: 'Conflict {num} of {total}' - previous: '< Previous' - next: 'Next >' + count: "Conflict {num} of {total}" + previous: "< Previous" + next: "Next >" keep_local: Keep mine keep_remote: Use theirs restore: Restore @@ -1055,10 +1054,10 @@ en: help: "Another user changed some of the same map features you changed. Click on each feature below for more details about the conflict, and choose whether to keep your changes or the other user's changes." merge_remote_changes: conflict: - deleted: 'This feature has been deleted by {user}.' - location: 'This feature was moved by both you and {user}.' - nodelist: 'Nodes were changed by both you and {user}.' - memberlist: 'Relation members were changed by both you and {user}.' + deleted: "This feature has been deleted by {user}." + location: "This feature was moved by both you and {user}." + nodelist: "Nodes were changed by both you and {user}." + memberlist: "Relation members were changed by both you and {user}." tags: 'You changed the "{tag}" tag to "{local}" and {user} changed it to "{remote}".' success: just_edited: "You just edited OpenStreetMap!" @@ -1137,80 +1136,80 @@ en: close_comment: Close and Comment ignore_comment: Remove and Comment error_parts: - this_node: 'this node' - this_way: 'this way' - this_relation: 'this relation' - this_oneway: 'this oneway' - this_highway: 'this highway' - this_railway: 'this railway' - this_waterway: 'this waterway' - this_cycleway: 'this cycleway' - this_cycleway_footpath: 'this cycleway/footpath' - this_riverbank: 'this riverbank' - this_crossing: 'this crossing' - this_railway_crossing: 'this railway crossing' - this_bridge: 'this bridge' - this_tunnel: 'this tunnel' - this_boundary: 'this boundary' - this_turn_restriction: 'this turn restriction' - this_roundabout: 'this roundabout' - this_mini_roundabout: 'this mini-roundabout' - this_track: 'this track' - this_feature: 'this feature' - highway: 'highway' - railway: 'railway' - waterway: 'waterway' - cycleway: 'cycleway' - cycleway_footpath: 'cycleway/footpath' - riverbank: 'riverbank' - 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' + this_node: "this node" + this_way: "this way" + this_relation: "this relation" + this_oneway: "this oneway" + this_highway: "this highway" + this_railway: "this railway" + this_waterway: "this waterway" + this_cycleway: "this cycleway" + this_cycleway_footpath: "this cycleway/footpath" + this_riverbank: "this riverbank" + this_crossing: "this crossing" + this_railway_crossing: "this railway crossing" + this_bridge: "this bridge" + this_tunnel: "this tunnel" + this_boundary: "this boundary" + this_turn_restriction: "this turn restriction" + this_roundabout: "this roundabout" + this_mini_roundabout: "this mini-roundabout" + this_track: "this track" + this_feature: "this feature" + highway: "highway" + railway: "railway" + waterway: "waterway" + cycleway: "cycleway" + cycleway_footpath: "cycleway/footpath" + riverbank: "riverbank" + 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}.' + 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' + 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.' + 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.' + 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.' + 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.' + 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}.' + title: "Almost junction" + description: "{var1} is very close but not connected to way {var2}." 60: - title: 'Deprecated tag' + title: "Deprecated tag" description: '{var1} uses deprecated tag "{var2}". Please use "{var3}" instead.' 70: - title: 'Missing tag' + title: "Missing tag" description: '{var1} has an empty tag: "{var2}".' 71: - description: '{var1} has no tags.' + description: "{var1} has no tags." 72: - description: '{var1} is not member of any way and doesn''t have any tags.' + 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: @@ -1218,85 +1217,85 @@ en: 75: description: '{var1} has a name "{var2}" but no other tags.' 90: - title: 'Motorway without ref tag' + 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.' + 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' + 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.' + 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.' + title: "Disconnected way" + description: "{var1} is not connected to the rest of the map." 150: - title: 'Railway crossing without tag' + 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}.' + 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}' + title: "FIXME tagged item" + description: "{var1} has a FIXME tag: {var2}" 180: - title: 'Relation without type' + 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.' + 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}.' + title: "Overlapping ways" + description: "{var1} overlaps the {var2} {var3}." 210: - title: 'Self-intersecting way' - description: 'There is an unspecified issue with self intersecting ways.' + 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.' + 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.' + description: "{var1} has only two different nodes and contains one of them more than once." 220: - title: 'Misspelled tag' + 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.' + 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})' + 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' + 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.' + title: "Boundary issue" + description: "There is an unspecified issue with this boundary." 281: - title: 'Boundary missing name' - description: '{var1} has no name.' + title: "Boundary missing name" + description: "{var1} has no name." 282: - title: 'Boundary missing admin level' + 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.' + 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.' + title: "Boundary is split" + description: "The boundary of {var1} splits here." 285: - title: 'Boundary admin_level too high' + 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.' + title: "Restriction issue" + description: "There is an unspecified issue with this restriction." 291: - title: 'Restriction missing type' - description: '{var1} has an unrecognized restriction type.' + 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.' @@ -1310,62 +1309,62 @@ en: 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' + 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" way' description: '{var1} does not match the direction of "to" way {var2}.' 298: - title: 'Redundant restriction - oneway' + title: "Redundant restriction - oneway" description: '{var1} may be redundant. Entry already prohibited by "oneway" tag on {var2}.' 300: - title: 'Missing maxspeed' + 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.' + 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).' + 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.' + 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.' + title: "Roundabout weakly connected" + description: "{var1} has only {var2} other road(s) connected. Roundabouts typically have 3 or more." 320: - title: 'Improper link connection' + 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}.' + 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' + 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.' + 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' + 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' + 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.' + 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.' + 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.' + 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.' + 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}.' + 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: @@ -1464,6 +1463,9 @@ en: report: "Report" captured_by: "Captured by {username}" hd: "High resolution" + kyfromabove: + title: KyFromAbove + tooltip: "Oblique aerial imagery from Kentucky" street_side: minzoom_tooltip: "Zoom in to see street-side photos" local_photos: @@ -1512,7 +1514,7 @@ en: open_data_h: "Open Data" open_data: "Edits that you make on this map will be visible to everyone who uses OpenStreetMap. Your edits can be based on personal knowledge, on-the-ground surveying, or imagery collected from aerial or street level photos. Copying from commercial sources, like Google Maps, [is strictly forbidden](https://www.openstreetmap.org/copyright)." before_start_h: "Before you start" - before_start: "You should be familiar with OpenStreetMap and this editor before you start editing. iD contains a walkthrough to teach you the basics of editing OpenStreetMap. Press the \"{start_the_walkthrough}\" button on this screen to start the tutorial—it takes only about 15 minutes." + before_start: 'You should be familiar with OpenStreetMap and this editor before you start editing. iD contains a walkthrough to teach you the basics of editing OpenStreetMap. Press the "{start_the_walkthrough}" button on this screen to start the tutorial—it takes only about 15 minutes.' open_source_h: "Open Source" open_source: "The iD editor is a collaborative open source project, and you are using version {version} now. The source code is available [on GitHub](https://github.com/openstreetmap/iD)." open_source_attribution: "This project includes icons from [Maki](https://github.com/mapbox/maki) (CC0), [temaki](https://github.com/ideditor/temaki) (CC0), [Fontawesome](https://github.com/FortAwesome/Font-Awesome) (CC BY 4.0) and [Map Machine](https://github.com/enzet/map-machine) (CC BY 4.0)." @@ -1554,11 +1556,11 @@ en: type: "You can press the feature type to change the feature to a different type. Everything that exists in the real world can be added to OpenStreetMap, so there are thousands of feature types to choose from." type_picker: "The type picker displays the most common feature types, such as parks, hospitals, restaurants, roads, and buildings. You can search for anything by typing what you're looking for in the search box. You can also press the {inspect} **Info** icon next to the feature type to learn more about it." fields_h: "Fields" - fields_all_fields: "The \"{fields}\" section contains all of the feature's details that you may edit. In OpenStreetMap, all of the fields are optional, and it's OK to leave a field blank if you are unsure." + fields_all_fields: 'The "{fields}" section contains all of the feature''s details that you may edit. In OpenStreetMap, all of the fields are optional, and it''s OK to leave a field blank if you are unsure.' fields_example: "Each feature type will display different fields. For example, a road may display fields for its surface and speed limit, but a restaurant may display fields for the type of food it serves and the hours it is open." - fields_add_field: "You can also use the \"Add field\" dropdown to add more fields, such as a description, Wikipedia link, wheelchair access, and more." + fields_add_field: 'You can also use the "Add field" dropdown to add more fields, such as a description, Wikipedia link, wheelchair access, and more.' tags_h: "Tags" - tags_all_tags: "Below the fields section, you can expand the \"{tags}\" section to edit any of the OpenStreetMap *tags* for the selected feature. Each tag consists of a *key* and *value*, data elements that define all of the features stored in OpenStreetMap." + tags_all_tags: 'Below the fields section, you can expand the "{tags}" section to edit any of the OpenStreetMap *tags* for the selected feature. Each tag consists of a *key* and *value*, data elements that define all of the features stored in OpenStreetMap.' tags_resources: "Editing a feature's tags requires intermediate knowledge about OpenStreetMap. You should consult resources like the [OpenStreetMap Wiki](https://wiki.openstreetmap.org/wiki/Main_Page) or [Taginfo](https://taginfo.openstreetmap.org/) to learn more about accepted OpenStreetMap tagging practices." points: title: Points @@ -1617,8 +1619,8 @@ en: title: Relations intro: "A *relation* is a special type of feature in OpenStreetMap that groups together other features. The features that belong to a relation are called *members*, and each member can have a *role* in the relation." edit_relation_h: "Editing Relations" - edit_relation: "At the bottom of the feature editor, you can expand the \"{relations}\" section to see if the selected feature is a member of any relations. You can then select a relation to edit it." - edit_relation_add: "To add a feature to a relation, select the feature, then press the {plus} add button in the \"{relations}\" section of the feature editor. You can choose from a list of nearby relations, or choose the \"{new_relation}\" option." + edit_relation: 'At the bottom of the feature editor, you can expand the "{relations}" section to see if the selected feature is a member of any relations. You can then select a relation to edit it.' + edit_relation_add: 'To add a feature to a relation, select the feature, then press the {plus} add button in the "{relations}" section of the feature editor. You can choose from a list of nearby relations, or choose the "{new_relation}" option.' edit_relation_delete: "You can also press the {delete_icon} **{delete}** button to remove the selected feature from the relation. If you remove all of the members from a relation, the relation will be deleted automatically." maintain_relation_h: "Maintaining Relations" maintain_relation: "For the most part, iD will maintain relations automatically as you edit. You should take care when replacing features that might be members of relations. For example if you delete a section of road and draw a new section of road to replace it, you should add the new section to the same relations (routes, turn restrictions, etc.) as the original." @@ -1629,14 +1631,14 @@ en: multipolygon_merge: "Merging several lines or areas will create a new multipolygon relation with all selected areas as members. iD will choose the inner and outer roles automatically, based on which features are contained inside other features." turn_restriction_h: "Turn restrictions" turn_restriction: "A *turn restriction* relation is a group of several road segments in an intersection. Turn restrictions consist of a *from* road, *via* node or roads, and a *to* road." - turn_restriction_field: "To edit turn restrictions, select a junction node where two or more roads meet. The feature editor will display a special \"{turn_restrictions}\" field containing a model of the intersection." - turn_restriction_editing: "In the \"{turn_restrictions}\" field, select a \"from\" road, and see whether turns are allowed or restricted to any of the \"to\" roads. You can press the turn icons to toggle them between allowed and restricted. iD will create relations automatically and set the from, via, and to roles based on your choices." + turn_restriction_field: 'To edit turn restrictions, select a junction node where two or more roads meet. The feature editor will display a special "{turn_restrictions}" field containing a model of the intersection.' + turn_restriction_editing: 'In the "{turn_restrictions}" field, select a "from" road, and see whether turns are allowed or restricted to any of the "to" roads. You can press the turn icons to toggle them between allowed and restricted. iD will create relations automatically and set the from, via, and to roles based on your choices.' route_h: "Routes" route: "A *route* relation is a group of one or more line features that together form a route network, like a bus route, train route, or highway route." - route_add: "To add a feature to a route relation, select the feature and scroll down to the \"{relations}\" section of the feature editor, then press the {plus} add button to add this feature to a nearby existing relation or a new relation." + route_add: 'To add a feature to a route relation, select the feature and scroll down to the "{relations}" section of the feature editor, then press the {plus} add button to add this feature to a nearby existing relation or a new relation.' boundary_h: "Boundaries" boundary: "A *boundary* relation is a group of one or more line features that together form an administrative boundary." - boundary_add: "To add a feature to a boundary relation, select the feature and scroll down to the \"{relations}\" section of the feature editor, then press the {plus} add button to add this feature to a nearby existing relation or a new relation." + boundary_add: 'To add a feature to a boundary relation, select the feature and scroll down to the "{relations}" section of the feature editor, then press the {plus} add button to add this feature to a nearby existing relation or a new relation.' operations: title: Operations intro: "*Operations* are special commands you can use to edit features. {rightclick} Right-click or {longpress_icon} long-press any feature to view the available operations." @@ -1674,7 +1676,7 @@ en: choosing: "To see which imagery sources are available for editing, open the {layers_icon} **{background_settings}** panel on the side of the map." sources: "The [Bing Maps](https://www.bing.com/maps/) satellite layer or the best local imagery is set as the default background. Depending on where you are editing, multiple imagery sources are available. Some may be newer or have higher resolution, so it is always useful to check and see which layer is the best one to use as a mapping reference." offsets_h: "Adjusting Imagery Offset" - offset: "Imagery is sometimes offset slightly from accurate map data. If you see a lot of roads or buildings shifted from the background imagery, it may be the imagery that's incorrect, so don't move them all to match the background. Instead, you can adjust the background so that it matches the existing data by expanding the \"{imagery_offset}\" section at the bottom of the Background Settings pane." + offset: 'Imagery is sometimes offset slightly from accurate map data. If you see a lot of roads or buildings shifted from the background imagery, it may be the imagery that''s incorrect, so don''t move them all to match the background. Instead, you can adjust the background so that it matches the existing data by expanding the "{imagery_offset}" section at the bottom of the Background Settings pane.' offset_change: "Press the small triangle buttons to adjust the imagery offset in small steps, or hold the {leftclick} left mouse button and drag within the gray square to slide the imagery into alignment." streetlevel: title: Street Level Photos @@ -1705,8 +1707,8 @@ en: title: About about: "This field allows you to inspect and modify turn restrictions. It displays a model of the selected intersection including other nearby connected roads." from_via_to: "A turn restriction always contains: one **FROM way**, one **TO way**, and either one **VIA node** or one or more **VIA ways**." - maxdist: "The \"{distField}\" slider controls how far to search for additional connected roads." - maxvia: "The \"{viaField}\" slider adjusts how many via ways may be included in the search. (Tip: simple is better)" + maxdist: 'The "{distField}" slider controls how far to search for additional connected roads.' + maxvia: 'The "{viaField}" slider adjusts how many via ways may be included in the search. (Tip: simple is better)' inspecting: title: Inspecting about: "Hover over any **FROM** segment to see whether it has any turn restrictions. Each possible **TO** destination will be drawn with a colored shadow showing whether a restriction exists." @@ -1714,12 +1716,12 @@ en: allow_shadow: "{allowShadow} **TO Allowed**" restrict_shadow: "{restrictShadow} **TO Restricted**" only_shadow: "{onlyShadow} **TO Only**" - restricted: "\"Restricted\" means that there is a turn restriction, for example \"No Left Turn\"." - only: "\"Only\" means that a vehicle taking that path may only make that choice, for example \"Only Straight On\"." + restricted: '"Restricted" means that there is a turn restriction, for example "No Left Turn".' + only: '"Only" means that a vehicle taking that path may only make that choice, for example "Only Straight On".' modifying: title: Modifying about: "To modify turn restrictions, first click on any starting **FROM** segment to select it. The selected segment will pulse, and all possible **TO** destinations will appear as turn symbols." - indicators: "Then, click on a turn symbol to toggle it between \"Allowed\", \"Restricted\", and \"Only\"." + indicators: 'Then, click on a turn symbol to toggle it between "Allowed", "Restricted", and "Only".' allow_turn: "{allowTurn} **TO Allowed**" restrict_turn: "{restrictTurn} **TO Restricted**" only_turn: "{onlyTurn} **TO Only**" @@ -1727,8 +1729,8 @@ en: title: Tips simple: "**Prefer simple restrictions over complex ones.**" simple_example: "For example, avoid creating a via-way restriction if a simpler via-node turn restriction will do." - indirect: "**Some restrictions display the text \"(indirect)\" and are drawn lighter.**" - indirect_example: "These restrictions exist because of another nearby restriction. For example, an \"Only Straight On\" restriction will indirectly create \"No Turn\" restrictions for all other paths through the intersection." + indirect: '**Some restrictions display the text "(indirect)" and are drawn lighter.**' + indirect_example: 'These restrictions exist because of another nearby restriction. For example, an "Only Straight On" restriction will indirectly create "No Turn" restrictions for all other paths through the intersection.' indirect_noedit: "You may not edit indirect restrictions. Instead, edit the nearby direct restriction." issues: title: Issues @@ -1787,7 +1789,7 @@ en: highway-highway: reference: Intersecting highways should share a junction vertex. area_as_point: - message: '{feature} should be an area, not a point' + message: "{feature} should be an area, not a point" close_nodes: title: "Very Close Points" tip: "Find redundant and crowded points" @@ -1858,8 +1860,8 @@ en: message_language: '{feature} has the suspicious name "{name}" in {language}' reference: "Names should be the actual, on-the-ground names of features." help_request: - title: 'Help Requests' - tip: 'Find features where others requested assistance' + title: "Help Requests" + tip: "Find features where others requested assistance" incompatible_source: title: Suspicious Sources tip: "Find features with suspicious source tags" @@ -1873,17 +1875,17 @@ en: title: Invalid Formatting tip: Find tags with unexpected formats email: - message: '{feature} has an invalid email address' - message_multi: '{feature} has multiple invalid email addresses' + message: "{feature} has an invalid email address" + message_multi: "{feature} has multiple invalid email addresses" reference: 'Email addresses must look like "user@example.com".' website: - message: '{feature} has an invalid URL' - message_multi: '{feature} has multiple invalid URLs' + message: "{feature} has an invalid URL" + message_multi: "{feature} has multiple invalid URLs" reference: 'URLs must start with "http://" or "https://".' line_as_area: - message: '{feature} should be a line, not an area' + message: "{feature} should be a line, not an area" line_as_point: - message: '{feature} should be a line, not a point' + message: "{feature} should be a line, not a point" mismatched_geometry: title: Mismatched Geometry tip: "Find features with conflicting tags and geometry" @@ -1929,18 +1931,18 @@ en: message_incomplete: "{feature} looks like a common feature with incomplete tags" reference: "Some features, for example retail chains or post offices, are expected to have certain tags in common." point_as_area: - message: '{feature} should be a point, not an area' + message: "{feature} should be a point, not an area" point_as_line: - message: '{feature} should be a point, not a line' + message: "{feature} should be a point, not a line" point_as_vertex: - message: '{feature} should be a standalone point based on its tags' + message: "{feature} should be a standalone point based on its tags" reference: "Some features shouldn't be part of lines or areas." private_data: title: Private Information tip: "Find features that may contain private information" reference: "Sensitive data like personal phone numbers should not be tagged." contact: - message: '{feature} might be tagged with private contact information' + message: "{feature} might be tagged with private contact information" suspicious_name: title: Suspicious Names tip: "Find features with generic or suspicious names" @@ -1977,7 +1979,7 @@ en: buildings: reference: "Buildings with unsquare corners can often be drawn more accurately." vertex_as_point: - message: '{feature} should be attached to a line or area based on its tags' + message: "{feature} should be attached to a line or area based on its tags" reference: "Some features shouldn't be standalone points." osm_api_limits: title: OSM Data Limitations @@ -2083,7 +2085,7 @@ en: title: Tag as disconnected annotation: Tagged very close features as disconnected. tag_as_not: - title: "Tag as not the same \"{name}\"" + title: 'Tag as not the same "{name}"' annotation: "Added not: tag" tag_as_unsquare: title: Tag as physically unsquare @@ -2381,7 +2383,7 @@ en: title: "Keyboard Shortcuts" tooltip: "Show the keyboard shortcuts screen." toggle: - key: '?' + key: "?" key: alt: Alt backspace: Backspace diff --git a/modules/services/index.js b/modules/services/index.js index af98a7c3c70..f71f07feaab 100644 --- a/modules/services/index.js +++ b/modules/services/index.js @@ -15,6 +15,7 @@ import serviceWikidata from './wikidata'; import serviceWikipedia from './wikipedia'; import serviceMapilio from './mapilio'; import servicePanoramax from './panoramax'; +import serviceKyFromAbove from './kyfromabove'; export let services = { @@ -34,7 +35,8 @@ export let services = { wikidata: serviceWikidata, wikipedia: serviceWikipedia, mapilio: serviceMapilio, - panoramax: servicePanoramax + panoramax: servicePanoramax, + kyfromabove: serviceKyFromAbove }; export { @@ -54,5 +56,6 @@ export { serviceWikidata, serviceWikipedia, serviceMapilio, - servicePanoramax + servicePanoramax, + serviceKyFromAbove }; diff --git a/modules/services/kyfromabove.js b/modules/services/kyfromabove.js new file mode 100644 index 00000000000..bd19d3d1327 --- /dev/null +++ b/modules/services/kyfromabove.js @@ -0,0 +1,165 @@ +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { json as d3_json } from 'd3-fetch'; +import RBush from 'rbush'; + +import { geoExtent } from '../geo'; +import { utilQsString, utilRebind, utilTiler } from '../util'; +import { uiKyObliqueViewer } from '../ui/ky_oblique_viewer'; + +const featureServer = 'https://services.arcgis.com/p3e6s1qwiHne8T2h/ArcGIS/rest/services/KyFromAbove_Phase3_Oblique_Imagery_Centroids/FeatureServer/0'; +const tileZoom = 14; +const tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true); +const dispatch = d3_dispatch('loadedImages'); + +let _kyCache; +let _viewer; + +function abortRequest(controller) { + controller.abort(); +} + +async function loadTile(tile) { + if (_kyCache.loaded[tile.id] || _kyCache.inflight[tile.id]) return; + + const bbox = tile.extent.bbox(); + const params = { + f: 'geojson', + geometryType: 'esriGeometryEnvelope', + geometry: [bbox.minX, bbox.minY, bbox.maxX, bbox.maxY].join(','), + spatialRel: 'esriSpatialRelIntersects', + outFields: '*', + returnGeometry: true, + inSR: 4326, + outSR: 4326 + }; + + const controller = new AbortController(); + _kyCache.inflight[tile.id] = controller; + + const url = `${featureServer}/query?${utilQsString(params)}`; + + try { + const data = await d3_json(url, { signal: controller.signal }); + _kyCache.loaded[tile.id] = true; + delete _kyCache.inflight[tile.id]; + + if (!data || !data.features) return; + + const features = data.features.map(feature => { + const loc = feature.geometry.coordinates; + const props = feature.properties; + const key = props.ShotID || props.OBJECTID; + + const d = { + service: 'kyfromabove', + loc: loc, + key: key, + ca: props.CameraBearing || 0, + captured_at: props.FlightDate ? new Date(props.FlightDate) : null, + shots: { + nadir: props.Nadir_URL, + forward: props.Forward_URL, + backward: props.Backward_URL, + left: props.Left_URL, + right: props.Right_URL + } + }; + + _kyCache.points.set(key, d); + + return { + minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d + }; + }); + + _kyCache.rtree.load(features); + dispatch.call('loadedImages'); + } catch (err) { + if (err.name !== 'AbortError') { + _kyCache.loaded[tile.id] = true; + delete _kyCache.inflight[tile.id]; + } + } +} + +export default { + init: function() { + this.event = utilRebind(this, dispatch, 'on'); + this.reset(); + }, + + reset: function() { + if (_kyCache) { + Object.values(_kyCache.inflight).forEach(abortRequest); + } + _kyCache = { + points: new Map(), + rtree: new RBush(), + loaded: {}, + inflight: {} + }; + }, + + images: function(projection) { + const viewport = projection.clipExtent(); + const min = [viewport[0][0], viewport[1][1]]; + const max = [viewport[1][0], viewport[0][1]]; + const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox(); + + return _kyCache.rtree.search(bbox).map(d => d.data); + }, + + loadImages: function(projection) { + const tiles = tiler.getTiles(projection); + + // abort inflight requests that are no longer needed + Object.keys(_kyCache.inflight).forEach(k => { + const wanted = tiles.find(tile => k === tile.id); + if (!wanted) { + abortRequest(_kyCache.inflight[k]); + delete _kyCache.inflight[k]; + } + }); + + tiles.forEach(tile => loadTile(tile)); + }, + + ensureViewerLoaded: function(context) { + if (!_viewer) { + _viewer = uiKyObliqueViewer(context); + const photoviewer = context.container().select('.photoviewer'); + photoviewer.call(_viewer); + } + return Promise.resolve(); + }, + + selectImage: function(_, key) { + const d = this.cachedImage(key); + if (_viewer) { + _viewer.image(d); + } + return this; + }, + + showViewer: function() { + if (_viewer) { + _viewer.show(); + } + return this; + }, + + hideViewer: function() { + if (_viewer) { + _viewer.hide(); + } + return this; + }, + + cachedImage: function(key) { + return _kyCache.points.get(key); + }, + + cache: function() { + return _kyCache; + } +}; diff --git a/modules/svg/kyfromabove.js b/modules/svg/kyfromabove.js new file mode 100644 index 00000000000..786dff5b612 --- /dev/null +++ b/modules/svg/kyfromabove.js @@ -0,0 +1,207 @@ +import _throttle from 'lodash-es/throttle'; +import { select as d3_select } from 'd3-selection'; +import { svgPointTransform } from './helpers'; +import { services } from '../services'; + + +export function svgKyFromAbove(projection, context, dispatch) { + const throttledRedraw = _throttle(() => dispatch.call('change'), 1000); + const minZoom = 14; + const minMarkerZoom = 16; + const minViewfieldZoom = 18; + let layer = d3_select(null); + let _kyfromabove; + + function init() { + if (svgKyFromAbove.initialized) return; + svgKyFromAbove.enabled = false; + svgKyFromAbove.initialized = true; + } + + function getService() { + if (services.kyfromabove && !_kyfromabove) { + _kyfromabove = services.kyfromabove; + _kyfromabove.event + .on('loadedImages.svgKyFromAbove', throttledRedraw); + } else if (!services.kyfromabove && _kyfromabove) { + _kyfromabove = null; + } + return _kyfromabove; + } + + function showLayer() { + const service = getService(); + if (!service) return; + editOn(); + layer + .style('opacity', 0) + .transition() + .duration(250) + .style('opacity', 1) + .on('end', () => dispatch.call('change')); + } + + function hideLayer() { + throttledRedraw.cancel(); + layer + .transition() + .duration(250) + .style('opacity', 0) + .on('end', editOff); + } + + function editOn() { + layer.style('display', 'block'); + } + + function editOff() { + layer.selectAll('.viewfield-group').remove(); + layer.style('display', 'none'); + } + + function click(d3_event, d) { + const service = getService(); + if (!service) return; + + if (service.ensureViewerLoaded) { + service.ensureViewerLoaded(context) + .then(() => { + service.selectImage(context, d.key).showViewer(context); + }); + } else { + // Fallback or debug + } + + context.map().centerEase(d.loc); + } + + function mouseover(d3_event, d) { + const service = getService(); + if (service && service.setStyles) service.setStyles(context, d); + } + + function mouseout() { + const service = getService(); + if (service && service.setStyles) service.setStyles(context, null); + } + + function transform(d) { + let t = svgPointTransform(projection)(d); + if (d.ca) { + t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; + } + return t; + } + + function update() { + const viewer = context.container().select('.photoviewer'); + const selected = viewer.empty() ? undefined : viewer.datum(); + const z = ~~context.map().zoom(); + const showMarkers = (z >= minMarkerZoom); + const showViewfields = (z >= minViewfieldZoom); + const service = getService(); + let images = []; + + if (service) { + service.loadImages(projection); + images = showMarkers ? service.images(projection) : []; + } + + const groups = layer.selectAll('.markers').selectAll('.viewfield-group') + .data(images, (d) => d.key); + + groups.exit().remove(); + + const groupsEnter = groups.enter() + .append('g') + .attr('class', 'viewfield-group') + .on('mouseenter', mouseover) + .on('mouseleave', mouseout) + .on('click', click); + + groupsEnter + .append('g') + .attr('class', 'viewfield-scale'); + + const markers = groups + .merge(groupsEnter) + .sort((a, b) => { + return (a === selected) ? 1 + : (b === selected) ? -1 + : b.loc[1] - a.loc[1]; + }) + .attr('transform', transform) + .select('.viewfield-scale'); + + markers.selectAll('circle') + .data([0]) + .enter() + .append('circle') + .attr('r', '6'); + + const viewfields = markers.selectAll('.viewfield') + .data(showViewfields ? [0] : []); + + viewfields.exit().remove(); + + viewfields.enter() + .insert('path', 'circle') + .attr('class', 'viewfield') + .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') + .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'); + } + + function drawImages(selection) { + const enabled = svgKyFromAbove.enabled; + const service = getService(); + + layer = selection.selectAll('.layer-kyfromabove') + .data(service ? [0] : []); + + layer.exit().remove(); + + const layerEnter = layer.enter() + .append('g') + .attr('class', 'layer-kyfromabove') + .style('display', enabled ? 'block' : 'none'); + + layerEnter + .append('g') + .attr('class', 'markers'); + + layer = layerEnter.merge(layer); + + if (enabled) { + if (service && ~~context.map().zoom() >= minZoom) { + editOn(); + update(); + } else { + editOff(); + } + } + } + + drawImages.enabled = function (_) { + if (!arguments.length) return svgKyFromAbove.enabled; + svgKyFromAbove.enabled = _; + if (svgKyFromAbove.enabled) { + showLayer(); + } else { + hideLayer(); + } + dispatch.call('change'); + return this; + }; + + drawImages.supported = function () { + return !!getService(); + }; + + drawImages.rendered = function(zoom) { + return zoom >= minZoom; + }; + + init(); + + return drawImages; +} diff --git a/modules/svg/layers.js b/modules/svg/layers.js index b087cbc0be0..609bd3f6c06 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -16,6 +16,7 @@ import { svgMapillaryMapFeatures } from './mapillary_map_features'; import { svgKartaviewImages } from './kartaview_images'; import { svgMapilioImages } from './mapilio_images'; import { svgPanoramaxImages } from './panoramax_images'; +import { svgKyFromAbove } from './kyfromabove'; import { svgOsm } from './osm'; import { svgNotes } from './notes'; import { svgTouch } from './touch'; @@ -41,6 +42,7 @@ export function svgLayers(projection, context) { { id: 'mapilio', layer: svgMapilioImages(projection, context, dispatch) }, { id: 'vegbilder', layer: svgVegbilder(projection, context, dispatch) }, { id: 'panoramax', layer: svgPanoramaxImages(projection, context, dispatch) }, + { id: 'kyfromabove', layer: svgKyFromAbove(projection, context, dispatch) }, { id: 'local-photos', layer: svgLocalPhotos(projection, context, dispatch) }, { id: 'debug', layer: svgDebug(projection, context, dispatch) }, { id: 'geolocate', layer: svgGeolocate(projection, context, dispatch) }, diff --git a/modules/ui/ky_oblique_viewer.js b/modules/ui/ky_oblique_viewer.js new file mode 100644 index 00000000000..d562b78d3cd --- /dev/null +++ b/modules/ui/ky_oblique_viewer.js @@ -0,0 +1,130 @@ +import { fromUrl } from 'geotiff'; +import { select as d3_select } from 'd3-selection'; + +export function uiKyObliqueViewer() { + let _image; + let _angle = 'nadir'; + let _canvas; + let _tiff; + let _loading = false; + + function render(selection) { + let wrap = selection.selectAll('.ky-oblique-wrapper') + .data([0]); + + let wrapEnter = wrap.enter() + .append('div') + .attr('class', 'photo-wrapper ky-oblique-wrapper hide'); + + wrapEnter + .append('div') + .attr('class', 'photo-attribution fillD'); + + let controlsEnter = wrapEnter + .append('div') + .attr('class', 'photo-controls-wrap') + .append('div') + .attr('class', 'photo-controls'); + + ['nadir', 'forward', 'backward', 'left', 'right'].forEach(angle => { + controlsEnter + .append('button') + .attr('class', 'angle-button') + .classed('active', angle === _angle) + .text(angle.charAt(0).toUpperCase() + angle.slice(1)) + .on('click', () => setAngle(angle)); + }); + + wrapEnter + .append('div') + .attr('class', 'ky-oblique-image-wrap') + .append('canvas') + .attr('class', 'ky-oblique-canvas'); + + wrap = wrapEnter.merge(wrap); + _canvas = wrap.select('canvas').node(); + + if (_image) { + updateImage(); + } + } + + async function setAngle(angle) { + _angle = angle; + d3_select('.ky-oblique-wrapper').selectAll('.angle-button') + .classed('active', d => d === _angle); + + if (_image) { + await updateImage(); + } + } + + async function updateImage() { + if (!_image || !_image.shots || !_image.shots[_angle]) return; + if (_loading) return; + + _loading = true; + const url = _image.shots[_angle]; + + try { + _tiff = await fromUrl(url); + const image = await _tiff.getImage(); + const width = image.getWidth(); + const height = image.getHeight(); + + _canvas.width = width; + _canvas.height = height; + + const rgb = await image.readRGB(); + const ctx = _canvas.getContext('2d'); + const imageData = ctx.createImageData(width, height); + + for (let i = 0; i < rgb.length; i++) { + imageData.data[i] = rgb[i]; + } + + // readRGB returns RGB, we need RGBA for imageData + // geotiff.js readRGB often returns a Uint8Array with 3 values per pixel + // But canvas needs 4. Let's adjust. + // Wait, geotiff.js readRGB might return typed array. + + const data = imageData.data; + let j = 0; + for (let i = 0; i < rgb.length; i += 3) { + data[j++] = rgb[i]; + data[j++] = rgb[i+1]; + data[j++] = rgb[i+2]; + data[j++] = 255; // Alpha + } + + ctx.putImageData(imageData, 0, 0); + + const attribution = d3_select('.ky-oblique-wrapper .photo-attribution'); + attribution.text(`KyFromAbove - Shot ID: ${_image.key} - Angle: ${_angle}`); + + } catch { + // Failed to load COG + } finally { + /* eslint-disable-next-line require-atomic-updates */ + _loading = false; + } + } + + render.image = function(_) { + if (!arguments.length) return _image; + _image = _; + return render; + }; + + render.show = function() { + d3_select('.ky-oblique-wrapper').classed('hide', false); + return render; + }; + + render.hide = function() { + d3_select('.ky-oblique-wrapper').classed('hide', true); + return render; + }; + + return render; +} diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index b3679fcda98..b5847e34a94 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -12,7 +12,7 @@ export function uiSectionPhotoOverlays(context) { let _savedLayers = []; let _layersHidden = false; - const _streetLayerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'mapilio', 'vegbilder', 'panoramax']; + const _streetLayerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'mapilio', 'vegbilder', 'panoramax', 'kyfromabove']; var settingsLocalPhotos = uiSettingsLocalPhotos(context) .on('change', localPhotosChanged); diff --git a/package-lock.json b/package-lock.json index 017a639159c..2140979e609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "exifr": "^7.1.3", "fast-deep-equal": "~3.1.1", "fast-json-stable-stringify": "2.1.0", + "geotiff": "^3.0.3", "idb-keyval": "^6.2.2", "lodash-es": "~4.17.15", "marked": "~17.0.0", @@ -351,6 +352,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -391,6 +393,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1257,6 +1260,7 @@ "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -1451,6 +1455,12 @@ "dev": true, "license": "ISC" }, + "node_modules/@petamoriken/float16": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", + "license": "MIT" + }, "node_modules/@rapideditor/country-coder": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/@rapideditor/country-coder/-/country-coder-5.6.1.tgz", @@ -2595,6 +2605,7 @@ "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -2728,6 +2739,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -3078,6 +3090,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3392,6 +3405,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3521,6 +3535,7 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3536,7 +3551,8 @@ "resolved": "https://registry.npmjs.org/cldr-core/-/cldr-core-48.1.0.tgz", "integrity": "sha512-Br9lHQHRhFVyQ/4dY2AnTOuJVEwzDSFR9tE4EzaZxdtux4BFZ4V6oY0SiSAe/8H9+CJ6njpBiPJdDv+vdXnvfA==", "dev": true, - "license": "Unicode-3.0" + "license": "Unicode-3.0", + "peer": true }, "node_modules/cldr-localenames-full": { "version": "48.1.0", @@ -4214,6 +4230,7 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -4904,6 +4921,7 @@ "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -5471,6 +5489,25 @@ "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", "license": "MIT" }, + "node_modules/geotiff": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-3.0.3.tgz", + "integrity": "sha512-yRoDQDYxWYiB421p0cbxJvdy79OlQW+rxDI9GDbIUeWCAh6YAZ0vlTKF448EAiEuuUpBsNaegd2flavF0p+kvw==", + "license": "MIT", + "dependencies": { + "@petamoriken/float16": "^3.4.7", + "lerc": "^3.0.0", + "pako": "^2.0.4", + "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", + "web-worker": "^1.5.0", + "xml-utils": "^1.10.2", + "zstddec": "^0.2.0" + }, + "engines": { + "node": ">=10.19" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5858,7 +5895,8 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/ieee754": { "version": "1.2.1", @@ -6485,6 +6523,7 @@ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", @@ -6589,6 +6628,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==", + "license": "Apache-2.0" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11174,6 +11219,7 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -11329,6 +11375,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", "dev": true, + "peer": true, "engines": { "node": ">=8.0.0" } @@ -11920,6 +11967,292 @@ } } }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.2.tgz", + "integrity": "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.2.tgz", + "integrity": "sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.2.tgz", + "integrity": "sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.2.tgz", + "integrity": "sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.2.tgz", + "integrity": "sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.2.tgz", + "integrity": "sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.2.tgz", + "integrity": "sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.2.tgz", + "integrity": "sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.2.tgz", + "integrity": "sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.2.tgz", + "integrity": "sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.2.tgz", + "integrity": "sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.2.tgz", + "integrity": "sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.2.tgz", + "integrity": "sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.2.tgz", + "integrity": "sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.2.tgz", + "integrity": "sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.2.tgz", + "integrity": "sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.2.tgz", + "integrity": "sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.2.tgz", + "integrity": "sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.2.tgz", + "integrity": "sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.2.tgz", + "integrity": "sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.2.tgz", + "integrity": "sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/netlify-cli/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.2.tgz", + "integrity": "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/netlify-cli/node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -12034,6 +12367,7 @@ "integrity": "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -12576,6 +12910,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -12618,6 +12953,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15224,9 +15560,9 @@ } }, "node_modules/netlify-cli/node_modules/express/node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -15984,6 +16320,20 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/netlify-cli/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/netlify-cli/node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -19226,6 +19576,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -19275,6 +19626,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -20041,6 +20393,49 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, + "node_modules/netlify-cli/node_modules/rollup": { + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz", + "integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.2", + "@rollup/rollup-android-arm64": "4.52.2", + "@rollup/rollup-darwin-arm64": "4.52.2", + "@rollup/rollup-darwin-x64": "4.52.2", + "@rollup/rollup-freebsd-arm64": "4.52.2", + "@rollup/rollup-freebsd-x64": "4.52.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.2", + "@rollup/rollup-linux-arm-musleabihf": "4.52.2", + "@rollup/rollup-linux-arm64-gnu": "4.52.2", + "@rollup/rollup-linux-arm64-musl": "4.52.2", + "@rollup/rollup-linux-loong64-gnu": "4.52.2", + "@rollup/rollup-linux-ppc64-gnu": "4.52.2", + "@rollup/rollup-linux-riscv64-gnu": "4.52.2", + "@rollup/rollup-linux-riscv64-musl": "4.52.2", + "@rollup/rollup-linux-s390x-gnu": "4.52.2", + "@rollup/rollup-linux-x64-gnu": "4.52.2", + "@rollup/rollup-linux-x64-musl": "4.52.2", + "@rollup/rollup-openharmony-arm64": "4.52.2", + "@rollup/rollup-win32-arm64-msvc": "4.52.2", + "@rollup/rollup-win32-ia32-msvc": "4.52.2", + "@rollup/rollup-win32-x64-gnu": "4.52.2", + "@rollup/rollup-win32-x64-msvc": "4.52.2", + "fsevents": "~2.3.2" + } + }, "node_modules/netlify-cli/node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -21232,6 +21627,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -22757,7 +23153,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", - "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/pannellum": { @@ -22766,6 +23161,12 @@ "integrity": "sha512-AHMRYdLPxetwhBg4lO5EgoZ55C/e+wrJF2WaFQIQA18HSVcvLvsdXEjDL7WpD8MPIFwNnH9qoUDsUqP8cF4pgA==", "license": "MIT" }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -22995,6 +23396,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -23102,6 +23504,18 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/quickselect": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", @@ -24583,6 +24997,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -24760,6 +25175,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -24930,6 +25346,12 @@ "node": ">=18" } }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -25255,6 +25677,12 @@ "node": ">=18" } }, + "node_modules/xml-utils": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", + "integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==", + "license": "CC0-1.0" + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -25333,6 +25761,12 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zstddec": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.2.0.tgz", + "integrity": "sha512-oyPnDa1X5c13+Y7mA/FDMNJrn4S8UNBe0KCqtDmor40Re7ALrPN6npFwyYVRRh+PqozZQdeg23QtbcamZnG5rA==", + "license": "MIT AND BSD-3-Clause" } } } diff --git a/package.json b/package.json index d090386460b..c4139d674de 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "exifr": "^7.1.3", "fast-deep-equal": "~3.1.1", "fast-json-stable-stringify": "2.1.0", + "geotiff": "^3.0.3", "idb-keyval": "^6.2.2", "lodash-es": "~4.17.15", "marked": "~17.0.0", From 6d49af884d8c4a7611bbad08457381e407014ff2 Mon Sep 17 00:00:00 2001 From: Sikandar Date: Thu, 26 Feb 2026 23:36:46 +0500 Subject: [PATCH 2/4] Address PR feedback: fix lint, update attribution, cleanup CSS --- css/60_photos.css | 61 ++++++++++++++++++--------------- modules/services/kyfromabove.js | 1 + modules/ui/ky_oblique_viewer.js | 8 ++--- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index a3e70f9b722..4be326f94a6 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -7,7 +7,7 @@ li.list-item-photos.active:after { right: 0; width: 8px; } -.ideditor[dir="rtl"] li.list-item-photos.active:after { +.ideditor[dir='rtl'] li.list-item-photos.active:after { right: auto; left: 0; } @@ -27,11 +27,11 @@ li.list-item-photos.active:after { padding: 5px; background-color: var(--bg-color); } -.ideditor[dir="ltr"] .photoviewer { +.ideditor[dir='ltr'] .photoviewer { margin-left: 10px; margin-right: 2px; } -.ideditor[dir="rtl"] .photoviewer { +.ideditor[dir='rtl'] .photoviewer { margin-right: 10px; margin-left: 2px; } @@ -95,6 +95,7 @@ li.list-item-photos.active:after { width: 100%; } + .photo-wrapper { width: 100%; height: 100%; @@ -186,8 +187,9 @@ li.list-item-photos.active:after { fill-opacity: 0.9; } + .viewfield-group.currentView .viewfield-scale { - transform: scale(2, 2); + transform: scale(2,2); } .sequence { @@ -201,6 +203,7 @@ li.list-item-photos.active:after { stroke-opacity: 1; } + /* Streetside Image Layer */ li.list-item-photos.list-item-streetside.active:after { background-color: #0fffc4; @@ -213,7 +216,7 @@ li.list-item-photos.list-item-streetside.active:after { } .layer-streetside-images .sequence { stroke: #0fffc4; - stroke-opacity: 0.85; /* bump opacity - only one per road */ + stroke-opacity: 0.85; /* bump opacity - only one per road */ } /* Vegbilder Image Layer */ @@ -228,9 +231,10 @@ li.list-item-photos.list-item-vegbilder.active:after { } .layer-vegbilder .sequence { stroke: #ed1c2e; - stroke-opacity: 0.85; /* bump opacity - only one per road */ + stroke-opacity: 0.85; /* bump opacity - only one per road */ } + /* Mapillary Image Layer */ li.list-item-photos.list-item-mapillary.active:after { background-color: #55ff22; @@ -245,6 +249,7 @@ li.list-item-photos.list-item-mapillary.active:after { stroke: #55ff22; } + /* Mapillary Traffic Signs and Map Features Layers */ .layer-mapillary-detections { pointer-events: none; @@ -279,6 +284,7 @@ li.list-item-photos.list-item-mapillary.active:after { outline: 3px solid rgba(255, 238, 0, 1); } + /* KartaView Image Layer */ li.list-item-photos.list-item-kartaview.active:after { background-color: #20c4ff; @@ -293,6 +299,7 @@ li.list-item-photos.list-item-kartaview.active:after { stroke: #20c4ff; } + /* Mapilio Image Layer */ li.list-item-photos.list-item-mapilio.active:after { background-color: #0056f1; @@ -305,8 +312,8 @@ li.list-item-photos.list-item-mapilio.active:after { } .layer-mapilio .viewfield-group:not(.currentView):not(.hovered) * { stroke: #ffffff; - stroke-opacity: 0.6; - fill-opacity: 0.6; + stroke-opacity: .6; + fill-opacity: .6; } .layer-mapilio .sequence { stroke: #0056f1; @@ -318,7 +325,7 @@ li.list-item-photos.list-item-mapilio.active:after { gap: 4px; } .photo-controls-mapilio button { - padding: 0 6px; + padding:0 6px; pointer-events: initial; } .ideditor .mapilio-wrapper { @@ -353,8 +360,8 @@ li.list-item-photos.list-item-panoramax.active:after { .layer-panoramax .viewfield-group * { fill: #ff6f00; stroke: #ffffff; - stroke-opacity: 0.6; - fill-opacity: 0.6; + stroke-opacity: .6; + fill-opacity: .6; } .layer-panoramax .sequence { stroke: #ff6f00; @@ -366,7 +373,7 @@ li.list-item-photos.list-item-panoramax.active:after { gap: 4px; } .photo-controls-panoramax button { - padding: 0 6px; + padding:0 6px; pointer-events: initial; } @@ -397,11 +404,12 @@ label.panoramax-hd { width: 100%; } -.yearSliderSpan { +.yearSliderSpan{ padding: 2px; } -.list-item-date-slider label { + +.list-item-date-slider label{ display: block !important; } @@ -438,8 +446,8 @@ label.panoramax-hd { } .ms-wrapper .pnlm-compass.pnlm-control, -.vegbilder-wrapper .pnlm-compass.pnlm-control, -.panoramax-wrapper .pnlm-compass.pnlm-control { +.vegbilder-wrapper .pnlm-compass.pnlm-control, +.panoramax-wrapper .pnlm-compass.pnlm-control { width: 26px; height: 26px; left: 4px; @@ -465,10 +473,11 @@ label.streetside-hires { margin-top: 6px; } + /* Mapillary viewer */ #ideditor-mly .domRenderer .TagSymbol { font-size: 10px; - background-color: rgba(0, 0, 0, 0.4); + background-color: rgba(0,0,0,0.4); padding: 0 4px; border-radius: 4px; top: -25px; @@ -479,15 +488,13 @@ label.streetside-hires { align-items: center; } -.mly-wrapper - .mapillary-attribution-container - .mapillary-attribution-icon-container { +.mly-wrapper .mapillary-attribution-container .mapillary-attribution-icon-container { display: flex; align-items: center; } .mapillary-attribution-image-container { - height: auto; + height: auto; } .mly-wrapper .mapillary-attribution-container .mapillary-attribution-username { @@ -591,7 +598,7 @@ label.streetside-hires { height: 18px; width: 18px; line-height: 18px; - background: rgba(0, 0, 0, 0.65); + background: rgba(0,0,0,0.65); color: #eee; border-radius: 0; } @@ -602,12 +609,12 @@ label.streetside-hires { border-radius: 0 3px 3px 0; } .photo-controls button:active { - background: rgba(0, 0, 0, 0.85); + background: rgba(0,0,0,0.85); color: #fff; } @media (hover: hover) { .photo-controls button:hover { - background: rgba(0, 0, 0, 0.85); + background: rgba(0,0,0,0.85); color: #fff; } } @@ -717,14 +724,15 @@ label.streetside-hires { gap: 4px; } .photo-controls-local button { - padding: 0 6px; + padding:0 6px; pointer-events: initial; } .photo-controls-local button:disabled { - background: rgba(255, 255, 255, 0.25); + background: rgba(255,255,255,.25); } + /* KyFromAbove */ .ky-oblique-wrapper { position: absolute; @@ -757,7 +765,6 @@ label.streetside-hires { .ky-oblique-wrapper .photo-controls-wrap { z-index: 20; } - .ky-oblique-wrapper .angle-button { background: rgba(0, 0, 0, 0.5); color: #fff; diff --git a/modules/services/kyfromabove.js b/modules/services/kyfromabove.js index bd19d3d1327..aceec5406cb 100644 --- a/modules/services/kyfromabove.js +++ b/modules/services/kyfromabove.js @@ -40,6 +40,7 @@ async function loadTile(tile) { try { const data = await d3_json(url, { signal: controller.signal }); + /* eslint-disable-next-line require-atomic-updates */ _kyCache.loaded[tile.id] = true; delete _kyCache.inflight[tile.id]; diff --git a/modules/ui/ky_oblique_viewer.js b/modules/ui/ky_oblique_viewer.js index d562b78d3cd..78ec9be0618 100644 --- a/modules/ui/ky_oblique_viewer.js +++ b/modules/ui/ky_oblique_viewer.js @@ -83,11 +83,7 @@ export function uiKyObliqueViewer() { imageData.data[i] = rgb[i]; } - // readRGB returns RGB, we need RGBA for imageData - // geotiff.js readRGB often returns a Uint8Array with 3 values per pixel - // But canvas needs 4. Let's adjust. - // Wait, geotiff.js readRGB might return typed array. - + // geotiff.js readRGB returns RGB, we need RGBA for imageData const data = imageData.data; let j = 0; for (let i = 0; i < rgb.length; i += 3) { @@ -100,7 +96,7 @@ export function uiKyObliqueViewer() { ctx.putImageData(imageData, 0, 0); const attribution = d3_select('.ky-oblique-wrapper .photo-attribution'); - attribution.text(`KyFromAbove - Shot ID: ${_image.key} - Angle: ${_angle}`); + attribution.text('KyFromAbove'); } catch { // Failed to load COG From 677d0cd70f61625eab098b852ecb618899bc33de Mon Sep 17 00:00:00 2001 From: Sikandar Date: Fri, 27 Feb 2026 23:01:49 +0500 Subject: [PATCH 3/4] Fix CI build failures: proper race condition fix and linebreak-style for kyfromabove - modules/services/kyfromabove.js: Fix require-atomic-updates by capturing a local const cache = _kyCache snapshot before the await, so all post-await writes go to the correct cache object. Removes the eslint-disable comment that was silencing a real race condition. - modules/ui/ky_oblique_viewer.js: Fix linebreak-style error (CRLF -> LF), align indentation to 2-space (repo standard), convert updateImage() from async/await to a promise chain to resolve require-atomic-updates on _loading, make setAngle() synchronous (fixes require-await), and remove the redundant first RGB copy loop. - package-lock.json: Regenerate to resolve merge conflict with develop branch. --- modules/services/kyfromabove.js | 17 ++- modules/ui/ky_oblique_viewer.js | 229 ++++++++++++++++---------------- 2 files changed, 125 insertions(+), 121 deletions(-) diff --git a/modules/services/kyfromabove.js b/modules/services/kyfromabove.js index aceec5406cb..3ea53559bdc 100644 --- a/modules/services/kyfromabove.js +++ b/modules/services/kyfromabove.js @@ -38,11 +38,14 @@ async function loadTile(tile) { const url = `${featureServer}/query?${utilQsString(params)}`; + // Capture a local snapshot so post-await writes go to the same cache object + // even if reset() is called while the request is in-flight. + const cache = _kyCache; + try { const data = await d3_json(url, { signal: controller.signal }); - /* eslint-disable-next-line require-atomic-updates */ - _kyCache.loaded[tile.id] = true; - delete _kyCache.inflight[tile.id]; + cache.loaded[tile.id] = true; + delete cache.inflight[tile.id]; if (!data || !data.features) return; @@ -66,19 +69,19 @@ async function loadTile(tile) { } }; - _kyCache.points.set(key, d); + cache.points.set(key, d); return { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d }; }); - _kyCache.rtree.load(features); + cache.rtree.load(features); dispatch.call('loadedImages'); } catch (err) { if (err.name !== 'AbortError') { - _kyCache.loaded[tile.id] = true; - delete _kyCache.inflight[tile.id]; + cache.loaded[tile.id] = true; + delete cache.inflight[tile.id]; } } } diff --git a/modules/ui/ky_oblique_viewer.js b/modules/ui/ky_oblique_viewer.js index 78ec9be0618..01bfa5ae010 100644 --- a/modules/ui/ky_oblique_viewer.js +++ b/modules/ui/ky_oblique_viewer.js @@ -2,125 +2,126 @@ import { fromUrl } from 'geotiff'; import { select as d3_select } from 'd3-selection'; export function uiKyObliqueViewer() { - let _image; - let _angle = 'nadir'; - let _canvas; - let _tiff; - let _loading = false; - - function render(selection) { - let wrap = selection.selectAll('.ky-oblique-wrapper') - .data([0]); - - let wrapEnter = wrap.enter() - .append('div') - .attr('class', 'photo-wrapper ky-oblique-wrapper hide'); - - wrapEnter - .append('div') - .attr('class', 'photo-attribution fillD'); - - let controlsEnter = wrapEnter - .append('div') - .attr('class', 'photo-controls-wrap') - .append('div') - .attr('class', 'photo-controls'); - - ['nadir', 'forward', 'backward', 'left', 'right'].forEach(angle => { - controlsEnter - .append('button') - .attr('class', 'angle-button') - .classed('active', angle === _angle) - .text(angle.charAt(0).toUpperCase() + angle.slice(1)) - .on('click', () => setAngle(angle)); - }); - - wrapEnter - .append('div') - .attr('class', 'ky-oblique-image-wrap') - .append('canvas') - .attr('class', 'ky-oblique-canvas'); - - wrap = wrapEnter.merge(wrap); - _canvas = wrap.select('canvas').node(); - - if (_image) { - updateImage(); - } + let _image; + let _angle = 'nadir'; + let _canvas; + let _tiff; + let _loading = false; + + function render(selection) { + let wrap = selection.selectAll('.ky-oblique-wrapper') + .data([0]); + + let wrapEnter = wrap.enter() + .append('div') + .attr('class', 'photo-wrapper ky-oblique-wrapper hide'); + + wrapEnter + .append('div') + .attr('class', 'photo-attribution fillD'); + + let controlsEnter = wrapEnter + .append('div') + .attr('class', 'photo-controls-wrap') + .append('div') + .attr('class', 'photo-controls'); + + ['nadir', 'forward', 'backward', 'left', 'right'].forEach(angle => { + controlsEnter + .append('button') + .attr('class', 'angle-button') + .classed('active', angle === _angle) + .text(angle.charAt(0).toUpperCase() + angle.slice(1)) + .on('click', () => setAngle(angle)); + }); + + wrapEnter + .append('div') + .attr('class', 'ky-oblique-image-wrap') + .append('canvas') + .attr('class', 'ky-oblique-canvas'); + + wrap = wrapEnter.merge(wrap); + _canvas = wrap.select('canvas').node(); + + if (_image) { + updateImage(); } + } - async function setAngle(angle) { - _angle = angle; - d3_select('.ky-oblique-wrapper').selectAll('.angle-button') - .classed('active', d => d === _angle); - - if (_image) { - await updateImage(); - } - } + function setAngle(angle) { + _angle = angle; + d3_select('.ky-oblique-wrapper').selectAll('.angle-button') + .classed('active', function(d) { return d === _angle; }); - async function updateImage() { - if (!_image || !_image.shots || !_image.shots[_angle]) return; - if (_loading) return; - - _loading = true; - const url = _image.shots[_angle]; - - try { - _tiff = await fromUrl(url); - const image = await _tiff.getImage(); - const width = image.getWidth(); - const height = image.getHeight(); - - _canvas.width = width; - _canvas.height = height; - - const rgb = await image.readRGB(); - const ctx = _canvas.getContext('2d'); - const imageData = ctx.createImageData(width, height); - - for (let i = 0; i < rgb.length; i++) { - imageData.data[i] = rgb[i]; - } - - // geotiff.js readRGB returns RGB, we need RGBA for imageData - const data = imageData.data; - let j = 0; - for (let i = 0; i < rgb.length; i += 3) { - data[j++] = rgb[i]; - data[j++] = rgb[i+1]; - data[j++] = rgb[i+2]; - data[j++] = 255; // Alpha - } - - ctx.putImageData(imageData, 0, 0); - - const attribution = d3_select('.ky-oblique-wrapper .photo-attribution'); - attribution.text('KyFromAbove'); - - } catch { - // Failed to load COG - } finally { - /* eslint-disable-next-line require-atomic-updates */ - _loading = false; - } + if (_image) { + updateImage(); } + } + + function updateImage() { + if (!_image || !_image.shots || !_image.shots[_angle]) return; + if (_loading) return; + + const url = _image.shots[_angle]; + _loading = true; + + fromUrl(url) + .then(tiff => { + _tiff = tiff; + return _tiff.getImage(); + }) + .then(image => { + const width = image.getWidth(); + const height = image.getHeight(); + + _canvas.width = width; + _canvas.height = height; + + return image.readRGB().then(rgb => { + // readRGB returns a Uint8Array with 3 values per pixel (R, G, B). + // Canvas ImageData needs 4 values per pixel (R, G, B, A). + const ctx = _canvas.getContext('2d'); + const imageData = ctx.createImageData(width, height); + const data = imageData.data; + + let j = 0; + for (let i = 0; i < rgb.length; i += 3) { + data[j++] = rgb[i]; + data[j++] = rgb[i + 1]; + data[j++] = rgb[i + 2]; + data[j++] = 255; // Alpha + } + + ctx.putImageData(imageData, 0, 0); + + const attribution = d3_select('.ky-oblique-wrapper .photo-attribution'); + attribution.text('KyFromAbove'); + }); + }) + .catch(() => { + // Failed to load COG + }) + .finally(() => { + _loading = false; + }); + } + + render.image = function(_) { + if (!arguments.length) return _image; + _image = _; + return render; + }; - render.image = function(_) { - if (!arguments.length) return _image; - _image = _; - return render; - }; - - render.show = function() { - d3_select('.ky-oblique-wrapper').classed('hide', false); - return render; - }; - - render.hide = function() { - d3_select('.ky-oblique-wrapper').classed('hide', true); - return render; - }; + render.show = function() { + d3_select('.ky-oblique-wrapper').classed('hide', false); + return render; + }; + render.hide = function() { + d3_select('.ky-oblique-wrapper').classed('hide', true); return render; + }; + + return render; } From 880afb6a2a5618cb83f83854eef8b06a93517b44 Mon Sep 17 00:00:00 2001 From: Sikandar Date: Tue, 3 Mar 2026 17:46:35 +0500 Subject: [PATCH 4/4] Fix failing layers test count --- test/spec/svg/layers.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index 390b59099fd..c1cd1614b6d 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -26,7 +26,7 @@ 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(18); + expect(nodes.length).to.eql(19); 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; @@ -41,10 +41,11 @@ describe('iD.svgLayers', function () { expect(d3.select(nodes[11]).classed('mapilio')).to.be.true; expect(d3.select(nodes[12]).classed('vegbilder')).to.be.true; expect(d3.select(nodes[13]).classed('panoramax')).to.be.true; - expect(d3.select(nodes[14]).classed('local-photos')).to.be.true; - expect(d3.select(nodes[15]).classed('debug')).to.be.true; - expect(d3.select(nodes[16]).classed('geolocate')).to.be.true; - expect(d3.select(nodes[17]).classed('touch')).to.be.true; + expect(d3.select(nodes[14]).classed('kyfromabove')).to.be.true; + expect(d3.select(nodes[15]).classed('local-photos')).to.be.true; + expect(d3.select(nodes[16]).classed('debug')).to.be.true; + expect(d3.select(nodes[17]).classed('geolocate')).to.be.true; + expect(d3.select(nodes[18]).classed('touch')).to.be.true; }); });