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

@ -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>