Merge branch 'develop' into shrine_layer

This commit is contained in:
Pieter Vander Vennet 2025-03-13 16:58:21 +01:00
commit 50280bb072
18 changed files with 168 additions and 122 deletions

View file

@ -76,7 +76,7 @@ export default class SaveFeatureSourceToLocalStorage {
const storage = TileLocalStorage.construct<Feature[]>(backend, layername, maxCacheAge)
this.storage = storage
const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>()
features.features.addCallbackAndRunD((features) => {
features.features.stabilized(5000).addCallbackAndRunD((features) => {
if (
features.some((f) => {
let totalPoints = 0
@ -116,7 +116,7 @@ export default class SaveFeatureSourceToLocalStorage {
tileSaver = new SingleTileSaver(src, featureProperties)
singleTileSavers.set(tileIndex, tileSaver)
}
// Don't cache not-uploaded features yet - they'll be cached when the receive their id
// Don't cache not-uploaded features yet - they'll be cached when they receive their id
features = features.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/))
tileSaver.saveFeatures(features)
})

View file

@ -1,8 +1,4 @@
import GeocodingProvider, {
GeocodeResult,
GeocodingOptions,
SearchResult,
} from "./GeocodingProvider"
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { Store, Stores } from "../UIEventSource"
@ -44,12 +40,12 @@ export default class CombinedSearcher implements GeocodingProvider {
return results
}
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
const results = await Promise.all(this._providers.map((pr) => pr.search(query, options)))
return CombinedSearcher.merge(results)
}
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
return Stores.concat(
this._providersWithSuggest.map((pr) => pr.suggest(query, options))
).map((gcrss) => CombinedSearcher.merge(gcrss))

View file

@ -1,4 +1,4 @@
import { SearchResult } from "./GeocodingProvider"
import { GeocodeResult } from "./GeocodingProvider"
import { Store } from "../UIEventSource"
import { FeatureSource } from "../FeatureSource/FeatureSource"
import { Feature, Geometry } from "geojson"
@ -6,7 +6,7 @@ import { Feature, Geometry } from "geojson"
export default class GeocodingFeatureSource implements FeatureSource {
public features: Store<Feature<Geometry, Record<string, string>>[]>
constructor(provider: Store<SearchResult[]>) {
constructor(provider: Store<GeocodeResult[]>) {
this.features = provider.map((geocoded) => {
if (geocoded === undefined) {
return []

View file

@ -42,7 +42,6 @@ export type GeocodeResult = {
payload?: object
source?: string
}
export type SearchResult = GeocodeResult
export interface GeocodingOptions {
bbox?: BBox

View file

@ -1,4 +1,4 @@
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
import ThemeViewState from "../../Models/ThemeViewState"
import { Utils } from "../../Utils"
import { Feature } from "geojson"
@ -26,7 +26,7 @@ export default class LocalElementSearch implements GeocodingProvider {
this._limit = limit
}
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
return this.searchEntries(query, options, false).data
}
@ -92,7 +92,7 @@ export default class LocalElementSearch implements GeocodingProvider {
query: string,
options?: GeocodingOptions,
matchStart?: boolean
): Store<SearchResult[]> {
): Store<GeocodeResult[]> {
if (query.length < 3) {
return new ImmutableStore([])
}
@ -126,7 +126,7 @@ export default class LocalElementSearch implements GeocodingProvider {
}
return results.map((entry) => {
const [osm_type, osm_id] = entry.feature.properties.id.split("/")
return <SearchResult>{
return <GeocodeResult>{
lon: entry.center[0],
lat: entry.center[1],
osm_type,
@ -141,7 +141,7 @@ export default class LocalElementSearch implements GeocodingProvider {
})
}
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
return this.searchEntries(query, options, true)
}
}

View file

@ -3,7 +3,7 @@ import { BBox } from "../BBox"
import Constants from "../../Models/Constants"
import { FeatureCollection } from "geojson"
import Locale from "../../UI/i18n/Locale"
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
export class NominatimGeocoding implements GeocodingProvider {
private readonly _host
@ -15,7 +15,7 @@ export class NominatimGeocoding implements GeocodingProvider {
this._host = host
}
public search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
public search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
const b = options?.bbox ?? BBox.global
const url = `${this._host}search?format=json&limit=${
this.limit

View file

@ -1,4 +1,4 @@
import GeocodingProvider, { type SearchResult } from "../Search/GeocodingProvider"
import GeocodingProvider, { GeocodeResult, GeocodingUtils } from "../Search/GeocodingProvider"
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
import CombinedSearcher from "../Search/CombinedSearcher"
import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch"
@ -16,12 +16,13 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { FeatureSource } from "../FeatureSource/FeatureSource"
import { Feature } from "geojson"
import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch"
import { BBox } from "../BBox"
export default class SearchState {
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
public readonly searchIsFocused = new UIEventSource(false)
public readonly suggestions: Store<SearchResult[]>
public readonly suggestions: Store<GeocodeResult[]>
public readonly filterSuggestions: Store<FilterSearchResult[]>
public readonly themeSuggestions: Store<MinimalThemeInformation[]>
public readonly layerSuggestions: Store<LayerConfig[]>
@ -60,7 +61,7 @@ export default class SearchState {
return new ImmutableStore(true)
}
return Stores.concat(suggestions).map((suggestions) =>
suggestions.some((list, i) => list === undefined)
suggestions.some(list => list === undefined)
)
})
this.suggestions = suggestionsList.bindD((suggestions) =>
@ -100,7 +101,7 @@ export default class SearchState {
this.showSearchDrawer = new UIEventSource(false)
this.searchIsFocused.addCallbackAndRunD((sugg) => {
this.searchIsFocused.addCallbackAndRunD(sugg => {
if (sugg) {
this.showSearchDrawer.set(true)
}
@ -124,7 +125,6 @@ export default class SearchState {
const state = this.state
const layersToShow = payload.map((fsr) => fsr.layer.id)
console.log("Layers to show are", layersToShow)
for (const otherLayer of state.layerState.filteredLayers.values()) {
const layer = otherLayer.layerDef
if (!layer.isNormal()) {
@ -167,4 +167,45 @@ export default class SearchState {
this.state.featureProperties.trackFeature(f)
this.state.selectedElement.set(f)
}
public moveToBestMatch() {
const suggestion = this.suggestions.data?.[0]
if (suggestion) {
this.applyGeocodeResult(suggestion)
}
if (this.suggestionsSearchRunning.data) {
this.suggestionsSearchRunning.addCallback(() => {
this.applyGeocodeResult(this.suggestions.data?.[0])
return true // unregister
})
}
}
applyGeocodeResult(entry: GeocodeResult) {
if (!entry) {
console.error("ApplyGeocodeResult got undefined/null")
}
console.log("Moving to", entry.description)
const state = this.state
if (entry.boundingbox) {
const [lat0, lat1, lon0, lon1] = entry.boundingbox
state.mapProperties.bounds.set(
new BBox([
[lon0, lat0],
[lon1, lat1]
]).pad(0.01)
)
} else {
state.mapProperties.flyTo(
entry.lon,
entry.lat,
GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17
)
}
if (entry.feature?.properties?.id) {
state.selectedElement.set(entry.feature)
}
state.userRelatedState.recentlyVisitedSearch.add(entry)
this.closeIfFullscreen()
}
}

View file

@ -4,6 +4,7 @@ import UserRelatedState from "../Logic/State/UserRelatedState"
import { Utils } from "../Utils"
import Zoomcontrol from "../UI/Zoomcontrol"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
export type PageType = (typeof MenuState.pageNames)[number]
@ -27,7 +28,7 @@ export class MenuState {
"favourites",
"usersettings",
"share",
"menu",
"menu"
] as const
/**
@ -38,6 +39,9 @@ export class MenuState {
undefined
)
public static readonly nearbyImagesFeature: UIEventSource<object> = new UIEventSource<object>(
undefined
)
public readonly pageStates: Record<PageType, UIEventSource<boolean>>
public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>(
@ -45,6 +49,7 @@ export class MenuState {
)
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
private readonly _selectedElement: UIEventSource<any> | undefined
private isClosingAll = false
constructor(selectedElement: UIEventSource<any> | undefined) {
this._selectedElement = selectedElement
@ -129,29 +134,49 @@ export class MenuState {
* Returns 'true' if at least one menu was opened
*/
public closeAll(): boolean {
console.log("Closing all")
if (this.isClosingAll) {
return true
}
this.isClosingAll = true
const ps = this.pageStates
if (ps.menu.data) {
ps.menu.set(false)
return true
}
try {
if (MenuState.previewedImage.data !== undefined) {
MenuState.previewedImage.setData(undefined)
return true
}
for (const key in ps) {
const toggle = ps[key]
const wasOpen = toggle.data
toggle.setData(false)
if (wasOpen) {
if (ps.menu.data) {
ps.menu.set(false)
return true
}
}
if (this._selectedElement.data) {
this._selectedElement.setData(undefined)
return true
if (MenuState.previewedImage.data !== undefined) {
MenuState.previewedImage.setData(undefined)
return true
}
if (MenuState.nearbyImagesFeature.data !== undefined) {
MenuState.nearbyImagesFeature.setData(undefined)
return true
}
for (const key in ps) {
const toggle = ps[key]
const wasOpen = toggle.data
toggle.setData(false)
if (wasOpen) {
return true
}
}
if (this._selectedElement.data) {
this._selectedElement.setData(undefined)
return true
}
} finally {
this.isClosingAll = false
}
}
public setPreviewedImage(img?: Partial<ProvidedImage>) {
if (img === undefined && !this.isClosingAll) {
return
}
MenuState.previewedImage.setData(img)
}
}

View file

@ -116,7 +116,7 @@ export class AvailableRasterLayers {
availableLayersBboxes.map(
(eliPolygons) => {
const loc = location.data
const lonlat: [number, number] = [loc.lon, loc.lat]
const lonlat: [number, number] = [loc?.lon ?? 0, loc?.lat ?? 0]
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
if (eliPolygon.geometry === null) {
return true // global ELI-layer

View file

@ -14,6 +14,8 @@ export default class Hotkeys {
}[]
> = new UIEventSource([])
private static readonly seenKeys: Set<string> = new Set()
/**
* Register a hotkey
* @param key
@ -51,6 +53,9 @@ export default class Hotkeys {
}
}
const keyString = JSON.stringify(key)
this.seenKeys.add(keyString)
this._docs.data.push({ key, documentation, alsoTriggeredBy })
this._docs.ping()
if (Utils.runningFromConsole) {

View file

@ -56,6 +56,7 @@
<slot name="header" />
</h1>
{/if}
<slot name="closebutton" />
</svelte:fragment>
<slot />
{#if $$slots.footer}

View file

@ -11,7 +11,6 @@
import ImageOperations from "./ImageOperations.svelte"
import Popup from "../Base/Popup.svelte"
import { onDestroy } from "svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { Feature, Point } from "geojson"
import Loading from "../Base/Loading.svelte"
import Translations from "../i18n/Translations"
@ -19,8 +18,9 @@
import DotMenu from "../Base/DotMenu.svelte"
import LoadingPlaceholder from "../Base/LoadingPlaceholder.svelte"
import { MenuState } from "../../Models/MenuState"
import ThemeViewState from "../../Models/ThemeViewState"
export let image: Partial<ProvidedImage>
export let image: Partial<ProvidedImage> & { id: string; url: string }
let fallbackImage: string = undefined
if (image.provider === Mapillary.singleton) {
fallbackImage = "./assets/svg/blocked.svg"
@ -28,7 +28,7 @@
let imgEl: HTMLImageElement
export let imgClass: string = undefined
export let state: SpecialVisualizationState = undefined
export let state: ThemeViewState = undefined
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
let previewedImage: UIEventSource<Partial<ProvidedImage>> = MenuState.previewedImage
export let canZoom = previewedImage !== undefined
@ -36,9 +36,7 @@
let showBigPreview = new UIEventSource(false)
onDestroy(
showBigPreview.addCallbackAndRun((shown) => {
if (!shown) {
previewedImage?.set(undefined)
}
state.guistate.setPreviewedImage(shown ? image : undefined)
})
)
if (previewedImage) {

View file

@ -89,17 +89,6 @@
imgClass="max-h-64 w-auto sm:h-32 md:h-64"
attributionFormat="minimal"
>
<!--
<div slot="preview-action" class="self-center" >
<LoginToggle {state} silentFail={true}>
{#if linkable}
<label class="normal-background p-2 rounded-full pointer-events-auto">
<input bind:checked={$isLinked} type="checkbox" />
<SpecialTranslation t={t.link} {tags} {state} {layer} {feature} />
</label>
{/if}
</LoginToggle>
</div>-->
</AttributedImage>
<LoginToggle {state} silentFail={true}>
{#if linkable}

View file

@ -1,22 +1,19 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import type { OsmTags } from "../../Models/OsmFeature"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import NearbyImages from "./NearbyImages.svelte"
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
import Camera_plus from "../../assets/svg/Camera_plus.svelte"
import LoginToggle from "../Base/LoginToggle.svelte"
import { ariaLabel } from "../../Utils/ariaLabel"
import { Accordion, AccordionItem, Modal } from "flowbite-svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import Popup from "../Base/Popup.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import { onDestroy } from "svelte"
import { MenuState } from "../../Models/MenuState"
import { CloseButton } from "flowbite-svelte"
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
export let state: ThemeViewState
export let lon: number
export let lat: number
export let feature: Feature
@ -27,6 +24,16 @@
let enableLogin = state.featureSwitches.featureSwitchEnableLogin
export let shown = new UIEventSource(false)
onDestroy(MenuState.nearbyImagesFeature.addCallback(something => {
if (something !== feature) {
shown.set(false)
}
}))
onDestroy(shown.addCallbackAndRun(isShown => {
if (isShown) {
MenuState.nearbyImagesFeature.set(feature)
}
}))
</script>
{#if enableLogin.data}
@ -37,10 +44,9 @@
>
<Tr t={t.seeNearby} />
</button>
<Popup {shown} bodyPadding="p-4">
<span slot="header">
<Tr t={t.seeNearby} />
</span>
<Popup {shown} bodyPadding="p-4" dismissable={false}>
<Tr slot="header" t={t.seeNearby} />
<CloseButton slot="closebutton" on:click={() => shown?.set(false)} />
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer} />
</Popup>
{/if}

View file

@ -24,13 +24,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
"dragRotate",
"dragPan",
"keyboard",
"touchZoomRotate",
"touchZoomRotate"
]
private static maplibre_zoom_handlers = [
"scrollZoom",
"boxZoom",
"doubleClickZoom",
"touchZoomRotate",
"touchZoomRotate"
]
readonly location: UIEventSource<{ lon: number; lat: number }>
private readonly isFlying = new UIEventSource(false)
@ -44,14 +44,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
readonly lastClickLocation: Store<
| undefined
| {
lon: number
lat: number
mode: "left" | "right" | "middle"
/**
* The nearest feature from a MapComplete layer
*/
nearestFeature?: Feature
}
lon: number
lat: number
mode: "left" | "right" | "middle"
/**
* The nearest feature from a MapComplete layer
*/
nearestFeature?: Feature
}
>
readonly minzoom: UIEventSource<number>
readonly maxzoom: UIEventSource<number>
@ -141,7 +141,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
const features = map
.queryRenderedFeatures([
[point.x - buffer, point.y - buffer],
[point.x + buffer, point.y + buffer],
[point.x + buffer, point.y + buffer]
])
.filter((f) => f.source.startsWith("mapcomplete_"))
if (features.length === 1) {
@ -281,9 +281,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
return {
map: mlmap,
ui: new SvelteUIElement(MaplibreMap, {
map: mlmap,
map: mlmap
}),
mapproperties: new MapLibreAdaptor(mlmap),
mapproperties: new MapLibreAdaptor(mlmap)
}
}
@ -347,7 +347,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
) {
const event = {
date: new Date(),
key: key,
key: key
}
for (let i = 0; i < this._onKeyNavigation.length; i++) {
@ -536,7 +536,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
const bounds = map.getBounds()
const bbox = new BBox([
[bounds.getEast(), bounds.getNorth()],
[bounds.getWest(), bounds.getSouth()],
[bounds.getWest(), bounds.getSouth()]
])
if (this.bounds.data === undefined || !isSetup) {
this.bounds.setData(bbox)
@ -611,8 +611,11 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
if (!map) {
return
}
console.log("Bounds are", bbox?.asGeometry())
if (bbox) {
map?.setMaxBounds(bbox.toLngLat())
if (GeoOperations.surfaceAreaInSqMeters(bbox.asGeojsonCached()) > 1) {
map?.setMaxBounds(bbox.toLngLat())
}
} else {
map?.setMaxBounds(null)
}
@ -730,14 +733,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
type: "raster-dem",
url:
"https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" +
Constants.maptilerApiKey,
Constants.maptilerApiKey
})
try {
while (!map?.isStyleLoaded()) {
await Utils.waitFor(250)
}
map.setTerrain({
source: id,
source: id
})
} catch (e) {
console.error(e)
@ -762,7 +765,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
if (this.scaleControl === undefined) {
this.scaleControl = new ScaleControl({
maxWidth: 100,
unit: "metric",
unit: "metric"
})
}
if (!map.hasControl(this.scaleControl)) {
@ -775,7 +778,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
window.requestAnimationFrame(() => {
this._maplibreMap.data?.flyTo({
zoom,
center: [lon, lat],
center: [lon, lat]
})
})
}

View file

@ -590,7 +590,11 @@ export default class ShowDataLayer {
}
const bbox = BBox.bboxAroundAll(features.map(BBox.get))
window.requestAnimationFrame(() => {
map.resize()
try {
map.resize()
} catch (e) {
console.error("Could not resize the map in preparation of zoomToCurrentFeatures; the error is:", e)
}
map.fitBounds(bbox.toLngLat(), {
padding: { top: 10, bottom: 10, left: 10, right: 10 },
animate: false,

View file

@ -5,17 +5,14 @@
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { UIEventSource } from "../../Logic/UIEventSource"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { createEventDispatcher } from "svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { BBox } from "../../Logic/BBox"
import ToSvelte from "../Base/ToSvelte.svelte"
import Icon from "../Map/Icon.svelte"
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp"
import DefaultIcon from "../Map/DefaultIcon.svelte"
import { WithSearchState } from "../../Models/ThemeViewState/WithSearchState"
export let entry: GeocodeResult
export let state: SpecialVisualizationState
export let state: WithSearchState
let layer: LayerConfig
let tags: UIEventSource<Record<string, string>>
@ -36,34 +33,15 @@
let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat]))
function select() {
if (entry.boundingbox) {
const [lat0, lat1, lon0, lon1] = entry.boundingbox
state.mapProperties.bounds.set(
new BBox([
[lon0, lat0],
[lon1, lat1],
]).pad(0.01)
)
} else {
state.mapProperties.flyTo(
entry.lon,
entry.lat,
GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17
)
}
if (entry.feature?.properties?.id) {
state.selectedElement.set(entry.feature)
}
state.userRelatedState.recentlyVisitedSearch.add(entry)
state.searchState.closeIfFullscreen()
state.searchState.applyGeocodeResult(entry)
}
</script>
<button class="unstyled link-no-underline searchresult w-full" on:click={() => select()}>
<div class="flex w-full items-center gap-y-2 p-2">
{#if layer}
<div class="h-6">
<DefaultIcon {layer} properties={entry.feature.properties} clss="w-6 h-6" />
<div class="h-6 w-6">
<DefaultIcon {layer} properties={entry.feature.properties} />
</div>
{:else if entry.category}
<Icon

View file

@ -367,6 +367,7 @@
<div class="flex flex-grow items-center justify-end">
<div class="w-full sm:w-64">
<Searchbar
on:search={() => state.searchState.moveToBestMatch()}
value={state.searchState.searchTerm}
isFocused={state.searchState.searchIsFocused}
/>