Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-01-16 04:27:59 +01:00
commit c672fe7668
138 changed files with 14304 additions and 1299 deletions

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import { createEventDispatcher, onDestroy } from "svelte"
import { twMerge } from "tailwind-merge"
export let accept: string
@ -9,10 +9,52 @@
export let cls: string = ""
let drawAttention = false
let inputElement: HTMLInputElement
let id = Math.random() * 1000000000 + ""
let formElement: HTMLFormElement
let id = "fileinput_" + Math.round(Math.random() * 1000000000000)
function handleDragEvent(e: DragEvent) {
if (e.target["id"] == id) {
return
}
if(formElement.contains(e.target) || document.getElementsByClassName("selected-element-view")[0]?.contains(e.target)){
e.preventDefault()
if(e.type === "drop"){
console.log("Got a 'drop'", e)
drawAttention = false
dispatcher("submit", e.dataTransfer.files)
return
}
drawAttention = true
e.dataTransfer.dropEffect = "copy"
return
/*
drawAttention = false
dispatcher("submit", e.dataTransfer.files)
console.log("Committing")*/
}
drawAttention = false
e.preventDefault()
e.dataTransfer.effectAllowed = "none"
e.dataTransfer.dropEffect = "none"
}
window.addEventListener("dragenter", handleDragEvent)
window.addEventListener("dragover", handleDragEvent)
window.addEventListener("drop", handleDragEvent)
onDestroy(() => {
window.removeEventListener("dragenter", handleDragEvent)
window.removeEventListener("dragover", handleDragEvent)
window.removeEventListener("drop", handleDragEvent)
})
</script>
<form
bind:this={formElement}
on:change|preventDefault={() => {
drawAttention = false
dispatcher("submit", inputElement.files)
@ -24,27 +66,24 @@
on:dragenter|preventDefault|stopPropagation={(e) => {
console.log("Dragging enter")
drawAttention = true
e.dataTransfer.drop = "copy"
e.dataTransfer.dropEffect = "copy"
}}
on:dragstart={() => {
console.log("DragStart")
drawAttention = false
}}
on:drop|preventDefault|stopPropagation={(e) => {
console.log("Got a 'drop'")
drawAttention = false
dispatcher("submit", e.dataTransfer.files)
}}
>
<label
class={twMerge(cls, drawAttention ? "glowing-shadow" : "")}
style="margin-left:0"
tabindex="0"
for={"fileinput" + id}
for={id}
on:click={() => {
console.log("Clicked", inputElement)
inputElement.click()
}}
style="margin-left:0"
tabindex="0"
>
<slot />
</label>
@ -52,7 +91,7 @@
{accept}
bind:this={inputElement}
class="hidden"
id={"fileinput" + id}
{id}
{multiple}
name="file-input"
type="file"

View file

@ -28,7 +28,7 @@
style="z-index: 21"
use:trapFocus
>
<div class="content normal-background" on:click|stopPropagation={() => {}}>
<div class="h-full content normal-background" on:click|stopPropagation={() => {}}>
<div class="h-full rounded-xl">
<slot />
</div>
@ -47,7 +47,6 @@
<style>
.content {
height: 100%;
border-radius: 0.5rem;
overflow-x: hidden;
box-shadow: 0 0 1rem #00000088;

View file

@ -2,6 +2,8 @@ import { VariableUiElement } from "./VariableUIElement"
import Locale from "../i18n/Locale"
import Link from "./Link"
import Svg from "../../Svg"
import SvelteUIElement from "./SvelteUIElement"
import Translate from "../../assets/svg/Translate.svelte"
/**
* The little 'translate'-icon next to every icon + some static helper functions
@ -20,7 +22,7 @@ export default class LinkToWeblate extends VariableUiElement {
if (context === undefined || context.indexOf(":") < 0) {
return undefined
}
const icon = Svg.translate_svg().SetClass(
const icon = new SvelteUIElement(Translate).SetClass(
"rounded-full inline-block w-3 h-3 ml-1 weblate-link self-center"
)
if (availableTranslations[ln] === undefined) {

View file

@ -1,30 +0,0 @@
import BaseUIElement from "../BaseUIElement"
export class Paragraph extends BaseUIElement {
public readonly content: string | BaseUIElement
constructor(html: string | BaseUIElement) {
super()
this.content = html ?? ""
}
AsMarkdown(): string {
let c: string
if (typeof this.content !== "string") {
c = this.content.AsMarkdown()
} else {
c = this.content
}
return "\n\n" + c + "\n\n"
}
protected InnerConstructElement(): HTMLElement {
const e = document.createElement("p")
if (typeof this.content !== "string") {
e.appendChild(this.content.ConstructElement())
} else {
e.innerHTML = this.content
}
return e
}
}

View file

@ -66,32 +66,4 @@ export class SubtleButton extends UIElement {
this.SetClass(classes)
return button
}
public OnClickWithLoading(
loadingText: BaseUIElement | string,
action: () => Promise<void>
): BaseUIElement {
const state = new UIEventSource<"idle" | "running">("idle")
const button = this
button.onClick(async () => {
state.setData("running")
try {
await action()
} catch (e) {
console.error(e)
} finally {
state.setData("idle")
}
})
const loading = new Lazy(() => new Loading(loadingText))
return new VariableUiElement(
state.map((st) => {
if (st === "idle") {
return button
}
return loading
})
)
}
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
/**
* A mapcontrol button which allows the user to select a different background.
* Even though the componenet is very small, it gets it's own class as it is often reused
* Even though the component is very small, it gets it's own class as it is often reused
*/
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
import type { SpecialVisualizationState } from "../SpecialVisualization"

View file

@ -43,7 +43,7 @@
<Tr t={Translations.t.general.returnToTheMap} />
</button>
{:else}
<div class="flex h-full w-full flex-col gap-y-2 overflow-y-auto p-1 px-4" tabindex="-1">
<div class="flex h-full flex-col gap-y-2 overflow-y-auto p-1 px-4 w-full selected-element-view" tabindex="-1">
{#each $knownTagRenderings as config (config.id)}
<TagRenderingEditable
{tags}

View file

@ -0,0 +1,181 @@
<script lang="ts">
import LoginToggle from "../Base/LoginToggle.svelte"
import Translations from "../i18n/Translations"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Translation } from "../i18n/Translation"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Invalid from "../../assets/svg/Invalid.svelte"
import Tr from "../Base/Tr.svelte"
import Confirm from "../../assets/svg/Confirm.svelte"
import Upload from "../../assets/svg/Upload.svelte"
import Loading from "../Base/Loading.svelte"
import Close from "../../assets/svg/Close.svelte"
import { placeholder } from "../../Utils/placeholder"
import { ariaLabel } from "../../Utils/ariaLabel"
import { selectDefault } from "../../Utils/selectDefault"
export let trace: (title: string) => string
export let state: {
layout: LayoutConfig
osmConnection: OsmConnection
readonly featureSwitchUserbadge: Store<boolean>
}
export let options: {
whenUploaded?: () => void | Promise<void>
} = undefined
let t = Translations.t.general.uploadGpx
let currentStep = new UIEventSource<"init" | "please_confirm" | "uploading" | "done" | "error">("init")
let traceVisibilities: {
key: "private" | "public"
name: Translation
docs: Translation
}[] = [
{
key: "private",
...t.modes.private,
},
{
key: "public",
...t.modes.public,
},
]
let gpxServerIsOnline: Store<boolean> = state.osmConnection.gpxServiceIsOnline.map((serviceState) => serviceState === "online")
/**
* More or less the same as the coalescing-operator '??', except that it checks for empty strings too
*/
function createDefault(s: string, defaultValue: string): string {
if (defaultValue.length < 1) {
throw "Default value should have some characters"
}
if (s === undefined || s === null || s === "") {
return defaultValue
}
return s
}
let title: string = undefined
let description: string = undefined
let visibility = <UIEventSource<"public" | "private">>state?.osmConnection?.GetPreference("gps.trace.visibility") ?? new UIEventSource<"public" | "private">("private")
async function uploadTrace() {
try {
currentStep.setData("uploading")
const titleStr = createDefault(
title,
"Track with mapcomplete",
)
const descriptionStr = createDefault(
description,
"Track created with MapComplete with theme " + state?.layout?.id,
)
await state?.osmConnection?.uploadGpxTrack(trace(titleStr), {
visibility: visibility.data ?? "private",
description: descriptionStr,
filename: titleStr + ".gpx",
labels: ["MapComplete", state?.layout?.id],
})
if (options?.whenUploaded !== undefined) {
await options.whenUploaded()
}
currentStep.setData("done")
} catch (e) {
currentStep.setData("error")
console.error(e)
}
}
</script>
<LoginToggle {state}>
{#if !$gpxServerIsOnline}
<div class="flex border alert items-center">
<Invalid class="w-8 h-8 m-2" />
<Tr t={t.gpxServiceOffline} cls="p-2" />
</div>
{:else if $currentStep === "error"}
<div class="alert flex w-full gap-x-2">
<Invalid class="w-8 h-8"/>
<Tr t={Translations.t.general.error} />
</div>
{:else if $currentStep === "init"}
<button class="flex w-full m-0" on:click={() => {currentStep.setData("please_confirm")}}>
<Upload class="w-12 h-12" />
<Tr t={t.title} />
</button>
{:else if $currentStep === "please_confirm"}
<form on:submit|preventDefault={() => uploadTrace()}
class="flex flex-col border-interactive interactive px-2 gap-y-1">
<h2>
<Tr t={t.title} />
</h2>
<Tr t={t.intro0} />
<Tr t={t.intro1} />
<h3>
<Tr t={t.meta.title} />
</h3>
<Tr t={t.meta.intro} />
<input type="text" use:ariaLabel={t.meta.titlePlaceholder} use:placeholder={t.meta.titlePlaceholder}
bind:value={title} />
<Tr t={t.meta.descriptionIntro} />
<textarea use:ariaLabel={t.meta.descriptionPlaceHolder} use:placeholder={t.meta.descriptionPlaceHolder}
bind:value={description} />
<Tr t={t.choosePermission} />
{#each traceVisibilities as option}
<label>
<input
type="radio"
name="visibility"
value={option.key}
bind:group={$visibility}
use:selectDefault={visibility}
/>
<Tr t={option.name} cls="font-bold" />
-
<Tr t={option.docs} />
</label>
{/each}
<div class="flex flex-wrap-reverse justify-between items-stretch">
<button class="flex gap-x-2 w-1/2 flex-grow" on:click={() => currentStep.setData("init")}>
<Close class="w-8 h-8" />
<Tr t={Translations.t.general.cancel} />
</button>
<button class="flex gap-x-2 primary flex-grow" on:click={() => uploadTrace()}>
<Upload class="w-8 h-8" />
<Tr t={t.confirm} />
</button>
</div>
</form>
{:else if $currentStep === "uploading"}
<Loading>
<Tr t={t.uploading} />
</Loading>
{:else if $currentStep === "done"}
<div class="flex p-2 rounded-xl border-2 subtle-border items-center">
<Confirm class="w-12 h-12 mr-2" />
<Tr t={t.uploadFinished} />
</div>
{/if}
</LoginToggle>

View file

@ -1,153 +0,0 @@
import Toggle from "../Input/Toggle"
import { RadioButton } from "../Input/RadioButton"
import { FixedInputElement } from "../Input/FixedInputElement"
import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import { TextField } from "../Input/TextField"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Title from "../Base/Title"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Translation } from "../i18n/Translation"
import { LoginToggle } from "../Popup/LoginButton"
import SvelteUIElement from "../Base/SvelteUIElement"
import Upload from "../../assets/svg/Upload.svelte"
export default class UploadTraceToOsmUI extends LoginToggle {
constructor(
trace: (title: string) => string,
state: {
layout: LayoutConfig
osmConnection: OsmConnection
readonly featureSwitchUserbadge: Store<boolean>
},
options?: {
whenUploaded?: () => void | Promise<void>
}
) {
const t = Translations.t.general.uploadGpx
const uploadFinished = new UIEventSource(false)
const traceVisibilities: {
key: "private" | "public"
name: Translation
docs: Translation
}[] = [
{
key: "private",
...t.modes.private,
},
{
key: "public",
...t.modes.public,
},
]
const dropdown = new RadioButton<"private" | "public">(
traceVisibilities.map(
(tv) =>
new FixedInputElement<"private" | "public">(
new Combine([
Translations.W(tv.name).SetClass("font-bold"),
tv.docs,
]).SetClass("flex flex-col"),
tv.key
)
),
{
value: <any>state?.osmConnection?.GetPreference("gps.trace.visibility"),
}
)
const description = new TextField({
placeholder: t.meta.descriptionPlaceHolder,
})
const title = new TextField({
placeholder: t.meta.titlePlaceholder,
})
const clicked = new UIEventSource<boolean>(false)
const confirmPanel = new Combine([
new Title(t.title),
t.intro0,
t.intro1,
t.choosePermission,
dropdown,
new Title(t.meta.title, 4),
t.meta.intro,
title,
t.meta.descriptionIntro,
description,
new Combine([
new SubtleButton(Svg.close_svg(), Translations.t.general.cancel)
.onClick(() => {
clicked.setData(false)
})
.SetClass(""),
new SubtleButton(new SvelteUIElement(Upload, {}), t.confirm).OnClickWithLoading(
t.uploading,
async () => {
const titleStr = UploadTraceToOsmUI.createDefault(
title.GetValue().data,
"Track with mapcomplete"
)
const descriptionStr = UploadTraceToOsmUI.createDefault(
description.GetValue().data,
"Track created with MapComplete with theme " + state?.layout?.id
)
await state?.osmConnection?.uploadGpxTrack(trace(title.GetValue().data), {
visibility: dropdown.GetValue().data,
description: descriptionStr,
filename: titleStr + ".gpx",
labels: ["MapComplete", state?.layout?.id],
})
if (options?.whenUploaded !== undefined) {
await options.whenUploaded()
}
uploadFinished.setData(true)
}
),
]).SetClass("flex flex-wrap flex-wrap-reverse justify-between items-stretch"),
]).SetClass("flex flex-col p-4 rounded border-2 m-2 border-subtle")
super(
new Toggle(
new Toggle(
new Combine([
Svg.confirm_svg().SetClass("w-12 h-12 mr-2"),
t.uploadFinished,
]).SetClass("flex p-2 rounded-xl border-2 subtle-border items-center"),
new Toggle(
confirmPanel,
new SubtleButton(new SvelteUIElement(Upload), t.title)
.onClick(() => clicked.setData(true))
.SetClass("w-full"),
clicked
),
uploadFinished
),
new Combine([
Svg.invalid_svg().SetClass("w-8 h-8 m-2"),
t.gpxServiceOffline.SetClass("p-2"),
]).SetClass("flex border alert items-center"),
state.osmConnection.gpxServiceIsOnline.map(
(serviceState) => serviceState === "online"
)
),
undefined,
state
)
}
private static createDefault(s: string, defaultValue: string) {
if (defaultValue.length < 1) {
throw "Default value should have some characters"
}
if (s === undefined || s === null || s === "") {
return defaultValue
}
return s
}
}

View file

@ -5,12 +5,16 @@
import panzoom from "panzoom"
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import { UIEventSource } from "../../Logic/UIEventSource"
import Zoomcontrol from "../Zoomcontrol"
import { onDestroy, onMount } from "svelte"
export let image: ProvidedImage
let panzoomInstance = undefined
let panzoomEl: HTMLElement
export let isLoaded: UIEventSource<boolean> = undefined
onDestroy(Zoomcontrol.createLock())
$: {
if (panzoomEl) {
panzoomInstance = panzoom(panzoomEl, {

View file

@ -16,7 +16,7 @@
import Translations from "../i18n/Translations"
import LoginToggle from "../Base/LoginToggle.svelte"
export let tags: Store<OsmTags>
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
export let lon: number
export let lat: number

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Store } from "../../Logic/UIEventSource"
import { UIEventSource } from "../../Logic/UIEventSource"
import type { OsmTags } from "../../Models/OsmFeature"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { Feature } from "geojson"
@ -12,7 +12,7 @@
import LoginToggle from "../Base/LoginToggle.svelte"
import { ariaLabel } from "../../Utils/ariaLabel"
export let tags: Store<OsmTags>
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
export let lon: number
export let lat: number

View file

@ -65,7 +65,7 @@
{/if}
{#if $failed > 0}
<div class="alert flex flex-col">
{#if failed === 1}
{#if $failed === 1}
<Tr cls="self-center" t={t.upload.one.failed} />
{:else}
<Tr cls="self-center" t={t.upload.multiple.someFailed.Subs({ count: $failed })} />

View file

@ -51,6 +51,15 @@
}
}
function onKeyPress(e: KeyboardEvent){
if(e.key === "Enter"){
e.stopPropagation()
e.preventDefault()
dispatch("submit")
}
}
initValueAndDenom()
$: {
@ -126,7 +135,7 @@
let htmlElem: HTMLInputElement | HTMLTextAreaElement
let dispatch = createEventDispatcher<{ selected }>()
let dispatch = createEventDispatcher<{ selected, submit }>()
$: {
if (htmlElem !== undefined) {
htmlElem.onfocus = () => dispatch("selected")
@ -144,6 +153,7 @@
inputmode={validator?.inputmode ?? "text"}
placeholder={_placeholder}
bind:this={htmlElem}
on:keypress={onKeyPress}
/>
{:else}
<div class={twMerge("inline-flex", cls)}>
@ -153,6 +163,7 @@
class="w-full"
inputmode={validator?.inputmode ?? "text"}
placeholder={_placeholder}
on:keypress={onKeyPress}
/>
{#if !$isValid}
<ExclamationIcon class="-ml-6 h-6 w-6" />

View file

@ -13,7 +13,7 @@
let iconItem = icon.icon?.GetRenderValue($tags)?.Subs($tags)?.txt
$: iconItem = icon.icon?.GetRenderValue($tags)?.Subs($tags)?.txt
let color = icon.color?.GetRenderValue(tags)?.txt ?? "#000000"
let color = icon.color?.GetRenderValue($tags)?.txt ?? "#000000"
$: color = icon.color?.GetRenderValue($tags)?.txt ?? "#000000"
</script>

View file

@ -1,47 +1,48 @@
<script lang="ts">
import Pin from "../../assets/svg/Pin.svelte"
import Square from "../../assets/svg/Square.svelte"
import Circle from "../../assets/svg/Circle.svelte"
import Checkmark from "../../assets/svg/Checkmark.svelte"
import Clock from "../../assets/svg/Clock.svelte"
import Close from "../../assets/svg/Close.svelte"
import Crosshair from "../../assets/svg/Crosshair.svelte"
import Help from "../../assets/svg/Help.svelte"
import Home from "../../assets/svg/Home.svelte"
import Invalid from "../../assets/svg/Invalid.svelte"
import Location from "../../assets/svg/Location.svelte"
import Location_empty from "../../assets/svg/Location_empty.svelte"
import Location_locked from "../../assets/svg/Location_locked.svelte"
import Note from "../../assets/svg/Note.svelte"
import Resolved from "../../assets/svg/Resolved.svelte"
import Ring from "../../assets/svg/Ring.svelte"
import Scissors from "../../assets/svg/Scissors.svelte"
import Teardrop from "../../assets/svg/Teardrop.svelte"
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"
import Triangle from "../../assets/svg/Triangle.svelte"
import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte"
import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte"
import Gps_arrow from "../../assets/svg/Gps_arrow.svelte"
import { HeartIcon } from "@babeard/svelte-heroicons/solid"
import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline"
import Confirm from "../../assets/svg/Confirm.svelte"
import Not_found from "../../assets/svg/Not_found.svelte"
import { twMerge } from "tailwind-merge"
import Direction_gradient from "../../assets/svg/Direction_gradient.svelte"
import Mastodon from "../../assets/svg/Mastodon.svelte"
import Party from "../../assets/svg/Party.svelte"
import AddSmall from "../../assets/svg/AddSmall.svelte"
import { LinkIcon } from "@babeard/svelte-heroicons/mini"
import Pin from "../../assets/svg/Pin.svelte"
import Square from "../../assets/svg/Square.svelte"
import Circle from "../../assets/svg/Circle.svelte"
import Checkmark from "../../assets/svg/Checkmark.svelte"
import Clock from "../../assets/svg/Clock.svelte"
import Close from "../../assets/svg/Close.svelte"
import Crosshair from "../../assets/svg/Crosshair.svelte"
import Help from "../../assets/svg/Help.svelte"
import Home from "../../assets/svg/Home.svelte"
import Invalid from "../../assets/svg/Invalid.svelte"
import Location from "../../assets/svg/Location.svelte"
import Location_empty from "../../assets/svg/Location_empty.svelte"
import Location_locked from "../../assets/svg/Location_locked.svelte"
import Note from "../../assets/svg/Note.svelte"
import Resolved from "../../assets/svg/Resolved.svelte"
import Ring from "../../assets/svg/Ring.svelte"
import Scissors from "../../assets/svg/Scissors.svelte"
import Teardrop from "../../assets/svg/Teardrop.svelte"
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"
import Triangle from "../../assets/svg/Triangle.svelte"
import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte"
import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte"
import Gps_arrow from "../../assets/svg/Gps_arrow.svelte"
import { HeartIcon } from "@babeard/svelte-heroicons/solid"
import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline"
import Confirm from "../../assets/svg/Confirm.svelte"
import Not_found from "../../assets/svg/Not_found.svelte"
import { twMerge } from "tailwind-merge"
import Direction_gradient from "../../assets/svg/Direction_gradient.svelte"
import Mastodon from "../../assets/svg/Mastodon.svelte"
import Party from "../../assets/svg/Party.svelte"
import AddSmall from "../../assets/svg/AddSmall.svelte"
import { LinkIcon } from "@babeard/svelte-heroicons/mini"
/**
* Renders a single icon.
*
* Icons -placed on top of each other- form a 'Marker' together
*/
/**
* Renders a single icon.
*
* Icons -placed on top of each other- form a 'Marker' together
*/
export let icon: string | undefined
export let color: string | undefined = undefined
export let clss: string | undefined = undefined
export let icon: string | undefined
export let color: string | undefined = undefined
export let clss: string | undefined = undefined
</script>
{#if icon}
@ -100,9 +101,9 @@
{:else if icon === "invalid"}
<Invalid {color} class={clss} />
{:else if icon === "heart"}
<HeartIcon class={clss} />
<HeartIcon style="--svg-color: {color}" class={twMerge(clss,"apply-fill")} />
{:else if icon === "heart_outline"}
<HeartOutlineIcon class={clss} />
<HeartOutlineIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} />
{:else if icon === "confirm"}
<Confirm class={clss} {color} />
{:else if icon === "direction"}
@ -115,9 +116,10 @@
<Party {color} class={clss} />
{:else if icon === "addSmall"}
<AddSmall {color} class={clss} />
{:else if icon === "link"}
<LinkIcon class={clss}/>
{:else if icon === "link"}
<LinkIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")}/>
{:else}
<img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true" alt="" />
{/if}
{/if}

View file

@ -551,7 +551,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
}
}
private async setBackground(): Promise<void> {
private async setBackground(retryAttempts: number = 3): Promise<void> {
const map = this._maplibreMap.data
if (!map) {
return
@ -585,12 +585,23 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
} else {
// Make sure that the default maptiler style is loaded as it gives an overlay with roads
const maptiler = AvailableRasterLayers.maptilerDefaultLayer.properties
try {
await this.awaitStyleIsLoaded()
if (!map.getSource(maptiler.id)) {
this.removeCurrentLayer(map)
map.addSource(maptiler.id, MapLibreAdaptor.prepareWmsSource(maptiler))
map.setStyle(maptiler.url)
await this.awaitStyleIsLoaded()
}
}catch (e) {
if(retryAttempts > 0){
window.requestAnimationFrame(() => {
console.log("Retrying to set the background ("+retryAttempts+" attempts remaining)... Failed because",e)
this.setBackground(retryAttempts-1)
})
}
}
}
if (!map.getLayer(addLayerBeforeId)) {

View file

@ -76,7 +76,7 @@
</script>
{#if hasLayers}
<form class="flex h-full w-full flex-col">
<form class="flex h-full w-full flex-col" on:submit|preventDefault={() => {}}>
<button
tabindex="-1"
on:click={() => apply()}

View file

@ -20,6 +20,7 @@
import ToSvelte from "../../Base/ToSvelte.svelte"
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import FilteredLayer from "../../../Models/FilteredLayer"
import Confirm from "../../../assets/svg/Confirm.svelte"
export let importFlow: ImportFlow<ImportFlowArguments>
let state = importFlow.state
@ -159,7 +160,7 @@
{#if importFlow.args.icon}
<img src={importFlow.args.icon} />
{:else}
<ToSvelte construct={Svg.confirm_svg().SetClass("w-8 h-8 pr-4")} />
<Confirm class="w-8 h-8 pr-4"/>
{/if}
</span>
<slot name="confirm-text">

View file

@ -1,4 +1,3 @@
import Svg from "../../Svg"
import { UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import { Translation } from "../i18n/Translation"
@ -10,6 +9,9 @@ import { And } from "../../Logic/Tags/And"
import { Tag } from "../../Logic/Tags/Tag"
import { SpecialVisualizationState } from "../SpecialVisualization"
import { Feature, Point } from "geojson"
import SvelteUIElement from "../Base/SvelteUIElement"
import Confirm from "../../assets/svg/Confirm.svelte"
import Relocation from "../../assets/svg/Relocation.svelte"
export interface MoveReason {
text: Translation | string
@ -32,10 +34,7 @@ export class MoveWizardState {
constructor(id: string, options: MoveConfig, state: SpecialVisualizationState) {
this._state = state
const t = Translations.t.move
this.reasons = MoveWizardState.initReasons(options)
if (this.reasons.length > 0) {
this.checkIsAllowed(id)
}
@ -49,7 +48,7 @@ export class MoveWizardState {
reasons.push({
text: t.reasons.reasonRelocation,
invitingText: t.inviteToMove.reasonRelocation,
icon: Svg.relocation_svg(),
icon: new SvelteUIElement(Relocation),
changesetCommentValue: "relocated",
lockBounds: false,
background: undefined,
@ -63,7 +62,7 @@ export class MoveWizardState {
reasons.push({
text: t.reasons.reasonInaccurate,
invitingText: t.inviteToMove.reasonInaccurate,
icon: Svg.crosshair_svg(),
icon: new SvelteUIElement(Confirm),
changesetCommentValue: "improve_accuracy",
lockBounds: true,
includeSearch: false,

View file

@ -9,6 +9,8 @@ import { LoginToggle } from ".././LoginButton"
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
import { UIEventSource } from "../../../Logic/UIEventSource"
import Constants from "../../../Models/Constants"
import SvelteUIElement from "../../Base/SvelteUIElement"
import Checkmark from "../../../assets/svg/Checkmark.svelte"
export class CloseNoteButton implements SpecialVisualization {
public readonly funcName = "close_note"
@ -61,7 +63,7 @@ export class CloseNoteButton implements SpecialVisualization {
zoomButton: string
} = <any>Utils.ParseVisArgs(this.args, args)
let icon = Svg.checkmark_svg()
let icon: BaseUIElement = new SvelteUIElement(Checkmark)
if (params.icon !== "checkmark.svg" && (args[2] ?? "") !== "") {
icon = new Img(args[1])
}

View file

@ -4,7 +4,7 @@
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import TagRenderingAnswer from "./TagRenderingAnswer.svelte"
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { PencilAltIcon } from "@rgossiaux/svelte-heroicons/solid"
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
import { onDestroy } from "svelte"
import Tr from "../../Base/Tr.svelte"
@ -27,12 +27,12 @@
/**
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
*/
export let editMode = !config.IsKnown(tags.data) // || showQuestionIfUnknown;
export let editMode = !config.IsKnown(tags.data)
if (tags) {
onDestroy(
tags.addCallbackD((tags) => {
editMode = !config.IsKnown(tags)
})
}),
)
}

View file

@ -159,7 +159,7 @@
}
}
function onSave() {
function onSave(e) {
if (selectedTags === undefined) {
return
}
@ -198,7 +198,9 @@
function onInputKeypress(e: KeyboardEvent) {
if (e.key === "Enter") {
onSave()
e.preventDefault()
e.stopPropagation()
onSave(e)
}
}

View file

@ -1,8 +1,9 @@
import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import { GeoOperations } from "../../Logic/GeoOperations"
import Constants from "../../Models/Constants"
import SvelteUIElement from "../Base/SvelteUIElement"
import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI.svelte"
/**
* Wrapper around 'UploadTraceToOsmUI'
@ -20,10 +21,9 @@ export class UploadToOsmViz implements SpecialVisualization {
__: string[]
) {
const locations = state.historicalUserLocations.features.data
return new UploadTraceToOsmUI((title) => GeoOperations.toGpx(locations, title), state, {
whenUploaded: async () => {
state.historicalUserLocations.features.setData([])
},
return new SvelteUIElement(UploadTraceToOsmUI, {
state,
trace: (title: string) => GeoOperations.toGpx(locations, title)
})
}
}

View file

@ -41,7 +41,6 @@
<TagRenderingEditable
{config}
selectedElement={undefined}
showQuestionIfUnknown={true}
{state}
{tags}
/>

View file

@ -172,6 +172,7 @@ export abstract class EditJsonState<T> {
public setValueAt(path: ReadonlyArray<string | number>, v: any) {
let entry = this.configuration.data
console.trace("Setting value at", path,"to",v)
const isUndefined =
v === undefined ||
v === null ||

View file

@ -117,7 +117,6 @@
selectedElement={state.exampleFeature}
{config}
editingEnabled={new ImmutableStore(true)}
showQuestionIfUnknown={true}
{state}
{tags}
/>

View file

@ -147,6 +147,9 @@
return { ...<object>v }
}
if (schema.type === "boolean") {
if(v === null || v === undefined){
return v
}
return v === "true" || v === "yes" || v === "1"
}
if (mightBeBoolean(schema.type)) {
@ -187,7 +190,6 @@
editMode={startInEditMode}
{config}
selectedElement={undefined}
showQuestionIfUnknown={true}
{state}
{tags}
/>

View file

@ -215,7 +215,6 @@
<TagRenderingEditable
{config}
selectedElement={undefined}
showQuestionIfUnknown={!schema.hints?.ifunset}
{state}
{tags}
/>

View file

@ -139,7 +139,6 @@
<TagRenderingEditable
config={configBuiltin}
selectedElement={undefined}
showQuestionIfUnknown={true}
{state}
{tags}
/>

View file

@ -1,16 +1,7 @@
<script lang="ts">
// Testing grounds
import Motion from "../Sensors/Motion"
import { Store, Stores } from "../Logic/UIEventSource"
let maxAcc = Motion.singleton.maxAcc
let shaken = Motion.singleton.lastShakeEvent
let recentlyShaken = Stores.Chronic(250).mapD(
(now) => now.getTime() - 3000 < shaken.data?.getTime()
)
import Icon from "./Map/Icon.svelte"
</script>
Acc: {$maxAcc}
{#if $recentlyShaken}
<div class="text-5xl text-red-500">SHAKEN</div>
{/if}
<Icon clss="h-16 w-16" icon="heart" color="#ff0000"/>

View file

@ -96,6 +96,9 @@
if (element.properties.id.startsWith("current_view")) {
return currentViewLayer
}
if(element.properties.id === "location_track"){
return layout.layers.find(l => l.id === "gps_track")
}
return state.layout.getMatchingLayer(element.properties)
},
)
@ -392,7 +395,7 @@
<!-- Floatover with the selected element, if applicable -->
<FloatOver
on:close={() => {
selectedElement.setData(undefined)
state.selectedElement.setData(undefined)
}}
>
<div class="flex h-full w-full">

View file

@ -9,9 +9,15 @@ import Combine from "../Base/Combine"
import Img from "../Base/Img"
import { WikimediaImageProvider } from "../../Logic/ImageProviders/WikimediaImageProvider"
import Link from "../Base/Link"
import Svg from "../../Svg"
import BaseUIElement from "../BaseUIElement"
import { Utils } from "../../Utils"
import SvelteUIElement from "../Base/SvelteUIElement"
import * as Wikidata_icon from "../../assets/svg/Wikidata.svelte"
import Gender_male from "../../assets/svg/Gender_male.svelte"
import Gender_female from "../../assets/svg/Gender_female.svelte"
import Gender_inter from "../../assets/svg/Gender_inter.svelte"
import Gender_trans from "../../assets/svg/Gender_trans.svelte"
import Gender_queer from "../../assets/svg/Gender_queer.svelte"
export default class WikidataPreviewBox extends VariableUiElement {
private static isHuman = [{ p: 31 /*is a*/, q: 5 /* human */ }]
@ -28,22 +34,36 @@ export default class WikidataPreviewBox extends VariableUiElement {
requires: WikidataPreviewBox.isHuman,
property: "P21",
display: new Map([
["Q6581097", () => Svg.gender_male_svg().SetStyle("width: 1rem; height: auto")],
["Q6581072", () => Svg.gender_female_svg().SetStyle("width: 1rem; height: auto")],
["Q1097630", () => Svg.gender_inter_svg().SetStyle("width: 1rem; height: auto")],
[
"Q6581097",
() => new SvelteUIElement(Gender_male).SetStyle("width: 1rem; height: auto"),
],
[
"Q6581072",
() => new SvelteUIElement(Gender_female).SetStyle("width: 1rem; height: auto"),
],
[
"Q1097630",
() => new SvelteUIElement(Gender_inter).SetStyle("width: 1rem; height: auto"),
],
[
"Q1052281",
() =>
Svg.gender_trans_svg().SetStyle(
new SvelteUIElement(Gender_trans).SetStyle(
"width: 1rem; height: auto"
) /*'transwomen'*/,
],
[
"Q2449503",
() =>
Svg.gender_trans_svg().SetStyle("width: 1rem; height: auto") /*'transmen'*/,
new SvelteUIElement(Gender_trans).SetStyle(
"width: 1rem; height: auto"
) /*'transmen'*/,
],
[
"Q48270",
() => new SvelteUIElement(Gender_queer).SetStyle("width: 1rem; height: auto"),
],
["Q48270", () => Svg.gender_queer_svg().SetStyle("width: 1rem; height: auto")],
]),
textMode: new Map([
["Q6581097", "♂️"],
@ -116,7 +136,9 @@ export default class WikidataPreviewBox extends VariableUiElement {
wikidata.id,
options?.noImages
? wikidata.id
: Svg.wikidata_svg().SetStyle("width: 2.5rem").SetClass("block"),
: new SvelteUIElement(Wikidata_icon)
.SetStyle("width: 2.5rem")
.SetClass("block"),
]).SetClass("flex"),
Wikidata.IdToArticle(wikidata.id),
true

74
src/UI/Zoomcontrol.ts Normal file
View file

@ -0,0 +1,74 @@
import { Stores, UIEventSource } from "../Logic/UIEventSource"
import { Utils } from "../Utils"
/**
* Utilities to (re)set user zoom (this is when the user enlarges HTML-elements by pinching out a non-map element).
* If the user zooms in and goes back to the map, it should reset to 1.0
*/
export default class Zoomcontrol {
private static readonly singleton = new Zoomcontrol()
private static readonly initialValue =
"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0, user-scalable=1"
private static readonly noZoom =
"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=1"
private readonly viewportElement: HTMLMetaElement
private readonly _allowZooming: UIEventSource<boolean>
private readonly _lockTokens: Set<any> = new Set<any>()
private constructor() {
if (Utils.runningFromConsole) {
return
}
const metaElems = document.getElementsByTagName("head")[0].getElementsByTagName("meta")
this.viewportElement = Array.from(metaElems).find(
(meta) => meta.getAttribute("name") === "viewport"
)
this._allowZooming = new UIEventSource<boolean>(true)
this._allowZooming.addCallback((allowed) => {
this.apply(allowed ? Zoomcontrol.initialValue : Zoomcontrol.noZoom)
})
Stores.Chronic(1000).addCallback((_) =>
console.log(this.viewportElement.getAttribute("content"))
)
}
private _resetZoom() {
this.apply(Zoomcontrol.noZoom)
requestAnimationFrame(() => {
// Does not work on firefox, see https://bugzilla.mozilla.org/show_bug.cgi?id=1873934
this.allowZoomIfUnlocked()
})
}
private apply(fullSpec: string) {
this.viewportElement?.setAttribute("content", fullSpec)
}
public static createLock(): () => void {
return Zoomcontrol.singleton._createLock()
}
private allowZoomIfUnlocked() {
if (this._lockTokens.size > 0) {
return
}
this.apply(Zoomcontrol.initialValue)
}
private _createLock(): () => void {
const token = {}
const lockTokens = this._lockTokens
lockTokens.add(token)
this._resetZoom()
return () => {
lockTokens.delete(token)
this.allowZoomIfUnlocked()
}
}
public static resetzoom() {
this.singleton._resetZoom()
}
}