UX: add possibility to select map features by only using the keyboard, see #1181
This commit is contained in:
parent
7469a0d607
commit
48171d30f5
9 changed files with 304 additions and 104 deletions
|
@ -404,6 +404,7 @@
|
||||||
"key": "Key combination",
|
"key": "Key combination",
|
||||||
"openLayersPanel": "Opens the layers and filters panel",
|
"openLayersPanel": "Opens the layers and filters panel",
|
||||||
"selectAerial": "Set the background to aerial or satellite imagery. Toggles between the two best, available layers",
|
"selectAerial": "Set the background to aerial or satellite imagery. Toggles between the two best, available layers",
|
||||||
|
"selectItem": "Select the POI which is closest to the map center (crosshair). Only when in keyboard navigation is used",
|
||||||
"selectMap": "Set the background to a map from external sources. Toggles between the two best, available layers",
|
"selectMap": "Set the background to a map from external sources. Toggles between the two best, available layers",
|
||||||
"selectMapnik": "Set the background layer to OpenStreetMap-carto",
|
"selectMapnik": "Set the background layer to OpenStreetMap-carto",
|
||||||
"selectOsmbasedmap": "Set the background layer to on OpenStreetMap-based map (or disable the background raster layer)",
|
"selectOsmbasedmap": "Set the background layer to on OpenStreetMap-based map (or disable the background raster layer)",
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { FeatureSource } from "../FeatureSource"
|
||||||
|
import { Store, Stores, UIEventSource } from "../../UIEventSource"
|
||||||
|
import { Feature } from "geojson"
|
||||||
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
|
import FilteringFeatureSource from "./FilteringFeatureSource"
|
||||||
|
import LayerState from "../../State/LayerState"
|
||||||
|
|
||||||
|
export default class NearbyFeatureSource implements FeatureSource {
|
||||||
|
public readonly features: Store<Feature[]>
|
||||||
|
private readonly _targetPoint: Store<{ lon: number; lat: number }>
|
||||||
|
private readonly _numberOfNeededFeatures: number
|
||||||
|
private readonly _currentZoom: Store<number>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
targetPoint: Store<{ lon: number; lat: number }>,
|
||||||
|
sources: ReadonlyMap<string, FilteringFeatureSource>,
|
||||||
|
numberOfNeededFeatures?: number,
|
||||||
|
layerState?: LayerState,
|
||||||
|
currentZoom?: Store<number>
|
||||||
|
) {
|
||||||
|
this._targetPoint = targetPoint.stabilized(100)
|
||||||
|
this._numberOfNeededFeatures = numberOfNeededFeatures
|
||||||
|
this._currentZoom = currentZoom.stabilized(500)
|
||||||
|
|
||||||
|
const allSources: Store<{ feat: Feature; d: number }[]>[] = []
|
||||||
|
let minzoom = 999
|
||||||
|
|
||||||
|
const result = new UIEventSource<Feature[]>(undefined)
|
||||||
|
this.features = Stores.ListStabilized(result)
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
let features: { feat: Feature; d: number }[] = []
|
||||||
|
for (const src of allSources) {
|
||||||
|
features.push(...src.data)
|
||||||
|
}
|
||||||
|
features.sort((a, b) => a.d - b.d)
|
||||||
|
if (numberOfNeededFeatures !== undefined) {
|
||||||
|
features = features.slice(0, numberOfNeededFeatures)
|
||||||
|
}
|
||||||
|
result.setData(features.map((f) => f.feat))
|
||||||
|
}
|
||||||
|
|
||||||
|
sources.forEach((source, layer) => {
|
||||||
|
const flayer = layerState?.filteredLayers.get(layer)
|
||||||
|
minzoom = Math.min(minzoom, flayer.layerDef.minzoom)
|
||||||
|
const calcSource = this.createSource(
|
||||||
|
source.features,
|
||||||
|
flayer.layerDef.minzoom,
|
||||||
|
flayer.isDisplayed
|
||||||
|
)
|
||||||
|
calcSource.addCallbackAndRunD((features) => {
|
||||||
|
update()
|
||||||
|
})
|
||||||
|
allSources.push(calcSource)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts the given source by distance, slices down to the required number
|
||||||
|
*/
|
||||||
|
private createSource(
|
||||||
|
source: Store<Feature[]>,
|
||||||
|
minZoom: number,
|
||||||
|
isActive?: Store<boolean>
|
||||||
|
): Store<{ feat: Feature; d: number }[]> {
|
||||||
|
const empty = []
|
||||||
|
return source.stabilized(100).map(
|
||||||
|
(feats) => {
|
||||||
|
if (isActive && !isActive.data) {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._currentZoom.data < minZoom) {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
const point = this._targetPoint.data
|
||||||
|
const lonLat = <[number, number]>[point.lon, point.lat]
|
||||||
|
const withDistance = feats.map((feat) => ({
|
||||||
|
d: GeoOperations.distanceBetween(
|
||||||
|
lonLat,
|
||||||
|
GeoOperations.centerpointCoordinates(feat)
|
||||||
|
),
|
||||||
|
feat,
|
||||||
|
}))
|
||||||
|
withDistance.sort((a, b) => a.d - b.d)
|
||||||
|
if (this._numberOfNeededFeatures !== undefined) {
|
||||||
|
return withDistance.slice(0, this._numberOfNeededFeatures)
|
||||||
|
}
|
||||||
|
return withDistance
|
||||||
|
},
|
||||||
|
[this._targetPoint, isActive, this._currentZoom]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import { GlobalFilter } from "./GlobalFilter"
|
||||||
|
|
||||||
export default class FilteredLayer {
|
export default class FilteredLayer {
|
||||||
/**
|
/**
|
||||||
* Wether or not the specified layer is shown
|
* Whether or not the specified layer is enabled by the user
|
||||||
*/
|
*/
|
||||||
readonly isDisplayed: UIEventSource<boolean>
|
readonly isDisplayed: UIEventSource<boolean>
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,6 +14,7 @@ export interface MapProperties {
|
||||||
readonly allowRotating: UIEventSource<true | boolean>
|
readonly allowRotating: UIEventSource<true | boolean>
|
||||||
readonly lastClickLocation: Store<{ lon: number; lat: number }>
|
readonly lastClickLocation: Store<{ lon: number; lat: number }>
|
||||||
readonly allowZooming: UIEventSource<true | boolean>
|
readonly allowZooming: UIEventSource<true | boolean>
|
||||||
|
readonly lastKeyNavigation: UIEventSource<number>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportableMap {
|
export interface ExportableMap {
|
||||||
|
|
|
@ -57,6 +57,7 @@ import FilteredLayer from "./FilteredLayer"
|
||||||
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
|
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
|
||||||
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
||||||
import { Imgur } from "../Logic/ImageProviders/Imgur"
|
import { Imgur } from "../Logic/ImageProviders/Imgur"
|
||||||
|
import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -95,6 +96,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
|
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
|
||||||
readonly currentView: FeatureSource<Feature<Polygon>>
|
readonly currentView: FeatureSource<Feature<Polygon>>
|
||||||
readonly featuresInView: FeatureSource
|
readonly featuresInView: FeatureSource
|
||||||
|
/**
|
||||||
|
* Contains a few (<10) >features that are near the center of the map.
|
||||||
|
*/
|
||||||
|
readonly closestFeatures: FeatureSource
|
||||||
readonly newFeatures: WritableFeatureSource
|
readonly newFeatures: WritableFeatureSource
|
||||||
readonly layerState: LayerState
|
readonly layerState: LayerState
|
||||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||||
|
@ -131,6 +136,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.map = new UIEventSource<MlMap>(undefined)
|
this.map = new UIEventSource<MlMap>(undefined)
|
||||||
const initial = new InitialMapPositioning(layout)
|
const initial = new InitialMapPositioning(layout)
|
||||||
this.mapProperties = new MapLibreAdaptor(this.map, initial)
|
this.mapProperties = new MapLibreAdaptor(this.map, initial)
|
||||||
|
|
||||||
const geolocationState = new GeoLocationState()
|
const geolocationState = new GeoLocationState()
|
||||||
|
|
||||||
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
|
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
|
||||||
|
@ -234,6 +240,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
|
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
|
||||||
|
|
||||||
this.dataIsLoading = layoutSource.isLoading
|
this.dataIsLoading = layoutSource.isLoading
|
||||||
|
|
||||||
const indexedElements = this.indexedFeatures
|
const indexedElements = this.indexedFeatures
|
||||||
|
@ -331,7 +338,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
)
|
)
|
||||||
|
|
||||||
this.perLayerFiltered = this.showNormalDataOn(this.map)
|
this.perLayerFiltered = this.showNormalDataOn(this.map)
|
||||||
|
this.closestFeatures = new NearbyFeatureSource(
|
||||||
|
this.mapProperties.location,
|
||||||
|
this.perLayerFiltered,
|
||||||
|
3,
|
||||||
|
this.layerState,
|
||||||
|
this.mapProperties.zoom
|
||||||
|
)
|
||||||
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
|
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
|
||||||
this.imageUploadManager = new ImageUploadManager(
|
this.imageUploadManager = new ImageUploadManager(
|
||||||
layout,
|
layout,
|
||||||
|
@ -364,6 +377,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> {
|
public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> {
|
||||||
const filteringFeatureSource = new Map<string, FilteringFeatureSource>()
|
const filteringFeatureSource = new Map<string, FilteringFeatureSource>()
|
||||||
this.perLayer.forEach((fs, layerName) => {
|
this.perLayer.forEach((fs, layerName) => {
|
||||||
|
@ -404,6 +418,17 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
return filteringFeatureSource
|
return filteringFeatureSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public openNewDialog() {
|
||||||
|
this.selectedLayer.setData(undefined)
|
||||||
|
this.selectedElement.setData(undefined)
|
||||||
|
|
||||||
|
const { lon, lat } = this.mapProperties.location.data
|
||||||
|
const feature = this.lastClickObject.createFeature(lon, lat)
|
||||||
|
this.featureProperties.trackFeature(feature)
|
||||||
|
this.selectedElement.setData(feature)
|
||||||
|
this.selectedLayer.setData(this.newPointDialog.layerDef)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Various small methods that need to be called
|
* Various small methods that need to be called
|
||||||
*/
|
*/
|
||||||
|
@ -425,6 +450,21 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the feature that is 'i' closest to the map center
|
||||||
|
* @param i
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private selectClosestAtCenter(i: number = 0) {
|
||||||
|
const toSelect = this.closestFeatures.features.data[i]
|
||||||
|
if (!toSelect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const layer = this.layout.getMatchingLayer(toSelect.properties)
|
||||||
|
this.selectedElement.setData(undefined)
|
||||||
|
this.selectedLayer.setData(layer)
|
||||||
|
this.selectedElement.setData(toSelect)
|
||||||
|
}
|
||||||
private initHotkeys() {
|
private initHotkeys() {
|
||||||
Hotkeys.RegisterHotkey(
|
Hotkeys.RegisterHotkey(
|
||||||
{ nomod: "Escape", onUp: true },
|
{ nomod: "Escape", onUp: true },
|
||||||
|
@ -436,6 +476,36 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.mapProperties.lastKeyNavigation.addCallbackAndRunD((_) => {
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{
|
||||||
|
nomod: " ",
|
||||||
|
onUp: true,
|
||||||
|
},
|
||||||
|
Translations.t.hotkeyDocumentation.selectItem,
|
||||||
|
() => this.selectClosestAtCenter(0)
|
||||||
|
)
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{
|
||||||
|
nomod: "Spacebar",
|
||||||
|
onUp: true,
|
||||||
|
},
|
||||||
|
Translations.t.hotkeyDocumentation.selectItem,
|
||||||
|
() => this.selectClosestAtCenter(0)
|
||||||
|
)
|
||||||
|
for (let i = 1; i < 9; i++) {
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{
|
||||||
|
nomod: "" + i,
|
||||||
|
onUp: true,
|
||||||
|
},
|
||||||
|
Translations.t.hotkeyDocumentation.selectItem,
|
||||||
|
() => this.selectClosestAtCenter(i - 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true // unregister
|
||||||
|
})
|
||||||
|
|
||||||
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => {
|
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => {
|
||||||
if (!enable) {
|
if (!enable) {
|
||||||
return
|
return
|
||||||
|
@ -531,17 +601,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public openNewDialog() {
|
|
||||||
this.selectedLayer.setData(undefined)
|
|
||||||
this.selectedElement.setData(undefined)
|
|
||||||
|
|
||||||
const { lon, lat } = this.mapProperties.location.data
|
|
||||||
const feature = this.lastClickObject.createFeature(lon, lat)
|
|
||||||
this.featureProperties.trackFeature(feature)
|
|
||||||
this.selectedElement.setData(feature)
|
|
||||||
this.selectedLayer.setData(this.newPointDialog.layerDef)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the special layers to the map
|
* Add the special layers to the map
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -38,9 +38,9 @@
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<!-- Title element-->
|
<!-- Title element-->
|
||||||
<h3>
|
<h3>
|
||||||
|
|
||||||
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} />
|
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} />
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 p-1 pt-0.5 sm:pt-1"
|
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 p-1 pt-0.5 sm:pt-1"
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Feature } from "geojson";
|
||||||
|
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||||
|
import SelectedElementTitle from "./SelectedElementTitle.svelte";
|
||||||
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
|
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte";
|
||||||
|
|
||||||
|
export let state: SpecialVisualizationState;
|
||||||
|
export let feature: Feature
|
||||||
|
let id = feature.properties.id
|
||||||
|
let tags = state.featureProperties.getStore(id);
|
||||||
|
let layer: LayerConfig = state.layout.getMatchingLayer(tags.data)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<TagRenderingAnswer config={layer.title} selectedElement={feature} {state} {tags} {layer} />
|
||||||
|
|
|
@ -9,7 +9,6 @@ import SvelteUIElement from "../Base/SvelteUIElement"
|
||||||
import MaplibreMap from "./MaplibreMap.svelte"
|
import MaplibreMap from "./MaplibreMap.svelte"
|
||||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||||
import * as htmltoimage from "html-to-image"
|
import * as htmltoimage from "html-to-image"
|
||||||
import { draw } from "svelte/transition"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||||
|
@ -41,6 +40,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
readonly lastClickLocation: Store<undefined | { lon: number; lat: number }>
|
readonly lastClickLocation: Store<undefined | { lon: number; lat: number }>
|
||||||
readonly minzoom: UIEventSource<number>
|
readonly minzoom: UIEventSource<number>
|
||||||
readonly maxzoom: UIEventSource<number>
|
readonly maxzoom: UIEventSource<number>
|
||||||
|
/**
|
||||||
|
* When was the last navigation by arrow keys?
|
||||||
|
* If set, this is a hint to use arrow compatibility
|
||||||
|
* Number of _seconds_ since epoch
|
||||||
|
*/
|
||||||
|
readonly lastKeyNavigation: UIEventSource<number> = new UIEventSource<number>(undefined)
|
||||||
private readonly _maplibreMap: Store<MLMap>
|
private readonly _maplibreMap: Store<MLMap>
|
||||||
/**
|
/**
|
||||||
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
||||||
|
@ -128,6 +133,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
map.on("dblclick", (e) => {
|
map.on("dblclick", (e) => {
|
||||||
handleClick(e)
|
handleClick(e)
|
||||||
})
|
})
|
||||||
|
map.getContainer().addEventListener("keydown", (event) => {
|
||||||
|
if (
|
||||||
|
event.key === "ArrowRight" ||
|
||||||
|
event.key === "ArrowLeft" ||
|
||||||
|
event.key === "ArrowUp" ||
|
||||||
|
event.key === "ArrowDown"
|
||||||
|
) {
|
||||||
|
this.lastKeyNavigation.setData(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.rasterLayer.addCallbackAndRun((_) =>
|
this.rasterLayer.addCallbackAndRun((_) =>
|
||||||
|
|
|
@ -1,120 +1,127 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||||
import { Map as MlMap } from "maplibre-gl"
|
import { Map as MlMap } from "maplibre-gl";
|
||||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
||||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||||
import MapControlButton from "./Base/MapControlButton.svelte"
|
import MapControlButton from "./Base/MapControlButton.svelte";
|
||||||
import ToSvelte from "./Base/ToSvelte.svelte"
|
import ToSvelte from "./Base/ToSvelte.svelte";
|
||||||
import If from "./Base/If.svelte"
|
import If from "./Base/If.svelte";
|
||||||
import { GeolocationControl } from "./BigComponents/GeolocationControl"
|
import { GeolocationControl } from "./BigComponents/GeolocationControl";
|
||||||
import type { Feature } from "geojson"
|
import type { Feature } from "geojson";
|
||||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
|
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||||
import Filterview from "./BigComponents/Filterview.svelte"
|
import Filterview from "./BigComponents/Filterview.svelte";
|
||||||
import ThemeViewState from "../Models/ThemeViewState"
|
import ThemeViewState from "../Models/ThemeViewState";
|
||||||
import type { MapProperties } from "../Models/MapProperties"
|
import type { MapProperties } from "../Models/MapProperties";
|
||||||
import Geosearch from "./BigComponents/Geosearch.svelte"
|
import Geosearch from "./BigComponents/Geosearch.svelte";
|
||||||
import Translations from "./i18n/Translations"
|
import Translations from "./i18n/Translations";
|
||||||
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||||
import Tr from "./Base/Tr.svelte"
|
import Tr from "./Base/Tr.svelte";
|
||||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
|
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
|
||||||
import FloatOver from "./Base/FloatOver.svelte"
|
import FloatOver from "./Base/FloatOver.svelte";
|
||||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
|
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
|
||||||
import Constants from "../Models/Constants"
|
import Constants from "../Models/Constants";
|
||||||
import TabbedGroup from "./Base/TabbedGroup.svelte"
|
import TabbedGroup from "./Base/TabbedGroup.svelte";
|
||||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
import UserRelatedState from "../Logic/State/UserRelatedState";
|
||||||
import LoginToggle from "./Base/LoginToggle.svelte"
|
import LoginToggle from "./Base/LoginToggle.svelte";
|
||||||
import LoginButton from "./Base/LoginButton.svelte"
|
import LoginButton from "./Base/LoginButton.svelte";
|
||||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
import CopyrightPanel from "./BigComponents/CopyrightPanel";
|
||||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
|
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
|
||||||
import ModalRight from "./Base/ModalRight.svelte"
|
import ModalRight from "./Base/ModalRight.svelte";
|
||||||
import { Utils } from "../Utils"
|
import { Utils } from "../Utils";
|
||||||
import Hotkeys from "./Base/Hotkeys"
|
import Hotkeys from "./Base/Hotkeys";
|
||||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
import { VariableUiElement } from "./Base/VariableUIElement";
|
||||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
import SvelteUIElement from "./Base/SvelteUIElement";
|
||||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
|
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
|
||||||
import LevelSelector from "./BigComponents/LevelSelector.svelte"
|
import LevelSelector from "./BigComponents/LevelSelector.svelte";
|
||||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
|
||||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
|
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
|
||||||
import Svg from "../Svg"
|
import Svg from "../Svg";
|
||||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
|
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
|
||||||
import type { RasterLayerPolygon } from "../Models/RasterLayers"
|
import type { RasterLayerPolygon } from "../Models/RasterLayers";
|
||||||
import { AvailableRasterLayers } from "../Models/RasterLayers"
|
import { AvailableRasterLayers } from "../Models/RasterLayers";
|
||||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
|
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
|
||||||
import IfHidden from "./Base/IfHidden.svelte"
|
import IfHidden from "./Base/IfHidden.svelte";
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte";
|
||||||
import { OpenJosm } from "./BigComponents/OpenJosm"
|
import { OpenJosm } from "./BigComponents/OpenJosm";
|
||||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
|
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
|
||||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
|
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
|
||||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
|
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
|
||||||
import StateIndicator from "./BigComponents/StateIndicator.svelte"
|
import StateIndicator from "./BigComponents/StateIndicator.svelte";
|
||||||
import LanguagePicker from "./LanguagePicker"
|
import LanguagePicker from "./LanguagePicker";
|
||||||
import Locale from "./i18n/Locale"
|
import Locale from "./i18n/Locale";
|
||||||
import ShareScreen from "./BigComponents/ShareScreen.svelte"
|
import ShareScreen from "./BigComponents/ShareScreen.svelte";
|
||||||
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
|
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte";
|
||||||
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
|
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte";
|
||||||
|
import Cross from "../assets/svg/Cross.svelte";
|
||||||
|
import Summary from "./BigComponents/Summary.svelte";
|
||||||
|
|
||||||
export let state: ThemeViewState
|
export let state: ThemeViewState;
|
||||||
let layout = state.layout
|
let layout = state.layout;
|
||||||
|
|
||||||
let maplibremap: UIEventSource<MlMap> = state.map
|
let maplibremap: UIEventSource<MlMap> = state.map;
|
||||||
let selectedElement: UIEventSource<Feature> = state.selectedElement
|
let selectedElement: UIEventSource<Feature> = state.selectedElement;
|
||||||
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
|
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer;
|
||||||
|
|
||||||
|
let currentZoom = state.mapProperties.zoom;
|
||||||
|
let showCrosshair = state.userRelatedState.showCrosshair;
|
||||||
|
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation;
|
||||||
|
let centerFeatures = state.closestFeatures.features;
|
||||||
|
$: console.log("Centerfeatures are", $centerFeatures)
|
||||||
const selectedElementView = selectedElement.map(
|
const selectedElementView = selectedElement.map(
|
||||||
(selectedElement) => {
|
(selectedElement) => {
|
||||||
// Svelte doesn't properly reload some of the legacy UI-elements
|
// Svelte doesn't properly reload some of the legacy UI-elements
|
||||||
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
||||||
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
||||||
const layer = selectedLayer.data
|
const layer = selectedLayer.data;
|
||||||
if (selectedElement === undefined || layer === undefined) {
|
if (selectedElement === undefined || layer === undefined) {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
|
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = state.featureProperties.getStore(selectedElement.properties.id)
|
const tags = state.featureProperties.getStore(selectedElement.properties.id);
|
||||||
return new SvelteUIElement(SelectedElementView, {
|
return new SvelteUIElement(SelectedElementView, {
|
||||||
state,
|
state,
|
||||||
layer,
|
layer,
|
||||||
selectedElement,
|
selectedElement,
|
||||||
tags,
|
tags
|
||||||
}).SetClass("h-full w-full")
|
}).SetClass("h-full w-full");
|
||||||
},
|
},
|
||||||
[selectedLayer]
|
[selectedLayer]
|
||||||
)
|
);
|
||||||
|
|
||||||
const selectedElementTitle = selectedElement.map(
|
const selectedElementTitle = selectedElement.map(
|
||||||
(selectedElement) => {
|
(selectedElement) => {
|
||||||
// Svelte doesn't properly reload some of the legacy UI-elements
|
// Svelte doesn't properly reload some of the legacy UI-elements
|
||||||
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
||||||
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
||||||
const layer = selectedLayer.data
|
const layer = selectedLayer.data;
|
||||||
if (selectedElement === undefined || layer === undefined) {
|
if (selectedElement === undefined || layer === undefined) {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = state.featureProperties.getStore(selectedElement.properties.id)
|
const tags = state.featureProperties.getStore(selectedElement.properties.id);
|
||||||
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags })
|
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags });
|
||||||
},
|
},
|
||||||
[selectedLayer]
|
[selectedLayer]
|
||||||
)
|
);
|
||||||
|
|
||||||
let mapproperties: MapProperties = state.mapProperties
|
let mapproperties: MapProperties = state.mapProperties;
|
||||||
let featureSwitches: FeatureSwitchState = state.featureSwitches
|
let featureSwitches: FeatureSwitchState = state.featureSwitches;
|
||||||
let availableLayers = state.availableLayers
|
let availableLayers = state.availableLayers;
|
||||||
let userdetails = state.osmConnection.userDetails
|
let userdetails = state.osmConnection.userDetails;
|
||||||
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
|
let currentViewLayer = layout.layers.find((l) => l.id === "current_view");
|
||||||
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
|
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer;
|
||||||
let rasterLayerName =
|
let rasterLayerName =
|
||||||
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
|
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name;
|
||||||
onDestroy(
|
onDestroy(
|
||||||
rasterLayer.addCallbackAndRunD((l) => {
|
rasterLayer.addCallbackAndRunD((l) => {
|
||||||
rasterLayerName = l.properties.name
|
rasterLayerName = l.properties.name;
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
|
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
|
||||||
|
@ -216,6 +223,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if $arrowKeysWereUsed !== undefined}
|
||||||
|
<div class="pointer-events-auto interactive p-1">
|
||||||
|
{#each $centerFeatures as feat, i (feat.properties.id)}
|
||||||
|
<div class="flex">
|
||||||
|
<b>{i+1}.</b><Summary {state} feature={feat}/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="flex flex-col items-end">
|
<div class="flex flex-col items-end">
|
||||||
<!-- bottom right elements -->
|
<!-- bottom right elements -->
|
||||||
<If condition={state.floors.map((f) => f.length > 1)}>
|
<If condition={state.floors.map((f) => f.length > 1)}>
|
||||||
|
@ -247,15 +263,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LoginToggle ignoreLoading={true} {state}>
|
<LoginToggle ignoreLoading={true} {state}>
|
||||||
<If condition={state.userRelatedState.showCrosshair.map((s) => s === "yes")}>
|
{#if $showCrosshair === "yes" && ($currentZoom >= 17 || $arrowKeysWereUsed !== undefined) }
|
||||||
<If condition={state.mapProperties.zoom.map((z) => z >= 17)}>
|
<div
|
||||||
<div
|
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center"
|
||||||
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center"
|
>
|
||||||
>
|
<Cross class="h-4 w-4" />
|
||||||
<ToSvelte construct={Svg.cross_svg()} />
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</If>
|
|
||||||
</If>
|
|
||||||
</LoginToggle>
|
</LoginToggle>
|
||||||
|
|
||||||
<If
|
<If
|
||||||
|
|
Loading…
Reference in a new issue