Feature: show geocoded images on the map when hovered, show interactive minimap on nearbyImages element

This commit is contained in:
Pieter Vander Vennet 2024-09-12 01:31:00 +02:00
parent d079ba91aa
commit f3fdc95bd0
23 changed files with 404 additions and 182 deletions

View file

@ -0,0 +1,70 @@
{
"id": "geocoded_image",
"source": "special",
"name": null,
"tagRenderings": [],
"pointRendering": [
{
"location": [
"point",
"centroid"
],
"marker": [
{
"icon": "direction_gradient",
"color": {
"render": "#44cc22",
"mappings": [
{
"if": "selected=yes",
"then": "#cccc22"
}
]
}
}
],
"rotation": "{rotation}deg",
"rotationAlignment": "map",
"pitchAlignment": "map",
"iconSize": "60,60"
},
{
"location": [
"point",
"centroid"
],
"rotationAlignment": "map",
"pitchAlignment": "map",
"marker": [
{
"icon": "circle",
"color": {
"render": "#44cc22",
"mappings": [
{
"if": "selected=yes",
"then": "#cccc22"
}
]
}
}
],
"iconSize": "14,14"
},
{
"location": [
"point",
"centroid"
],
"rotationAlignment": "map",
"pitchAlignment": "map",
"marker": [
{
"icon": "ring",
"color": "#000"
}
],
"iconSize": "14,14"
}
]
}

View file

@ -32,7 +32,7 @@
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="svg1" /> inkscape:current-layer="svg1" />
<path <path
style="fill:#000000" style="fill:#000000;"
class="selectable" class="selectable"
d="M 375,187.5 C 375,291.05469 291.05469,375 187.5,375 83.945312,375 0,291.05469 0,187.5 0,83.945312 83.945312,0 187.5,0 291.05469,0 375,83.945312 375,187.5 Z m 0,0" d="M 375,187.5 C 375,291.05469 291.05469,375 187.5,375 83.945312,375 0,291.05469 0,187.5 0,83.945312 83.945312,0 187.5,0 291.05469,0 375,83.945312 375,187.5 Z m 0,0"
id="path1" /> id="path1" />

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,101 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg <svg
xmlns:dc="http://purl.org/dc/elements/1.1/" version="1.0"
xmlns:cc="http://creativecommons.org/ns#" width="860.50732pt"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" height="860.50732pt"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 860.50732 860.50732"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" preserveAspectRatio="xMidYMid meet"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" id="svg14"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.0" xmlns="http://www.w3.org/2000/svg"
width="860.50732pt" xmlns:svg="http://www.w3.org/2000/svg"
height="860.50732pt" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
viewBox="0 0 860.50732 860.50732" xmlns:cc="http://creativecommons.org/ns#"
preserveAspectRatio="xMidYMid meet" xmlns:dc="http://purl.org/dc/elements/1.1/">
id="svg14" <defs
sodipodi:docname="direction_gradient.svg" id="defs18">
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"> <linearGradient
<defs id="linearGradient832">
id="defs18"> <stop
<linearGradient style="stop-color:#000000;stop-opacity:1;"
inkscape:collect="always" offset="0"
id="linearGradient832"> id="stop828" />
<stop <stop
style="stop-color:#000000;stop-opacity:1;" style="stop-color:#000000;stop-opacity:0;"
offset="0" offset="1"
id="stop828"/> id="stop830" />
<stop </linearGradient>
style="stop-color:#000000;stop-opacity:0;" <radialGradient
offset="1" xlink:href="#linearGradient832"
id="stop830"/> id="radialGradient838"
</linearGradient> cx="430.25363"
<radialGradient cy="519.61188"
inkscape:collect="always" fx="430.25363"
xlink:href="#linearGradient832" fy="519.61188"
id="radialGradient838" r="305.54589"
cx="430.25363" gradientTransform="matrix(0.95288409,-0.94890664,0.94542304,0.94938587,-470.98122,345.21193)"
cy="519.61188" gradientUnits="userSpaceOnUse" />
fx="430.25363" </defs>
fy="519.61188" <metadata
r="305.54589" id="metadata2">
gradientTransform="matrix(0.95288409,-0.94890664,0.94542304,0.94938587,-470.98122,345.21193)"
gradientUnits="userSpaceOnUse"/>
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="999"
id="namedview16"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="0.70710678"
inkscape:cx="279.00239"
inkscape:cy="856.75313"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg14">
<sodipodi:guide
position="430.25363,862.49682"
orientation="1,0"
id="guide832"
inkscape:locked="false"/>
<sodipodi:guide
position="-42.427977,430.25368"
orientation="0,1"
id="guide834"
inkscape:locked="false"/>
<sodipodi:guide
position="398.27788,720.18823"
orientation="0,1"
id="guide840"
inkscape:locked="false"/>
</sodipodi:namedview>
<metadata
id="metadata2">
Created by potrace 1.15, written by Peter Selinger 2001-2017 Created by potrace 1.15, written by Peter Selinger 2001-2017
<rdf:RDF> <rdf:RDF>
<cc:Work <cc:Work
rdf:about=""> rdf:about="">
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:type <dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title/> </cc:Work>
</cc:Work> </rdf:RDF>
</rdf:RDF> </metadata>
</metadata> <path
<path style="fill:url(#radialGradient838);fill-opacity:1;stroke:none;stroke-width:2.83464575;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
style="fill:url(#radialGradient838);fill-opacity:1;stroke:none;stroke-width:2.83464575;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M 735.79979,124.70799 C 654.79116,43.598883 544.88842,-2.0206645 430.25389,-2.121103 315.61937,-2.0206592 205.71663,43.598888 124.70801,124.70799 l 305.54588,305.54589 z"
d="M 735.79979,124.70799 C 654.79116,43.598883 544.88842,-2.0206645 430.25389,-2.121103 315.61937,-2.0206592 205.71663,43.598888 124.70801,124.70799 l 305.54588,305.54589 z" id="path836" />
id="path836"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -234,7 +234,7 @@
{ {
"id": "nearby_images", "id": "nearby_images",
"render": { "render": {
"*": "{nearby_images(open,readonly)}" "*": "{nearby_images(,readonly)}"
} }
} }
], ],

View file

@ -1,6 +1,6 @@
{ {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.46.4", "version": "0.46.5",
"repository": "https://github.com/pietervdvn/MapComplete", "repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily", "description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues", "bugs": "https://github.com/pietervdvn/MapComplete/issues",
@ -91,7 +91,7 @@
"generate:contributor-list": "vite-node scripts/generateContributors.ts", "generate:contributor-list": "vite-node scripts/generateContributors.ts",
"generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i.bak \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js && rm public/service-worker.js.bak", "generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i.bak \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js && rm public/service-worker.js.bak",
"reset:layeroverview": "npm run prep:layeroverview && npm run generate:layeroverview && npm run refresh:layeroverview", "reset:layeroverview": "npm run prep:layeroverview && npm run generate:layeroverview && npm run refresh:layeroverview",
"prep:layeroverview": "mkdir -p ./src/assets/generated/layers; echo {\\\"themes\\\":[]} > ./src/assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./src/assets/generated/known_layers.json && rm -f ./src/assets/generated/layers/*.json && rm -f ./src/assets/generated/themes/*.json && cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json && echo '{}' > ./src/assets/generated/layers/favourite.json && echo '{}' > ./src/assets/generated/layers/summary.json && echo '{}' > ./src/assets/generated/layers/last_click.json && echo '[]' > ./src/assets/generated/theme_overview.json", "prep:layeroverview": "mkdir -p ./src/assets/generated/layers; echo {\\\"themes\\\":[]} > ./src/assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./src/assets/generated/known_layers.json && rm -f ./src/assets/generated/layers/*.json && rm -f ./src/assets/generated/themes/*.json && cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json && echo '{}' > ./src/assets/generated/layers/favourite.json && echo '{}' > ./src/assets/generated/layers/summary.json && echo '{}' > ./src/assets/generated/layers/last_click.json && echo '[]' > ./src/assets/generated/theme_overview.json echo '{}' > ./src/assets/generated/geocoded_image.json",
"generate": "npm run generate:licenses && npm run generate:images && npm run generate:charging-stations && npm run generate:translations && npm run refresh:layeroverview && npm run generate:service-worker", "generate": "npm run generate:licenses && npm run generate:images && npm run generate:charging-stations && npm run generate:translations && npm run refresh:layeroverview && npm run generate:service-worker",
"generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -", "generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -",
"clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm", "clean:tests": "find . -type f -name \"*.doctest.ts\" | xargs -r rm",

View file

@ -1761,14 +1761,14 @@ input[type="range"].range-lg::-moz-range-thumb {
height: 3.5rem; height: 3.5rem;
} }
.h-16 {
height: 4rem;
}
.h-48 { .h-48 {
height: 12rem; height: 12rem;
} }
.h-16 {
height: 4rem;
}
.h-40 { .h-40 {
height: 10rem; height: 10rem;
} }
@ -3251,11 +3251,6 @@ input[type="range"].range-lg::-moz-range-thumb {
background-color: rgb(0 0 0 / var(--tw-bg-opacity)); background-color: rgb(0 0 0 / var(--tw-bg-opacity));
} }
.bg-indigo-100 {
--tw-bg-opacity: 1;
background-color: rgb(229 237 255 / var(--tw-bg-opacity));
}
.bg-gray-100 { .bg-gray-100 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity)); background-color: rgb(243 244 246 / var(--tw-bg-opacity));
@ -3286,6 +3281,11 @@ input[type="range"].range-lg::-moz-range-thumb {
background-color: rgb(253 246 178 / var(--tw-bg-opacity)); background-color: rgb(253 246 178 / var(--tw-bg-opacity));
} }
.bg-indigo-100 {
--tw-bg-opacity: 1;
background-color: rgb(229 237 255 / var(--tw-bg-opacity));
}
.bg-purple-100 { .bg-purple-100 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(237 235 254 / var(--tw-bg-opacity)); background-color: rgb(237 235 254 / var(--tw-bg-opacity));
@ -4032,6 +4032,10 @@ input[type="range"].range-lg::-moz-range-thumb {
padding-right: 1rem; padding-right: 1rem;
} }
.pt-2 {
padding-top: 0.5rem;
}
.pb-1\.5 { .pb-1\.5 {
padding-bottom: 0.375rem; padding-bottom: 0.375rem;
} }
@ -5922,11 +5926,6 @@ svg.apply-fill path {
border-color: rgb(209 213 219 / var(--tw-border-opacity)); border-color: rgb(209 213 219 / var(--tw-border-opacity));
} }
.hover\:bg-indigo-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(205 219 254 / var(--tw-bg-opacity));
}
.hover\:bg-gray-100:hover { .hover\:bg-gray-100:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity)); background-color: rgb(243 244 246 / var(--tw-bg-opacity));
@ -5962,6 +5961,11 @@ svg.apply-fill path {
background-color: rgb(252 233 106 / var(--tw-bg-opacity)); background-color: rgb(252 233 106 / var(--tw-bg-opacity));
} }
.hover\:bg-indigo-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(205 219 254 / var(--tw-bg-opacity));
}
.hover\:bg-purple-200:hover { .hover\:bg-purple-200:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(220 215 254 / var(--tw-bg-opacity)); background-color: rgb(220 215 254 / var(--tw-bg-opacity));
@ -8110,6 +8114,10 @@ svg.apply-fill path {
height: 2.75rem; height: 2.75rem;
} }
.sm\:h-32 {
height: 8rem;
}
.sm\:h-64 { .sm\:h-64 {
height: 16rem; height: 16rem;
} }
@ -8299,6 +8307,10 @@ svg.apply-fill path {
display: none; display: none;
} }
.md\:h-64 {
height: 16rem;
}
.md\:h-auto { .md\:h-auto {
height: auto; height: auto;
} }
@ -8482,6 +8494,11 @@ svg.apply-fill path {
padding-bottom: 2rem; padding-bottom: 2rem;
} }
.md\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.md\:pr-2 { .md\:pr-2 {
padding-right: 0.5rem; padding-right: 0.5rem;
} }

View file

@ -9,7 +9,14 @@ export interface ProvidedImage {
key: string key: string
provider: ImageProvider provider: ImageProvider
id: string id: string
date?: Date date?: Date,
/**
* Compass angle of the taken image
* 0 = north, 90° = East
*/
rotation?: number
lat?: number,
lon?: number
} }
export default abstract class ImageProvider { export default abstract class ImageProvider {

View file

@ -162,12 +162,14 @@ export class Mapillary extends ImageProvider {
const metadataUrl = const metadataUrl =
"https://graph.mapillary.com/" + "https://graph.mapillary.com/" +
mapillaryId + mapillaryId +
"?fields=thumb_1024_url,thumb_original_url,captured_at,creator&access_token=" + "?fields=thumb_1024_url,thumb_original_url,captured_at,compass_angle,geometry,creator&access_token=" +
Constants.mapillary_client_token_v4 Constants.mapillary_client_token_v4
const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60) const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60)
const url = <string>response["thumb_1024_url"] const url = <string>response["thumb_1024_url"]
const url_hd = <string>response["thumb_original_url"] const url_hd = <string>response["thumb_original_url"]
const date = new Date() const date = new Date()
const rotation = (720 - Number(response["compass_angle"])) % 360
const geometry = response["geometry"]
date.setTime(response["captured_at"]) date.setTime(response["captured_at"])
return <ProvidedImage>{ return <ProvidedImage>{
id: "" + mapillaryId, id: "" + mapillaryId,
@ -176,6 +178,9 @@ export class Mapillary extends ImageProvider {
provider: this, provider: this,
date, date,
key, key,
rotation,
lat: geometry.coordinates[1],
lon: geometry.coordinates[0]
} }
} }
} }

View file

@ -56,7 +56,7 @@ export interface P4CPicture {
author? author?
license? license?
detailsUrl?: string detailsUrl?: string
direction? direction?: number,
osmTags?: object /*To copy straight into OSM!*/ osmTags?: object /*To copy straight into OSM!*/
thumbUrl: string thumbUrl: string
details: { details: {

View file

@ -25,6 +25,7 @@ export default class Constants {
"last_click", "last_click",
"favourite", "favourite",
"summary", "summary",
"geocoded_image"
] as const ] as const
/** /**
* Special layers which are not included in a theme by default * Special layers which are not included in a theme by default

View file

@ -718,7 +718,7 @@ export class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJ
if (json.marker && !Array.isArray(json.marker)) { if (json.marker && !Array.isArray(json.marker)) {
context.enter("marker").err("The marker in a pointRendering should be an array") context.enter("marker").err("The marker in a pointRendering should be an array")
} }
if (json.location.length == 0) { if (!(json.location?.length > 0)) {
context context
.enter("location") .enter("location")
.err( .err(

View file

@ -5,7 +5,7 @@ import { Store, UIEventSource } from "../Logic/UIEventSource"
import { import {
FeatureSource, FeatureSource,
IndexedFeatureSource, IndexedFeatureSource,
WritableFeatureSource, WritableFeatureSource
} from "../Logic/FeatureSource/FeatureSource" } from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection" import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { ExportableMap, MapProperties } from "./MapProperties" import { ExportableMap, MapProperties } from "./MapProperties"
@ -51,7 +51,7 @@ import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveF
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
import NoElementsInViewDetector, { import NoElementsInViewDetector, {
FeatureViewState, FeatureViewState
} from "../Logic/Actors/NoElementsInViewDetector" } from "../Logic/Actors/NoElementsInViewDetector"
import FilteredLayer from "./FilteredLayer" import FilteredLayer from "./FilteredLayer"
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
@ -64,13 +64,12 @@ import { GeolocationControlState } from "../UI/BigComponents/GeolocationControl"
import Zoomcontrol from "../UI/Zoomcontrol" import Zoomcontrol from "../UI/Zoomcontrol"
import { import {
SummaryTileSource, SummaryTileSource,
SummaryTileSourceRewriter, SummaryTileSourceRewriter
} from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource" } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
import summaryLayer from "../assets/generated/layers/summary.json" import summaryLayer from "../assets/generated/layers/summary.json"
import last_click_layerconfig from "../assets/generated/layers/last_click.json" import last_click_layerconfig from "../assets/generated/layers/last_click.json"
import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson"
import Locale from "../UI/i18n/Locale"
import Hash from "../Logic/Web/Hash" import Hash from "../Logic/Web/Hash"
import { GeoOperations } from "../Logic/GeoOperations" import { GeoOperations } from "../Logic/GeoOperations"
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
@ -154,6 +153,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
public readonly toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage> public readonly toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage>
public readonly nearbyImageSearcher: CombinedFetcher public readonly nearbyImageSearcher: CombinedFetcher
/**
* Geocoded images that should be shown on the main map; probably only the currently hovered image
*/
public readonly geocodedImages: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([ ])
constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) { constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) {
Utils.initDomPurify() Utils.initDomPurify()
@ -178,7 +181,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
"oauth_token", "oauth_token",
undefined, undefined,
"Used to complete the login" "Used to complete the login"
), )
}) })
this.userRelatedState = new UserRelatedState( this.userRelatedState = new UserRelatedState(
this.osmConnection, this.osmConnection,
@ -257,8 +260,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
bbox.asGeoJson({ bbox.asGeoJson({
zoom: this.mapProperties.zoom.data, zoom: this.mapProperties.zoom.data,
...this.mapProperties.location.data, ...this.mapProperties.location.data,
id: "current_view_" + currentViewIndex, id: "current_view_" + currentViewIndex
}), })
] ]
}) })
) )
@ -275,7 +278,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
featurePropertiesStore: this.featureProperties, featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection, osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations, historicalUserLocations: this.geolocation.historicalUserLocations,
featureSwitches: this.featureSwitches, featureSwitches: this.featureSwitches
}, },
layout?.isLeftRightSensitive() ?? false, layout?.isLeftRightSensitive() ?? false,
(e, extraMsg) => this.reportError(e, extraMsg) (e, extraMsg) => this.reportError(e, extraMsg)
@ -303,7 +306,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
"leftover features, such as", "leftover features, such as",
features[0].properties features[0].properties
) )
}, }
} }
) )
this.perLayer = perLayer.perLayer this.perLayer = perLayer.perLayer
@ -359,7 +362,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
{ {
currentZoom: this.mapProperties.zoom, currentZoom: this.mapProperties.zoom,
layerState: this.layerState, layerState: this.layerState,
bounds: this.visualFeedbackViewportBounds, bounds: this.visualFeedbackViewportBounds
} }
) )
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
@ -453,7 +456,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer, doShowLayer,
metaTags: this.userRelatedState.preferencesAsTags, metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement, selectedElement: this.selectedElement,
fetchStore: (id) => this.featureProperties.getStore(id), fetchStore: (id) => this.featureProperties.getStore(id)
}) })
}) })
return filteringFeatureSource return filteringFeatureSource
@ -480,7 +483,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer: flayerGps.isDisplayed, doShowLayer: flayerGps.isDisplayed,
layer: flayerGps.layerDef, layer: flayerGps.layerDef,
metaTags: this.userRelatedState.preferencesAsTags, metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement, selectedElement: this.selectedElement
}) })
} }
@ -569,7 +572,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
nomod: " ", nomod: " ",
onUp: true, onUp: true
}, },
docs.selectItem, docs.selectItem,
() => { () => {
@ -595,7 +598,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
nomod: "" + i, nomod: "" + i,
onUp: true, onUp: true
}, },
doc, doc,
() => this.selectClosestAtCenter(i - 1) () => this.selectClosestAtCenter(i - 1)
@ -608,7 +611,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
} }
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
nomod: "b", nomod: "b"
}, },
docs.openLayersPanel, docs.openLayersPanel,
() => { () => {
@ -619,7 +622,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
) )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
nomod: "s", nomod: "s"
}, },
Translations.t.hotkeyDocumentation.openFilterPanel, Translations.t.hotkeyDocumentation.openFilterPanel,
() => { () => {
@ -697,7 +700,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
shift: "T", shift: "T"
}, },
Translations.t.hotkeyDocumentation.translationMode, Translations.t.hotkeyDocumentation.translationMode,
() => { () => {
@ -734,7 +737,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.mapProperties.zoom.map((z) => Math.max(Math.floor(z), 0)), this.mapProperties.zoom.map((z) => Math.max(Math.floor(z), 0)),
this.mapProperties, this.mapProperties,
{ {
isActive: this.mapProperties.zoom.map((z) => z < maxzoom), isActive: this.mapProperties.zoom.map((z) => z < maxzoom)
} }
) )
@ -755,6 +758,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
gps_location: this.geolocation.currentUserLocation, gps_location: this.geolocation.currentUserLocation,
gps_location_history: this.geolocation.historicalUserLocations, gps_location_history: this.geolocation.historicalUserLocations,
gps_track: this.geolocation.historicalUserLocationsTrack, gps_track: this.geolocation.historicalUserLocationsTrack,
geocoded_image: new StaticFeatureSource(this.geocodedImages),
selected_element: new StaticFeatureSource( selected_element: new StaticFeatureSource(
this.selectedElement.map((f) => (f === undefined ? empty : [f])) this.selectedElement.map((f) => (f === undefined ? empty : [f]))
), ),
@ -766,7 +770,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
current_view: this.currentView, current_view: this.currentView,
favourite: this.favourites, favourite: this.favourites,
summary: this.featureSummary, summary: this.featureSummary,
last_click: this.lastClickObject, last_click: this.lastClickObject
} }
this.closestFeatures.registerSource(specialLayers.favourite, "favourite") this.closestFeatures.registerSource(specialLayers.favourite, "favourite")
@ -821,7 +825,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer: flayer.isDisplayed, doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef, layer: flayer.layerDef,
metaTags: this.userRelatedState.preferencesAsTags, metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement, selectedElement: this.selectedElement
}) })
}) })
const summaryLayerConfig = new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer") const summaryLayerConfig = new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer")
@ -829,7 +833,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
features: specialLayers.summary, features: specialLayers.summary,
layer: summaryLayerConfig, layer: summaryLayerConfig,
// doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom), // doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom),
selectedElement: this.selectedElement, selectedElement: this.selectedElement
}) })
const lastClickLayerConfig = new LayerConfig( const lastClickLayerConfig = new LayerConfig(
@ -840,14 +844,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
lastClickLayerConfig.isShown === undefined lastClickLayerConfig.isShown === undefined
? specialLayers.last_click ? specialLayers.last_click
: specialLayers.last_click.features.mapD((fs) => : specialLayers.last_click.features.mapD((fs) =>
fs.filter((f) => { fs.filter((f) => {
const matches = lastClickLayerConfig.isShown.matchesProperties( const matches = lastClickLayerConfig.isShown.matchesProperties(
f.properties f.properties
) )
console.debug("LastClick ", f, "matches", matches) console.debug("LastClick ", f, "matches", matches)
return matches return matches
}) })
) )
new ShowDataLayer(this.map, { new ShowDataLayer(this.map, {
features: new StaticFeatureSource(lastClickFiltered), features: new StaticFeatureSource(lastClickFiltered),
layer: lastClickLayerConfig, layer: lastClickLayerConfig,
@ -858,9 +862,9 @@ export default class ThemeViewState implements SpecialVisualizationState {
} }
this.map.data.flyTo({ this.map.data.flyTo({
zoom: Constants.minZoomLevelToAddNewPoint, zoom: Constants.minZoomLevelToAddNewPoint,
center: GeoOperations.centerpointCoordinates(feature), center: GeoOperations.centerpointCoordinates(feature)
}) })
}, }
}) })
} }
@ -871,6 +875,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.selectedElement.addCallback((selected) => { this.selectedElement.addCallback((selected) => {
if (selected === undefined) { if (selected === undefined) {
this.focusOnMap() this.focusOnMap()
this.geocodedImages.set([])
} else { } else {
this.lastClickObject.clear() this.lastClickObject.clear()
} }
@ -953,8 +958,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
userid: this.osmConnection.userDetails.data?.uid, userid: this.osmConnection.userDetails.data?.uid,
pendingChanges: this.changes.pendingChanges.data, pendingChanges: this.changes.pendingChanges.data,
previousChanges: this.changes.allChanges.data, previousChanges: this.changes.allChanges.data,
changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings), changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings)
}), })
}) })
} catch (e) { } catch (e) {
console.error("Could not upload an error report") console.error("Could not upload an error report")

View file

@ -132,6 +132,7 @@
<div class="flex h-32 w-max gap-x-2"> <div class="flex h-32 w-max gap-x-2">
{#each $unknownImages as image (image)} {#each $unknownImages as image (image)}
<AttributedImage <AttributedImage
{state}
imgClass="h-32 w-max shrink-0" imgClass="h-32 w-max shrink-0"
image={{ url: image }} image={{ url: image }}
previewedImage={state.previewedImage} previewedImage={state.previewedImage}

View file

@ -7,10 +7,12 @@
import { Mapillary } from "../../Logic/ImageProviders/Mapillary" import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import { MagnifyingGlassPlusIcon } from "@babeard/svelte-heroicons/outline" import { MagnifyingGlassPlusIcon } from "@babeard/svelte-heroicons/outline"
import { CloseButton, Modal } from "flowbite-svelte" import { CloseButton } from "flowbite-svelte"
import ImageOperations from "./ImageOperations.svelte" import ImageOperations from "./ImageOperations.svelte"
import Popup from "../Base/Popup.svelte" import Popup from "../Base/Popup.svelte"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { Feature, Point } from "geojson"
export let image: Partial<ProvidedImage> export let image: Partial<ProvidedImage>
let fallbackImage: string = undefined let fallbackImage: string = undefined
@ -20,19 +22,43 @@
let imgEl: HTMLImageElement let imgEl: HTMLImageElement
export let imgClass: string = undefined export let imgClass: string = undefined
export let state: SpecialVisualizationState = undefined
export let attributionFormat: "minimal" | "medium" | "large" = "medium" export let attributionFormat: "minimal" | "medium" | "large" = "medium"
export let previewedImage: UIEventSource<ProvidedImage> export let previewedImage: UIEventSource<ProvidedImage>
export let canZoom = previewedImage !== undefined export let canZoom = previewedImage !== undefined
let loaded = false let loaded = false
let showBigPreview = new UIEventSource(false) let showBigPreview = new UIEventSource(false)
onDestroy(showBigPreview.addCallbackAndRun(shown=>{ onDestroy(showBigPreview.addCallbackAndRun(shown => {
if(!shown){ if (!shown) {
previewedImage.set(false) previewedImage.set(false)
} }
})) }))
onDestroy(previewedImage.addCallbackAndRun(previewedImage => { onDestroy(previewedImage.addCallbackAndRun(previewedImage => {
showBigPreview.set(previewedImage?.id === image.id) showBigPreview.set(previewedImage?.id === image.id)
})) }))
function highlight(entered: boolean = true) {
if (!entered) {
state?.geocodedImages.set([])
return
}
if (isNaN(image.lon) || isNaN(image.lat)) {
return
}
const f: Feature<Point> = {
type: "Feature",
properties: {
id: image.id,
rotation: image.rotation
},
geometry: {
type: "Point",
coordinates: [image.lon, image.lat]
}
}
console.log(f)
state?.geocodedImages.set([f])
}
</script> </script>
<Popup shown={showBigPreview} bodyPadding="p-0" dismissable={true}> <Popup shown={showBigPreview} bodyPadding="p-0" dismissable={true}>
@ -48,7 +74,10 @@
</div> </div>
</Popup> </Popup>
<div class="relative shrink-0"> <div class="relative shrink-0">
<div class="relative w-fit"> <div class="relative w-fit"
on:mouseenter={() => highlight()}
on:mouseleave={() => highlight(false)}
>
<img <img
bind:this={imgEl} bind:this={imgEl}
on:load={() => (loaded = true)} on:load={() => (loaded = true)}
@ -68,7 +97,7 @@
{#if canZoom && loaded} {#if canZoom && loaded}
<div <div
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full" class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
on:click={() => previewedImage.set(image)}> on:click={() => previewedImage.set(image)}>
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" /> <MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
</div> </div>
{/if} {/if}

View file

@ -8,7 +8,6 @@ import ImageProvider, { ProvidedImage } from "../../Logic/ImageProviders/ImagePr
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Changes } from "../../Logic/Osm/Changes" import { Changes } from "../../Logic/Osm/Changes"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Feature } from "geojson"
import SvelteUIElement from "../Base/SvelteUIElement" import SvelteUIElement from "../Base/SvelteUIElement"
import AttributedImage from "./AttributedImage.svelte" import AttributedImage from "./AttributedImage.svelte"
@ -30,6 +29,7 @@ export class ImageCarousel extends Toggle {
try { try {
let image: BaseUIElement = new SvelteUIElement(AttributedImage, { let image: BaseUIElement = new SvelteUIElement(AttributedImage, {
image: url, image: url,
state,
previewedImage: state?.previewedImage, previewedImage: state?.previewedImage,
}) })

View file

@ -14,8 +14,8 @@
import AttributedImage from "./AttributedImage.svelte" import AttributedImage from "./AttributedImage.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte" import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
import LoginToggle from "../Base/LoginToggle.svelte" import LoginToggle from "../Base/LoginToggle.svelte"
import ImagePreview from "./ImagePreview.svelte" import { onDestroy } from "svelte"
import FloatOver from "../Base/FloatOver.svelte" import { Utils } from "../../Utils"
export let tags: UIEventSource<OsmTags> export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
@ -23,6 +23,8 @@
export let feature: Feature export let feature: Feature
export let layer: LayerConfig export let layer: LayerConfig
export let highlighted: UIEventSource<string> = undefined
export let linkable = true export let linkable = true
let targetValue = Object.values(image.osmTags)[0] let targetValue = Object.values(image.osmTags)[0]
let isLinked = new UIEventSource(Object.values(tags.data).some((v) => targetValue === v)) let isLinked = new UIEventSource(Object.values(tags.data).some((v) => targetValue === v))
@ -33,7 +35,7 @@
key: undefined, key: undefined,
provider: AllImageProviders.byName(image.provider), provider: AllImageProviders.byName(image.provider),
date: new Date(image.date), date: new Date(image.date),
id: Object.values(image.osmTags)[0], id: Object.values(image.osmTags)[0]
} }
async function applyLink(isLinked: boolean) { async function applyLink(isLinked: boolean) {
@ -44,7 +46,7 @@
if (isLinked) { if (isLinked) {
const action = new LinkImageAction(currentTags.id, key, url, tags, { const action = new LinkImageAction(currentTags.id, key, url, tags, {
theme: tags.data._orig_theme ?? state.layout.id, theme: tags.data._orig_theme ?? state.layout.id,
changeType: "link-image", changeType: "link-image"
}) })
await state.changes.applyAction(action) await state.changes.applyAction(action)
} else { } else {
@ -53,7 +55,7 @@
if (v === url) { if (v === url) {
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, { const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, {
theme: tags.data._orig_theme ?? state.layout.id, theme: tags.data._orig_theme ?? state.layout.id,
changeType: "remove-image", changeType: "remove-image"
}) })
state.changes.applyAction(action) state.changes.applyAction(action)
} }
@ -62,16 +64,30 @@
} }
isLinked.addCallback((isLinked) => applyLink(isLinked)) isLinked.addCallback((isLinked) => applyLink(isLinked))
let element: HTMLDivElement
if (highlighted) {
onDestroy(
highlighted.addCallbackD(highlightedUrl => {
if (highlightedUrl === image.pictureUrl) {
Utils.scrollIntoView(element)
}
})
)
}
</script> </script>
<div <div
class="flex w-fit shrink-0 flex-col overflow-hidden rounded-lg" class="flex w-fit shrink-0 flex-col overflow-hidden rounded-lg"
class:border-interactive={$isLinked} class:border-interactive={$isLinked || $highlighted === image.pictureUrl}
style="border-width: 2px" style="border-width: 2px"
bind:this={element}
> >
<AttributedImage <AttributedImage
{state}
image={providedImage} image={providedImage}
imgClass="max-h-64 w-auto" imgClass="max-h-64 w-auto sm:h-32 md:h-64"
previewedImage={state.previewedImage} previewedImage={state.previewedImage}
attributionFormat="minimal" attributionFormat="minimal"
> >

View file

@ -7,13 +7,23 @@
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch" import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch"
import LinkableImage from "./LinkableImage.svelte" import LinkableImage from "./LinkableImage.svelte"
import type { Feature } from "geojson" import type { Feature, Point } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import MapillaryLink from "../BigComponents/MapillaryLink.svelte" import MapillaryLink from "../BigComponents/MapillaryLink.svelte"
import MaplibreMap from "../Map/MaplibreMap.svelte"
import { Map as MlMap } from "maplibre-gl"
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import ShowDataLayer from "../Map/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import * as geocoded_image from "../../assets/generated/layers/geocoded_image.json"
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { onDestroy } from "svelte"
import { BBox } from "../../Logic/BBox"
export let tags: UIEventSource<OsmTags> export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
@ -42,12 +52,100 @@
[loadedImages] [loadedImages]
) )
let asFeatures = result.map(p4cs => p4cs.map(p4c => (<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [p4c.coordinates.lng, p4c.coordinates.lat]
},
properties: {
id: p4c.pictureUrl,
rotation: p4c.direction
}
})))
let selected = new UIEventSource<P4CPicture>(undefined)
let selectedAsFeature = selected.mapD(s => {
return [<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [s.coordinates.lng, s.coordinates.lat]
},
properties: {
id: s.pictureUrl,
selected: "yes",
rotation: s.direction
}
}]
})
let someLoading = imageState.state.mapD((stateRecord) => let someLoading = imageState.state.mapD((stateRecord) =>
Object.values(stateRecord).some((v) => v === "loading") Object.values(stateRecord).some((v) => v === "loading")
) )
let errors = imageState.state.mapD((stateRecord) => let errors = imageState.state.mapD((stateRecord) =>
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error") Object.keys(stateRecord).filter((k) => stateRecord[k] === "error")
) )
let highlighted = new UIEventSource<string>(undefined)
onDestroy(highlighted.addCallbackD(hl => {
const p4c = result.data?.find(i => i.pictureUrl === hl)
selected.set(p4c)
}
))
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let mapProperties = new MapLibreAdaptor(map, {
rasterLayer: state.mapProperties.rasterLayer,
rotation: state.mapProperties.rotation,
pitch: state.mapProperties.pitch,
zoom: new UIEventSource<number>(16),
location: new UIEventSource({ lon, lat }),
})
const geocodedImageLayer = new LayerConfig(<LayerConfigJson>geocoded_image)
new ShowDataLayer(map, {
features: new StaticFeatureSource(asFeatures),
layer: geocodedImageLayer,
zoomToFeatures: true,
onClick: (feature) => {
highlighted.set(feature.properties.id)
}
})
ShowDataLayer.showMultipleLayers(
map,
new StaticFeatureSource([feature]),
state.layout.layers
)
onDestroy(
asFeatures.addCallbackAndRunD(features => {
if(features.length == 0){
return
}
let bbox = BBox.get(features[0])
for (const f of features) {
bbox = bbox.unionWith(BBox.get(f))
}
mapProperties.maxbounds.set(bbox.pad(1.1))
})
)
new ShowDataLayer(map, {
features: new StaticFeatureSource(selectedAsFeature),
layer: geocodedImageLayer,
onClick: (feature) => {
highlighted.set(feature.properties.id)
}
})
</script> </script>
<div class="flex flex-col"> <div class="flex flex-col">
@ -62,12 +160,24 @@
{:else} {:else}
<div class="flex w-full space-x-4 overflow-x-auto" style="scroll-snap-type: x proximity"> <div class="flex w-full space-x-4 overflow-x-auto" style="scroll-snap-type: x proximity">
{#each $result as image (image.pictureUrl)} {#each $result as image (image.pictureUrl)}
<span class="w-fit shrink-0" style="scroll-snap-align: start"> <span class="w-fit shrink-0" style="scroll-snap-align: start"
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} /> on:mouseenter={() => {highlighted.set(image.pictureUrl)}}
on:mouseleave={() =>{ highlighted.set(undefined); selected.set(undefined)}}
>
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} {highlighted} />
</span> </span>
{/each} {/each}
</div> </div>
{/if} {/if}
<span class="self-end pt-2">
<MapillaryLink
large={false}
mapProperties={{ zoom: new ImmutableStore(16), location: new ImmutableStore({ lon, lat }) }}
/>
</span>
<div class="my-2 flex justify-between"> <div class="my-2 flex justify-between">
<div> <div>
{#if $someLoading && $result.length > 0} {#if $someLoading && $result.length > 0}
@ -80,9 +190,10 @@
/> />
{/if} {/if}
</div> </div>
<MapillaryLink </div>
large={false}
mapProperties={{ zoom: new ImmutableStore(16), location: new ImmutableStore({ lon, lat }) }}
/> <div class="h-48">
<MaplibreMap interactive={false} {map} {mapProperties} />
</div> </div>
</div> </div>

View file

@ -11,8 +11,9 @@
import Camera_plus from "../../assets/svg/Camera_plus.svelte" import Camera_plus from "../../assets/svg/Camera_plus.svelte"
import LoginToggle from "../Base/LoginToggle.svelte" import LoginToggle from "../Base/LoginToggle.svelte"
import { ariaLabel } from "../../Utils/ariaLabel" import { ariaLabel } from "../../Utils/ariaLabel"
import { Accordion, AccordionItem } from "flowbite-svelte" import { Accordion, AccordionItem, Modal } from "flowbite-svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import Popup from "../Base/Popup.svelte"
export let tags: UIEventSource<OsmTags> export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
@ -24,15 +25,16 @@
export let layer: LayerConfig export let layer: LayerConfig
const t = Translations.t.image.nearby const t = Translations.t.image.nearby
let expanded = false
let enableLogin = state.featureSwitches.featureSwitchEnableLogin let enableLogin = state.featureSwitches.featureSwitchEnableLogin
export let shown = new UIEventSource(false)
</script> </script>
{#if enableLogin.data} {#if enableLogin.data}
<AccordionSingle> <button on:click={() => {shown.set(!shown.data)}}><Tr t={t.seeNearby}/> </button>
<span slot="header" class="p-2 text-base"> <Popup {shown} bodyPadding="p-4">
<span slot="header">
<Tr t={t.seeNearby} /> <Tr t={t.seeNearby} />
</span> </span>
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer} /> <NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer} />
</AccordionSingle> </Popup>
{/if} {/if}

View file

@ -121,7 +121,7 @@
<HeartOutlineIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} /> <HeartOutlineIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} />
{:else if icon === "confirm"} {:else if icon === "confirm"}
<Confirm class={clss} {color} /> <Confirm class={clss} {color} />
{:else if icon === "direction"} {:else if icon === "direction" || icon === "direction_gradient"}
<Direction_gradient class={clss} {color} /> <Direction_gradient class={clss} {color} />
{:else if icon === "not_found"} {:else if icon === "not_found"}
<Not_found class={twMerge(clss, "no-image-background")} {color} /> <Not_found class={twMerge(clss, "no-image-background")} {color} />

View file

@ -159,10 +159,9 @@ class PointRenderingLayer {
}) })
if (this._onClick) { if (this._onClick) {
const self = this el.addEventListener("click", (ev)=> {
el.addEventListener("click", function (ev) {
ev.preventDefault() ev.preventDefault()
self._onClick(feature) this._onClick(feature)
// Workaround to signal the MapLibreAdaptor to ignore this click // Workaround to signal the MapLibreAdaptor to ignore this click
ev["consumed"] = true ev["consumed"] = true
}) })

View file

@ -93,6 +93,7 @@ export interface SpecialVisualizationState {
readonly previewedImage: UIEventSource<ProvidedImage> readonly previewedImage: UIEventSource<ProvidedImage>
readonly nearbyImageSearcher: CombinedFetcher readonly nearbyImageSearcher: CombinedFetcher
readonly geolocation: GeoLocationHandler readonly geolocation: GeoLocationHandler
readonly geocodedImages : UIEventSource<Feature[]>
showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer
reportError(message: string): Promise<void> reportError(message: string): Promise<void>

View file

@ -1,4 +1,4 @@
<script> <script>
export let color = "#000000" export let color = "#000000"
</script> </script>
<svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown on:focus xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns="http://www.w3.org/2000/svg" version="1.0" width="860.50732pt" height="860.50732pt" viewBox="0 0 860.50732 860.50732" preserveAspectRatio="xMidYMid meet" id="svg14" sodipodi:docname="direction_gradient.svg" inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"> <defs id="defs18"> <linearGradient inkscape:collect="always" id="linearGradient832"> <stop style="stop-color:{color};stop-opacity:1;" offset="0" id="stop828"/> <stop style="stop-color:{color};stop-opacity:0;" offset="1" id="stop830"/> </linearGradient> <radialGradient inkscape:collect="always" xlink:href="#linearGradient832" id="radialGradient838" cx="430.25363" cy="519.61188" fx="430.25363" fy="519.61188" r="305.54589" gradientTransform="matrix(0.95288409,-0.94890664,0.94542304,0.94938587,-470.98122,345.21193)" gradientUnits="userSpaceOnUse"/> </defs> <sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1920" inkscape:window-height="999" id="namedview16" showgrid="false" showguides="true" inkscape:guide-bbox="true" inkscape:zoom="0.70710678" inkscape:cx="279.00239" inkscape:cy="856.75313" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg14"> <sodipodi:guide position="430.25363,862.49682" orientation="1,0" id="guide832" inkscape:locked="false"/> <sodipodi:guide position="-42.427977,430.25368" orientation="0,1" id="guide834" inkscape:locked="false"/> <sodipodi:guide position="398.27788,720.18823" orientation="0,1" id="guide840" inkscape:locked="false"/> </sodipodi:namedview> <metadata id="metadata2"> Created by potrace 1.15, written by Peter Selinger 2001-2017 <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> <dc:title/> </cc:Work> </rdf:RDF> </metadata> <path style="fill:url(#radialGradient838);fill-opacity:1;stroke:none;stroke-width:2.83464575;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M 735.79979,124.70799 C 654.79116,43.598883 544.88842,-2.0206645 430.25389,-2.121103 315.61937,-2.0206592 205.71663,43.598888 124.70801,124.70799 l 305.54588,305.54589 z" id="path836" inkscape:connector-curvature="0" sodipodi:nodetypes="ccccc"/> </svg> <svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown on:focus version="1.0" width="860.50732pt" height="860.50732pt" viewBox="0 0 860.50732 860.50732" preserveAspectRatio="xMidYMid meet" id="svg14" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> <defs id="defs18"> <linearGradient id="linearGradient832"> <stop style="stop-color:{color};stop-opacity:1;" offset="0" id="stop828" /> <stop style="stop-color:{color};stop-opacity:0;" offset="1" id="stop830" /> </linearGradient> <radialGradient xlink:href="#linearGradient832" id="radialGradient838" cx="430.25363" cy="519.61188" fx="430.25363" fy="519.61188" r="305.54589" gradientTransform="matrix(0.95288409,-0.94890664,0.94542304,0.94938587,-470.98122,345.21193)" gradientUnits="userSpaceOnUse" /> </defs> <metadata id="metadata2"> Created by potrace 1.15, written by Peter Selinger 2001-2017 <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> </cc:Work> </rdf:RDF> </metadata> <path style="fill:url(#radialGradient838);fill-opacity:1;stroke:none;stroke-width:2.83464575;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M 735.79979,124.70799 C 654.79116,43.598883 544.88842,-2.0206645 430.25389,-2.121103 315.61937,-2.0206592 205.71663,43.598888 124.70801,124.70799 l 305.54588,305.54589 z" id="path836" /> </svg>

View file

@ -0,0 +1,4 @@
<script>
export let color = "#000000"
</script>
<!-- Created with Inkscape (http://www.inkscape.org/) --> <svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown on:focus width="120" height="120" viewBox="0 0 120 120" version="1.1" id="svg1" xml:space="preserve" inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" sodipodi:docname="unsnap.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#999999" borderopacity="1" inkscape:showpageshadow="2" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" showguides="true" inkscape:zoom="4.5168066" inkscape:cx="51.695815" inkscape:cy="69.186048" inkscape:window-width="1920" inkscape:window-height="995" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="layer1"><sodipodi:guide position="315.49944,61.936443" orientation="0,-1" id="guide2" inkscape:locked="false" /></sodipodi:namedview><defs id="defs1" /><g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-5,-5)"><path id="path1-2" style="fill:{color};fill-opacity:1;stroke-width:3.93092" d="m 91.670867,40.074491 c 0.948679,0.909931 1.380066,2.234124 1.148907,3.527969 l -3.447995,17.260743 c -0.381611,2.137177 -2.526217,4.001294 -4.081205,2.486091 L 79.257979,57.469775 64.262133,72.31877 c -1.383257,1.369713 -3.807955,0.909932 -4.298111,0.288099 -1.707154,-2.165786 -0.138139,-3.968458 0.177549,-4.304093 L 74.600311,52.930303 68.567707,47.05079 c -1.554665,-1.51554 0.253754,-3.707343 2.380393,-4.143751 l 17.16644,-3.89041 c 1.287478,-0.264342 2.622313,0.132882 3.556328,1.057866 z" sodipodi:nodetypes="cccccsssccccc" /><g id="g8" transform="matrix(-1,0,0,1,132.686,0)" /><g id="g10" transform="matrix(0.99755231,-0.06992414,-0.06992414,-0.99755231,14.642674,124.44485)"><path style="color:{color};fill:{color};stroke-linecap:round;-inkscape-stroke:none" d="M 10,90 45,10" id="path3" /><path id="path4" style="color:{color};fill:#808080;stroke-linecap:round;-inkscape-stroke:none;stroke:none;stroke-opacity:1;fill-opacity:1" d="M 45.097656,5.0019531 A 5,5 0 0 0 43.177734,5.34375 5,5 0 0 0 40.419922,7.9960938 L 35.865234,18.40625 c 3.405007,0.669609 6.469474,2.331825 8.867188,4.679688 L 49.580078,12.003906 A 5,5 0 0 0 47.003906,5.4199219 5,5 0 0 0 45.097656,5.0019531 Z M 22.177734,49.691406 5.4199219,87.996094 a 5,5 0 0 0 2.5761719,6.583984 5,5 0 0 0 6.5839842,-2.576172 L 31.621094,53.052734 c -3.513941,-0.175553 -6.76611,-1.396873 -9.44336,-3.361328 z" /><path style="fill:#808080;fill-opacity:1;stroke:{color};stroke-width:0;stroke-linecap:round;stroke-opacity:1" id="path9" sodipodi:type="arc" sodipodi:cx="32.616085" sodipodi:cy="35.55938" sodipodi:rx="12.741771" sodipodi:ry="12.741771" sodipodi:start="0" sodipodi:end="6.26046" sodipodi:open="true" sodipodi:arc-type="arc" d="M 45.357856,35.55938 A 12.741771,12.741771 0 0 1 32.688475,48.300945 12.741771,12.741771 0 0 1 19.875137,35.704157 12.741771,12.741771 0 0 1 32.398925,22.81946 12.741771,12.741771 0 0 1 45.354566,35.269844" /></g><path style="fill:{color};fill-opacity:1;stroke:{color};stroke-width:0;stroke-linecap:round;stroke-opacity:1" id="path10" sodipodi:type="arc" sodipodi:cx="-106.61823" sodipodi:cy="26.136267" sodipodi:rx="12.741771" sodipodi:ry="12.741771" sodipodi:start="0" sodipodi:end="6.26046" sodipodi:open="true" sodipodi:arc-type="arc" d="m -93.876462,26.136267 a 12.741771,12.741771 0 0 1 -12.669378,12.741565 12.741771,12.741771 0 0 1 -12.81334,-12.596788 12.741771,12.741771 0 0 1 12.52379,-12.884697 12.741771,12.741771 0 0 1 12.955638,12.450384" transform="scale(-1,1)" /></g></svg>