forked from MapComplete/MapComplete
Merge branch 'develop' into shrine_layer
This commit is contained in:
commit
50280bb072
18 changed files with 168 additions and 122 deletions
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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 []
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue