chore: automated housekeeping...

This commit is contained in:
Pieter Vander Vennet 2024-07-21 10:52:51 +02:00
parent 14b2799f08
commit 4add2d1aff
151 changed files with 4561 additions and 3315 deletions

View file

@ -38,12 +38,12 @@
<slot name="close-button">
<!-- The close button is placed _after_ the default slot in order to always paint it on top -->
<div
class="absolute right-10 top-10 cursor-pointer border-none p-0 m-0 bg-white rounded-full border-0"
class="absolute right-10 top-10 m-0 cursor-pointer rounded-full border-0 border-none bg-white p-0"
style="margin: -0.25rem"
on:click={() => dispatch("close")}
use:ariaLabel={Translations.t.general.backToMap}
>
<XCircleIcon class="w-8 h-8" />
<XCircleIcon class="h-8 w-8" />
</div>
</slot>
</div>

View file

@ -30,18 +30,18 @@ export default class Hotkeys {
public static RegisterHotkey(
key: (
| {
ctrl: string
}
ctrl: string
}
| {
shift: string
}
shift: string
}
| {
alt: string
}
alt: string
}
| {
nomod: string
}
) & {
nomod: string
}
) & {
onUp?: boolean
},
documentation: string | Translation,
@ -63,7 +63,7 @@ export default class Hotkeys {
return
}
if (key["ctrl"] !== undefined) {
document.addEventListener("keydown", function(event) {
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.key === keycode) {
if (action() !== false) {
event.preventDefault()
@ -71,7 +71,7 @@ export default class Hotkeys {
}
})
} else if (key["shift"] !== undefined) {
document.addEventListener(type, function(event) {
document.addEventListener(type, function (event) {
if (Hotkeys.textElementSelected(event)) {
// A text element is selected, we don't do anything special
return
@ -83,7 +83,7 @@ export default class Hotkeys {
}
})
} else if (key["alt"] !== undefined) {
document.addEventListener(type, function(event) {
document.addEventListener(type, function (event) {
if (event.altKey && event.key === keycode) {
if (action() !== false) {
event.preventDefault()
@ -91,7 +91,7 @@ export default class Hotkeys {
}
})
} else if (key["nomod"] !== undefined) {
document.addEventListener(type, function(event) {
document.addEventListener(type, function (event) {
if (Hotkeys.textElementSelected(event) && keycode !== "Escape") {
// A text element is selected, we don't do anything special
return
@ -106,18 +106,17 @@ export default class Hotkeys {
}
}
static prepareDocumentation(docs: {
key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean }
documentation: string | Translation
alsoTriggeredBy: Translation[]
}[]){
static prepareDocumentation(
docs: {
key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean }
documentation: string | Translation
alsoTriggeredBy: Translation[]
}[]
) {
let byKey: [string, string | Translation, Translation[] | undefined][] = docs
.map(({ key, documentation, alsoTriggeredBy }) => {
const modifiers = Object.keys(key).filter(
(k) => k !== "nomod" && k !== "onUp"
)
let keycode: string =
key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
const modifiers = Object.keys(key).filter((k) => k !== "nomod" && k !== "onUp")
let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
if (keycode.length == 1) {
keycode = keycode.toUpperCase()
}
@ -128,7 +127,7 @@ export default class Hotkeys {
return <[string, string | Translation, Translation[] | undefined]>[
modifiers.join("+"),
documentation,
alsoTriggeredBy
alsoTriggeredBy,
]
})
.sort()
@ -141,36 +140,41 @@ export default class Hotkeys {
return byKey
}
static generateDocumentationFor(docs: {
key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean }
documentation: string | Translation
alsoTriggeredBy: Translation[]
}[], language: string): string {
static generateDocumentationFor(
docs: {
key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean }
documentation: string | Translation
alsoTriggeredBy: Translation[]
}[],
language: string
): string {
const tr = Translations.t.hotkeyDocumentation
function t(t: Translation | string){
if(typeof t === "string"){
function t(t: Translation | string) {
if (typeof t === "string") {
return t
}
return t.textFor(language)
}
const contents: string[][] = this.prepareDocumentation(docs)
.map(([key, doc, alsoTriggeredBy]) => {
let keyEl: string = [key, ...(alsoTriggeredBy??[])].map(k => "`"+t(k)+"`").join(" ")
return [keyEl, t(doc)]
})
const contents: string[][] = this.prepareDocumentation(docs).map(
([key, doc, alsoTriggeredBy]) => {
let keyEl: string = [key, ...(alsoTriggeredBy ?? [])]
.map((k) => "`" + t(k) + "`")
.join(" ")
return [keyEl, t(doc)]
}
)
return [
"# "+t(tr.title),
"# " + t(tr.title),
t(tr.intro),
MarkdownUtils.table(
[t(tr.key), t(tr.action)],
contents
)
].join("\n")
MarkdownUtils.table([t(tr.key), t(tr.action)], contents),
].join("\n")
}
public static generateDocumentation(language?: string){
return Hotkeys.generateDocumentationFor(Hotkeys._docs.data, language?? Locale.language.data)
public static generateDocumentation(language?: string) {
return Hotkeys.generateDocumentationFor(
Hotkeys._docs.data,
language ?? Locale.language.data
)
}
private static textElementSelected(event: KeyboardEvent): boolean {

View file

@ -2,7 +2,7 @@ import Combine from "./Combine"
import Translations from "../i18n/Translations"
import BaseUIElement from "../BaseUIElement"
import SvelteUIElement from "./SvelteUIElement"
import {default as LoadingSvg} from "../../assets/svg/Loading.svelte"
import { default as LoadingSvg } from "../../assets/svg/Loading.svelte"
export default class Loading extends Combine {
constructor(msg?: BaseUIElement | string) {
const t = Translations.W(msg) ?? Translations.t.general.loading

View file

@ -27,7 +27,7 @@ export default class SvelteUIElement<
constructor(svelteElement, props?: Props, events?: Events, slots?: Slots) {
super()
this._svelteComponent = <any> svelteElement
this._svelteComponent = <any>svelteElement
this._props = props ?? <Props>{}
this._events = events
this._slots = slots
@ -49,15 +49,15 @@ export default class SvelteUIElement<
return el
}
public getClass(){
if(this.clss.size === 0){
public getClass() {
if (this.clss.size === 0) {
return undefined
}
return this.clss
}
public getStyle(){
if(this.style === ""){
public getStyle() {
if (this.style === "") {
return undefined
}
return this.style

View file

@ -98,7 +98,7 @@ export default class TableOfContents {
const intro = md.substring(0, firstTitleIndex)
const splitPoint = intro.lastIndexOf("\n")
return md.substring(0, splitPoint) +"\n" + toc + md.substring(splitPoint)
return md.substring(0, splitPoint) + "\n" + toc + md.substring(splitPoint)
}
public static generateStructure(

View file

@ -1,5 +1,4 @@
<script lang="ts">
import Translations from "../i18n/Translations"
</script>

View file

@ -7,14 +7,14 @@
let elem: HTMLElement
let html: HTMLElement
let isSvelte = false
let uiElement : BaseUIElement | SvelteUIElement | undefined
let uiElement: BaseUIElement | SvelteUIElement | undefined
let svelteElem: SvelteUIElement
onMount(() => {
uiElement = typeof construct === "function" ? construct() : construct
if (uiElement?.["isSvelte"]) {
isSvelte = true
svelteElem = <SvelteUIElement> uiElement
svelteElem = <SvelteUIElement>uiElement
return
}
@ -32,7 +32,12 @@
</script>
{#if isSvelte}
<svelte:component this={svelteElem?._svelteComponent} {...svelteElem._props} class={svelteElem.getClass()} style={svelteElem.getStyle()}/>
<svelte:component
this={svelteElem?._svelteComponent}
{...svelteElem._props}
class={svelteElem.getClass()}
style={svelteElem.getStyle()}
/>
{:else}
<span bind:this={elem} />
{/if}

View file

@ -53,31 +53,28 @@
<Tr t={Translations.t.general.menu.filter} />
</div>
{#each layout.layers as layer}
<Filterview
zoomlevel={state.mapProperties.zoom}
filteredLayer={state.layerState.filteredLayers.get(layer.id)}
highlightedLayer={state.guistate.highlightedLayerInFilters}
/>
{/each}
<div class="mt-1 flex self-end">
<button class="small" class:disabled={allEnabled} on:click={() => enableAll(true)}>
<Tr t={Translations.t.general.filterPanel.enableAll} />
</button>
<button class="small" class:disabled={allDisabled} on:click={() => enableAll(false)}>
<Tr t={Translations.t.general.filterPanel.disableAll} />
</button>
</div>
{#each layout.tileLayerSources as tilesource}
<OverlayToggle
layerproperties={tilesource}
state={state.overlayLayerStates.get(tilesource.id)}
highlightedLayer={state.guistate.highlightedLayerInFilters}
zoomlevel={state.mapProperties.zoom}
/>
{/each}
{#each layout.layers as layer}
<Filterview
zoomlevel={state.mapProperties.zoom}
filteredLayer={state.layerState.filteredLayers.get(layer.id)}
highlightedLayer={state.guistate.highlightedLayerInFilters}
/>
{/each}
<div class="mt-1 flex self-end">
<button class="small" class:disabled={allEnabled} on:click={() => enableAll(true)}>
<Tr t={Translations.t.general.filterPanel.enableAll} />
</button>
<button class="small" class:disabled={allDisabled} on:click={() => enableAll(false)}>
<Tr t={Translations.t.general.filterPanel.disableAll} />
</button>
</div>
{#each layout.tileLayerSources as tilesource}
<OverlayToggle
layerproperties={tilesource}
state={state.overlayLayerStates.get(tilesource.id)}
highlightedLayer={state.guistate.highlightedLayerInFilters}
zoomlevel={state.mapProperties.zoom}
/>
{/each}
</TitledPanel>

View file

@ -43,13 +43,10 @@
{#if filteredLayer.layerDef.name}
<div class:focus={$highlightedLayer === filteredLayer.layerDef.id} class="mb-1.5">
<Checkbox selected={isDisplayed}>
<div class="block h-6 w-6 no-image-background" class:opacity-50={!$isDisplayed}>
<ToSvelte
construct={() => layer.defaultIcon()}
/>
<div class="no-image-background block h-6 w-6" class:opacity-50={!$isDisplayed}>
<ToSvelte construct={() => layer.defaultIcon()} />
</div>
<Tr t={filteredLayer.layerDef.name} />
{#if $zoomlevel < layer.minzoom}

View file

@ -1,5 +1,4 @@
<script lang="ts">
import Hotkeys from "../Base/Hotkeys"
import { Translation } from "../i18n/Translation"
import { Utils } from "../../Utils"
@ -10,14 +9,13 @@
let keys = Hotkeys._docs
const t = Translations.t.hotkeyDocumentation
let byKey = Hotkeys.prepareDocumentation($keys)
$: {
byKey = Hotkeys.prepareDocumentation($keys)
}
</script>
<AccordionSingle>
<AccordionSingle>
<div slot="header">
<Tr t={t.title} />
</div>
@ -25,30 +23,27 @@
<table>
<tr>
<th>
<Tr t={t.key}></Tr>
</th>
<th>
<Tr t={t.action} />
</th>
<Tr t={t.key} />
</th>
<th>
<Tr t={t.action} />
</th>
</tr>
{#each byKey as [key, doc, alsoTriggeredBy] }
{#each byKey as [key, doc, alsoTriggeredBy]}
<tr>
<td class="flex items-center justify-center">
{#if alsoTriggeredBy}
<div class="flex items-center justify-center gap-x-1">
<div class="literal-code w-fit h-fit">{key}</div>
<div class="literal-code w-fit h-fit">{alsoTriggeredBy}</div>
<div class="literal-code h-fit w-fit">{key}</div>
<div class="literal-code h-fit w-fit">{alsoTriggeredBy}</div>
</div>
{:else}
<div class="literal-code w-fit h-fit flex items-center w-full">{key}</div>
<div class="literal-code flex h-fit w-fit w-full items-center">{key}</div>
{/if}
</td>
<td>
<Tr t={doc} />
</td>
<Tr t={doc} />
</td>
</tr>
{/each}
</table>

View file

@ -9,22 +9,19 @@
import { ariaLabel } from "../../Utils/ariaLabel"
import { Translation } from "../i18n/Translation"
const dispatch = createEventDispatcher<{search: string}>()
const dispatch = createEventDispatcher<{ search: string }>()
export let searchValue: UIEventSource<string>
export let placeholderText: Translation = Translations.t.general.search.search
export let feedback = new UIEventSource<string>(undefined)
let isRunning: boolean = false
let inputElement: HTMLInputElement
function _performSearch(){
function _performSearch() {
dispatch("search", searchValue.data)
}
</script>
<div class="normal-background flex justify-between rounded-full">
@ -32,21 +29,24 @@
{#if isRunning}
<Loading>{Translations.t.general.search.searching}</Loading>
{:else}
<div class="flex w-full border border-gray-300 rounded-full">
<input
type="search"
class="w-full outline-none mx-2"
bind:this={inputElement}
on:keypress={(keypr) => {
feedback.set(undefined)
return keypr.key === "Enter" ? _performSearch() : undefined
}}
bind:value={$searchValue}
use:placeholder={placeholderText}
use:ariaLabel={Translations.t.general.search.search}
/>
<SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={event => _performSearch()} />
<div class="flex w-full rounded-full border border-gray-300">
<input
type="search"
class="mx-2 w-full outline-none"
bind:this={inputElement}
on:keypress={(keypr) => {
feedback.set(undefined)
return keypr.key === "Enter" ? _performSearch() : undefined
}}
bind:value={$searchValue}
use:placeholder={placeholderText}
use:ariaLabel={Translations.t.general.search.search}
/>
<SearchIcon
aria-hidden="true"
class="h-6 w-6 self-end"
on:click={(event) => _performSearch()}
/>
</div>
{#if $feedback !== undefined}
<!-- The feedback is _always_ shown for screenreaders and to make sure that the searchfield can still be selected by tabbing-->

View file

@ -73,7 +73,7 @@
</div>
<slot name="close-button">
<div
class="mt-2 h-fit shrink-0 rounded-full border-none p-0 cursor-pointer"
class="mt-2 h-fit shrink-0 cursor-pointer rounded-full border-none p-0"
on:click={() => state.selectedElement.setData(undefined)}
style="border: 0 !important; padding: 0 !important;"
use:ariaLabel={Translations.t.general.backToMap}

View file

@ -22,7 +22,9 @@
selectedElement.properties.id
)
let isAddNew = tags.mapD(t => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false)
let isAddNew = tags.mapD(
(t) => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false
)
function getLayer(properties: Record<string, string>) {
if (properties.id === "settings") {
@ -81,7 +83,6 @@
class="selected-element-view flex h-full w-full flex-col gap-y-1 overflow-y-auto"
class:p1={!$isAddNew}
class:px-4={!$isAddNew}
tabindex="-1"
>
{#each $knownTagRenderings as config (config.id)}

View file

@ -49,7 +49,9 @@ export default class DeleteImage extends Toggle {
.Clone()
.SetClass("bg-white pl-4 pr-4")
.SetStyle("border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;")
const openDelete = new SvelteUIElement(Delete_icon).SetStyle("width: 2em; height: 2em; display:block;")
const openDelete = new SvelteUIElement(Delete_icon).SetStyle(
"width: 2em; height: 2em; display:block;"
)
const deleteDialog = new ClickableToggle(
new Combine([deleteButton, cancelButton]).SetClass("flex flex-col background-black"),
openDelete

View file

@ -8,7 +8,7 @@
/**
* A small element showing the attribution of a single image
*/
export let image: Partial<ProvidedImage> & {id: string, url: string}
export let image: Partial<ProvidedImage> & { id: string; url: string }
let license: Store<LicenseInfo> = UIEventSource.FromPromise(
image.provider?.DownloadAttribution(image)
)
@ -16,9 +16,9 @@
</script>
{#if $license !== undefined}
<div class="no-images flex rounded-lg bg-black p-0.5 pl-3 pr-3 text-sm text-white items-center">
<div class="no-images flex items-center rounded-lg bg-black p-0.5 pl-3 pr-3 text-sm text-white">
{#if icon !== undefined}
<div class="w-6 h-6 mr-2">
<div class="mr-2 h-6 w-6">
<ToSvelte construct={icon} />
</div>
{/if}

View file

@ -26,47 +26,63 @@
let imagesProvider = state.nearbyImageSearcher
let loadedImages = AllImageProviders.LoadImagesFor(tags).mapD(loaded => new Set(loaded.map(img => img.url)))
let loadedImages = AllImageProviders.LoadImagesFor(tags).mapD(
(loaded) => new Set(loaded.map((img) => img.url))
)
let imageState = imagesProvider.getImagesAround(lon, lat)
let result: Store<P4CPicture[]> = imageState.images.mapD((pics: P4CPicture[]) => pics.filter((p: P4CPicture) =>
!loadedImages.data.has(p.pictureUrl) // We don't show any image which is already linked
&& !p.details.isSpherical,
).slice(0, 25), [loadedImages])
let someLoading = imageState.state.mapD(stateRecord => Object.values(stateRecord).some(v => v === "loading"))
let errors = imageState.state.mapD(stateRecord => Object.keys(stateRecord).filter(k => stateRecord[k] === "error"))
let result: Store<P4CPicture[]> = imageState.images.mapD(
(pics: P4CPicture[]) =>
pics
.filter(
(p: P4CPicture) =>
!loadedImages.data.has(p.pictureUrl) && // We don't show any image which is already linked
!p.details.isSpherical
)
.slice(0, 25),
[loadedImages]
)
let someLoading = imageState.state.mapD((stateRecord) =>
Object.values(stateRecord).some((v) => v === "loading")
)
let errors = imageState.state.mapD((stateRecord) =>
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error")
)
</script>
<div class="flex flex-col">
{#if $result.length === 0}
{#if $someLoading}
<div class="flex justify-center m-4">
<div class="m-4 flex justify-center">
<Loading />
</div>
{:else }
{:else}
<Tr t={Translations.t.image.nearby.noNearbyImages} cls="alert" />
{/if}
{:else}
<div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity">
{#each $result as image (image.pictureUrl)}
<span class="w-fit shrink-0" style="scroll-snap-align: start">
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />
</span>
<span class="w-fit shrink-0" style="scroll-snap-align: start">
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />
</span>
{/each}
</div>
{/if}
<div class="flex justify-between my-2">
<div class="my-2 flex justify-between">
<div>
{#if $someLoading && $result.length > 0}
<Loading />
{/if}
{#if $errors.length > 0}
<Tr cls="alert font-sm block" t={Translations.t.image.nearby.failed.Subs({service: $errors.join(", ")}) } />
<Tr
cls="alert font-sm block"
t={Translations.t.image.nearby.failed.Subs({ service: $errors.join(", ") })}
/>
{/if}
</div>
<MapillaryLink large={false}
mapProperties={{zoom: new ImmutableStore(16), location: new ImmutableStore({lon, lat})}} />
<MapillaryLink
large={false}
mapProperties={{ zoom: new ImmutableStore(16), location: new ImmutableStore({ lon, lat }) }}
/>
</div>
</div>

View file

@ -3,7 +3,6 @@
* Allows to search through wikidata and to select one value
*/
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import { ImmutableStore, Store, Stores, UIEventSource } from "../../../Logic/UIEventSource"
@ -30,13 +29,13 @@
let selectedMany: Record<string, boolean> = {}
let previouslySeen = new Map<string, WikidataResponse>()
$:{
$: {
if (selectedWikidataSingle) {
value.setData(selectedWikidataSingle.id)
}
}
$:{
$: {
const v = []
for (const id in selectedMany) {
if (selectedMany[id]) {
@ -46,11 +45,10 @@
value.setData(v.join(";"))
}
let tooShort = new ImmutableStore<{ success: WikidataResponse[] }>({ success: undefined })
let searchResult: Store<{ success?: WikidataResponse[]; error?: any }> = searchValue
.bind((searchText) => {
let searchResult: Store<{ success?: WikidataResponse[]; error?: any }> = searchValue.bind(
(searchText) => {
if (searchText.length < 3 && !searchText.match(/[qQ][0-9]+/)) {
return tooShort
}
@ -62,18 +60,19 @@
lang,
maxCount: 5,
notInstanceOf,
instanceOf
instanceOf,
})
WikidataValidator._searchCache.set(key, promise)
}
return Stores.FromPromiseWithErr(promise)
})
}
)
let selectedWithoutSearch: Store<WikidataResponse[]> = searchResult.map(sr => {
let selectedWithoutSearch: Store<WikidataResponse[]> = searchResult.map((sr) => {
for (const wikidataItem of sr?.success ?? []) {
previouslySeen.set(wikidataItem.id, wikidataItem)
}
let knownIds: Set<string> = new Set(sr?.success?.map(item => item.id))
let knownIds: Set<string> = new Set(sr?.success?.map((item) => item.id))
const seen = [selectedWikidataSingle]
for (const id in selectedMany) {
if (selectedMany[id]) {
@ -81,9 +80,8 @@
seen.push(item)
}
}
return Utils.NoNull(seen).filter(i => !knownIds.has(i.id))
return Utils.NoNull(seen).filter((i) => !knownIds.has(i.id))
})
</script>
<h3>
@ -91,33 +89,37 @@
</h3>
<form>
<SearchField {searchValue} placeholderText={placeholder}></SearchField>
<SearchField {searchValue} placeholderText={placeholder} />
{#if $searchValue.trim().length === 0}
<Tr cls="w-full flex justify-center p-4" t={ t.doSearch} />
<Tr cls="w-full flex justify-center p-4" t={t.doSearch} />
{:else if $searchValue.trim().length < 3}
<Tr t={ t.searchToShort} />
<Tr t={t.searchToShort} />
{:else if $searchResult === undefined}
<div class="w-full flex justify-center p-4">
<div class="flex w-full justify-center p-4">
<Loading>
<Tr t={Translations.t.general.loading} />
</Loading>
</div>
{:else if $searchResult.error !== undefined}
<div class="w-full flex justify-center p-4">
<div class="flex w-full justify-center p-4">
<Tr cls="alert" t={t.failed} />
</div>
{:else if $searchResult.success}
{#if $searchResult.success.length === 0}
<Tr cls="w-full flex justify-center p-4" t={ t.noResults.Subs({search: $searchValue})} />
<Tr cls="w-full flex justify-center p-4" t={t.noResults.Subs({ search: $searchValue })} />
{:else}
{#each $searchResult.success as wikidata}
<label class="low-interaction m-4 p-2 rounded-xl flex items-center">
<label class="low-interaction m-4 flex items-center rounded-xl p-2">
{#if allowMultiple}
<input type="checkbox" bind:checked={selectedMany[wikidata.id]} />
{:else}
<input type="radio" name="selectedWikidata" value={wikidata} bind:group={selectedWikidataSingle} />
<input
type="radio"
name="selectedWikidata"
value={wikidata}
bind:group={selectedWikidataSingle}
/>
{/if}
<Wikidatapreview {wikidata} />
@ -127,17 +129,19 @@
{/if}
{#each $selectedWithoutSearch as wikidata}
<label class="low-interaction m-4 p-2 rounded-xl flex items-center">
<label class="low-interaction m-4 flex items-center rounded-xl p-2">
{#if allowMultiple}
<input type="checkbox" bind:checked={selectedMany[wikidata.id]} />
{:else}
<input type="radio" name="selectedWikidata" value={wikidata} bind:group={selectedWikidataSingle} />
<input
type="radio"
name="selectedWikidata"
value={wikidata}
bind:group={selectedWikidataSingle}
/>
{/if}
<Wikidatapreview {wikidata} />
</label>
{/each}
</form>

View file

@ -28,13 +28,15 @@
export let feature: Feature
export let args: (string | number | boolean)[] = undefined
export let state: SpecialVisualizationState
</script>
{#if type === "translation"}
<TranslationInput {value} on:submit {args} />
{:else if type === "direction"}
<DirectionInput {value} mapProperties={InputHelpers.constructMapProperties( { feature, args: args ?? [] })} />
<DirectionInput
{value}
mapProperties={InputHelpers.constructMapProperties({ feature, args: args ?? [] })}
/>
{:else if type === "date"}
<DateInput {value} />
{:else if type === "color"}
@ -50,5 +52,5 @@
{:else if type === "slope"}
<SlopeInput {value} {feature} {state} />
{:else if type === "wikidata"}
<WikidataInputHelper {value} {feature} {state} {args}/>
<WikidataInputHelper {value} {feature} {state} {args} />
{/if}

View file

@ -63,6 +63,4 @@ export default class InputHelpers {
}
return mapProperties
}
}

View file

@ -32,7 +32,6 @@
preferredFiltered = preferredLanguages?.filter((l) => availableLanguages.indexOf(l) >= 0)
})
export let clss: string = undefined
let current = Locale.language
</script>

View file

@ -100,9 +100,10 @@ export default class Validators {
private static _byType = Validators._byTypeConstructor()
public static HelpText(): string {
const explanations: string[] = Validators.AllValidators.flatMap((type) =>
["### "+type.name, type.explanation]
)
const explanations: string[] = Validators.AllValidators.flatMap((type) => [
"### " + type.name,
type.explanation,
])
return [
"# Available types for text fields",
"The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them",

View file

@ -3,12 +3,18 @@ import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
export default class StringValidator extends Validator {
constructor(type?: string, doc?: string, inputmode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search", textArea?: boolean) {
super(type ?? "string",
constructor(
type?: string,
doc?: string,
inputmode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search",
textArea?: boolean
) {
super(
type ?? "string",
doc ?? "A simple piece of text which is at most 255 characters long",
inputmode,
textArea)
textArea
)
}
isValid(s: string): boolean {

View file

@ -42,8 +42,10 @@ export default class WikidataValidator extends Validator {
"notInstanceof",
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results",
],
["multiple",
"If 'yes' or 'true', will allow to select multiple values at once"]
[
"multiple",
"If 'yes' or 'true', will allow to select multiple values at once",
],
]
),
]),
@ -145,7 +147,12 @@ Another example is to search for species and trees:
* WikidataValidator.removePostAndPrefixes("Elf-Julistraat", [], {"nl":["straat", "laan"], "en": ["street"]}, "nl") // => "Elf-Juli"
* WikidataValidator.removePostAndPrefixes("Elf-Julistraat", [], {"nl":["straat", "laan"], "en": ["street"]}, "en") // => "Elf-Julistraat"
*/
public static removePostAndPrefixes(searchTerm: string, prefixesToRemove: string[] | Record<string, string[]>, postfixesToRemove: string[] | Record<string, string[]>, language: string): string {
public static removePostAndPrefixes(
searchTerm: string,
prefixesToRemove: string[] | Record<string, string[]>,
postfixesToRemove: string[] | Record<string, string[]>,
language: string
): string {
const prefixes = prefixesToRemove
const postfixes = postfixesToRemove
const prefixesUnwrapped: RegExp[] = (
@ -156,7 +163,6 @@ Another example is to search for species and trees:
Array.isArray(postfixes) ? postfixes : postfixes[language] ?? []
).map((s) => new RegExp(s + "$", "i"))
let clipped = searchTerm.trim()
for (const postfix of postfixesUnwrapped) {

View file

@ -1,57 +1,57 @@
<script lang="ts">/**
<script lang="ts">
/**
*
Wrapper around 'WikidataInput.svelte' which handles the arguments
*/
import { UIEventSource } from "../../Logic/UIEventSource"
import Locale from "../i18n/Locale"
import { Utils } from "../../Utils"
import Wikidata from "../../Logic/Web/Wikidata"
import WikidataInput from "./Helpers/WikidataInput.svelte"
import type { Feature } from "geojson"
import { onDestroy } from "svelte"
import WikidataValidator from "./Validators/WikidataValidator"
import { UIEventSource } from "../../Logic/UIEventSource"
import Locale from "../i18n/Locale"
import { Utils } from "../../Utils"
import Wikidata from "../../Logic/Web/Wikidata"
import WikidataInput from "./Helpers/WikidataInput.svelte"
import type { Feature } from "geojson"
import { onDestroy } from "svelte"
import WikidataValidator from "./Validators/WikidataValidator"
export let args: (string | number | boolean)[] = []
export let feature: Feature
export let args: (string | number | boolean)[] = []
export let feature: Feature
export let value: UIEventSource<string>
export let value: UIEventSource<string>
let searchKey: string = <string>args[0] ?? "name"
let searchKey: string = <string>args[0] ?? "name"
let searchFor: string =
searchKey
.split(";")
.map((k) => feature?.properties[k]?.toLowerCase())
.find((foundValue) => !!foundValue) ?? ""
let searchFor: string =
searchKey
.split(";")
.map((k) => feature?.properties[k]?.toLowerCase())
.find((foundValue) => !!foundValue) ?? ""
const options: any = args[1]
const options: any = args[1]
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
onDestroy(
Locale.language.addCallbackAndRunD(lg => {
console.log(options)
if (searchFor !== undefined && options !== undefined) {
const post = options["removePostfixes"] ?? []
const pre = options["removePrefixes"] ?? []
const clipped = WikidataValidator.removePostAndPrefixes(searchFor, pre, post, lg)
console.log("Got clipped value:", clipped, post, pre)
searchForValue.setData(clipped)
}
})
)
onDestroy(
Locale.language.addCallbackAndRunD((lg) => {
console.log(options)
if (searchFor !== undefined && options !== undefined) {
const post = options["removePostfixes"] ?? []
const pre = options["removePrefixes"] ?? []
const clipped = WikidataValidator.removePostAndPrefixes(searchFor, pre, post, lg)
console.log("Got clipped value:", clipped, post, pre)
searchForValue.setData(clipped)
}
})
)
let instanceOf: number[] = Utils.NoNull(
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let notInstanceOf: number[] = Utils.NoNull(
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let instanceOf: number[] = Utils.NoNull(
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let notInstanceOf: number[] = Utils.NoNull(
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let allowMultipleArg = options?.["multiple"]
let allowMultiple = allowMultipleArg === "yes" || (""+allowMultipleArg) === "true"
let allowMultipleArg = options?.["multiple"]
let allowMultiple = allowMultipleArg === "yes" || "" + allowMultipleArg === "true"
</script>
<WikidataInput searchValue={searchForValue} {value} {instanceOf} {notInstanceOf} {allowMultiple}/>
<WikidataInput searchValue={searchForValue} {value} {instanceOf} {notInstanceOf} {allowMultiple} />

View file

@ -55,24 +55,24 @@
center: { lng: lon, lat },
maxZoom: 24,
interactive: true,
attributionControl: false
attributionControl: false,
}
_map = new maplibre.Map(options)
window.requestAnimationFrame(() => {
_map.resize()
})
_map.on("load", function() {
_map.on("load", function () {
_map.resize()
const canvas = _map.getCanvas()
canvas.addEventListener("webglcontextlost", (e) => {
console.warn("A MapLibreMap lost their context. Recovery is", autorecovery, e)
try{
try {
_map?.remove()
}catch (e) {
} catch (e) {
console.debug("Could not remove map due to", e)
}
if(autorecovery){
if (autorecovery) {
requestAnimationFrame(() => {
console.warn("Attempting map recovery")
_map = new maplibre.Map(options)
@ -92,23 +92,19 @@
map.set(_map)
}
onMount(() => initMap())
onDestroy(async () => {
await Utils.waitFor(100)
requestAnimationFrame(
() => {
try {
_map?.remove()
console.log("Removed map")
map = null
} catch (e) {
console.error("Could not destroy map")
}
requestAnimationFrame(() => {
try {
_map?.remove()
console.log("Removed map")
map = null
} catch (e) {
console.error("Could not destroy map")
}
)
})
})
</script>

View file

@ -52,7 +52,7 @@
</script>
<TitledPanel>
<Tr slot="title" t={Translations.t.general.backgroundMap} />
<Tr slot="title" t={Translations.t.general.backgroundMap} />
<div class="grid h-full w-full grid-cols-1 gap-2 md:grid-cols-2">
<RasterLayerPicker

View file

@ -77,7 +77,11 @@
{#if hasLayers}
<form class="flex h-full w-full flex-col" on:submit|preventDefault={() => {}}>
<button tabindex="-1" on:click={() => apply()} class="m-0 p-0 rounded-none h-full w-full cursor-pointer border-none">
<button
tabindex="-1"
on:click={() => apply()}
class="m-0 h-full w-full cursor-pointer rounded-none border-none p-0"
>
<span class="pointer-events-none relative h-full w-full">
<OverlayMap
interactive={false}

View file

@ -152,8 +152,8 @@ class PointRenderingLayer {
}
const el = html.ConstructElement()
store.addCallbackAndRunD(tags => {
if(tags._deleted === "yes"){
store.addCallbackAndRunD((tags) => {
if (tags._deleted === "yes") {
html.SetClass("grayscale")
}
})

View file

@ -45,11 +45,15 @@
/>
</Loading>
{:else}
<WikidatapreviewWithLoading wikidataId={wikidataId} imageStyle="max-width: 8rem; width: unset; height: 8rem">
<WikidatapreviewWithLoading
{wikidataId}
imageStyle="max-width: 8rem; width: unset; height: 8rem"
>
<div slot="extra">
<Tr cls="thanks w-fit self-center" t={ t.matchPercentage
.Subs({ match: Math.round(species.score * 100) })}/>
<Tr
cls="thanks w-fit self-center"
t={t.matchPercentage.Subs({ match: Math.round(species.score * 100) })}
/>
</div>
</WikidatapreviewWithLoading>
{/if}

View file

@ -117,7 +117,7 @@
theme: state.layout?.id ?? "unkown",
changeType: "create",
snapOnto: snapToWay,
reusePointWithinMeters: 1
reusePointWithinMeters: 1,
})
await state.changes.applyAction(newElementAction)
state.newFeatures.features.ping()
@ -190,11 +190,12 @@
<TitledPanel>
<Tr slot="title" t={Translations.t.general.add.intro} />
<div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8" />
<Tr
t={Translations.t.general.add.layerNotEnabled.Subs({ layer: selectedPreset.layer.name })}
t={Translations.t.general.add.layerNotEnabled.Subs({
layer: selectedPreset.layer.name,
})}
/>
</div>
@ -202,9 +203,9 @@
<button
class="flex w-full gap-x-1"
on:click={() => {
abort()
state.guistate.openFilterView(selectedPreset.layer)
}}
abort()
state.guistate.openFilterView(selectedPreset.layer)
}}
>
<Layers class="w-12" />
<Tr t={Translations.t.general.add.openLayerControl} />
@ -213,9 +214,9 @@
<button
class="primary flex w-full gap-x-1"
on:click={() => {
layerIsDisplayed.setData(true)
abort()
}}
layerIsDisplayed.setData(true)
abort()
}}
>
<EyeIcon class="w-12" />
<Tr
@ -224,89 +225,86 @@
</button>
</div>
</TitledPanel>
{:else if $layerHasFilters}
<TitledPanel>
<Tr slot="title" t={Translations.t.general.add.intro} />
<!-- 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={() => {
abort()
state.guistate.openFilterView(selectedPreset.layer)
}}
>
<Layers class="w-12" />
<Tr t={Translations.t.general.add.openLayerControl} />
</button>
</div>
<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={() => {
abort()
state.guistate.openFilterView(selectedPreset.layer)
}}
>
<Layers class="w-12" />
<Tr t={Translations.t.general.add.openLayerControl} />
</button>
</div>
</TitledPanel>
{:else if !confirmedCategory}
<!-- Second, confirm the category -->
<TitledPanel>
<Tr slot="title"
<Tr
slot="title"
t={Translations.t.general.add.confirmTitle.Subs({ title: selectedPreset.preset.title })}
/>
{#if selectedPreset.preset.description}
<Tr t={selectedPreset.preset.description} />
{/if}
{#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}
</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" />
{/each}
</span>
{/if}
<TagHint
embedIn={(tags) => t.presetInfo.Subs({ tags })}
{state}
tags={new And(selectedPreset.preset.tags)}
/>
{#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" />
{/each}
</span>
{/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">
<ToSvelte construct={selectedPreset.icon} />
<Confirm class="absolute bottom-0 right-0 h-4 w-4" />
</div>
<div class="w-full">
<Tr t={selectedPreset.text} />
</div>
</NextButton>
</div>
<NextButton on:click={() => (confirmedCategory = true)} clss="primary w-full">
<div slot="image" class="relative">
<ToSvelte construct={selectedPreset.icon} />
<Confirm class="absolute bottom-0 right-0 h-4 w-4" />
</div>
<div class="w-full">
<Tr t={selectedPreset.text} />
</div>
</NextButton>
</div>
</TitledPanel>
{:else if _globalFilter?.length > 0 && _globalFilter?.length > checkedOfGlobalFilters}
<Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12" />
<SubtleButton

View file

@ -80,7 +80,7 @@
text: Translations.t.general.add.addNew.Subs(
{ category: preset.title },
preset.title["context"]
)
),
}
presets.push(simplified)
}
@ -100,7 +100,6 @@
<TitledPanel>
<Tr slot="title" t={Translations.t.general.add.intro} />
{#each presets as preset}
<NextButton on:click={() => dispatch("select", preset)}>
<ToSvelte slot="image" construct={() => preset.icon} />

View file

@ -76,7 +76,7 @@
state.guistate.openFilterView(filteredLayer.layerDef)
}}
>
<Layers class="w-12"/>
<Layers class="w-12" />
<Tr t={Translations.t.general.add.openLayerControl} />
</button>
@ -119,7 +119,7 @@
state.guistate.openFilterView(filteredLayer.layerDef)
}}
>
<Layers class="w-12"/>
<Layers class="w-12" />
<Tr t={Translations.t.general.add.openLayerControl} />
</button>
</div>

View file

@ -31,11 +31,7 @@
let idList = [value]
if (Array.isArray(value)) {
idList = value
} else if (
key !== "id" &&
typeof value === "string" &&
value?.startsWith("[")
) {
} else if (key !== "id" && typeof value === "string" && value?.startsWith("[")) {
// This is a list of values
idList = JSON.parse(value)
}
@ -59,7 +55,7 @@
let mla = new MapLibreAdaptor(mlmap, {
rasterLayer: state.mapProperties.rasterLayer,
zoom: new UIEventSource<number>(17),
maxzoom: new UIEventSource<number>(17)
maxzoom: new UIEventSource<number>(17),
})
mla.allowMoving.setData(false)

View file

@ -123,7 +123,7 @@
<div class="h-56 w-full">
<NewPointLocationInput value={coordinate} {state}>
<div class="h-20 w-full pb-10" slot="image">
<Note class="h-10 w-full"/>
<Note class="h-10 w-full" />
</div>
</NewPointLocationInput>
</div>

View file

@ -200,7 +200,7 @@
allowDeleteOfFreeform &&
!$freeformInput &&
!$freeformInputUnvalidated &&
!checkedMappings?.some(m => m) &&
!checkedMappings?.some((m) => m) &&
$tags[config.freeform.key] // We need to have a current value in order to delete it
) {
selectedTags = new Tag(config.freeform.key, "")
@ -495,7 +495,7 @@
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel" />
<slot name="save-button" {selectedTags}>
{#if config.freeform?.key && allowDeleteOfFreeform && !checkedMappings?.some(m => m) && !$freeformInput && !$freeformInputUnvalidated && $tags[config.freeform.key]}
{#if config.freeform?.key && allowDeleteOfFreeform && !checkedMappings?.some((m) => m) && !$freeformInput && !$freeformInputUnvalidated && $tags[config.freeform.key]}
<button
class="primary flex"
on:click|stopPropagation|preventDefault={() => onSave()}
@ -521,13 +521,11 @@
<TagHint {state} tags={selectedTags} currentProperties={$tags} />
<span class="flex flex-wrap">
{#if $featureSwitchIsTesting}
<div class="alert">
Testmode &nbsp;
</div>
<div class="alert">Testmode &nbsp;</div>
{/if}
{#if $featureSwitchIsTesting || $featureSwitchIsDebugging}
<a class="small" on:click={() => console.log("Configuration is ", config)}>
{config.id}
{config.id}
</a>
{/if}
</span>

View file

@ -17,14 +17,12 @@ export default class QueryParameterDocumentation {
'"URL-parameters are extra parts of the URL used to set the state.',
"For example, if the url is `https://mapcomplete.org/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`, " +
"the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely: ",
MarkdownUtils.list(
[
"The url-parameter `lat` is `51.0` in this instance",
"The url-parameter `lon` is `4.3` in this instance",
"The url-parameter `z` is `5` in this instance",
"The url-parameter `test` is `true` in this instance",
]
),
MarkdownUtils.list([
"The url-parameter `lat` is `51.0` in this instance",
"The url-parameter `lon` is `4.3` in this instance",
"The url-parameter `z` is `5` in this instance",
"The url-parameter `test` is `true` in this instance",
]),
"Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.",
]
@ -72,7 +70,7 @@ export default class QueryParameterDocumentation {
]
this.UrlParamDocs().forEach((value, key) => {
const c = [
"## "+key,
"## " + key,
value,
QueryParameters.defaults[key] === undefined
? "No default value set"

View file

@ -407,10 +407,10 @@ export default class SpecialVisualizations {
new HistogramViz(),
new StealViz(),
{
funcName : "minimap",
docs :"A small map showing the selected feature.",
needsUrls : [],
args : [
funcName: "minimap",
docs: "A small map showing the selected feature.",
needsUrls: [],
args: [
{
doc: "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close",
name: "zoomlevel",
@ -422,17 +422,17 @@ export default class SpecialVisualizations {
defaultValue: "id",
},
],
example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`",
example:
"`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`",
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature
): SvelteUIElement {
return new SvelteUIElement(MinimapViz, {state, args, feature, tagSource})
}
return new SvelteUIElement(MinimapViz, { state, args, feature, tagSource })
},
},
{
funcName: "split_button",

View file

@ -2,6 +2,6 @@ import { Store } from "../../Logic/UIEventSource"
export interface MCService {
name: string
status: Store<"online" | "degraded" | "offline">,
status: Store<"online" | "degraded" | "offline">
message?: Store<undefined | string>
}

View file

@ -1,22 +1,23 @@
<script lang="ts">
import StatusIcon from "./StatusIcon.svelte"
import type { MCService } from "./MCService.js"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import StatusIcon from "./StatusIcon.svelte"
import type { MCService } from "./MCService.js"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
export let service: MCService
let status = service.status
let msg = service.message
export let service: MCService
let status = service.status
let msg = service.message
</script>
<AccordionSingle>
<h3 slot="header" class="flex items-center m-0"> <StatusIcon status={$status}/> {service.name}</h3>
<div class="mx-4">
{#if $msg}
{$msg}
<h3 slot="header" class="m-0 flex items-center">
<StatusIcon status={$status} />
{service.name}
</h3>
<div class="mx-4">
{#if $msg}
{$msg}
{:else}
No extra information available
No extra information available
{/if}
</div>
</div>
</AccordionSingle>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import StatusIcon from "./StatusIcon.svelte"
import type { MCService } from "./MCService"
@ -10,7 +9,6 @@
import Loading from "../Base/Loading.svelte"
import Checkbox from "../Base/Checkbox.svelte"
let services: MCService[] = []
let recheckSignal: UIEventSource<any> = new UIEventSource<any>(undefined)
@ -22,25 +20,21 @@
function check() {
const promise = raw ? Utils.download(url) : Utils.downloadJson(url)
promise
?.then((d) => src.setData({ success: d }))
?.catch((err) => src.setData({ error: err }))
promise?.then((d) => src.setData({ success: d }))?.catch((err) => src.setData({ error: err }))
}
check()
recheckSignal.addCallback(_ => check())
checkSignal.addCallback(_ => {
recheckSignal.addCallback((_) => check())
checkSignal.addCallback((_) => {
if (autoCheckAgain.data) {
check()
}
})
return src
}
function simpleMessage(s: Store<{ success: any } | { error: any }>): Store<string> {
return s.mapD(s => {
return s.mapD((s) => {
if (s["success"]) {
return JSON.stringify(s["success"])
}
@ -53,7 +47,7 @@
const osmApi = connection.apiIsOnline
services.push({
name: connection.Backend(),
status: osmApi.mapD(serviceState => {
status: osmApi.mapD((serviceState) => {
switch (serviceState) {
case "offline":
return "offline"
@ -76,7 +70,7 @@
const status = testDownload(s + "/overview")
services.push({
name: s,
status: status.mapD(s => {
status: status.mapD((s) => {
if (s["error"]) {
return "offline"
}
@ -89,7 +83,7 @@
}
return "online"
}),
message: status.mapD(s => {
message: status.mapD((s) => {
if (s["error"]) {
return s["error"]
}
@ -99,24 +93,20 @@
})
}
{
services.push(
{
name: Constants.GeoIpServer,
status: testDownload(Constants.GeoIpServer + "/status").mapD(result => {
if (result["success"].online) {
return "online"
}
if (result["error"]) {
return "offline"
} else {
return "degraded"
}
}),
message: simpleMessage(
testDownload(Constants.GeoIpServer + "/ip"),
),
},
)
services.push({
name: Constants.GeoIpServer,
status: testDownload(Constants.GeoIpServer + "/status").mapD((result) => {
if (result["success"].online) {
return "online"
}
if (result["error"]) {
return "offline"
} else {
return "degraded"
}
}),
message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip")),
})
}
{
@ -124,7 +114,7 @@
const status = testDownload(s.replace(/\/report$/, "/status"))
services.push({
name: s,
status: status.mapD(s => {
status: status.mapD((s) => {
if (s["error"]) {
return "offline"
}
@ -143,7 +133,7 @@
const status = testDownload(s + "/status")
services.push({
name: s,
status: status.mapD(s => {
status: status.mapD((s) => {
if (s["error"]) {
return "offline"
}
@ -162,7 +152,7 @@
const status = testDownload(s + "/summary/status.json")
services.push({
name: s,
status: status.mapD(s => {
status: status.mapD((s) => {
if (s["error"]) {
return "offline"
}
@ -180,7 +170,7 @@
return "online"
}),
message: status.mapD(s => {
message: status.mapD((s) => {
if (s["error"]) {
return s["error"]
}
@ -201,7 +191,7 @@
const status = testDownload(s + "/0.0.0.json")
services.push({
name: s,
status: status.mapD(s => {
status: status.mapD((s) => {
if (s["error"]) {
return "offline"
}
@ -211,12 +201,10 @@
}
return "degraded"
}),
message: status.map(s => JSON.stringify(s)),
message: status.map((s) => JSON.stringify(s)),
})
}
{
for (const defaultOverpassUrl of Constants.defaultOverpassUrls) {
const statusUrl = defaultOverpassUrl.replace(/\/interpreter$/, "/status")
@ -224,7 +212,7 @@
services.push({
name: "Overpass-server: " + defaultOverpassUrl,
status: status.mapD(result => {
status: status.mapD((result) => {
if (result["error"]) {
return "offline"
}
@ -247,7 +235,7 @@
{
services.push({
name: "Mangrove reviews",
status: testDownload("https://api.mangrove.reviews", true).mapD(r => {
status: testDownload("https://api.mangrove.reviews", true).mapD((r) => {
if (r["success"]) {
return "online"
}
@ -256,18 +244,17 @@
})
}
let all = new UIEventSource<"online" | "degraded" | "offline">("online")
let someLoading = new UIEventSource(true)
function setAll() {
const data = Utils.NoNull(services.map(s => s.status.data))
const data = Utils.NoNull(services.map((s) => s.status.data))
someLoading.setData(data.length !== services.length)
if (data.some(d => d === "offline")) {
if (data.some((d) => d === "offline")) {
all.setData("offline")
} else if (data.some(d => d === "degraded")) {
} else if (data.some((d) => d === "degraded")) {
all.setData("degraded")
} else if (data.some(d => d === "online")) {
} else if (data.some((d) => d === "online")) {
all.setData("online")
} else {
all.setData(undefined)
@ -291,7 +278,6 @@
*/
async function setTrafficLight(state: "online" | "degraded" | "offline") {
try {
const url = trafficLightUrl
const status = await Utils.downloadJson(url + "status")
console.log(status)
@ -300,7 +286,6 @@
return
}
switch (state) {
case "offline":
await Utils.download(url + "configure?mode=3")
break
@ -313,37 +298,32 @@
default:
await Utils.download(url + "configure?mode=7")
break
}
} catch (e) {
console.log("Could not connect to the traffic light")
}
}
all.addCallbackAndRunD(state => {
all.addCallbackAndRunD((state) => {
setTrafficLight(state)
})
enableTrafficLight.addCallbackAndRunD(_ => {
enableTrafficLight.addCallbackAndRunD((_) => {
setTrafficLight(all.data)
})
</script>
<h1>MapComplete status indicators</h1>
<div class="flex">
{#if $someLoading}
<Loading />
{/if}
<StatusIcon status={$all} cls="w-16 h-16" />
<button on:click={() => recheckSignal.ping()}>Check again</button>
<Checkbox selected={autoCheckAgain}>
Automatically check again every 10s
</Checkbox>
<Checkbox selected={autoCheckAgain}>Automatically check again every 10s</Checkbox>
</div>
{#if $trafficLightIsOnline?.["success"] }
{#if $trafficLightIsOnline?.["success"]}
<Checkbox selected={enableTrafficLight}>Enable traffic light</Checkbox>
{/if}
@ -351,12 +331,16 @@
<ServiceIndicator {service} />
{/each}
<button on:click={() => {
<button
on:click={() => {
fetch(Constants.ErrorReportServer, {
method: "POST",
body: JSON.stringify({
message: "Test via the status page, not an actual error",
version: Constants.vNumber,
}),
})
}}>Test error report function</button>
method: "POST",
body: JSON.stringify({
message: "Test via the status page, not an actual error",
version: Constants.vNumber,
}),
})
}}
>
Test error report function
</button>

View file

@ -13,16 +13,15 @@
</script>
{#if status === "online"}
<CheckCircle class={twJoin(cls,"rounded-full shrink-0")} style="color: #22cc22" />
<CheckCircle class={twJoin(cls, "shrink-0 rounded-full")} style="color: #22cc22" />
{:else if status === "degraded"}
<Exclamation class={twJoin(cls,"rounded-full shrink-0")} style="color: #eecc22" />
<Exclamation class={twJoin(cls, "shrink-0 rounded-full")} style="color: #eecc22" />
{:else if status === "offline"}
<XCircleIcon class={twJoin(cls,"rounded-full shrink-0")} style="color: #bb2222" />
<XCircleIcon class={twJoin(cls, "shrink-0 rounded-full")} style="color: #bb2222" />
{:else if status === undefined}
<div class={twJoin(cls,"rounded-full shrink-0")}>
<div class={twJoin(cls, "shrink-0 rounded-full")}>
<Loading />
</div>
{:else}
? {status}
{/if}

View file

@ -99,13 +99,18 @@
function genTitle(value: any, singular: string, i: number): Translation {
try {
if (schema.hints.title) {
const v = Function("value", "return " + schema.hints.title)(value)
return Translations.T(v)
}
} catch (e) {
console.log("Warning: could not translate a title for " + `${singular} ${i} with function ` + schema.hints.title + " and value " + JSON.stringify(value))
console.log(
"Warning: could not translate a title for " +
`${singular} ${i} with function ` +
schema.hints.title +
" and value " +
JSON.stringify(value)
)
}
return Translations.T(`${singular} ${i}`)
}

View file

@ -213,7 +213,7 @@
<main>
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
<MaplibreMap map={maplibremap} mapProperties={mapproperties} autorecovery={true}/>
<MaplibreMap map={maplibremap} mapProperties={mapproperties} autorecovery={true} />
</div>
{#if $visualFeedback}
@ -286,10 +286,8 @@
on:keydown={forwardEventToMap}
htmlElem={openCurrentViewLayerButton}
>
<div class="w-8 h-8 cursor-pointer">
<ToSvelte
construct={() => currentViewLayer.defaultIcon()}
/>
<div class="h-8 w-8 cursor-pointer">
<ToSvelte construct={() => currentViewLayer.defaultIcon()} />
</div>
</MapControlButton>
{/if}
@ -300,10 +298,8 @@
<div class="alert w-fit">Testmode</div>
</If>
{#if state.osmConnection.Backend().startsWith("https://master.apis.dev.openstreetmap.org")}
<div class="thanks">
Testserver
</div>
{/if}
<div class="thanks">Testserver</div>
{/if}
<If condition={state.featureSwitches.featureSwitchFakeUser}>
<div class="alert w-fit">Faking a user (Testmode)</div>
</If>
@ -551,13 +547,13 @@
state.guistate.backgroundLayerSelectionIsOpened.setData(false)
}}
>
<RasterLayerOverview
{availableLayers}
map={state.map}
mapproperties={state.mapProperties}
userstate={state.userRelatedState}
visible={state.guistate.backgroundLayerSelectionIsOpened}
/>
<RasterLayerOverview
{availableLayers}
map={state.map}
mapproperties={state.mapProperties}
userstate={state.userRelatedState}
visible={state.guistate.backgroundLayerSelectionIsOpened}
/>
</FloatOver>
</IfHidden>
@ -583,7 +579,7 @@
<div slot="content0" class="flex flex-col">
<AboutMapComplete {state} />
<div class="m-2 flex flex-col">
<HotkeyTable/>
<HotkeyTable />
</div>
</div>

View file

@ -6,45 +6,24 @@ 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 {
export default class WikidataPreviewBox {
private static isHuman = [{ p: 31 /*is a*/, q: 5 /* human */ }]
public static extraProperties: {
requires?: { p: number; q?: number }[]
property: string
textMode?: Map<string, string>
display:
| TypedTranslation<{ value }>
| Map<string, any>,
display: TypedTranslation<{ value }> | Map<string, any>
}[] = [
{
requires: WikidataPreviewBox.isHuman,
property: "P21",
display: new Map([
[
"Q6581097",
Gender_male
],
[
"Q6581072",
Gender_female
],
[
"Q1097630",
Gender_inter
],
[
"Q1052281",
Gender_trans /*'transwomen'*/
],
[
"Q2449503",
Gender_trans /*'transmen'*/
],
[
"Q48270",
Gender_queer
]
["Q6581097", Gender_male],
["Q6581072", Gender_female],
["Q1097630", Gender_inter],
["Q1052281", Gender_trans /*'transwomen'*/],
["Q2449503", Gender_trans /*'transmen'*/],
["Q48270", Gender_queer],
]),
textMode: new Map([
["Q6581097", "♂️"],
@ -52,19 +31,18 @@ export default class WikidataPreviewBox {
["Q1097630", "⚥️"],
["Q1052281", "🏳️‍⚧️" /*'transwomen'*/],
["Q2449503", "🏳️‍⚧️" /*'transmen'*/],
["Q48270", "🏳️‍🌈 ⚧"]
])
["Q48270", "🏳️‍🌈 ⚧"],
]),
},
{
property: "P569",
requires: WikidataPreviewBox.isHuman,
display: Translations.t.general.wikipedia.previewbox.born
display: Translations.t.general.wikipedia.previewbox.born,
},
{
property: "P570",
requires: WikidataPreviewBox.isHuman,
display: Translations.t.general.wikipedia.previewbox.died
}
display: Translations.t.general.wikipedia.previewbox.died,
},
]
}

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { Translation } from "../i18n/Translation"
import WikidataPreviewBox from "./WikidataPreviewBox"
import { WikidataResponse } from "../../Logic/Web/Wikidata"
@ -7,7 +6,7 @@
export let wikidata: WikidataResponse
let propertiesToRender = WikidataPreviewBox.extraProperties.filter(property => {
let propertiesToRender = WikidataPreviewBox.extraProperties.filter((property) => {
for (const requirement of property.requires) {
if (!wikidata.claims?.has("P" + requirement.p)) {
return false
@ -20,30 +19,30 @@
if (wikidata.claims?.get(key) === undefined) {
return false
}
return true
return true
}
})
function getProperty(property: {property: string}){
function getProperty(property: { property: string }) {
const key = property.property
const value = Array.from(wikidata.claims?.get(key)).join(", ")
return value
}
</script>
{#if propertiesToRender.length > 0}
<div class="flex justify-start items-center">
<div class="flex items-center justify-start">
{#each propertiesToRender as property}
{#if typeof property.display === "string" }
{#if typeof property.display === "string"}
{property.display}
{:else if property.display instanceof Translation}
<Tr cls="m-2 shrink-0"
t={property.display.Subs({value: getProperty(property)}) } />
<Tr cls="m-2 shrink-0" t={property.display.Subs({ value: getProperty(property) })} />
{:else}
<svelte:component this={property.display.get(getProperty(property))} class="h-6 w-fit m-1"/>
<svelte:component
this={property.display.get(getProperty(property))}
class="m-1 h-6 w-fit"
/>
{/if}
{/each}
</div>
{/if}

View file

@ -1,5 +1,4 @@
<script lang="ts">
import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata"
import { Translation } from "../i18n/Translation"
import { WikimediaImageProvider } from "../../Logic/ImageProviders/WikimediaImageProvider"
@ -12,31 +11,31 @@
let imageProperty: string | undefined = Array.from(wikidata?.claims?.get("P18") ?? [])[0]
let imageUrl = WikimediaImageProvider.singleton.PrepUrl(imageProperty)?.url
</script>
<div class="flex w-full p-2 flex-wrap">
<div class="flex w-full flex-wrap p-2">
{#if imageUrl}
<img src={imageUrl} style={imageStyle} class="mr-2" />
{/if}
<div class="flex flex-col flex-grow">
<div class="flex w-full justify-between flex-wrap">
<Tr cls="font-bold" t={ Translation.fromMap(wikidata.labels) } />
<a href={Wikidata.IdToArticle(wikidata.id)} target="_blank" class="flex must-link items-center">
<Wikidata_icon class="w-10" /> {wikidata.id}
<div class="flex flex-grow flex-col">
<div class="flex w-full flex-wrap justify-between">
<Tr cls="font-bold" t={Translation.fromMap(wikidata.labels)} />
<a
href={Wikidata.IdToArticle(wikidata.id)}
target="_blank"
class="must-link flex items-center"
>
<Wikidata_icon class="w-10" />
{wikidata.id}
</a>
</div>
<Tr t={ Translation.fromMap(wikidata.descriptions, true)} />
<Tr t={Translation.fromMap(wikidata.descriptions, true)} />
<div class="flex">
<WikidataQuickfacts {wikidata} />
<WikidataQuickfacts {wikidata} />
</div>
<slot name="extra"></slot>
<slot name="extra" />
</div>
</div>

View file

@ -10,13 +10,14 @@
export let wikidataId: Store<string>
export let imageStyle: string = undefined
let wikidata: Store<{ success: WikidataResponse } | { error: any }> = wikidataId.stabilized(100).bind((id) => {
if (id === undefined || id === "" || id === "Q") {
return null
}
return Wikidata.LoadWikidataEntry(id)
})
let wikidata: Store<{ success: WikidataResponse } | { error: any }> = wikidataId
.stabilized(100)
.bind((id) => {
if (id === undefined || id === "" || id === "Q") {
return null
}
return Wikidata.LoadWikidataEntry(id)
})
</script>
{#if $wikidata === undefined}
@ -28,7 +29,6 @@
{$wikidata["error"]}
</div>
{:else}
<Wikidatapreview {imageStyle} wikidata={$wikidata["success"]}>
<slot name="extra" slot="extra" />
</Wikidatapreview>

View file

@ -18,12 +18,11 @@
)
</script>
<div class="low-interaction border-gray-300 border-dashed rounded-xl p-2 flex flex-col">
<div class="low-interaction flex flex-col rounded-xl border-dashed border-gray-300 p-2">
{#if $titleOnly}
<Loading>{$wikipediaDetails.title}</Loading>
{/if}
{#if $wikipediaDetails.wikidata}
<Wikidatapreview wikidata={$wikipediaDetails.wikidata} />
{/if}
@ -36,7 +35,12 @@
{:else}
<FromHtml clss="wikipedia-article" src={$wikipediaDetails.firstParagraph} />
{#if $wikipediaDetails.articleUrl}
<a class="flex self-end my-2" href={$wikipediaDetails.articleUrl} rel="noreferrer" target="_blank">
<a
class="my-2 flex self-end"
href={$wikipediaDetails.articleUrl}
rel="noreferrer"
target="_blank"
>
<Wikipedia class="h-6 w-6" />
<Tr t={Translations.t.general.wikipedia.fromWikipedia} />
</a>

View file

@ -12,7 +12,7 @@ export default class Locale {
public static showLinkOnMobile: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public static language: UIEventSource<string> = Locale.setup()
public static getBestSupportedLanguage(browserLanguage?: string){
public static getBestSupportedLanguage(browserLanguage?: string) {
browserLanguage ??= navigator.languages?.[0] ?? navigator.language ?? "en"
console.log("Browser language is", browserLanguage)
const availableLanguages = Object.keys(native)
@ -22,10 +22,10 @@ export default class Locale {
}
browserLanguage = browserLanguage.replace(/[-_].*/g, "")
const hasBrowserLangFallback = availableLanguages.indexOf(browserLanguage) >= 0
if(hasBrowserLangFallback){
return browserLanguage
if (hasBrowserLangFallback) {
return browserLanguage
}
console.log("Language",browserLanguage,"not supported, defaulting to english")
console.log("Language", browserLanguage, "not supported, defaulting to english")
return "en"
}
@ -72,7 +72,7 @@ export default class Locale {
} else {
let browserLanguage = "en"
if (typeof navigator !== "undefined") {
browserLanguage = Locale.getBestSupportedLanguage()
browserLanguage = Locale.getBestSupportedLanguage()
}
source = LocalStorageSource.Get("language", browserLanguage)
}

View file

@ -22,7 +22,7 @@ export class Translation extends BaseUIElement {
constructor(
translations: string | Record<string, string>,
context?: string,
strictLanguages?: boolean,
strictLanguages?: boolean
) {
super()
this._strictLanguages = strictLanguages
@ -62,7 +62,7 @@ export class Translation extends BaseUIElement {
`. The offending object is: `,
translations[translationsKey],
"\n current translations are: ",
translations,
translations
)
throw (
"Error in an object depicting a translation: a non-string object was found. (" +
@ -75,7 +75,7 @@ export class Translation extends BaseUIElement {
if (count === 0) {
console.error(
"Constructing a translation, but the object containing translations is empty " +
(context ?? "No context given"),
(context ?? "No context given")
)
}
}
@ -94,7 +94,7 @@ export class Translation extends BaseUIElement {
[],
(f) => {
this.onDestroy = f
},
}
)
}
return this._currentLanguage
@ -113,7 +113,7 @@ export class Translation extends BaseUIElement {
static ExtractAllTranslationsFrom(
object: any,
context = "",
context = ""
): { context: string; tr: Translation }[] {
const allTranslations: { context: string; tr: Translation }[] = []
for (const key in object) {
@ -127,7 +127,7 @@ export class Translation extends BaseUIElement {
}
if (typeof v === "object") {
allTranslations.push(
...Translation.ExtractAllTranslationsFrom(v, context + "." + key),
...Translation.ExtractAllTranslationsFrom(v, context + "." + key)
)
}
}
@ -254,7 +254,7 @@ export class Translation extends BaseUIElement {
*/
public OnEveryLanguage(
f: (s: string, language: string) => string,
context?: string,
context?: string
): Translation {
const newTranslations = {}
for (const lang in this.translations) {
@ -337,7 +337,7 @@ export class Translation extends BaseUIElement {
const htmlElement = document.createElement("div")
htmlElement.innerHTML = render
const images = Array.from(htmlElement.getElementsByTagName("img")).map(
(img) => img.src,
(img) => img.src
)
allIcons.push(...images)
} else {
@ -350,7 +350,7 @@ export class Translation extends BaseUIElement {
.map((img) => img.match(/src=("[^"]+"|'[^']+'|[^/ ]+)/))
.filter((match) => match != null)
.map((match) =>
match[1].trim().replace(/^['"]/, "").replace(/['"]$/, ""),
match[1].trim().replace(/^['"]/, "").replace(/['"]$/, "")
)
allIcons.push(...sources)
}
@ -398,7 +398,7 @@ export class TypedTranslation<T extends Record<string, any>> extends Translation
}
PartialSubs<X extends string>(
text: Partial<T> & Record<X, string>,
text: Partial<T> & Record<X, string>
): TypedTranslation<Omit<T, X>> {
const newTranslations: Record<string, string> = {}
for (const lang in this.translations) {
@ -415,7 +415,7 @@ export class TypedTranslation<T extends Record<string, any>> extends Translation
PartialSubsTr<K extends string>(
key: string,
replaceWith: Translation,
replaceWith: Translation
): TypedTranslation<Omit<T, K>> {
const newTranslations: Record<string, string> = {}
const toSearch = "{" + key + "}"
@ -434,7 +434,7 @@ export class TypedTranslation<T extends Record<string, any>> extends Translation
for (const missingLanguage of missingLanguages) {
newTranslations[missingLanguage] = baseTemplate.replaceAll(
toSearch,
replaceWith.textFor(missingLanguage),
replaceWith.textFor(missingLanguage)
)
}