Usertest: See #1315, add a 'confirm'-button on top of the precise-input location if the map is tapped; fix: make sure the dragInvitation doesn't consume the first map interaction, get's hidden by a change in map location

This commit is contained in:
Pieter Vander Vennet 2023-06-14 23:21:19 +02:00
parent cc70e09e11
commit c5c6bba731
6 changed files with 429 additions and 387 deletions

View file

@ -11,9 +11,14 @@
function hide() {
mainElem.style.visibility = "hidden"
}
let initTime = Date.now()
if (hideSignal) {
onDestroy(
hideSignal.addCallbackD(() => {
if(initTime + 1000 > Date.now()){
console.log("Ignoring hide signal")
return
}
console.log("Received hide signal")
hide()
return true
@ -27,8 +32,8 @@
}
</script>
<div bind:this={mainElem} class="absolute bottom-0 right-0 h-full w-full">
<div id="hand-container" class="pointer-events-none">
<div bind:this={mainElem} class="absolute bottom-0 right-0 h-full w-full pointer-events-none">
<div id="hand-container">
<img src="./assets/svg/hand.svg" />
</div>
</div>

View file

@ -15,6 +15,7 @@
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../Utils"
import {createEventDispatcher} from "svelte";
/**
* An advanced location input, which has support to:
@ -44,6 +45,8 @@
lat: number
}>(undefined)
const dispatch = createEventDispatcher<{click: {lon: number, lat: number}}>()
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let initialMapProperties: Partial<MapProperties> = {
@ -106,6 +109,7 @@
<LocationInput
{map}
on:click={data => dispatch("click", data)}
mapProperties={initialMapProperties}
value={preciseLocation}
initialCoordinate={coordinate}

View file

@ -1,91 +1,96 @@
<script lang="ts">
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import type { MapProperties } from "../../../Models/MapProperties"
import { Map as MlMap } from "maplibre-gl"
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor"
import MaplibreMap from "../../Map/MaplibreMap.svelte"
import DragInvitation from "../../Base/DragInvitation.svelte"
import { GeoOperations } from "../../../Logic/GeoOperations"
import ShowDataLayer from "../../Map/ShowDataLayer"
import * as boundsdisplay from "../../../assets/layers/range/range.json"
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource"
import * as turf from "@turf/turf"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { onDestroy } from "svelte"
import {Store, UIEventSource} from "../../../Logic/UIEventSource"
import type {MapProperties} from "../../../Models/MapProperties"
import {Map as MlMap} from "maplibre-gl"
import {MapLibreAdaptor} from "../../Map/MapLibreAdaptor"
import MaplibreMap from "../../Map/MaplibreMap.svelte"
import DragInvitation from "../../Base/DragInvitation.svelte"
import {GeoOperations} from "../../../Logic/GeoOperations"
import ShowDataLayer from "../../Map/ShowDataLayer"
import * as boundsdisplay from "../../../assets/layers/range/range.json"
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource"
import * as turf from "@turf/turf"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import {createEventDispatcher, onDestroy} from "svelte"
/**
* A visualisation to pick a location on a map background
*/
export let value: UIEventSource<{ lon: number; lat: number }>
export let initialCoordinate: { lon: number; lat: number }
initialCoordinate = initialCoordinate ?? value.data
export let maxDistanceInMeters: number = undefined
export let mapProperties: Partial<MapProperties> & {
readonly location: UIEventSource<{ lon: number; lat: number }>
} = undefined
/**
* Called when setup is done, can be used to add more layers to the map
*/
export let onCreated: (
value: Store<{
lon: number
lat: number
}>,
map: Store<MlMap>,
mapProperties: MapProperties
) => void = undefined
/**
* A visualisation to pick a location on a map background
*/
export let value: UIEventSource<{ lon: number; lat: number }>
export let initialCoordinate: { lon: number; lat: number }
initialCoordinate = initialCoordinate ?? value.data
export let maxDistanceInMeters: number = undefined
export let mapProperties: Partial<MapProperties> & {
readonly location: UIEventSource<{ lon: number; lat: number }>
} = undefined
/**
* Called when setup is done, can be used to add more layers to the map
*/
export let onCreated: (
value: Store<{
lon: number
lat: number
}>,
map: Store<MlMap>,
mapProperties: MapProperties
) => void = undefined
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let mla = new MapLibreAdaptor(map, mapProperties)
mapProperties.location.syncWith(value)
if (onCreated) {
onCreated(value, map, mla)
}
const dispatch = createEventDispatcher<{ click: { lon: number, lat: number } }>()
let rangeIsShown = false
if (maxDistanceInMeters) {
onDestroy(
mla.location.addCallbackD((newLocation) => {
const l = [newLocation.lon, newLocation.lat]
const c: [number, number] = [initialCoordinate.lon, initialCoordinate.lat]
const d = GeoOperations.distanceBetween(l, c)
console.log("distance is", d, l, c)
if (d <= maxDistanceInMeters) {
return
}
// This is too far away - let's move back
const correctLocation = GeoOperations.along(c, l, maxDistanceInMeters - 10)
window.setTimeout(() => {
mla.location.setData({ lon: correctLocation[0], lat: correctLocation[1] })
}, 25)
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let mla = new MapLibreAdaptor(map, mapProperties)
mla.lastClickLocation.addCallbackAndRunD(lastClick => {
dispatch("click", lastClick)
})
mapProperties.location.syncWith(value)
if (onCreated) {
onCreated(value, map, mla)
}
if (!rangeIsShown) {
new ShowDataLayer(map, {
layer: new LayerConfig(boundsdisplay),
features: new StaticFeatureSource([
turf.circle(c, maxDistanceInMeters, {
units: "meters",
properties: { range: "yes", id: "0" },
}),
]),
})
rangeIsShown = true
}
})
)
}
let rangeIsShown = false
if (maxDistanceInMeters) {
onDestroy(
mla.location.addCallbackD((newLocation) => {
const l = [newLocation.lon, newLocation.lat]
const c: [number, number] = [initialCoordinate.lon, initialCoordinate.lat]
const d = GeoOperations.distanceBetween(l, c)
console.log("distance is", d, l, c)
if (d <= maxDistanceInMeters) {
return
}
// This is too far away - let's move back
const correctLocation = GeoOperations.along(c, l, maxDistanceInMeters - 10)
window.setTimeout(() => {
mla.location.setData({lon: correctLocation[0], lat: correctLocation[1]})
}, 25)
if (!rangeIsShown) {
new ShowDataLayer(map, {
layer: new LayerConfig(boundsdisplay),
features: new StaticFeatureSource([
turf.circle(c, maxDistanceInMeters, {
units: "meters",
properties: {range: "yes", id: "0"},
}),
]),
})
rangeIsShown = true
}
})
)
}
</script>
<div class="min-h-32 relative h-full cursor-pointer overflow-hidden">
<div class="absolute top-0 left-0 h-full w-full cursor-pointer">
<MaplibreMap {map} />
</div>
<div class="absolute top-0 left-0 h-full w-full cursor-pointer">
<MaplibreMap {map}/>
</div>
<div
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center p-8 opacity-50"
>
<img class="h-full max-h-24" src="./assets/svg/move-arrows.svg" />
</div>
<div
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center p-8 opacity-50"
>
<img class="h-full max-h-24" src="./assets/svg/move-arrows.svg"/>
</div>
<DragInvitation hideSignal={mla.location.stabilized(3000)} />
<DragInvitation hideSignal={mla.location}/>
</div>

View file

@ -83,7 +83,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
// Workaround, 'ShowPointLayer' sets this flag
return
}
console.log(e)
const lon = e.lngLat.lng
const lat = e.lngLat.lat
lastClickLocation.setData({ lon, lat })
@ -321,10 +320,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
if (this.location.data === undefined) {
this.location.setData({ lon: lng, lat })
} else if (!isSetup) {
const dt = this.location.data
dt.lon = map.getCenter().lng
dt.lat = map.getCenter().lat
this.location.ping()
const lon = map.getCenter().lng
const lat = map.getCenter().lat
this.location.setData({ lon, lat })
}
this.zoom.setData(Math.round(map.getZoom() * 10) / 10)
const bounds = map.getBounds()

View file

@ -1,339 +1,353 @@
<script lang="ts">
/**
* This component ties together all the steps that are needed to create a new point.
* There are many subcomponents which help with that
*/
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import PresetList from "./PresetList.svelte"
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import Tr from "../../Base/Tr.svelte"
import SubtleButton from "../../Base/SubtleButton.svelte"
import FromHtml from "../../Base/FromHtml.svelte"
import Translations from "../../i18n/Translations.js"
import TagHint from "../TagHint.svelte"
import { And } from "../../../Logic/Tags/And.js"
import LoginToggle from "../../Base/LoginToggle.svelte"
import Constants from "../../../Models/Constants.js"
import FilteredLayer from "../../../Models/FilteredLayer"
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import LoginButton from "../../Base/LoginButton.svelte"
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
import { OsmWay } from "../../../Logic/Osm/OsmObject"
import { Tag } from "../../../Logic/Tags/Tag"
import type { WayId } from "../../../Models/OsmFeature"
import Loading from "../../Base/Loading.svelte"
import type { GlobalFilter } from "../../../Models/GlobalFilter"
import { onDestroy } from "svelte"
import NextButton from "../../Base/NextButton.svelte"
import BackButton from "../../Base/BackButton.svelte"
import ToSvelte from "../../Base/ToSvelte.svelte"
import Svg from "../../../Svg"
import MapControlButton from "../../Base/MapControlButton.svelte"
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
/**
* This component ties together all the steps that are needed to create a new point.
* There are many subcomponents which help with that
*/
import type {SpecialVisualizationState} from "../../SpecialVisualization"
import PresetList from "./PresetList.svelte"
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import Tr from "../../Base/Tr.svelte"
import SubtleButton from "../../Base/SubtleButton.svelte"
import FromHtml from "../../Base/FromHtml.svelte"
import Translations from "../../i18n/Translations.js"
import TagHint from "../TagHint.svelte"
import {And} from "../../../Logic/Tags/And.js"
import LoginToggle from "../../Base/LoginToggle.svelte"
import Constants from "../../../Models/Constants.js"
import FilteredLayer from "../../../Models/FilteredLayer"
import {Store, UIEventSource} from "../../../Logic/UIEventSource"
import {EyeIcon, EyeOffIcon} from "@rgossiaux/svelte-heroicons/solid"
import LoginButton from "../../Base/LoginButton.svelte"
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
import {OsmWay} from "../../../Logic/Osm/OsmObject"
import {Tag} from "../../../Logic/Tags/Tag"
import type {WayId} from "../../../Models/OsmFeature"
import Loading from "../../Base/Loading.svelte"
import type {GlobalFilter} from "../../../Models/GlobalFilter"
import {onDestroy} from "svelte"
import NextButton from "../../Base/NextButton.svelte"
import BackButton from "../../Base/BackButton.svelte"
import ToSvelte from "../../Base/ToSvelte.svelte"
import Svg from "../../../Svg"
import MapControlButton from "../../Base/MapControlButton.svelte"
import {Square3Stack3dIcon} from "@babeard/svelte-heroicons/solid"
export let coordinate: { lon: number; lat: number }
export let state: SpecialVisualizationState
export let coordinate: { lon: number; lat: number }
export let state: SpecialVisualizationState
let selectedPreset: {
preset: PresetConfig
layer: LayerConfig
icon: string
tags: Record<string, string>
} = undefined
let checkedOfGlobalFilters: number = 0
let confirmedCategory = false
$: if (selectedPreset === undefined) {
confirmedCategory = false
creating = false
checkedOfGlobalFilters = 0
}
let selectedPreset: {
preset: PresetConfig
layer: LayerConfig
icon: string
tags: Record<string, string>
} = undefined
let checkedOfGlobalFilters: number = 0
let confirmedCategory = false
$: if (selectedPreset === undefined) {
confirmedCategory = false
creating = false
checkedOfGlobalFilters = 0
}
let flayer: FilteredLayer = undefined
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined
let layerHasFilters: Store<boolean> | undefined = undefined
let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters
let _globalFilter: GlobalFilter[] = []
onDestroy(
globalFilter.addCallbackAndRun((globalFilter) => {
console.log("Global filters are", globalFilter)
_globalFilter = globalFilter ?? []
})
)
$: {
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id)
layerIsDisplayed = flayer?.isDisplayed
layerHasFilters = flayer?.hasFilter
}
const t = Translations.t.general.add
const zoom = state.mapProperties.zoom
const isLoading = state.dataIsLoading
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined)
let creating = false
/**
* Call when the user should restart the flow by clicking on the map, e.g. because they disabled filters.
* Will delete the lastclick-location
*/
function abort() {
state.selectedElement.setData(undefined)
// When aborted, we force the contributors to place the pin _again_
// This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map
state.lastClickObject.features.setData([])
}
async function confirm() {
creating = true
const location: { lon: number; lat: number } = preciseCoordinate.data
const snapTo: WayId | undefined = <WayId>snappedToObject.data
const tags: Tag[] = selectedPreset.preset.tags.concat(
..._globalFilter.map((f) => f?.onNewPoint?.tags ?? [])
let flayer: FilteredLayer = undefined
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined
let layerHasFilters: Store<boolean> | undefined = undefined
let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters
let _globalFilter: GlobalFilter[] = []
onDestroy(
globalFilter.addCallbackAndRun((globalFilter) => {
console.log("Global filters are", globalFilter)
_globalFilter = globalFilter ?? []
})
)
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags)
$: {
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id)
layerIsDisplayed = flayer?.isDisplayed
layerHasFilters = flayer?.hasFilter
}
const t = Translations.t.general.add
let snapToWay: undefined | OsmWay = undefined
if (snapTo !== undefined) {
const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0)
if (downloaded !== "deleted") {
snapToWay = downloaded
}
const zoom = state.mapProperties.zoom
const isLoading = state.dataIsLoading
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined)
// Small helper variable: if the map is tapped, we should let the 'Next'-button grab some attention as users have to click _that_ to continue, not the map
let preciseInputIsTapped = false
let creating = false
/**
* Call when the user should restart the flow by clicking on the map, e.g. because they disabled filters.
* Will delete the lastclick-location
*/
function abort() {
state.selectedElement.setData(undefined)
// When aborted, we force the contributors to place the pin _again_
// This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map
state.lastClickObject.features.setData([])
preciseInputIsTapped = false
}
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
theme: state.layout?.id ?? "unkown",
changeType: "create",
snapOnto: snapToWay,
})
await state.changes.applyAction(newElementAction)
state.newFeatures.features.ping()
// The 'changes' should have created a new point, which added this into the 'featureProperties'
const newId = newElementAction.newElementId
console.log("Applied pending changes, fetching store for", newId)
const tagsStore = state.featureProperties.getStore(newId)
{
// Set some metainfo
const properties = tagsStore.data
if (snapTo) {
// metatags (starting with underscore) are not uploaded, so we can safely mark this
delete properties["_referencing_ways"]
properties["_referencing_ways"] = `["${snapTo}"]`
}
properties["_backend"] = state.osmConnection.Backend()
properties["_last_edit:timestamp"] = new Date().toISOString()
const userdetails = state.osmConnection.userDetails.data
properties["_last_edit:contributor"] = userdetails.name
properties["_last_edit:uid"] = "" + userdetails.uid
tagsStore.ping()
async function confirm() {
creating = true
const location: { lon: number; lat: number } = preciseCoordinate.data
const snapTo: WayId | undefined = <WayId>snappedToObject.data
const tags: Tag[] = selectedPreset.preset.tags.concat(
..._globalFilter.map((f) => f?.onNewPoint?.tags ?? [])
)
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags)
let snapToWay: undefined | OsmWay = undefined
if (snapTo !== undefined) {
const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0)
if (downloaded !== "deleted") {
snapToWay = downloaded
}
}
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
theme: state.layout?.id ?? "unkown",
changeType: "create",
snapOnto: snapToWay,
})
await state.changes.applyAction(newElementAction)
state.newFeatures.features.ping()
// The 'changes' should have created a new point, which added this into the 'featureProperties'
const newId = newElementAction.newElementId
console.log("Applied pending changes, fetching store for", newId)
const tagsStore = state.featureProperties.getStore(newId)
{
// Set some metainfo
const properties = tagsStore.data
if (snapTo) {
// metatags (starting with underscore) are not uploaded, so we can safely mark this
delete properties["_referencing_ways"]
properties["_referencing_ways"] = `["${snapTo}"]`
}
properties["_backend"] = state.osmConnection.Backend()
properties["_last_edit:timestamp"] = new Date().toISOString()
const userdetails = state.osmConnection.userDetails.data
properties["_last_edit:contributor"] = userdetails.name
properties["_last_edit:uid"] = "" + userdetails.uid
tagsStore.ping()
}
const feature = state.indexedFeatures.featuresById.data.get(newId)
abort()
state.selectedLayer.setData(selectedPreset.layer)
state.selectedElement.setData(feature)
tagsStore.ping()
}
const feature = state.indexedFeatures.featuresById.data.get(newId)
abort()
state.selectedLayer.setData(selectedPreset.layer)
state.selectedElement.setData(feature)
tagsStore.ping()
}
</script>
<LoginToggle ignoreLoading={true} {state}>
<!-- This component is basically one big if/then/else flow checking for many conditions and edge cases that (in some cases) have to be handled;
1. the first (and outermost) is of course: are we logged in?
2. What do we want to add?
3. Are all elements of this category visible? (i.e. there are no filters possibly hiding this, is the data still loading, ...) -->
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
</LoginButton>
{#if $isLoading}
<div class="alert">
<Loading>
<Tr t={Translations.t.general.add.stillLoading} />
</Loading>
</div>
{:else if $zoom < Constants.minZoomLevelToAddNewPoint}
<div class="alert">
<Tr t={Translations.t.general.add.zoomInFurther} />
</div>
{:else if selectedPreset === undefined}
<!-- First, select the correct preset -->
<PresetList
{state}
on:select={(event) => {
<!-- This component is basically one big if/then/else flow checking for many conditions and edge cases that (in some cases) have to be handled;
1. the first (and outermost) is of course: are we logged in?
2. What do we want to add?
3. Are all elements of this category visible? (i.e. there are no filters possibly hiding this, is the data still loading, ...) -->
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
<Tr slot="message" t={Translations.t.general.add.pleaseLogin}/>
</LoginButton>
{#if $isLoading}
<div class="alert">
<Loading>
<Tr t={Translations.t.general.add.stillLoading}/>
</Loading>
</div>
{:else if $zoom < Constants.minZoomLevelToAddNewPoint}
<div class="alert">
<Tr t={Translations.t.general.add.zoomInFurther}/>
</div>
{:else if selectedPreset === undefined}
<!-- First, select the correct preset -->
<PresetList
{state}
on:select={(event) => {
selectedPreset = event.detail
}}
/>
{:else if !$layerIsDisplayed}
<!-- Check that the layer is enabled, so that we don't add a duplicate -->
<div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8" />
<Tr
t={Translations.t.general.add.layerNotEnabled.Subs({ layer: selectedPreset.layer.name })}
/>
</div>
/>
{:else if !$layerIsDisplayed}
<!-- Check that the layer is enabled, so that we don't add a duplicate -->
<div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8"/>
<Tr
t={Translations.t.general.add.layerNotEnabled.Subs({ layer: selectedPreset.layer.name })}
/>
</div>
<div class="flex flex-wrap-reverse md:flex-nowrap">
<button
class="flex w-full gap-x-1"
on:click={() => {
<div class="flex flex-wrap-reverse md:flex-nowrap">
<button
class="flex w-full gap-x-1"
on:click={() => {
abort()
state.guistate.openFilterView(selectedPreset.layer)
}}
>
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
<Tr t={Translations.t.general.add.openLayerControl} />
</button>
>
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")}/>
<Tr t={Translations.t.general.add.openLayerControl}/>
</button>
<button
class="primary flex w-full gap-x-1"
on:click={() => {
<button
class="primary flex w-full gap-x-1"
on:click={() => {
layerIsDisplayed.setData(true)
abort()
}}
>
<EyeIcon class="w-12" />
<Tr t={Translations.t.general.add.enableLayer.Subs({ name: selectedPreset.layer.name })} />
</button>
</div>
{:else if $layerHasFilters}
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
<div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8" />
<Tr t={Translations.t.general.add.disableFiltersExplanation} />
</div>
<div class="flex flex-wrap-reverse md:flex-nowrap">
<button
class="primary flex w-full gap-x-1"
on:click={() => {
>
<EyeIcon class="w-12"/>
<Tr t={Translations.t.general.add.enableLayer.Subs({ name: selectedPreset.layer.name })}/>
</button>
</div>
{:else if $layerHasFilters}
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
<div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8"/>
<Tr t={Translations.t.general.add.disableFiltersExplanation}/>
</div>
<div class="flex flex-wrap-reverse md:flex-nowrap">
<button
class="primary flex w-full gap-x-1"
on:click={() => {
abort()
state.layerState.filteredLayers.get(selectedPreset.layer.id).disableAllFilters()
}}
>
<EyeOffIcon class="w-12" />
<Tr t={Translations.t.general.add.disableFilters} />
</button>
<button
class="flex w-full gap-x-1"
on:click={() => {
>
<EyeOffIcon class="w-12"/>
<Tr t={Translations.t.general.add.disableFilters}/>
</button>
<button
class="flex w-full gap-x-1"
on:click={() => {
abort()
state.guistate.openFilterView(selectedPreset.layer)
}}
>
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
<Tr t={Translations.t.general.add.openLayerControl} />
</button>
</div>
{:else if !confirmedCategory}
<!-- Second, confirm the category -->
<h2 class="mr-12">
<Tr
t={Translations.t.general.add.confirmTitle.Subs({ title: selectedPreset.preset.title })}
/>
</h2>
>
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")}/>
<Tr t={Translations.t.general.add.openLayerControl}/>
</button>
</div>
{:else if !confirmedCategory}
<!-- Second, confirm the category -->
<h2 class="mr-12">
<Tr
t={Translations.t.general.add.confirmTitle.Subs({ title: selectedPreset.preset.title })}
/>
</h2>
<Tr t={Translations.t.general.add.confirmIntro} />
<Tr t={Translations.t.general.add.confirmIntro}/>
{#if selectedPreset.preset.description}
<Tr t={selectedPreset.preset.description} />
{/if}
{#if selectedPreset.preset.exampleImages}
<h3>
{#if selectedPreset.preset.exampleImages.length === 1}
<Tr t={Translations.t.general.example} />
{:else}
<Tr t={Translations.t.general.examples} />
{#if selectedPreset.preset.description}
<Tr t={selectedPreset.preset.description}/>
{/if}
</h3>
<span class="flex flex-wrap items-stretch">
{#if selectedPreset.preset.exampleImages}
<h3>
{#if selectedPreset.preset.exampleImages.length === 1}
<Tr t={Translations.t.general.example}/>
{:else}
<Tr t={Translations.t.general.examples}/>
{/if}
</h3>
<span class="flex flex-wrap items-stretch">
{#each selectedPreset.preset.exampleImages as src}
<img {src} class="m-1 h-64 w-auto rounded-lg" />
<img {src} class="m-1 h-64 w-auto rounded-lg"/>
{/each}
</span>
{/if}
<TagHint
embedIn={(tags) => t.presetInfo.Subs({ tags })}
{state}
tags={new And(selectedPreset.preset.tags)}
/>
{/if}
<TagHint
embedIn={(tags) => t.presetInfo.Subs({ tags })}
{state}
tags={new And(selectedPreset.preset.tags)}
/>
<div class="flex w-full flex-wrap-reverse md:flex-nowrap">
<BackButton on:click={() => (selectedPreset = undefined)} clss="w-full">
<Tr t={t.backToSelect} />
</BackButton>
<div class="flex w-full flex-wrap-reverse md:flex-nowrap">
<BackButton on:click={() => (selectedPreset = undefined)} clss="w-full">
<Tr t={t.backToSelect}/>
</BackButton>
<NextButton on:click={() => (confirmedCategory = true)} clss="primary w-full">
<div slot="image" class="relative">
<FromHtml src={selectedPreset.icon} />
<img class="absolute bottom-0 right-0 h-4 w-4" src="./assets/svg/confirm.svg" />
<NextButton on:click={() => (confirmedCategory = true)} clss="primary w-full">
<div slot="image" class="relative">
<FromHtml src={selectedPreset.icon}/>
<img class="absolute bottom-0 right-0 h-4 w-4" src="./assets/svg/confirm.svg"/>
</div>
<div class="w-full">
<Tr t={selectedPreset.text}/>
</div>
</NextButton>
</div>
<div class="w-full">
<Tr t={selectedPreset.text} />
</div>
</NextButton>
</div>
{:else if _globalFilter?.length > 0 && _globalFilter?.length > checkedOfGlobalFilters}
<Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12" />
<SubtleButton
on:click={() => {
{:else if _globalFilter?.length > 0 && _globalFilter?.length > checkedOfGlobalFilters}
<Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12"/>
<SubtleButton
on:click={() => {
checkedOfGlobalFilters = checkedOfGlobalFilters + 1
}}
>
<img
slot="image"
src={_globalFilter[checkedOfGlobalFilters].onNewPoint?.icon ?? "./assets/svg/confirm.svg"}
class="h-12 w-12"
/>
<Tr
slot="message"
t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({
>
<img
slot="image"
src={_globalFilter[checkedOfGlobalFilters].onNewPoint?.icon ?? "./assets/svg/confirm.svg"}
class="h-12 w-12"
/>
<Tr
slot="message"
t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({
preset: selectedPreset.preset,
})}
/>
</SubtleButton>
<SubtleButton
on:click={() => {
/>
</SubtleButton>
<SubtleButton
on:click={() => {
globalFilter.setData([])
abort()
}}
>
<img slot="image" src="./assets/svg/close.svg" class="h-8 w-8" />
<Tr slot="message" t={Translations.t.general.cancel} />
</SubtleButton>
{:else if !creating}
<div class="relative w-full p-1">
<div class="h-96 max-h-screen w-full overflow-hidden rounded-xl">
<NewPointLocationInput
value={preciseCoordinate}
snappedTo={snappedToObject}
{state}
{coordinate}
targetLayer={selectedPreset.layer}
snapToLayers={selectedPreset.preset.preciseInput.snapToLayers}
/>
</div>
<div class="absolute bottom-0 left-0 p-4">
<MapControlButton
on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}
>
<Square3Stack3dIcon class="h-6 w-6" />
</MapControlButton>
</div>
</div>
<div class="flex flex-wrap-reverse md:flex-nowrap">
<BackButton on:click={() => (selectedPreset = undefined)} clss="w-full">
<Tr t={t.backToSelect} />
</BackButton>
<img slot="image" src="./assets/svg/close.svg" class="h-8 w-8"/>
<Tr slot="message" t={Translations.t.general.cancel}/>
</SubtleButton>
{:else if !creating}
<div class="relative w-full p-1">
<div class="h-96 max-h-screen w-full overflow-hidden rounded-xl">
<NewPointLocationInput
on:click={() => {preciseInputIsTapped = true}}
value={preciseCoordinate}
snappedTo={snappedToObject}
{state}
{coordinate}
targetLayer={selectedPreset.layer}
snapToLayers={selectedPreset.preset.preciseInput.snapToLayers}
/>
</div>
<NextButton on:click={confirm} clss="primary w-full">
<div class="flex w-full justify-end gap-x-2">
<Tr t={Translations.t.general.add.confirmLocation} />
<div class={(preciseInputIsTapped ? "" : "hidden") + " absolute top-4 flex justify-center w-full"}>
<NextButton on:click={confirm} clss="primary w-fit ">
<div class="flex w-full justify-end gap-x-2">
<Tr t={Translations.t.general.add.confirmLocation}/>
</div>
</NextButton>
</div>
<div class="absolute bottom-0 left-0 p-4">
<MapControlButton
on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}
>
<Square3Stack3dIcon class="h-6 w-6"/>
</MapControlButton>
</div>
</div>
</NextButton>
</div>
{:else}
<Loading>Creating point...</Loading>
{/if}
<div class="flex flex-wrap-reverse md:flex-nowrap">
<BackButton on:click={() => (selectedPreset = undefined)} clss="w-full">
<Tr t={t.backToSelect}/>
</BackButton>
<NextButton on:click={confirm} clss={"primary w-full"}>
<div class="flex w-full justify-end gap-x-2">
<Tr t={Translations.t.general.add.confirmLocation}/>
</div>
</NextButton>
</div>
{:else}
<Loading>Creating point...</Loading>
{/if}
</LoginToggle>

View file

@ -670,6 +670,10 @@ video {
position: fixed;
}
.\!fixed {
position: fixed !important;
}
.absolute {
position: absolute;
}
@ -795,10 +799,6 @@ video {
margin: 1px;
}
.m-12 {
margin: 3rem;
}
.mx-1 {
margin-left: 0.25rem;
margin-right: 0.25rem;
@ -1242,6 +1242,27 @@ video {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@-webkit-keyframes ping {
75%, 100% {
-webkit-transform: scale(2);
transform: scale(2);
opacity: 0;
}
}
@keyframes ping {
75%, 100% {
-webkit-transform: scale(2);
transform: scale(2);
opacity: 0;
}
}
.animate-ping {
-webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}
.cursor-pointer {
cursor: pointer;
}
@ -1730,11 +1751,6 @@ video {
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.text-green-600 {
--tw-text-opacity: 1;
color: rgb(22 163 74 / var(--tw-text-opacity));
}
.underline {
text-decoration-line: underline;
}