Refactoring: new background selector

This commit is contained in:
Pieter Vander Vennet 2023-05-18 15:44:54 +02:00
parent 5427a4cb05
commit 82093ffdf4
25 changed files with 658 additions and 269 deletions

View file

@ -10,7 +10,7 @@
<div class="absolute top-0 right-0 w-screen h-screen p-4 md:p-6" style="background-color: #00000088">
<div class="content normal-background">
<div class="rounded-xl">
<div class="rounded-xl h-full">
<slot></slot>
</div>
<slot name="close-button">

33
UI/Base/IfHidden.svelte Normal file
View file

@ -0,0 +1,33 @@
<script lang="ts">
import {UIEventSource} from "../../Logic/UIEventSource";
import {onDestroy} from "svelte";
/**
* Functions as 'If', but uses 'display:hidden' instead.
*/
export let condition: UIEventSource<boolean>;
let _c = condition.data;
let hasBeenShownPositive = false
let hasBeenShownNegative = false
onDestroy(condition.addCallbackAndRun(c => {
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`,
which will _unregister_ the callback if `c = true`! */
hasBeenShownPositive = hasBeenShownPositive || c
hasBeenShownNegative = hasBeenShownNegative || !c
_c = c;
return false
}))
</script>
{#if hasBeenShownPositive}
<span class={_c ? "" : "hidden"}>
<slot/>
</span>
{/if}
{#if hasBeenShownNegative}
<span class={_c ? "hidden" : ""}>
<slot name="else"/>
</span>
{/if}

View file

@ -16,11 +16,11 @@
{#if context}
{#if $linkOnMobile}
<a href={LinkToWeblate.hrefToWeblate($language, context)} target="_blank" class="mx-1">
<a href={LinkToWeblate.hrefToWeblate($language, context)} target="_blank" class="mx-1 weblate-link">
<img src="./assets/svg/translate.svg" class="w-3 h-3 rounded-full font-gray" />
</a>
{:else if $linkToWeblate}
<a href={LinkToWeblate.hrefToWeblate($language, context)} class="hidden-on-mobile mx-1" target="_blank">
<a href={LinkToWeblate.hrefToWeblate($language, context)} class="weblate-link hidden-on-mobile mx-1" target="_blank">
<img src="./assets/svg/translate.svg" class="w-3 h-3 rounded-full font-gray" />
</a>
{/if}

View file

@ -3,13 +3,13 @@
import type {RasterLayerPolygon} from "../../Models/RasterLayers";
import {AvailableRasterLayers} from "../../Models/RasterLayers";
import Tr from "../Base/Tr.svelte";
import {onDestroy} from "svelte";
import {createEventDispatcher, onDestroy} from "svelte";
import Translations from "../i18n/Translations";
import Svg from "../../Svg";
import {Map as MlMap} from "maplibre-gl"
import MaplibreMap from "../Map/MaplibreMap.svelte";
import {MapLibreAdaptor} from "../Map/MapLibreAdaptor";
import type {MapProperties} from "../../Models/MapProperties";
import OverlayMap from "../Map/OverlayMap.svelte";
import RasterLayerPicker from "../Map/RasterLayerPicker.svelte";
export let mapproperties: MapProperties
export let normalMap: UIEventSource<MlMap>
@ -28,63 +28,55 @@
*/
export let availableRasterLayers: Store<RasterLayerPolygon[]>
let altmap: UIEventSource<MlMap> = new UIEventSource(undefined)
let altproperties = new MapLibreAdaptor(altmap, {zoom: UIEventSource.feedFrom(mapproperties.zoom)})
altproperties.allowMoving.setData(false)
altproperties.allowZooming.setData(false)
let altmap0: UIEventSource<MlMap> = new UIEventSource(undefined)
let altproperties0 = new MapLibreAdaptor(altmap0, {zoom: altproperties.zoom})
// altproperties0.allowMoving.setData(false)
// altproperties0.allowZooming.setData(false)
let raster0 = new UIEventSource<RasterLayerPolygon>(undefined)
let raster1 = new UIEventSource<RasterLayerPolygon>(undefined)
let currentLayer: RasterLayerPolygon
function updatedAltLayer() {
const available = availableRasterLayers.data
const current = rasterLayer.data
const defaultLayer = AvailableRasterLayers.maplibre
const firstOther = available.find(l => l !== current && l !== defaultLayer)
altproperties.rasterLayer.setData(firstOther)
const secondOther = available.find(l => l !== current && l !== firstOther && l !== defaultLayer)
altproperties0.rasterLayer.setData(secondOther)
const firstOther = available.find(l => l !== defaultLayer)
const secondOther = available.find(l => l !== defaultLayer && l !== firstOther)
raster0.setData(firstOther === current ? defaultLayer : firstOther)
raster1.setData(secondOther === current ? defaultLayer : secondOther)
}
updatedAltLayer()
onDestroy(mapproperties.rasterLayer.addCallbackAndRunD(updatedAltLayer))
onDestroy(availableRasterLayers.addCallbackAndRunD(updatedAltLayer))
onDestroy(rasterLayer.addCallbackAndRunD(updatedAltLayer))
function pixelCenterOf(map: UIEventSource<MlMap>): [number, number] {
const rect = map?.data?.getCanvas()?.getBoundingClientRect()
if (!rect) {
return undefined
function use(rasterLayer: UIEventSource<RasterLayerPolygon>): (() => void) {
return () => {
currentLayer = undefined
mapproperties.rasterLayer.setData(rasterLayer.data)
}
const x = (rect.left + rect.right) / 2
const y = (rect.top + rect.bottom) / 2
return [x, y]
}
mapproperties.location.addCallbackAndRunD(({lon, lat}) => {
if (!normalMap.data || !altmap.data) {
return
}
const altMapCenter = pixelCenterOf(altmap)
const c = normalMap.data.unproject(altMapCenter)
altproperties.location.setData({lon: c.lng, lat: c.lat})
const altMapCenter0 = pixelCenterOf(altmap0)
const c0 = normalMap.data.unproject(altMapCenter0)
altproperties0.location.setData({lon: c0.lng, lat: c0.lat})
})
const dispatch = createEventDispatcher<{ copyright_clicked }>()
</script>
<div class="flex">
<div class="w-32 h-32 overflow-hidden border-interactive">
<MaplibreMap map={altmap}/>
<div class="flex items-end opacity-50 hover:opacity-100">
<div class="flex flex-col md:flex-row">
<button class="w-16 h-12 md:w-16 md:h-16 overflow-hidden m-0 p-0"
on:click={use(raster0)}>
<OverlayMap placedOverMap={normalMap} placedOverMapProperties={mapproperties} rasterLayer={raster0}/>
</button>
<button class="w-16 h-12 md:w-16 md:h-16 overflow-hidden m-0 p-0 " on:click={use(raster1)}>
<OverlayMap placedOverMap={normalMap} placedOverMapProperties={mapproperties} rasterLayer={raster1}/>
</button>
</div>
<div class="w-32 h-32 overflow-hidden border-interactive">
<MaplibreMap map={altmap0}/>
</div>
<div class="low-interaction flex flex-col">
<b>Current background:</b>
<Tr t={Translations.T(name)}/>
<div class="text-sm flex flex-col gap-y-1 h-fit ml-1">
<div class="low-interaction rounded p-1 w-64">
<RasterLayerPicker availableLayers={availableRasterLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
</div>
<button class="small" on:click={() => dispatch("copyright_clicked")}>
© OpenStreetMap
</button>
</div>
</div>

View file

@ -22,6 +22,7 @@ import ContributorCount from "../../Logic/ContributorCount"
import Img from "../Base/Img"
import { TypedTranslation } from "../i18n/Translation"
import GeoIndexedStore from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
import {RasterLayerPolygon} from "../../Models/RasterLayers";
export class OpenIdEditor extends VariableUiElement {
constructor(
@ -113,7 +114,7 @@ export default class CopyrightPanel extends Combine {
constructor(state: {
layout: LayoutConfig
mapProperties: { bounds: Store<BBox> }
mapProperties: { readonly bounds: Store<BBox>, readonly rasterLayer: Store<RasterLayerPolygon> }
osmConnection: OsmConnection
dataIsLoading: Store<boolean>
perLayer: ReadonlyMap<string, GeoIndexedStore>
@ -173,6 +174,29 @@ export default class CopyrightPanel extends Combine {
[
new Title(t.attributionTitle),
t.attributionContent,
new VariableUiElement(state.mapProperties.rasterLayer.mapD(layer => {
const props = layer.properties
const attrUrl = props.attribution?.url
const attrText = props.attribution?.text
let bgAttr: BaseUIElement | string = undefined
if(attrText && attrUrl){
bgAttr = "<a href='"+attrUrl+"' target='_blank'>"+attrText+"</a>"
}else if(attrUrl){
bgAttr = attrUrl
}else{
bgAttr = attrText
}
if(bgAttr){
return Translations.t.general.attribution.attributionBackgroundLayerWithCopyright.Subs({
name: props.name,
copyright: bgAttr
})
}
return Translations.t.general.attribution.attributionBackgroundLayer.Subs(props)
})),
maintainer,
dataContributors,
CopyrightPanel.CodeContributors(contributors, t.codeContributionsBy),

View file

@ -1,100 +1,116 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ToSvelte from "../Base/ToSvelte.svelte";
import Svg from "../../Svg.js";
import Translations from "../i18n/Translations";
import Loading from "../Base/Loading.svelte";
import Hotkeys from "../Base/Hotkeys";
import { Geocoding } from "../../Logic/Osm/Geocoding";
import { BBox } from "../../Logic/BBox";
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore";
import {createEventDispatcher} from "svelte";
import {UIEventSource} from "../../Logic/UIEventSource";
import type {Feature} from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ToSvelte from "../Base/ToSvelte.svelte";
import Svg from "../../Svg.js";
import Translations from "../i18n/Translations";
import Loading from "../Base/Loading.svelte";
import Hotkeys from "../Base/Hotkeys";
import {Geocoding} from "../../Logic/Osm/Geocoding";
import {BBox} from "../../Logic/BBox";
import {GeoIndexedStoreForLayer} from "../../Logic/FeatureSource/Actors/GeoIndexedStore";
import {createEventDispatcher, onDestroy} from "svelte";
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined;
export let bounds: UIEventSource<BBox>;
export let selectedElement: UIEventSource<Feature> | undefined = undefined;
export let selectedLayer: UIEventSource<LayerConfig> | undefined = undefined;
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined;
export let bounds: UIEventSource<BBox>;
export let selectedElement: UIEventSource<Feature> | undefined = undefined;
export let selectedLayer: UIEventSource<LayerConfig> | undefined = undefined;
let searchContents: string = undefined;
let searchContents: string = ""
export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
onDestroy(triggerSearch.addCallback(_ => {
console.log("TriggerRun pinged")
performSearch()
}))
let isRunning: boolean = false;
let isRunning: boolean = false;
let inputElement: HTMLInputElement;
let inputElement: HTMLInputElement;
let feedback: string = undefined;
Hotkeys.RegisterHotkey(
{ ctrl: "F" },
Translations.t.hotkeyDocumentation.selectSearch,
() => {
inputElement?.focus();
inputElement?.select();
}
);
const dispatch = createEventDispatcher<{searchCompleted}>()
async function performSearch() {
try {
isRunning = true;
searchContents = searchContents?.trim() ?? "";
if (searchContents === "") {
return;
}
const result = await Geocoding.Search(searchContents, bounds.data);
if (result.length == 0) {
feedback = Translations.t.general.search.nothing.txt;
return;
}
const poi = result[0];
const [lat0, lat1, lon0, lon1] = poi.boundingbox;
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01));
if (perLayer !== undefined) {
const id = poi.osm_type + "/" + poi.osm_id;
const layers = Array.from(perLayer?.values() ?? []);
for (const layer of layers) {
const found = layer.features.data.find(f => f.properties.id === id);
selectedElement?.setData(found);
selectedLayer?.setData(layer.layer.layerDef);
let feedback: string = undefined;
Hotkeys.RegisterHotkey(
{ctrl: "F"},
Translations.t.hotkeyDocumentation.selectSearch,
() => {
inputElement?.focus();
inputElement?.select();
}
);
const dispatch = createEventDispatcher<{ searchCompleted, searchIsValid: boolean }>()
$: {
if (!searchContents?.trim()) {
dispatch("searchIsValid", false)
}else{
dispatch("searchIsValid", true)
}
}
async function performSearch() {
try {
isRunning = true;
searchContents = searchContents?.trim() ?? "";
if (searchContents === "") {
return;
}
const result = await Geocoding.Search(searchContents, bounds.data);
if (result.length == 0) {
feedback = Translations.t.general.search.nothing.txt;
return;
}
const poi = result[0];
const [lat0, lat1, lon0, lon1] = poi.boundingbox;
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01));
if (perLayer !== undefined) {
const id = poi.osm_type + "/" + poi.osm_id;
const layers = Array.from(perLayer?.values() ?? []);
for (const layer of layers) {
const found = layer.features.data.find(f => f.properties.id === id);
selectedElement?.setData(found);
selectedLayer?.setData(layer.layer.layerDef);
}
}
searchContents = ""
dispatch("searchIsValid", false)
dispatch("searchCompleted")
} catch (e) {
console.error(e);
feedback = Translations.t.general.search.error.txt;
} finally {
isRunning = false;
}
}
dispatch("searchCompleted")
} catch (e) {
console.error(e);
feedback = Translations.t.general.search.error.txt;
} finally {
isRunning = false;
}
}
</script>
<div class="flex normal-background rounded-full pl-2 justify-between">
<form class="w-full">
<form class="w-full">
{#if isRunning}
<Loading>{Translations.t.general.search.searching}</Loading>
{:else if feedback !== undefined}
<div class="alert" on:click={() => feedback = undefined}>
{feedback}
</div>
{:else }
<input
type="search"
class="w-full"
bind:this={inputElement}
on:keypress={keypr => keypr.key === "Enter" ? performSearch() : undefined}
{#if isRunning}
<Loading>{Translations.t.general.search.searching}</Loading>
{:else if feedback !== undefined}
<div class="alert" on:click={() => feedback = undefined}>
{feedback}
</div>
{:else }
<input
type="search"
class="w-full"
bind:this={inputElement}
on:keypress={keypr => keypr.key === "Enter" ? performSearch() : undefined}
bind:value={searchContents}
placeholder={Translations.t.general.search.search}>
{/if}
bind:value={searchContents}
placeholder={Translations.t.general.search.search}>
{/if}
</form>
<div class="w-6 h-6 self-end" on:click={performSearch}>
<ToSvelte construct={Svg.search_svg}></ToSvelte>
</div>
</form>
<div class="w-6 h-6 self-end" on:click={performSearch}>
<ToSvelte construct={Svg.search_svg}></ToSvelte>
</div>
</div>

View file

@ -13,7 +13,7 @@
export let onMainScreen: boolean = true
const prefix = "mapcomplete-hidden-theme-"
const hiddenThemes: LayoutInformation[] = themeOverview["default"].filter(
const hiddenThemes: LayoutInformation[] = themeOverview.filter(
(layout) => layout.hideFromOverview
)
const userPreferences = state.osmConnection.preferencesHandler.preferences

View file

@ -39,7 +39,7 @@
{layer}></TagRenderingAnswer>
</h3>
<div class="title-icons flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2 gap-x-0.5 p-1 links-as-button">
<div class="no-weblate title-icons flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2 gap-x-0.5 p-1 links-as-button">
{#each layer.titleIcons as titleIconConfig}
{#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties({..._metatags, ..._tags}) ?? true) && titleIconConfig.IsKnown(_tags)}
<div class="w-8 h-8 flex items-center">

View file

@ -0,0 +1,87 @@
<script lang="ts">
import Translations from "../i18n/Translations";
import Svg from "../../Svg";
import Tr from "../Base/Tr.svelte";
import NextButton from "../Base/NextButton.svelte";
import Geosearch from "./Geosearch.svelte";
import IfNot from "../Base/IfNot.svelte";
import ToSvelte from "../Base/ToSvelte.svelte";
import ThemeViewState from "../../Models/ThemeViewState";
import If from "../Base/If.svelte";
import {UIEventSource} from "../../Logic/UIEventSource";
import {SearchIcon} from "@rgossiaux/svelte-heroicons/solid";
/**
* The theme introduction panel
*/
export let state: ThemeViewState
let layout = state.layout
let selectedElement = state.selectedElement
let selectedLayer = state.selectedLayer
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
let searchEnabled = false
function jumpToCurrentLocation() {
const glstate = state.geolocation.geolocationState
if (glstate.currentGPSLocation.data !== undefined) {
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
state.guistate.themeIsOpened.setData(false)
const coor = {lon: c.longitude, lat: c.latitude}
state.mapProperties.location.setData(coor)
}
if (glstate.permission.data !== "granted") {
glstate.requestPermission()
return
}
}
</script>
<Tr t={layout.description}></Tr>
<Tr t={Translations.t.general.welcomeExplanation.general}/>
{#if layout.layers.some((l) => l.presets?.length > 0)}
<If condition={state.featureSwitches.featureSwitchAddNew}>
<Tr t={Translations.t.general.welcomeExplanation.addNew}/>
</If>
{/if}
<!--toTheMap,
loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
-->
<Tr t={layout.descriptionTail}></Tr>
<NextButton clss="primary w-full" on:click={() => state.guistate.themeIsOpened.setData(false)}>
<div class="flex justify-center w-full text-2xl">
<Tr t={Translations.t.general.openTheMap}/>
</div>
</NextButton>
<div class="flex w-full flex-wrap sm:flex-nowrap">
<IfNot condition={state.geolocation.geolocationState.permission.map(p => p === "denied")}>
<button class="flex w-full gap-x-2 items-center" on:click={jumpToCurrentLocation}>
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")}/>
<Tr t={Translations.t.general.openTheMapAtGeolocation}/>
</button>
</IfNot>
<div class="flex gap-x-2 items-center w-full border rounded .button p-2 m-1 low-interaction">
<div class="w-full">
<Geosearch bounds={state.mapProperties.bounds}
on:searchCompleted={() => state.guistate.themeIsOpened.setData(false)}
on:searchIsValid={isValid => {searchEnabled= isValid}}
perLayer={state.perLayer}
{selectedElement}
{selectedLayer}
{triggerSearch}>
</Geosearch>
</div>
<button class={"flex gap-x-2 justify-between items-center "+(searchEnabled ? "" : "disabled")}
on:click={() => triggerSearch.ping()}>
<Tr t={Translations.t.general.search.searchShort}/>
<SearchIcon class="w-6 h-6"></SearchIcon>
</button>
</div>
</div>

View file

@ -275,7 +275,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
return new Promise<Blob>((resolve) => drawOn.toBlob((data) => resolve(data)))
}
private updateStores() {
private updateStores(): void {
const map = this._maplibreMap.data
if (!map) {
return
@ -293,7 +293,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
this.bounds.setData(bbox)
}
private SetZoom(z: number) {
private SetZoom(z: number): void {
const map = this._maplibreMap.data
if (!map || z === undefined) {
return
@ -303,7 +303,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
}
}
private MoveMapToCurrentLoc(loc: { lat: number; lon: number }) {
private MoveMapToCurrentLoc(loc: { lat: number; lon: number }): void {
const map = this._maplibreMap.data
if (!map || loc === undefined) {
return
@ -325,7 +325,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
}
}
private removeCurrentLayer(map: MLMap) {
private removeCurrentLayer(map: MLMap): void {
if (this._currentRasterLayer) {
// hide the previous layer
map.removeLayer(this._currentRasterLayer)
@ -333,7 +333,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
}
}
private async setBackground() {
private async setBackground(): Promise<void> {
const map = this._maplibreMap.data
if (!map) {
return
@ -363,6 +363,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
map.addSource(background.id, MapLibreAdaptor.prepareWmsSource(background))
map.resize()
map.addLayer(
{
id: background.id,

69
UI/Map/OverlayMap.svelte Normal file
View file

@ -0,0 +1,69 @@
<script lang="ts">
/**
* The overlay map is a bit a weird map:
* it is a HTML-component which is intended to be placed _over_ another map.
* It will align itself in order to seamlessly show the same location; but possibly in a different style
*/
import MaplibreMap from "./MaplibreMap.svelte";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import {Map as MlMap} from "maplibre-gl";
import {MapLibreAdaptor} from "./MapLibreAdaptor";
import type {MapProperties} from "../../Models/MapProperties";
import {onDestroy} from "svelte";
import type {RasterLayerPolygon} from "../../Models/RasterLayers";
export let placedOverMapProperties: MapProperties
export let placedOverMap: UIEventSource<MlMap>
export let rasterLayer: UIEventSource<RasterLayerPolygon>
export let visible: Store<boolean> = undefined
let altmap: UIEventSource<MlMap> = new UIEventSource(undefined)
let altproperties = new MapLibreAdaptor(altmap, {
rasterLayer,
zoom: UIEventSource.feedFrom(placedOverMapProperties.zoom)
})
altproperties.allowMoving.setData(false)
altproperties.allowZooming.setData(false)
function pixelCenterOf(map: UIEventSource<MlMap>): [number, number] {
const rect = map?.data?.getCanvas()?.getBoundingClientRect()
if (!rect) {
return undefined
}
const x = (rect.left + rect.right) / 2
const y = (rect.top + rect.bottom) / 2
return [x, y]
}
function updateLocation() {
if (!placedOverMap.data || !altmap.data) {
return
}
altmap.data.resize()
const {lon, lat} = placedOverMapProperties.location.data
const altMapCenter = pixelCenterOf(altmap)
const c = placedOverMap.data.unproject(altMapCenter)
altproperties.location.setData({lon: c.lng, lat: c.lat})
}
onDestroy(placedOverMapProperties.location.addCallbackAndRunD(updateLocation))
updateLocation()
window.setTimeout(updateLocation, 150)
window.setTimeout(updateLocation, 500)
if (visible) {
onDestroy(visible?.addCallbackAndRunD(v => {
if (!v) {
return
}
updateLocation()
window.setTimeout(updateLocation, 150)
window.setTimeout(updateLocation, 500)
}))
}
</script>
<MaplibreMap map={altmap}/>

View file

@ -0,0 +1,73 @@
<script lang="ts">
/**
* The RasterLayerOverview shows the available 4 categories of maps with a RasterLayerPicker
*/
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import type {RasterLayerPolygon} from "../../Models/RasterLayers";
import type {MapProperties} from "../../Models/MapProperties";
import {Map as MlMap} from "maplibre-gl";
import RasterLayerPicker from "./RasterLayerPicker.svelte";
import type {EliCategory} from "../../Models/RasterLayerProperties";
import UserRelatedState from "../../Logic/State/UserRelatedState";
import Translations from "../i18n/Translations";
import Tr from "../Base/Tr.svelte";
export let availableLayers: Store<RasterLayerPolygon[]>
export let mapproperties: MapProperties
export let userstate: UserRelatedState
export let map: Store<MlMap>
/**
* Used to toggle the background layers on/off
*/
export let visible: UIEventSource<boolean> = undefined
type CategoryType = "photo" | "map" | "other" | "osmbasedmap"
const categories: Record<CategoryType, EliCategory[]> = {
"photo": ["photo", "historicphoto"],
"map": ["map", "historicmap"],
"other": ["other", "elevation"],
"osmbasedmap": ["osmbasedmap"]
}
function availableForCategory(type: CategoryType): Store<RasterLayerPolygon[]> {
const keywords = categories[type]
return availableLayers.mapD(available => available.filter(layer =>
keywords.indexOf(<EliCategory>layer.properties.category) >= 0
))
}
const mapLayers = availableForCategory("map")
const osmbasedmapLayers = availableForCategory("osmbasedmap")
const photoLayers = availableForCategory("photo")
const otherLayers = availableForCategory("other")
function onApply() {
visible.setData(false)
}
function getPref(type: CategoryType): UIEventSource<string> {
return userstate.osmConnection.GetPreference("preferred-layer-" + type)
}
</script>
<div class="h-full flex flex-col">
<slot name="title">
<h2>
<Tr t={Translations.t.general.backgroundMap}/>
</h2>
</slot>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 h-full w-full">
<RasterLayerPicker availableLayers={photoLayers} favourite={getPref("photo")} {map} {mapproperties}
on:appliedLayer={onApply} {visible}/>
<RasterLayerPicker availableLayers={mapLayers} favourite={getPref("map")} {map} {mapproperties}
on:appliedLayer={onApply} {visible}/>
<RasterLayerPicker availableLayers={osmbasedmapLayers} favourite={getPref("osmbasedmap")}
{map} {mapproperties} on:appliedLayer={onApply} {visible}/>
<RasterLayerPicker availableLayers={otherLayers} favourite={getPref("other")} {map} {mapproperties}
on:appliedLayer={onApply} {visible}/>
</div>
</div>

View file

@ -1,18 +1,77 @@
<script lang="ts">
import type { Readable, Writable } from "svelte/store";
import type { RasterLayerPolygon } from "../../Models/RasterLayers";
import type {RasterLayerPolygon} from "../../Models/RasterLayers";
import OverlayMap from "./OverlayMap.svelte";
import type {MapProperties} from "../../Models/MapProperties";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import {Map as MlMap} from "maplibre-gl"
import {createEventDispatcher, onDestroy} from "svelte";
/***
* Chooses a background-layer out of available options
*/
export let availableLayers: Readable<RasterLayerPolygon[]>
export let value: Writable<RasterLayerPolygon>
/***
* Chooses a background-layer out of available options
*/
export let availableLayers: Store<RasterLayerPolygon[]>
export let mapproperties: MapProperties
export let map: Store<MlMap>
export let visible: Store<boolean> = undefined
let dispatch = createEventDispatcher<{appliedLayer}>()
export let favourite : UIEventSource<string> = undefined
let rasterLayer = new UIEventSource<RasterLayerPolygon>(availableLayers.data?.[0])
let hasLayers = true
onDestroy(availableLayers.addCallbackAndRun(layers => {
if (layers === undefined || layers.length === 0) {
hasLayers = false
return
}
hasLayers = true
rasterLayer.setData(layers[0])
}))
if(favourite){
onDestroy(favourite.addCallbackAndRunD(favourite => {
const fav = availableLayers.data?.find(l => l.properties.id === favourite)
if(!fav){
return
}
rasterLayer.setData(fav)
}))
}
onDestroy(rasterLayer.addCallbackAndRunD(selected => {
favourite?.setData(selected.properties.id)
}))
let rasterLayerOnMap = UIEventSource.feedFrom(rasterLayer)
if (visible) {
onDestroy(visible?.addCallbackAndRunD(visible => {
if (visible) {
rasterLayerOnMap.setData(rasterLayer.data ?? availableLayers.data[0])
} else {
rasterLayerOnMap.setData(undefined)
}
}))
}
</script>
<select bind:value={$value}>
{#each $availableLayers as availableLayer }
<option value={availableLayer}>
{availableLayer.properties.name}
</option>
{/each}
</select>
{#if hasLayers}
<div class="h-full w-full flex flex-col">
<button on:click={() => {mapproperties.rasterLayer.setData(rasterLayer.data);
dispatch("appliedLayer")
}} class="w-full h-full m-0 p-0">
<OverlayMap rasterLayer={rasterLayerOnMap} placedOverMap={map} placedOverMapProperties={mapproperties}
{visible}/>
</button>
<select bind:value={$rasterLayer} class="w-full">
{#each $availableLayers as availableLayer }
<option value={availableLayer}>
{availableLayer.properties.name}
</option>
{/each}
</select>
</div>
{/if}

View file

@ -11,12 +11,13 @@
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import Filterview from "./BigComponents/Filterview.svelte";
import RasterLayerPicker from "./Map/RasterLayerPicker.svelte";
import ThemeViewState from "../Models/ThemeViewState";
import type {MapProperties} from "../Models/MapProperties";
import Geosearch from "./BigComponents/Geosearch.svelte";
import Translations from "./i18n/Translations";
import {CogIcon, EyeIcon, MenuIcon, XCircleIcon} from "@rgossiaux/svelte-heroicons/solid";
import {Square3Stack3dIcon} from "@babeard/svelte-heroicons/solid";
import Tr from "./Base/Tr.svelte";
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
import FloatOver from "./Base/FloatOver.svelte";
@ -39,9 +40,12 @@
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
import Svg from "../Svg";
import {ShareScreen} from "./BigComponents/ShareScreen";
import NextButton from "./Base/NextButton.svelte";
import IfNot from "./Base/IfNot.svelte";
import BackgroundSwitcher from "./BigComponents/BackgroundSwitcher.svelte";
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
import type {Readable} from "svelte/store";
import type {RasterLayerPolygon} from "../Models/RasterLayers";
import RasterLayerPicker from "./Map/RasterLayerPicker.svelte";
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
import IfHidden from "./Base/IfHidden.svelte";
export let state: ThemeViewState;
let layout = state.layout;
@ -86,21 +90,7 @@
let availableLayers = state.availableLayers;
let userdetails = state.osmConnection.userDetails;
let currentViewLayer = layout.layers.find(l => l.id === "current_view")
function jumpToCurrentLocation() {
const glstate = state.geolocation.geolocationState
if (glstate.currentGPSLocation.data !== undefined) {
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
state.guistate.themeIsOpened.setData(false)
const coor = {lon: c.longitude, lat: c.latitude}
state.mapProperties.location.setData(coor)
}
if (glstate.permission.data !== "granted") {
glstate.requestPermission()
return
}
}
let rasterLayer: Readable<RasterLayerPolygon> = state.mapProperties.rasterLayer
</script>
@ -138,35 +128,48 @@
<ToSvelte construct={() => new ExtraLinkButton(state, layout.extraLink)}></ToSvelte>
<If condition={state.featureSwitchIsTesting}>
<div class="alert w-fit">
Testmode
Testmode
</div>
</If>
</div>
</div>
<div class="absolute bottom-0 left-0 mb-4 ml-4">
<BackgroundSwitcher availableRasterLayers={state.availableLayers} mapproperties={state.mapProperties} normalMap={state.map}/>
</div>
<div class="absolute bottom-0 right-0 mb-4 mr-4 flex flex-col items-end">
<If condition={state.floors.map(f => f.length > 1)}>
<div class="mr-0.5">
<LevelSelector floors={state.floors} layerState={state.layerState} zoom={state.mapProperties.zoom}/>
<div class="absolute bottom-0 left-0 mb-4 w-screen">
<div class="w-full flex justify-between px-4 items-end">
<div>
<!-- bottom left elements -->
<MapControlButton on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}>
<Square3Stack3dIcon class="w-6 h-6"/>
</MapControlButton>
<a class="opacity-50 hover:opacity-100 text-white cursor-pointer bg-black-transparent px-1 rounded-2xl"
on:click={() =>{ state.guistate.themeViewTab.setData("copyright"); state.guistate.themeIsOpened.setData(true)}}>
© OpenStreetMap | <span class="w-24">{$rasterLayer.properties.name}</span>
</a>
</div>
</If>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z+1)}>
<ToSvelte construct={Svg.plus_svg().SetClass("w-8 h-8")}/>
</MapControlButton>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z-1)}>
<ToSvelte construct={Svg.min_svg().SetClass("w-8 h-8")}/>
</MapControlButton>
<If condition={featureSwitches.featureSwitchGeolocation}>
<MapControlButton>
<ToSvelte
construct={new GeolocationControl(state.geolocation, mapproperties).SetClass("block w-8 h-8")}></ToSvelte>
</MapControlButton>
</If>
<div class="flex flex-col items-end">
<!-- bottom right elements -->
<If condition={state.floors.map(f => f.length > 1)}>
<div class="mr-0.5">
<LevelSelector floors={state.floors} layerState={state.layerState} zoom={state.mapProperties.zoom}/>
</div>
</If>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z+1)}>
<ToSvelte construct={Svg.plus_svg().SetClass("w-8 h-8")}/>
</MapControlButton>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z-1)}>
<ToSvelte construct={Svg.min_svg().SetClass("w-8 h-8")}/>
</MapControlButton>
<If condition={featureSwitches.featureSwitchGeolocation}>
<MapControlButton>
<ToSvelte
construct={new GeolocationControl(state.geolocation, mapproperties).SetClass("block w-8 h-8")}></ToSvelte>
</MapControlButton>
</If>
</div>
</div>
</div>
<If condition={selectedElementView.map(v => v !== undefined && selectedLayer.data !== undefined && !selectedLayer.data.popupInFloatover,[ selectedLayer] )}>
@ -204,43 +207,7 @@
<div class="m-4" slot="content0">
<Tr t={layout.description}></Tr>
<Tr t={Translations.t.general.welcomeExplanation.general}/>
{#if layout.layers.some((l) => l.presets?.length > 0)}
<If condition={state.featureSwitches.featureSwitchAddNew}>
<Tr t={Translations.t.general.welcomeExplanation.addNew}/>
</If>
{/if}
<!--toTheMap,
loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
-->
<Tr t={layout.descriptionTail}></Tr>
<NextButton clss="primary w-full" on:click={() => state.guistate.themeIsOpened.setData(false)}>
<div class="flex justify-center w-full text-2xl">
<Tr t={Translations.t.general.openTheMap}/>
</div>
</NextButton>
<div class="flex w-full">
<IfNot condition={state.geolocation.geolocationState.permission.map(p => p === "denied")}>
<button class="flex w-full gap-x-2 items-center" on:click={jumpToCurrentLocation}>
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")}/>
<span>
Jump to your location
</span>
</button>
</IfNot>
<div class="flex flex-col w-full border rounded low-interactive">
Search for a location:
<Geosearch bounds={state.mapProperties.bounds}
on:searchCompleted={() => state.guistate.themeIsOpened.setData(false)}
perLayer={state.perLayer}
{selectedElement}
{selectedLayer}/>
</div>
</div>
<ThemeIntroPanel {state}/>
</div>
@ -265,9 +232,6 @@
zoomlevel={state.mapProperties.zoom}
/>
{/each}
<If condition={state.featureSwitches.featureSwitchBackgroundSelection}>
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
</If>
</div>
<div class="flex" slot="title2">
<If condition={state.featureSwitches.featureSwitchEnableExport}>
@ -288,12 +252,22 @@
<div slot="title4">
<Tr t={Translations.t.general.sharescreen.title}/>
</div>
<ToSvelte construct={() => new ShareScreen(state)} slot="content4"/>
<div class="m-2" slot="content4">
<ToSvelte construct={() => new ShareScreen(state)}/>
</div>
</TabbedGroup>
</FloatOver>
</If>
<IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}>
<!-- background layer selector -->
<FloatOver on:close={() => state.guistate.backgroundLayerSelectionIsOpened.setData(false)}>
<div class="p-2 h-full">
<RasterLayerOverview userstate={state.userRelatedState} mapproperties={state.mapProperties} map={state.map} {availableLayers} visible={state.guistate.backgroundLayerSelectionIsOpened}/>
</div>
</FloatOver>
</IfHidden>
<If condition={state.guistate.menuIsOpened}>
<!-- Menu page -->