forked from MapComplete/MapComplete
Refactoring: new background selector
This commit is contained in:
parent
5427a4cb05
commit
82093ffdf4
25 changed files with 658 additions and 269 deletions
|
@ -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>
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
87
UI/BigComponents/ThemeIntroPanel.svelte
Normal file
87
UI/BigComponents/ThemeIntroPanel.svelte
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue