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) const storage = TileLocalStorage.construct<Feature[]>(backend, layername, maxCacheAge)
this.storage = storage this.storage = storage
const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>() const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>()
features.features.addCallbackAndRunD((features) => { features.features.stabilized(5000).addCallbackAndRunD((features) => {
if ( if (
features.some((f) => { features.some((f) => {
let totalPoints = 0 let totalPoints = 0
@ -116,7 +116,7 @@ export default class SaveFeatureSourceToLocalStorage {
tileSaver = new SingleTileSaver(src, featureProperties) tileSaver = new SingleTileSaver(src, featureProperties)
singleTileSavers.set(tileIndex, tileSaver) 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]+/)) features = features.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/))
tileSaver.saveFeatures(features) tileSaver.saveFeatures(features)
}) })

View file

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

View file

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

View file

@ -42,7 +42,6 @@ export type GeocodeResult = {
payload?: object payload?: object
source?: string source?: string
} }
export type SearchResult = GeocodeResult
export interface GeocodingOptions { export interface GeocodingOptions {
bbox?: BBox 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 ThemeViewState from "../../Models/ThemeViewState"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Feature } from "geojson" import { Feature } from "geojson"
@ -26,7 +26,7 @@ export default class LocalElementSearch implements GeocodingProvider {
this._limit = limit 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 return this.searchEntries(query, options, false).data
} }
@ -92,7 +92,7 @@ export default class LocalElementSearch implements GeocodingProvider {
query: string, query: string,
options?: GeocodingOptions, options?: GeocodingOptions,
matchStart?: boolean matchStart?: boolean
): Store<SearchResult[]> { ): Store<GeocodeResult[]> {
if (query.length < 3) { if (query.length < 3) {
return new ImmutableStore([]) return new ImmutableStore([])
} }
@ -126,7 +126,7 @@ export default class LocalElementSearch implements GeocodingProvider {
} }
return results.map((entry) => { return results.map((entry) => {
const [osm_type, osm_id] = entry.feature.properties.id.split("/") const [osm_type, osm_id] = entry.feature.properties.id.split("/")
return <SearchResult>{ return <GeocodeResult>{
lon: entry.center[0], lon: entry.center[0],
lat: entry.center[1], lat: entry.center[1],
osm_type, 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) return this.searchEntries(query, options, true)
} }
} }

View file

@ -3,7 +3,7 @@ import { BBox } from "../BBox"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import { FeatureCollection } from "geojson" import { FeatureCollection } from "geojson"
import Locale from "../../UI/i18n/Locale" import Locale from "../../UI/i18n/Locale"
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider" import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
export class NominatimGeocoding implements GeocodingProvider { export class NominatimGeocoding implements GeocodingProvider {
private readonly _host private readonly _host
@ -15,7 +15,7 @@ export class NominatimGeocoding implements GeocodingProvider {
this._host = host 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 b = options?.bbox ?? BBox.global
const url = `${this._host}search?format=json&limit=${ const url = `${this._host}search?format=json&limit=${
this.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 { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
import CombinedSearcher from "../Search/CombinedSearcher" import CombinedSearcher from "../Search/CombinedSearcher"
import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch" import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch"
@ -16,12 +16,13 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { FeatureSource } from "../FeatureSource/FeatureSource" import { FeatureSource } from "../FeatureSource/FeatureSource"
import { Feature } from "geojson" import { Feature } from "geojson"
import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch" import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch"
import { BBox } from "../BBox"
export default class SearchState { export default class SearchState {
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("") public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
public readonly searchIsFocused = new UIEventSource(false) public readonly searchIsFocused = new UIEventSource(false)
public readonly suggestions: Store<SearchResult[]> public readonly suggestions: Store<GeocodeResult[]>
public readonly filterSuggestions: Store<FilterSearchResult[]> public readonly filterSuggestions: Store<FilterSearchResult[]>
public readonly themeSuggestions: Store<MinimalThemeInformation[]> public readonly themeSuggestions: Store<MinimalThemeInformation[]>
public readonly layerSuggestions: Store<LayerConfig[]> public readonly layerSuggestions: Store<LayerConfig[]>
@ -60,7 +61,7 @@ export default class SearchState {
return new ImmutableStore(true) return new ImmutableStore(true)
} }
return Stores.concat(suggestions).map((suggestions) => return Stores.concat(suggestions).map((suggestions) =>
suggestions.some((list, i) => list === undefined) suggestions.some(list => list === undefined)
) )
}) })
this.suggestions = suggestionsList.bindD((suggestions) => this.suggestions = suggestionsList.bindD((suggestions) =>
@ -100,7 +101,7 @@ export default class SearchState {
this.showSearchDrawer = new UIEventSource(false) this.showSearchDrawer = new UIEventSource(false)
this.searchIsFocused.addCallbackAndRunD((sugg) => { this.searchIsFocused.addCallbackAndRunD(sugg => {
if (sugg) { if (sugg) {
this.showSearchDrawer.set(true) this.showSearchDrawer.set(true)
} }
@ -124,7 +125,6 @@ export default class SearchState {
const state = this.state const state = this.state
const layersToShow = payload.map((fsr) => fsr.layer.id) const layersToShow = payload.map((fsr) => fsr.layer.id)
console.log("Layers to show are", layersToShow)
for (const otherLayer of state.layerState.filteredLayers.values()) { for (const otherLayer of state.layerState.filteredLayers.values()) {
const layer = otherLayer.layerDef const layer = otherLayer.layerDef
if (!layer.isNormal()) { if (!layer.isNormal()) {
@ -167,4 +167,45 @@ export default class SearchState {
this.state.featureProperties.trackFeature(f) this.state.featureProperties.trackFeature(f)
this.state.selectedElement.set(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 { Utils } from "../Utils"
import Zoomcontrol from "../UI/Zoomcontrol" import Zoomcontrol from "../UI/Zoomcontrol"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
export type PageType = (typeof MenuState.pageNames)[number] export type PageType = (typeof MenuState.pageNames)[number]
@ -27,7 +28,7 @@ export class MenuState {
"favourites", "favourites",
"usersettings", "usersettings",
"share", "share",
"menu", "menu"
] as const ] as const
/** /**
@ -38,6 +39,9 @@ export class MenuState {
undefined undefined
) )
public static readonly nearbyImagesFeature: UIEventSource<object> = new UIEventSource<object>(
undefined
)
public readonly pageStates: Record<PageType, UIEventSource<boolean>> public readonly pageStates: Record<PageType, UIEventSource<boolean>>
public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>( public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>(
@ -45,6 +49,7 @@ export class MenuState {
) )
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined) public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
private readonly _selectedElement: UIEventSource<any> | undefined private readonly _selectedElement: UIEventSource<any> | undefined
private isClosingAll = false
constructor(selectedElement: UIEventSource<any> | undefined) { constructor(selectedElement: UIEventSource<any> | undefined) {
this._selectedElement = selectedElement this._selectedElement = selectedElement
@ -129,29 +134,49 @@ export class MenuState {
* Returns 'true' if at least one menu was opened * Returns 'true' if at least one menu was opened
*/ */
public closeAll(): boolean { public closeAll(): boolean {
console.log("Closing all") if (this.isClosingAll) {
return true
}
this.isClosingAll = true
const ps = this.pageStates const ps = this.pageStates
if (ps.menu.data) { try {
ps.menu.set(false)
return true
}
if (MenuState.previewedImage.data !== undefined) { if (ps.menu.data) {
MenuState.previewedImage.setData(undefined) ps.menu.set(false)
return true
}
for (const key in ps) {
const toggle = ps[key]
const wasOpen = toggle.data
toggle.setData(false)
if (wasOpen) {
return true return true
} }
}
if (this._selectedElement.data) { if (MenuState.previewedImage.data !== undefined) {
this._selectedElement.setData(undefined) MenuState.previewedImage.setData(undefined)
return true 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( availableLayersBboxes.map(
(eliPolygons) => { (eliPolygons) => {
const loc = location.data 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) => { const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
if (eliPolygon.geometry === null) { if (eliPolygon.geometry === null) {
return true // global ELI-layer return true // global ELI-layer

View file

@ -14,6 +14,8 @@ export default class Hotkeys {
}[] }[]
> = new UIEventSource([]) > = new UIEventSource([])
private static readonly seenKeys: Set<string> = new Set()
/** /**
* Register a hotkey * Register a hotkey
* @param key * @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.data.push({ key, documentation, alsoTriggeredBy })
this._docs.ping() this._docs.ping()
if (Utils.runningFromConsole) { if (Utils.runningFromConsole) {

View file

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

View file

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

View file

@ -89,17 +89,6 @@
imgClass="max-h-64 w-auto sm:h-32 md:h-64" imgClass="max-h-64 w-auto sm:h-32 md:h-64"
attributionFormat="minimal" 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> </AttributedImage>
<LoginToggle {state} silentFail={true}> <LoginToggle {state} silentFail={true}>
{#if linkable} {#if linkable}

View file

@ -1,22 +1,19 @@
<script lang="ts"> <script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import type { OsmTags } from "../../Models/OsmFeature" import type { OsmTags } from "../../Models/OsmFeature"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { Feature } from "geojson" import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import NearbyImages from "./NearbyImages.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 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 tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState export let state: ThemeViewState
export let lon: number export let lon: number
export let lat: number export let lat: number
export let feature: Feature export let feature: Feature
@ -27,6 +24,16 @@
let enableLogin = state.featureSwitches.featureSwitchEnableLogin let enableLogin = state.featureSwitches.featureSwitchEnableLogin
export let shown = new UIEventSource(false) 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> </script>
{#if enableLogin.data} {#if enableLogin.data}
@ -37,10 +44,9 @@
> >
<Tr t={t.seeNearby} /> <Tr t={t.seeNearby} />
</button> </button>
<Popup {shown} bodyPadding="p-4"> <Popup {shown} bodyPadding="p-4" dismissable={false}>
<span slot="header"> <Tr slot="header" t={t.seeNearby} />
<Tr t={t.seeNearby} /> <CloseButton slot="closebutton" on:click={() => shown?.set(false)} />
</span>
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer} /> <NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer} />
</Popup> </Popup>
{/if} {/if}

View file

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

View file

@ -5,17 +5,14 @@
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" 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 Icon from "../Map/Icon.svelte"
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp" import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp"
import DefaultIcon from "../Map/DefaultIcon.svelte" import DefaultIcon from "../Map/DefaultIcon.svelte"
import { WithSearchState } from "../../Models/ThemeViewState/WithSearchState"
export let entry: GeocodeResult export let entry: GeocodeResult
export let state: SpecialVisualizationState export let state: WithSearchState
let layer: LayerConfig let layer: LayerConfig
let tags: UIEventSource<Record<string, string>> let tags: UIEventSource<Record<string, string>>
@ -36,34 +33,15 @@
let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat])) let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat]))
function select() { function select() {
if (entry.boundingbox) { state.searchState.applyGeocodeResult(entry)
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()
} }
</script> </script>
<button class="unstyled link-no-underline searchresult w-full" on:click={() => select()}> <button class="unstyled link-no-underline searchresult w-full" on:click={() => select()}>
<div class="flex w-full items-center gap-y-2 p-2"> <div class="flex w-full items-center gap-y-2 p-2">
{#if layer} {#if layer}
<div class="h-6"> <div class="h-6 w-6">
<DefaultIcon {layer} properties={entry.feature.properties} clss="w-6 h-6" /> <DefaultIcon {layer} properties={entry.feature.properties} />
</div> </div>
{:else if entry.category} {:else if entry.category}
<Icon <Icon

View file

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