chore: automated housekeeping...

This commit is contained in:
Pieter Vander Vennet 2024-10-19 14:44:55 +02:00
parent c9ce29f206
commit 40e894df8b
294 changed files with 14209 additions and 4192 deletions

View file

@ -33,7 +33,7 @@
"oauth_token",
undefined,
"Used to complete the login"
)
),
})
const state = new UserRelatedState(osmConnection)
const t = Translations.t.index
@ -46,41 +46,56 @@
let searchIsFocused = new UIEventSource(true)
const officialThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(th => th.hideFromOverview === false)
const hiddenThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(th => th.hideFromOverview === true)
let visitedHiddenThemes: Store<MinimalThemeInformation[]> = UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection)
.map((knownIds) => hiddenThemes.filter((theme) =>
knownIds.indexOf(theme.id) >= 0 || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
))
const officialThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
(th) => th.hideFromOverview === false
)
const hiddenThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
(th) => th.hideFromOverview === true
)
let visitedHiddenThemes: Store<MinimalThemeInformation[]> =
UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection).map((knownIds) =>
hiddenThemes.filter(
(theme) =>
knownIds.indexOf(theme.id) >= 0 ||
state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
)
)
const customThemes: Store<MinimalThemeInformation[]> = Stores.ListStabilized<string>(state.installedUserThemes)
.mapD(stableIds => Utils.NoNullInplace(stableIds.map(id => state.getUnofficialTheme(id))))
const customThemes: Store<MinimalThemeInformation[]> = Stores.ListStabilized<string>(
state.installedUserThemes
).mapD((stableIds) => Utils.NoNullInplace(stableIds.map((id) => state.getUnofficialTheme(id))))
function filtered(themes: Store<MinimalThemeInformation[]>): Store<MinimalThemeInformation[]> {
return searchStable.map(search => {
if (!search) {
return themes.data
}
return searchStable.map(
(search) => {
if (!search) {
return themes.data
}
const start = new Date().getTime()
const scores = ThemeSearch.sortedByLowestScores(search, themes.data)
const end = new Date().getTime()
console.trace("Scores for", search , "are", scores, "searching took", end - start,"ms")
const strict = scores.filter(sc => sc.lowest < 2)
if (strict.length > 0) {
return strict.map(sc => sc.theme)
}
return scores.filter(sc => sc.lowest < 4).slice(0, 6).map(sc => sc.theme)
}, [themes])
const start = new Date().getTime()
const scores = ThemeSearch.sortedByLowestScores(search, themes.data)
const end = new Date().getTime()
console.trace("Scores for", search, "are", scores, "searching took", end - start, "ms")
const strict = scores.filter((sc) => sc.lowest < 2)
if (strict.length > 0) {
return strict.map((sc) => sc.theme)
}
return scores
.filter((sc) => sc.lowest < 4)
.slice(0, 6)
.map((sc) => sc.theme)
},
[themes]
)
}
let officialSearched : Store<MinimalThemeInformation[]>= filtered(new ImmutableStore(officialThemes))
let hiddenSearched: Store<MinimalThemeInformation[]> = filtered(visitedHiddenThemes)
let officialSearched: Store<MinimalThemeInformation[]> = filtered(
new ImmutableStore(officialThemes)
)
let hiddenSearched: Store<MinimalThemeInformation[]> = filtered(visitedHiddenThemes)
let customSearched: Store<MinimalThemeInformation[]> = filtered(customThemes)
let searchIsFocussed = new UIEventSource(false)
document.addEventListener("keydown", function(event) {
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.code === "KeyF") {
searchIsFocussed.set(true)
event.preventDefault()
@ -101,10 +116,7 @@
}
window.location.href = ThemeSearch.createUrlFor(candidate, undefined)
}
</script>
<main>
@ -136,7 +148,13 @@
</div>
</div>
<Searchbar value={search} placeholder={tr.searchForATheme} on:search={() => applySearch()} autofocus isFocused={searchIsFocussed} />
<Searchbar
value={search}
placeholder={tr.searchForATheme}
on:search={() => applySearch()}
autofocus
isFocused={searchIsFocussed}
/>
<ThemesList {search} {state} themes={$officialSearched} />
@ -166,8 +184,11 @@
</ThemesList>
{#if $customThemes.length > 0}
<ThemesList {search} {state} themes={$customSearched}
hasSelection={$officialSearched.length === 0 && $hiddenSearched.length === 0}
<ThemesList
{search}
{state}
themes={$customSearched}
hasSelection={$officialSearched.length === 0 && $hiddenSearched.length === 0}
>
<svelte:fragment slot="title">
<h3>
@ -177,7 +198,6 @@
</svelte:fragment>
</ThemesList>
{/if}
</LoginToggle>
<a

View file

@ -9,83 +9,82 @@
export let open = new UIEventSource(false)
export let dotsSize = `w-6 h-6`
export let dotsPosition = `top-0 right-0`
export let hideBackground= false
export let hideBackground = false
let menuPosition = ``
if(dotsPosition.indexOf("left-0") >= 0){
if (dotsPosition.indexOf("left-0") >= 0) {
menuPosition = "left-0"
}else{
} else {
menuPosition = `right-0`
}
if(dotsPosition.indexOf("top-0") > 0){
if (dotsPosition.indexOf("top-0") > 0) {
menuPosition += " bottom-0"
}else{
} else {
menuPosition += ` top-0`
}
function toggle() {
open.set(!open.data)
}
</script>
<div class="relative" style="z-index: 50">
<div
class="sidebar-unit absolute {menuPosition} collapsable normal-background button-unstyled "
class="sidebar-unit absolute {menuPosition} collapsable normal-background button-unstyled"
class:transition-background={hideBackground}
class:collapsed={!$open}>
class:collapsed={!$open}
>
<slot />
</div>
<DotsCircleHorizontal
class={ `absolute ${dotsPosition} ${dotsSize} dots-menu transition-colors ${$open?"dots-menu-opened":""}`}
on:click={toggle} />
class={`absolute ${dotsPosition} ${dotsSize} dots-menu transition-colors ${
$open ? "dots-menu-opened" : ""
}`}
on:click={toggle}
/>
</div>
<style>
.dots-menu {
z-index: 50;
}
.dots-menu {
z-index: 50;
}
:global(.dots-menu > path) {
fill: var(--interactive-background);
transition: fill 350ms linear;
cursor: pointer;
:global(.dots-menu > path) {
fill: var(--interactive-background);
transition: fill 350ms linear;
cursor: pointer;
}
}
:global(.dots-menu:hover > path, .dots-menu-opened > path) {
fill: var(--interactive-foreground);
}
:global(.dots-menu:hover > path, .dots-menu-opened > path) {
fill: var(--interactive-foreground)
}
.collapsable {
max-width: 50rem;
max-height: 10rem;
transition: max-width 500ms linear, max-height 500ms linear, border 500ms linear;
overflow: hidden;
flex-wrap: nowrap;
text-wrap: none;
width: max-content;
box-shadow: #ccc;
white-space: nowrap;
border: 1px solid var(--button-background);
background-color: white;
}
.collapsable {
max-width: 50rem;
max-height: 10rem;
transition: max-width 500ms linear, max-height 500ms linear, border 500ms linear;
overflow: hidden;
flex-wrap: nowrap;
text-wrap: none;
width: max-content;
box-shadow: #ccc;
white-space: nowrap;
border: 1px solid var(--button-background);
background-color: white;
}
.transition-background {
transition: background-color 150ms linear;
}
.transition-background {
transition: background-color 150ms linear;
}
.transition-background.collapsed {
background-color: #00000000;
}
.collapsed {
max-width: 0;
max-height: 0;
border: 2px solid #00000000;
pointer-events: none;
}
.transition-background.collapsed {
background-color: #00000000;
}
.collapsed {
max-width: 0;
max-height: 0;
border: 2px solid #00000000;
pointer-events: none;
}
</style>

View file

@ -8,11 +8,11 @@
let transitionParams = {
x: 640,
duration: 200,
easing: sineIn
easing: sineIn,
}
let hidden = !shown.data
shown.addCallback(sh => {
shown.addCallback((sh) => {
hidden = !sh
})
@ -23,19 +23,21 @@
})
</script>
<Drawer placement="right"
transitionType="fly" {transitionParams}
activateClickOutside={false}
divClass="overflow-y-auto z-3"
backdrop={false}
id="drawer-right"
width="w-full sm:w-80 md:w-96"
rightOffset="inset-y-0 right-0"
bind:hidden={hidden}>
<Drawer
placement="right"
transitionType="fly"
{transitionParams}
activateClickOutside={false}
divClass="overflow-y-auto z-3"
backdrop={false}
id="drawer-right"
width="w-full sm:w-80 md:w-96"
rightOffset="inset-y-0 right-0"
bind:hidden
>
<div class="low-interaction h-screen">
<div class="h-full" style={`padding-top: ${height}px`}>
<div class="flex flex-col h-full overflow-y-auto">
<div class="flex h-full flex-col overflow-y-auto">
<slot />
</div>
</div>

View file

@ -3,12 +3,10 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import Popup from "./Popup.svelte"
export let onlyLink: boolean = false
export let bodyPadding = "p-4 md:p-5 "
export let fullscreen: boolean = false
export let shown: UIEventSource<boolean>
</script>
{#if !onlyLink}

View file

@ -8,7 +8,8 @@
export let fullscreen: boolean = false
const shared = "in-page normal-background dark:bg-gray-800 rounded-lg border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md"
const shared =
"in-page normal-background dark:bg-gray-800 rounded-lg border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md"
let defaultClass = "relative flex flex-col mx-auto w-full divide-y " + shared
if (fullscreen) {
defaultClass = shared
@ -27,20 +28,23 @@
export let shown: UIEventSource<boolean>
export let dismissable = true
let _shown = false
shown.addCallbackAndRun(sh => {
shown.addCallbackAndRun((sh) => {
_shown = sh
})
</script>
<Modal open={_shown} on:close={() => shown.set(false)} outsideclose
size="xl"
{dismissable}
{defaultClass} {bodyClass} {dialogClass} {headerClass}
color="none">
<Modal
open={_shown}
on:close={() => shown.set(false)}
outsideclose
size="xl"
{dismissable}
{defaultClass}
{bodyClass}
{dialogClass}
{headerClass}
color="none"
>
<svelte:fragment slot="header">
{#if $$slots.header}
<h1 class="page-header w-full">

View file

@ -10,7 +10,7 @@
export let value: UIEventSource<string>
let _value = value.data ?? ""
value.addCallbackD(v => {
value.addCallbackD((v) => {
_value = v
})
$: value.set(_value)
@ -22,7 +22,7 @@
export let autofocus = false
isFocused?.addCallback(focussed => {
isFocused?.addCallback((focussed) => {
if (focussed) {
requestAnimationFrame(() => {
if (document.activeElement !== inputElement) {
@ -33,41 +33,44 @@
}
})
if(autofocus){
if (autofocus) {
isFocused.set(true)
}
</script>
<form
class="w-full"
on:submit|preventDefault={() => dispatch("search")}
>
<form class="w-full" on:submit|preventDefault={() => dispatch("search")}>
<label
class="neutral-label normal-background flex w-full items-center rounded-full border border-black box-shadow"
class="neutral-label normal-background box-shadow flex w-full items-center rounded-full border border-black"
>
<SearchIcon aria-hidden="true" class="h-8 w-8 ml-2" />
<SearchIcon aria-hidden="true" class="ml-2 h-8 w-8" />
<input
bind:this={inputElement}
on:focus={() => {isFocused?.setData(true)}}
on:blur={() => {isFocused?.setData(false)}}
on:focus={() => {
isFocused?.setData(true)
}}
on:blur={() => {
isFocused?.setData(false)
}}
type="search"
style=" --tw-ring-color: rgb(0 0 0 / 0) !important;"
class="px-0 ml-1 w-full outline-none border-none"
class="ml-1 w-full border-none px-0 outline-none"
on:keypress={(keypr) => {
return keypr.key === "Enter" ? dispatch("search") : undefined
}}
return keypr.key === "Enter" ? dispatch("search") : undefined
}}
bind:value={_value}
use:set_placeholder={placeholder}
use:ariaLabel={placeholder}
/>
{#if $value.length > 0}
<Backspace on:click={() => value.set("")} color="var(--button-background)" class="w-6 h-6 mr-3 cursor-pointer" />
<Backspace
on:click={() => value.set("")}
color="var(--button-background)"
class="mr-3 h-6 w-6 cursor-pointer"
/>
{:else}
<div class="w-6 mr-3" />
<div class="mr-3 w-6" />
{/if}
</label>
</form>

View file

@ -1,55 +1,64 @@
<div class="sidebar-unit">
<slot/>
<slot />
</div>
<style>
:global(.sidebar-unit) {
display: flex;
flex-direction: column;
row-gap: 0.25rem;
background: var(--background-color);
padding: 0.5rem;
border-radius: 0.5rem;
}
:global(.sidebar-unit) {
display: flex;
flex-direction: column;
row-gap: 0.25rem;
background: var(--background-color);
padding: 0.5rem;
border-radius: 0.5rem;
}
:global(.sidebar-unit > h3) {
margin-top: 0;
margin-bottom: 0.5rem;
padding: 0.25rem;
}
:global(.sidebar-unit > h3) {
margin-top: 0;
margin-bottom: 0.5rem;
padding: 0.25rem;
}
:global(.sidebar-button svg, .sidebar-button img, .sidebar-unit > button img, .sidebar-unit > button svg) {
width: 1.5rem;
height: 1.5rem;
margin-right: 0.5rem;
flex-shrink: 0;
}
:global(
.sidebar-button svg,
.sidebar-button img,
.sidebar-unit > button img,
.sidebar-unit > button svg
) {
width: 1.5rem;
height: 1.5rem;
margin-right: 0.5rem;
flex-shrink: 0;
}
:global(.sidebar-button .weblate-link > svg) {
width: 0.75rem;
height: 0.75rem;
flex-shrink: 0;
}
:global(.sidebar-button .weblate-link > svg) {
width: 0.75rem;
height: 0.75rem;
flex-shrink: 0;
}
:global(.sidebar-button, .sidebar-unit > a, .sidebar-unit > button) {
display: flex;
align-items: center;
border-radius: 0.25rem !important;
padding: 0.4rem 0.75rem !important;
text-decoration: none !important;
width: 100%;
text-align: start;
}
:global(.sidebar-button, .sidebar-unit > a, .sidebar-unit > button) {
display: flex;
align-items: center;
border-radius: 0.25rem !important;
padding: 0.4rem 0.75rem !important;
text-decoration: none !important;
width: 100%;
text-align: start;
}
:global(.sidebar-button > svg , .sidebar-button > img, .sidebar-unit > a img, .sidebar-unit > a svg, .sidebar-unit > button svg, .sidebar-unit > button img) {
margin-right: 0.5rem;
flex-shrink: 0;
}
:global(.sidebar-button:hover, .sidebar-unit > a:hover, .sidebar-unit > button:hover) {
background: var(--low-interaction-background) !important;
}
:global(
.sidebar-button > svg,
.sidebar-button > img,
.sidebar-unit > a img,
.sidebar-unit > a svg,
.sidebar-unit > button svg,
.sidebar-unit > button img
) {
margin-right: 0.5rem;
flex-shrink: 0;
}
:global(.sidebar-button:hover, .sidebar-unit > a:hover, .sidebar-unit > button:hover) {
background: var(--low-interaction-background) !important;
}
</style>

View file

@ -31,7 +31,7 @@
return state.sync(
(f) => f === 0,
[],
(b) => (b ? 0 : undefined),
(b) => (b ? 0 : undefined)
)
}
@ -92,7 +92,7 @@
{/if}
</div>
{:else if $isDebugging}
<div class="code">
{layer.id} (no name)
</div>
<div class="code">
{layer.id} (no name)
</div>
{/if}

View file

@ -55,14 +55,18 @@
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
</script>
<div class="low-interaction p-1 rounded-2xl px-3" class:interactive={$firstValue?.length > 0}>
<div class="low-interaction rounded-2xl p-1 px-3" class:interactive={$firstValue?.length > 0}>
{#each parts as part, i}
{#if part["subs"]}
<!-- This is a field! -->
<span class="mx-1">
<InputHelper value={fieldValues[part["subs"]]} type={fieldTypes[part["subs"]]}>
<ValidatedInput slot="fallback" value={fieldValues[part["subs"]]} type={fieldTypes[part["subs"]]}
{feedback} />
<ValidatedInput
slot="fallback"
value={fieldValues[part["subs"]]}
type={fieldTypes[part["subs"]]}
{feedback}
/>
</InputHelper>
</span>
{:else}
@ -70,6 +74,6 @@
{/if}
{/each}
{#if $feedback}
<Tr cls="alert" t={$feedback}/>
<Tr cls="alert" t={$feedback} />
{/if}
</div>

View file

@ -150,7 +150,6 @@
</LoginToggle>
<LanguagePicker />
</SidebarUnit>
<!-- Theme related: documentation links, download, ... -->
@ -218,7 +217,6 @@
<!-- Other links and tools for the given location: open iD/JOSM; community index, ... -->
<SidebarUnit>
<h3>
<Tr t={t.moreUtilsTitle} />
</h3>
@ -238,13 +236,13 @@
<MapillaryLink large={false} mapProperties={state.mapProperties} />
</If>
<a class="flex sidebar-button" href="geo:{$location.lat},{$location.lon}"><ShareIcon /><Tr t={t.openHereDifferentApp}/></a>
<a class="sidebar-button flex" href="geo:{$location.lat},{$location.lon}">
<ShareIcon /><Tr t={t.openHereDifferentApp} />
</a>
</SidebarUnit>
<!-- About MC: various outward links, legal info, ... -->
<SidebarUnit>
<h3>
<Tr t={Translations.t.general.menu.aboutMapComplete} />
</h3>
@ -275,11 +273,11 @@
</a>
<a class="flex" href="mailto:info@mapcomplete.org">
<EnvelopeOpen class="h-6 w-6"/>
<Tr t={Translations.t.general.attribution.emailCreators}/>
<EnvelopeOpen class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.emailCreators} />
</a>
<a class="flex" href="https://hosted.weblate.org/projects/mapcomplete/" target="_blank">
<TranslateIcon class="h-6 w-6"/>
<TranslateIcon class="h-6 w-6" />
<Tr t={Translations.t.translations.activateButton} />
</a>
@ -322,6 +320,3 @@
</div>
</SidebarUnit>
</div>

View file

@ -83,7 +83,11 @@
let featuresForLayer: FeatureSource = state.perLayer.get(targetLayer.id)
if (featuresForLayer) {
if (dontShow) {
featuresForLayer = new StaticFeatureSource(featuresForLayer.features.map(feats => feats.filter(f => dontShow.indexOf(f.properties.id) < 0)))
featuresForLayer = new StaticFeatureSource(
featuresForLayer.features.map((feats) =>
feats.filter((f) => dontShow.indexOf(f.properties.id) < 0)
)
)
}
new ShowDataLayer(map, {
layer: targetLayer,
@ -116,7 +120,7 @@
allowUnsnapped: true,
snappedTo,
snapLocation: value,
},
}
)
const withCorrectedAttributes = new StaticFeatureSource(
snappedLocation.features.mapD((feats) =>
@ -130,8 +134,8 @@
...f,
properties,
}
}),
),
})
)
)
// The actual point to be created, snapped at the new location
new ShowDataLayer(map, {
@ -140,14 +144,13 @@
})
withCorrectedAttributes.features.addCallbackAndRunD((f) => console.log("Snapped point is", f))
}
</script>
<LocationInput
{map}
on:click
{mapProperties}
value={ snapToLayers?.length > 0 ? new UIEventSource(undefined) : value}
value={snapToLayers?.length > 0 ? new UIEventSource(undefined) : value}
initialCoordinate={coordinate}
{maxDistanceInMeters}
>

View file

@ -6,7 +6,7 @@
import { twMerge } from "tailwind-merge"
import { PanoramaxXYZ, Panoramax } from "panoramax-js/dist"
import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte"
import {default as Panoramax_svg} from "../../assets/svg/Panoramax.svelte"
import { default as Panoramax_svg } from "../../assets/svg/Panoramax.svelte"
/*
A subtleButton which opens panoramax in a new tab at the current location
@ -19,11 +19,14 @@
}
let location = mapProperties.location
let zoom = mapProperties.zoom
let href = location.mapD(location =>
host.createViewLink({
location,
zoom: zoom.data,
}), [zoom])
let href = location.mapD(
(location) =>
host.createViewLink({
location,
zoom: zoom.data,
}),
[zoom]
)
export let large: boolean = true
</script>

View file

@ -116,7 +116,6 @@
</script>
<div class="link-underline flex flex-col">
<div class="flex flex-col">
<Tr t={tr.intro} />
<Copyable {state} text={linkToShare} />

View file

@ -6,7 +6,7 @@
import Translations from "../i18n/Translations"
import Marker from "../Map/Marker.svelte"
export let theme: MinimalThemeInformation & {isOfficial?: boolean}
export let theme: MinimalThemeInformation & { isOfficial?: boolean }
let isCustom: boolean = theme.id.startsWith("https://") || theme.id.startsWith("http://")
export let state: { layoutToUse?: { id: string }; osmConnection: OsmConnection }
@ -66,12 +66,12 @@
let href = createUrl(theme, isCustom, state)
</script>
<a class="low-interaction my-1 flex w-full items-center text-ellipsis rounded p-1" href={$href}>
<Marker icons={theme.icon} size="block h-8 w-8 sm:h-11 sm:w-11 m-1 sm:mx-2 md:mx-4 shrink-0" />
<a class="low-interaction my-1 flex w-full items-center text-ellipsis rounded p-1" href={$href}>
<Marker icons={theme.icon} size="block h-8 w-8 sm:h-11 sm:w-11 m-1 sm:mx-2 md:mx-4 shrink-0" />
<span class="flex flex-col overflow-hidden text-ellipsis text-xl font-bold">
<Tr cls="" t={title} />
<Tr cls="subtle text-base" t={description} />
<slot/>
</span>
</a>
<span class="flex flex-col overflow-hidden text-ellipsis text-xl font-bold">
<Tr cls="" t={title} />
<Tr cls="subtle text-base" t={description} />
<slot />
</span>
</a>

View file

@ -12,22 +12,18 @@
export let themes: MinimalThemeInformation[]
export let state: { osmConnection: OsmConnection }
export let hasSelection : boolean = true
export let hasSelection: boolean = true
</script>
<section class="w-full">
<slot name="title" />
<div class="theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3">
{#each themes as theme (theme.id)}
<ThemeButton
{theme}
{state}
>
<ThemeButton {theme} {state}>
{#if $search && hasSelection && themes?.[0] === theme}
<span class="thanks hidden-on-mobile" aria-hidden="true">
<Tr t={Translations.t.general.morescreen.enterToOpen} />
</span>
<span class="thanks hidden-on-mobile" aria-hidden="true">
<Tr t={Translations.t.general.morescreen.enterToOpen} />
</span>
{/if}
</ThemeButton>
{/each}

View file

@ -12,8 +12,5 @@
osmConnection: OsmConnection
}
let customThemes
</script>

View file

@ -22,7 +22,7 @@
JSON.stringify(contents),
"mapcomplete-favourites-" + new Date().toISOString() + ".geojson",
{
mimetype: "application/vnd.geo+json"
mimetype: "application/vnd.geo+json",
}
)
}
@ -33,7 +33,7 @@
gpx,
"mapcomplete-favourites-" + new Date().toISOString() + ".gpx",
{
mimetype: "{gpx=application/gpx+xml}"
mimetype: "{gpx=application/gpx+xml}",
}
)
}

View file

@ -3,8 +3,8 @@
export let expanded = false
export let noBorder = false
let defaultClass: string = undefined
if(noBorder){
let defaultClass: string = undefined
if (noBorder) {
defaultClass = "unstyled w-full flex-grow"
}
</script>

View file

@ -31,14 +31,18 @@
export let canZoom = previewedImage !== undefined
let loaded = false
let showBigPreview = new UIEventSource(false)
onDestroy(showBigPreview.addCallbackAndRun(shown => {
if (!shown) {
previewedImage.set(undefined)
}
}))
onDestroy(previewedImage.addCallbackAndRun(previewedImage => {
showBigPreview.set(previewedImage?.id === image.id)
}))
onDestroy(
showBigPreview.addCallbackAndRun((shown) => {
if (!shown) {
previewedImage.set(undefined)
}
})
)
onDestroy(
previewedImage.addCallbackAndRun((previewedImage) => {
showBigPreview.set(previewedImage?.id === image.id)
})
)
function highlight(entered: boolean = true) {
if (!entered) {
@ -72,43 +76,49 @@
</ImageOperations>
</div>
<div class="absolute top-4 right-4">
<CloseButton class="normal-background"
on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton>
<CloseButton
class="normal-background"
on:click={() => {
console.log("Closing")
previewedImage.set(undefined)
}}
/>
</div>
</Popup>
{#if image.status !== undefined && image.status !== "ready"}
<div class="h-full flex flex-col justify-center">
<div class="flex h-full flex-col justify-center">
<Loading>
<Tr t={Translations.t.image.processing}/>
<Tr t={Translations.t.image.processing} />
</Loading>
</div>
{:else}
<div class="relative shrink-0">
<div class="relative w-fit"
on:mouseenter={() => highlight()}
on:mouseleave={() => highlight(false)}
<div
class="relative w-fit"
on:mouseenter={() => highlight()}
on:mouseleave={() => highlight(false)}
>
<img
bind:this={imgEl}
on:load={() => (loaded = true)}
class={imgClass ?? ""}
class:cursor-zoom-in={canZoom}
on:click={() => {
previewedImage?.set(image)
}}
previewedImage?.set(image)
}}
on:error={() => {
if (fallbackImage) {
imgEl.src = fallbackImage
}
}}
if (fallbackImage) {
imgEl.src = fallbackImage
}
}}
src={image.url}
/>
{#if canZoom && loaded}
<div
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
on:click={() => previewedImage.set(image)}>
on:click={() => previewedImage.set(image)}
>
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
</div>
{/if}

View file

@ -56,7 +56,5 @@
</div>
<slot />
</div>
</div>

View file

@ -35,7 +35,7 @@
key: undefined,
provider: AllImageProviders.byName(image.provider),
date: new Date(image.date),
id: Object.values(image.osmTags)[0]
id: Object.values(image.osmTags)[0],
}
async function applyLink(isLinked: boolean) {
@ -46,7 +46,7 @@
if (isLinked) {
const action = new LinkImageAction(currentTags.id, key, url, tags, {
theme: tags.data._orig_theme ?? state.theme.id,
changeType: "link-image"
changeType: "link-image",
})
await state.changes.applyAction(action)
} else {
@ -55,7 +55,7 @@
if (v === url) {
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, {
theme: tags.data._orig_theme ?? state.theme.id,
changeType: "remove-image"
changeType: "remove-image",
})
state.changes.applyAction(action)
}
@ -67,9 +67,8 @@
let element: HTMLDivElement
if (highlighted) {
onDestroy(
highlighted.addCallbackD(highlightedUrl => {
highlighted.addCallbackD((highlightedUrl) => {
if (highlightedUrl === image.pictureUrl) {
Utils.scrollIntoView(element)
}

View file

@ -25,7 +25,6 @@
import { BBox } from "../../Logic/BBox"
import PanoramaxLink from "../BigComponents/PanoramaxLink.svelte"
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
export let lon: number
@ -38,7 +37,7 @@
let imagesProvider = state.nearbyImageSearcher
let loadedImages = AllImageProviders.LoadImagesFor(tags).mapD(
(loaded) => new Set(loaded.map((img) => img.url)),
(loaded) => new Set(loaded.map((img) => img.url))
)
let imageState = imagesProvider.getImagesAround(lon, lat)
let result: Store<P4CPicture[]> = imageState.images.mapD(
@ -47,53 +46,61 @@
.filter(
(p: P4CPicture) =>
!loadedImages.data.has(p.pictureUrl) && // We don't show any image which is already linked
!p.details.isSpherical,
!p.details.isSpherical
)
.slice(0, 25),
[loadedImages],
[loadedImages]
)
let asFeatures = result.map(p4cs => p4cs.map(p4c => (<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [p4c.coordinates.lng, p4c.coordinates.lat],
},
properties: {
id: p4c.pictureUrl,
rotation: p4c.direction,
},
})))
let asFeatures = result.map((p4cs) =>
p4cs.map(
(p4c) =>
<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [p4c.coordinates.lng, p4c.coordinates.lat],
},
properties: {
id: p4c.pictureUrl,
rotation: p4c.direction,
},
}
)
)
let selected = new UIEventSource<P4CPicture>(undefined)
let selectedAsFeature = selected.mapD(s => {
return [<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [s.coordinates.lng, s.coordinates.lat],
let selectedAsFeature = selected.mapD((s) => {
return [
<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [s.coordinates.lng, s.coordinates.lat],
},
properties: {
id: s.pictureUrl,
selected: "yes",
rotation: s.direction,
},
},
properties: {
id: s.pictureUrl,
selected: "yes",
rotation: s.direction,
},
}]
]
})
let someLoading = imageState.state.mapD((stateRecord) =>
Object.values(stateRecord).some((v) => v === "loading"),
Object.values(stateRecord).some((v) => v === "loading")
)
let errors = imageState.state.mapD((stateRecord) =>
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error"),
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error")
)
let highlighted = new UIEventSource<string>(undefined)
onDestroy(highlighted.addCallbackD(hl => {
const p4c = result.data?.find(i => i.pictureUrl === hl)
onDestroy(
highlighted.addCallbackD((hl) => {
const p4c = result.data?.find((i) => i.pictureUrl === hl)
selected.set(p4c)
},
))
})
)
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let mapProperties = new MapLibreAdaptor(map, {
@ -104,7 +111,6 @@
location: new UIEventSource({ lon, lat }),
})
const geocodedImageLayer = new LayerConfig(<LayerConfigJson>geocoded_image)
new ShowDataLayer(map, {
features: new StaticFeatureSource(asFeatures),
@ -115,15 +121,10 @@
},
})
ShowDataLayer.showMultipleLayers(
map,
new StaticFeatureSource([feature]),
state.theme.layers,
)
ShowDataLayer.showMultipleLayers(map, new StaticFeatureSource([feature]), state.theme.layers)
onDestroy(
asFeatures.addCallbackAndRunD(features => {
asFeatures.addCallbackAndRunD((features) => {
if (features.length == 0) {
return
}
@ -132,7 +133,7 @@
bbox = bbox.unionWith(BBox.get(f))
}
mapProperties.maxbounds.set(bbox.pad(4))
}),
})
)
new ShowDataLayer(map, {
@ -142,8 +143,6 @@
highlighted.set(feature.properties.id)
},
})
</script>
<div class="flex flex-col">
@ -158,16 +157,23 @@
{:else}
<div class="flex w-full space-x-4 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"
on:mouseenter={() => {highlighted.set(image.pictureUrl)}}
on:mouseleave={() =>{ highlighted.set(undefined); selected.set(undefined)}}
<span
class="w-fit shrink-0"
style="scroll-snap-align: start"
on:mouseenter={() => {
highlighted.set(image.pictureUrl)
}}
on:mouseleave={() => {
highlighted.set(undefined)
selected.set(undefined)
}}
>
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} {highlighted} />
</span>
{/each}
</div>
{/if}
<div class="w-full flex flex-wrap justify-end gap-x-8 pt-2">
<div class="flex w-full flex-wrap justify-end gap-x-8 pt-2">
<PanoramaxLink
large={false}
mapProperties={{ zoom: new ImmutableStore(16), location: new ImmutableStore({ lon, lat }) }}
@ -178,7 +184,6 @@
/>
</div>
<div class="my-2 flex justify-between">
<div>
{#if $someLoading && $result.length > 0}
@ -193,7 +198,6 @@
</div>
</div>
<div class="h-48">
<MaplibreMap interactive={false} {map} {mapProperties} />
</div>

View file

@ -30,7 +30,13 @@
</script>
{#if enableLogin.data}
<button on:click={() => {shown.set(!shown.data)}}><Tr t={t.seeNearby}/> </button>
<button
on:click={() => {
shown.set(!shown.data)
}}
>
<Tr t={t.seeNearby} />
</button>
<Popup {shown} bodyPadding="p-4">
<span slot="header">
<Tr t={t.seeNearby} />

View file

@ -49,16 +49,20 @@
}
if (layer?.id === "note") {
const uploadResult = await state?.imageUploadManager.uploadImageWithLicense(tags.data.id,
const uploadResult = await state?.imageUploadManager.uploadImageWithLicense(
tags.data.id,
state.osmConnection.userDetails.data?.name ?? "Anonymous",
file, "image", noBlur)
file,
"image",
noBlur
)
if (!uploadResult) {
return
}
const url = uploadResult.absoluteUrl
await state.osmConnection.addCommentToNote(tags.data.id, url)
NoteCommentElement.addCommentTo(url, <UIEventSource<any>>tags, {
osmConnection: state.osmConnection
osmConnection: state.osmConnection,
})
return
}
@ -88,7 +92,7 @@
multiple={true}
on:submit={(e) => handleFiles(e.detail)}
>
<div class="flex items-center text-2xl w-full justify-center">
<div class="flex w-full items-center justify-center text-2xl">
{#if image !== undefined}
<img src={image} aria-hidden="true" />
{:else}
@ -98,19 +102,17 @@
{labelText}
{:else}
<div class="flex flex-col">
<Tr t={t.addPicture} />
{#if noBlur}
<span class="subtle text-sm">
<Tr t={t.upload.noBlur}/>
</span>
<span class="subtle text-sm">
<Tr t={t.upload.noBlur} />
</span>
{/if}
</div>
{/if}
</div>
</FileSelector>
<div class="text-xs subtle italic">
<div class="subtle text-xs italic">
<Tr t={Translations.t.general.attribution.panoramaxLicenseCCBYSA} />
<span class="mx-1"></span>
<Tr t={t.respectPrivacy} />

View file

@ -20,7 +20,7 @@
let mla = new MapLibreAdaptor(map, mapProperties)
mla.allowMoving.setData(false)
mla.allowZooming.setData(false)
state?.mapProperties?.rasterLayer?.addCallbackAndRunD(l => mla.rasterLayer.set(l))
state?.mapProperties?.rasterLayer?.addCallbackAndRunD((l) => mla.rasterLayer.set(l))
let directionElem: HTMLElement | undefined
$: value.addCallbackAndRunD((degrees) => {

View file

@ -7,7 +7,7 @@
export let wd: number
export let h: number
export let type: "full" | "half"
let dispatch = createEventDispatcher<{ "start", "end", "move","clear" }>()
let dispatch = createEventDispatcher<{ start; end; move; clear }>()
let element: HTMLElement
function send(signal: "start" | "end" | "move", ev: Event) {
@ -39,32 +39,30 @@
element.onmouseenter = (ev) => send("move", ev)
element.onmouseup = (ev) => send("end", ev)
element.addEventListener("touchstart", ev => dispatch("start", ev))
element.addEventListener("touchend", ev => {
element.addEventListener("touchstart", (ev) => dispatch("start", ev))
element.addEventListener("touchend", (ev) => {
const el = elementUnderTouch(ev)
if (el?.onmouseup) {
el?.onmouseup(<any>ev)
}else{
} else {
// We dragged outside of the table
dispatch("clear")
}
})
element.addEventListener("touchmove", ev => {
element.addEventListener("touchmove", (ev) => {
const underTouch = elementUnderTouch(ev)
if(typeof underTouch?.onmouseenter !== "function"){
return
}
if (typeof underTouch?.onmouseenter !== "function") {
return
}
underTouch.onmouseenter(<any>ev)
underTouch.onmouseenter(<any>ev)
})
})
</script>
<td bind:this={element} id={"oh-"+type+"-"+h+"-"+wd}
class:border-black={(h + 1) % 6 === 0}
class={`oh-timecell oh-timecell-${type} oh-timecell-${wd} `}
<td
bind:this={element}
id={"oh-" + type + "-" + h + "-" + wd}
class:border-black={(h + 1) % 6 === 0}
class={`oh-timecell oh-timecell-${type} oh-timecell-${wd} `}
/>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { UIEventSource } from "../../../../Logic/UIEventSource"
import type { OpeningHour } from "../../../OpeningHours/OpeningHours"
import { OH as OpeningHours } from "../../../OpeningHours/OpeningHours"
@ -27,10 +26,9 @@
let element: HTMLTableElement
function range(n: number) {
return Utils.TimesT(n, n => n)
return Utils.TimesT(n, (n) => n)
}
function clearSelection() {
const allCells = Array.from(document.getElementsByClassName("oh-timecell"))
for (const timecell of allCells) {
@ -38,27 +36,33 @@
}
}
function setSelectionNormalized(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) {
function setSelectionNormalized(
weekdayStart: number,
weekdayEnd: number,
hourStart: number,
hourEnd: number
) {
for (let wd = weekdayStart; wd <= weekdayEnd; wd++) {
for (let h = (hourStart); h < (hourEnd); h++) {
for (let h = hourStart; h < hourEnd; h++) {
h = Math.floor(h)
if (h >= hourStart && h < hourEnd) {
const elFull = document.getElementById("oh-full-" + h + "-" + wd)
elFull?.classList?.add("oh-timecell-selected")
}
if (h + 0.5 < hourEnd) {
const elHalf = document.getElementById("oh-half-" + h + "-" + wd)
elHalf?.classList?.add("oh-timecell-selected")
}
}
}
}
function setSelection(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) {
function setSelection(
weekdayStart: number,
weekdayEnd: number,
hourStart: number,
hourEnd: number
) {
let hourA = hourStart
let hourB = hourEnd
if (hourA > hourB) {
@ -69,8 +73,12 @@
hourA -= 0.5
hourB += 0.5
}
setSelectionNormalized(Math.min(weekdayStart, weekdayEnd), Math.max(weekdayStart, weekdayEnd),
hourA, hourB)
setSelectionNormalized(
Math.min(weekdayStart, weekdayEnd),
Math.max(weekdayStart, weekdayEnd),
hourA,
hourB
)
}
let selectionStart: [number, number] = undefined
@ -100,8 +108,11 @@
let startMinutes = Math.round((start * 60) % 60)
let endMinutes = Math.round((end * 60) % 60)
let newOhs = [...value.data]
for (let wd = Math.min(selectionStart[0], weekday); wd <= Math.max(selectionStart[0], weekday); wd++) {
for (
let wd = Math.min(selectionStart[0], weekday);
wd <= Math.max(selectionStart[0], weekday);
wd++
) {
const oh: OpeningHour = {
startHour: Math.floor(start),
endHour: Math.floor(end),
@ -116,7 +127,6 @@
clearSelection()
}
let lasttouched: [number, number] = undefined
function moved(weekday: number, hour: number) {
@ -125,11 +135,10 @@
clearSelection()
setSelection(selectionStart[0], weekday, selectionStart[1], hour + 0.5)
}
const allRows = Array.from(element.getElementsByTagName("tr"))
const allRows = Array.from(element.getElementsByTagName("tr"))
for (const r of allRows) {
r.classList.remove("hover")
r.classList.remove("hovernext")
}
const selectedRow = allRows[hour * 2 + 2]
selectedRow?.classList?.add("hover")
@ -158,26 +167,33 @@
* @param oh
*/
function rangeStyle(oh: OpeningHour, totalHeight: number): string {
const top = (oh.startHour + oh.startMinutes / 60) * totalHeight / 24
const height = (oh.endHour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60) * totalHeight / 24
const top = ((oh.startHour + oh.startMinutes / 60) * totalHeight) / 24
const height =
((oh.endHour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60) * totalHeight) / 24
return `top: ${top}px; height: ${height}px; z-index: 20`
}
</script>
<table
bind:this={element}
class="oh-table no-weblate w-full" cellspacing="0" cellpadding="0"
class:hasselection={selectionStart !== undefined} class:hasnoselection={selectionStart === undefined}
on:mouseleave={mouseLeft}>
class="oh-table no-weblate w-full"
cellspacing="0"
cellpadding="0"
class:hasselection={selectionStart !== undefined}
class:hasnoselection={selectionStart === undefined}
on:mouseleave={mouseLeft}
>
<tr>
<!-- Header row -->
<th style="width: 9%">
<!-- Top-left cell -->
<slot name="top-left">
<button class="absolute top-0 left-0 p-1 rounded-full" on:click={() => value.set([])} style="z-index: 10">
<TrashIcon class="w-5 h-5" />
<button
class="absolute top-0 left-0 rounded-full p-1"
on:click={() => value.set([])}
style="z-index: 10"
>
<TrashIcon class="h-5 w-5" />
</button>
</slot>
</th>
@ -188,101 +204,116 @@
{/each}
</tr>
<tr class="h-0 nobold">
<tr class="nobold h-0">
<!-- Virtual row to add the ranges to-->
<td style="width: 9%" />
{#each range(7) as wd}
<td style="width: 13%; position: relative;">
<div class="h-0 pointer-events-none" style="z-index: 10">
{#each $value.filter(oh => oh.weekday === wd).map(oh => OpeningHours.rangeAs24Hr(oh)) as range }
<div class="absolute pointer-events-none px-1 md:px-2 w-full "
style={rangeStyle(range, totalHeight)}
<div class="pointer-events-none h-0" style="z-index: 10">
{#each $value
.filter((oh) => oh.weekday === wd)
.map((oh) => OpeningHours.rangeAs24Hr(oh)) as range}
<div
class="pointer-events-none absolute w-full px-1 md:px-2"
style={rangeStyle(range, totalHeight)}
>
<div class="rounded-xl border-interactive h-full low-interaction flex flex-col justify-between">
<div
class="border-interactive low-interaction flex h-full flex-col justify-between rounded-xl"
>
<div class:hidden={range.endHour - range.startHour < 3}>
{OpeningHours.hhmm(range.startHour, range.startMinutes)}
</div>
<button class="w-fit rounded-full p-1 self-center pointer-events-auto"
on:click={() => {
const cleaned = value.data.filter(v => !OpeningHours.isSame(v, range))
console.log("Cleaned", cleaned, OpeningHours.ToString(value.data))
value.set(cleaned)
}}>
<TrashIcon class="w-6 h-6" />
<button
class="pointer-events-auto w-fit self-center rounded-full p-1"
on:click={() => {
const cleaned = value.data.filter((v) => !OpeningHours.isSame(v, range))
console.log("Cleaned", cleaned, OpeningHours.ToString(value.data))
value.set(cleaned)
}}
>
<TrashIcon class="h-6 w-6" />
</button>
<div class:hidden={range.endHour - range.startHour < 3}>
{OpeningHours.hhmm(range.endHour, range.endMinutes)}
</div>
</div>
</div>
{/each}
</div>
</td>
{/each}
</tr>
{#each range(24) as h}
<tr style="height: 0.75rem; width: 9%"> <!-- even row, for the hour -->
<td rowspan={ h > 0 ? 2: 1 }
class="relative text-sm sm:text-base oh-left-col oh-timecell-full border-box interactive "
style={ h > 0 ? "top: -0.75rem" : "height:0; top: -0.75rem"}>
<tr style="height: 0.75rem; width: 9%">
<!-- even row, for the hour -->
<td
rowspan={h > 0 ? 2 : 1}
class="oh-left-col oh-timecell-full border-box interactive relative text-sm sm:text-base"
style={h > 0 ? "top: -0.75rem" : "height:0; top: -0.75rem"}
>
{#if h > 0}
<span class="hour-header w-full">
{h}:00
{h}:00
</span>
{/if}
</td>
{#each range(7) as wd}
<OHCell type="full" {h} {wd} on:start={() => startSelection(wd, h)} on:end={() => endSelection(wd, h)}
on:move={() => moved(wd, h)} on:clear={() => clearSelection()} />
<OHCell
type="full"
{h}
{wd}
on:start={() => startSelection(wd, h)}
on:end={() => endSelection(wd, h)}
on:move={() => moved(wd, h)}
on:clear={() => clearSelection()}
/>
{/each}
</tr>
<tr style="height: calc( 0.75rem - 1px) "> <!-- odd row, for the half hour -->
<tr style="height: calc( 0.75rem - 1px) ">
<!-- odd row, for the half hour -->
{#if h === 0}
<td/> <!-- extra cell to compensate for irregular header-->
<td />
<!-- extra cell to compensate for irregular header-->
{/if}
{#each range(7) as wd}
<OHCell type="half" {h} {wd} on:start={() => startSelection(wd, h + 0.5)}
on:end={() => endSelection(wd, h + 0.5)}
on:move={() => moved(wd, h + 0.5)} on:clear={() => clearSelection()} />
<OHCell
type="half"
{h}
{wd}
on:start={() => startSelection(wd, h + 0.5)}
on:end={() => endSelection(wd, h + 0.5)}
on:move={() => moved(wd, h + 0.5)}
on:clear={() => clearSelection()}
/>
{/each}
</tr>
{/each}
</table>
<style>
th {
top: 0;
position: sticky;
z-index: 10;
}
th {
top: 0;
position: sticky;
z-index: 10;
}
.hasselection tr:hover .hour-header, .hasselection tr.hover .hour-header {
border-bottom: 2px solid black;
}
.hasselection tr:hover + tr {
font-weight: bold;
}
.hasselection tr.hovernext {
font-weight: bold;
}
.hasnoselection tr:hover, .hasnoselection tr.hover {
font-weight: bold;
}
.hasselection tr:hover .hour-header,
.hasselection tr.hover .hour-header {
border-bottom: 2px solid black;
}
.hasselection tr:hover + tr {
font-weight: bold;
}
.hasselection tr.hovernext {
font-weight: bold;
}
.hasnoselection tr:hover,
.hasnoselection tr.hover {
font-weight: bold;
}
</style>

View file

@ -15,7 +15,6 @@
let postfix = ""
if (args) {
try {
const data = JSON.stringify(args)
if (data["prefix"]) {
prefix = data["prefix"]
@ -31,11 +30,15 @@
const state = new OpeningHoursState(value, prefix, postfix)
let expanded = new UIEventSource(false)
</script>
<Popup bodyPadding="p-0" shown={expanded}>
<OHTable value={state.normalOhs} />
<button on:click={() => expanded.set(false)} class="absolute left-0 bottom-0 primary pointer-events-auto h-8 w-10 rounded-full">
<Check class="shrink-0 w-6 h-6 m-0 p-0" color="white"/>
</button>
<button
on:click={() => expanded.set(false)}
class="primary pointer-events-auto absolute left-0 bottom-0 h-8 w-10 rounded-full"
>
<Check class="m-0 h-6 w-6 shrink-0 p-0" color="white" />
</button>
</Popup>
<button on:click={() => expanded.set(true)}>Pick opening hours</button>
<PublicHolidaySelector value={state.phSelectorValue} />

View file

@ -62,7 +62,7 @@ export default class Validators {
"velopark",
"nsi",
"currency",
"regex"
"regex",
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -94,7 +94,7 @@ export default class Validators {
new VeloparkValidator(),
new NameSuggestionIndexValidator(),
new CurrencyValidator(),
new RegexValidator()
new RegexValidator(),
]
private static _byType = Validators._byTypeConstructor()

View file

@ -7,23 +7,24 @@ export default class OpeningHoursValidator extends Validator {
"opening_hours",
[
"Has extra elements to easily input when a POI is opened.",
("### Helper arguments"),
"### Helper arguments",
"Only one helper argument named `options` can be provided. It is a JSON-object of type `{ prefix: string, postfix: string }`:",
MarkdownUtils.table(
["subarg", "doc"],
[
[
"prefix",
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse."
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse.",
],
[
"postfix",
"Piece of text that will always be added to the end of the generated opening hours"
]
]),
("### Example usage"),
"Piece of text that will always be added to the end of the generated opening hours",
],
]
),
"### Example usage",
"To add a conditional (based on time) access restriction:\n\n```\n" +
`
`
"freeform": {
"key": "access:conditional",
"type": "opening_hours",
@ -34,7 +35,7 @@ export default class OpeningHoursValidator extends Validator {
}
]
}` +
"\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`"
"\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`",
].join("\n")
)
}

View file

@ -48,15 +48,12 @@ export default class PhoneValidator extends Validator {
}
let countryCode: CountryCode = undefined
if (country) {
countryCode = <CountryCode> country()?.toUpperCase()
countryCode = <CountryCode>country()?.toUpperCase()
}
if (this.isShortCode(str, countryCode)) {
return str
}
return parsePhoneNumberFromString(
str,
countryCode
)?.formatInternational()
return parsePhoneNumberFromString(str, countryCode)?.formatInternational()
}
/**

View file

@ -3,16 +3,16 @@ import { s } from "vitest/dist/env-afee91f0"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
export default class RegexValidator extends StringValidator{
export default class RegexValidator extends StringValidator {
constructor() {
super("regex", "Validates a regex")
}
getFeedback(s: string): Translation | undefined {
try{
try {
new RegExp(s)
}catch (e) {
return Translations.T("Not a valid Regex: "+e)
} catch (e) {
return Translations.T("Not a valid Regex: " + e)
}
}

View file

@ -17,12 +17,12 @@ export default class WikidataValidator extends Validator {
[
[
"key",
"the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search"
"the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search",
],
[
"options",
"A JSON-object of type `{ removePrefixes: Record<string, string[]>, removePostfixes: Record<string, string[]>, ... }`. See the more detailed explanation below"
]
"A JSON-object of type `{ removePrefixes: Record<string, string[]>, removePostfixes: Record<string, string[]>, ... }`. See the more detailed explanation below",
],
]
),
"#### Suboptions",
@ -31,28 +31,26 @@ export default class WikidataValidator extends Validator {
[
[
"removePrefixes",
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes"
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes",
],
[
"removePostfixes",
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes."
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.",
],
[
"instanceOf",
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans"
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans",
],
[
"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"
"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"],
]
)
),
].join("\n\n")
private static readonly docsExampleUsage: string = "### Example usage\n\n" +
private static readonly docsExampleUsage: string =
"### Example usage\n\n" +
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
\`\`\`json
@ -96,9 +94,13 @@ Another example is to search for species and trees:
\`\`\`
`
constructor() {
super("wikidata", "A wikidata identifier, e.g. Q42.\n\n" + WikidataValidator.docs + WikidataValidator.docsExampleUsage)
super(
"wikidata",
"A wikidata identifier, e.g. Q42.\n\n" +
WikidataValidator.docs +
WikidataValidator.docsExampleUsage
)
}
public isValid(str): boolean {

View file

@ -157,18 +157,18 @@
<LockClosed class={clss} {color} />
{:else if icon === "key"}
<Key class={clss} {color} />
{:else if icon==="globe_alt"}
{:else if icon === "globe_alt"}
<GlobeAltIcon class={clss} {color} />
{:else if icon === "building_office_2"}
<BuildingOffice2 class={clss} {color} />
{:else if icon === "house"}
<HomeIcon class={clss} {color} />
{:else if icon === "train"}
<Train {color} class={clss}/>
{:else if icon === "train"}
<Train {color} class={clss} />
{:else if icon === "airport"}
<Airport {color} class={clss}/>
<Airport {color} class={clss} />
{:else if icon === "building_storefront"}
<BuildingStorefront {color} class={clss}/>
<BuildingStorefront {color} class={clss} />
{:else if icon === "snap"}
<Snap class={clss} />
{:else if Utils.isEmoji(icon)}

View file

@ -1,5 +1,10 @@
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import maplibregl, { Map as MLMap, Map as MlMap, ScaleControl, SourceSpecification } from "maplibre-gl"
import maplibregl, {
Map as MLMap,
Map as MlMap,
ScaleControl,
SourceSpecification,
} from "maplibre-gl"
import { RasterLayerPolygon } from "../../Models/RasterLayers"
import { Utils } from "../../Utils"
import { BBox } from "../../Logic/BBox"
@ -23,13 +28,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
"dragRotate",
"dragPan",
"keyboard",
"touchZoomRotate"
"touchZoomRotate",
]
private static maplibre_zoom_handlers = [
"scrollZoom",
"boxZoom",
"doubleClickZoom",
"touchZoomRotate"
"touchZoomRotate",
]
readonly location: UIEventSource<{ lon: number; lat: number }>
private readonly isFlying = new UIEventSource(false)
@ -225,7 +230,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
this.allowZooming.addCallbackAndRun((allowZooming) => self.setAllowZooming(allowZooming))
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
this.useTerrain?.addCallbackAndRun((useTerrain) => self.setTerrain(useTerrain))
this.showScale?.addCallbackAndRun(showScale => self.setScale(showScale))
this.showScale?.addCallbackAndRun((showScale) => self.setScale(showScale))
}
/**
@ -240,9 +245,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
return {
map: mlmap,
ui: new SvelteUIElement(MaplibreMap, {
map: mlmap
map: mlmap,
}),
mapproperties: new MapLibreAdaptor(mlmap)
mapproperties: new MapLibreAdaptor(mlmap),
}
}
@ -310,7 +315,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
) {
const event = {
date: new Date(),
key: key
key: key,
}
for (let i = 0; i < this._onKeyNavigation.length; i++) {
@ -499,7 +504,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
const bounds = map.getBounds()
const bbox = new BBox([
[bounds.getEast(), bounds.getNorth()],
[bounds.getWest(), bounds.getSouth()]
[bounds.getWest(), bounds.getSouth()],
])
if (this.bounds.data === undefined || !isSetup) {
this.bounds.setData(bbox)
@ -693,14 +698,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
type: "raster-dem",
url:
"https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" +
Constants.maptilerApiKey
Constants.maptilerApiKey,
})
try {
while (!map?.isStyleLoaded()) {
await Utils.waitFor(250)
}
map.setTerrain({
source: id
source: id,
})
} catch (e) {
console.error(e)
@ -716,17 +721,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
return
}
if (!showScale) {
if(this.scaleControl){
if (this.scaleControl) {
map.removeControl(this.scaleControl)
this.scaleControl = undefined
}
return
}
if (this.scaleControl === undefined) {
this.scaleControl = new ScaleControl({
maxWidth: 100,
unit: "metric"
unit: "metric",
})
}
if (!map.hasControl(this.scaleControl)) {
@ -739,7 +743,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
window.requestAnimationFrame(() => {
this._maplibreMap.data?.flyTo({
zoom,
center: [lon, lat]
center: [lon, lat],
})
})
}

View file

@ -48,8 +48,11 @@ class PointRenderingLayer {
this._onClick = onClick
this._selectedElement = selectedElement
const self = this
if(!features?.features){
throw "Could not setup a PointRenderingLayer; features?.features is undefined/null. The layer is "+layer.id
if (!features?.features) {
throw (
"Could not setup a PointRenderingLayer; features?.features is undefined/null. The layer is " +
layer.id
)
}
features.features?.addCallbackAndRunD((features) => self.updateFeatures(features))
visibility?.addCallbackAndRunD((visible) => {
@ -163,7 +166,7 @@ class PointRenderingLayer {
})
if (this._onClick) {
el.addEventListener("click", (ev)=> {
el.addEventListener("click", (ev) => {
ev.preventDefault()
this._onClick(feature)
// Workaround to signal the MapLibreAdaptor to ignore this click

View file

@ -859,8 +859,14 @@ This list will be sorted
return ranges
}
public static isSame(a: OpeningHour, b: OpeningHour){
return a.weekday === b.weekday && a.startHour === b.startHour && a.startMinutes === b.startMinutes && a.endHour === b.endHour && a.endMinutes === b.endMinutes
public static isSame(a: OpeningHour, b: OpeningHour) {
return (
a.weekday === b.weekday &&
a.startHour === b.startHour &&
a.startMinutes === b.startMinutes &&
a.endHour === b.endHour &&
a.endMinutes === b.endMinutes
)
}
private static multiply(
weekdays: number[],
@ -930,11 +936,12 @@ This list will be sorted
* OH.rangeAs24Hr(oh).endHour // => 24
*/
static rangeAs24Hr(oh: OpeningHour) {
if(oh.endHour === 0){
return {
...oh, endHour : 24
}
}
if (oh.endHour === 0) {
return {
...oh,
endHour: 24,
}
}
return oh
}
}

View file

@ -14,7 +14,7 @@ export default class OpeningHoursState {
constructor(
value: UIEventSource<string> = new UIEventSource<string>(""),
prefix = "",
postfix = "",
postfix = ""
) {
let valueWithoutPrefix = value
if (prefix !== "" && postfix !== "") {
@ -44,7 +44,7 @@ export default class OpeningHoursState {
}
return prefix + noPrefix + postfix
},
}
)
}
@ -80,7 +80,6 @@ export default class OpeningHoursState {
}
this.phSelectorValue = new UIEventSource<string>(ph ?? "")
// Note: MUST be bound AFTER the leftover rules!
this.normalOhs = valueWithoutPrefix.sync(
(str) => {
@ -111,7 +110,7 @@ export default class OpeningHoursState {
return oldString // We pass a reference to the old string to stabilize the EventSource
}
return str
},
}
)
/*
const leftoverWarning = new VariableUiElement(
@ -127,8 +126,5 @@ export default class OpeningHoursState {
])
})
)*/
}
}

View file

@ -64,8 +64,8 @@
{:else if error !== undefined}
<Tr cls="alert" t={t.error.Subs({ error })} />
<button on:click={() => detectSpecies()}>
<ArrowPath class="w-6 h-6"/>
<Tr t={Translations.t.general.retry}/>
<ArrowPath class="h-6 w-6" />
<Tr t={Translations.t.general.retry} />
</button>
{:else if $imageUrls.length === 0}
<!-- No urls are available, show the explanation instead-->

View file

@ -36,18 +36,17 @@
let allCalculatedTags = new Set<string>([...calculatedTags, ...metaKeys])
let search = new UIEventSource<string>("")
function downloadAsJson(){
function downloadAsJson() {
Utils.offerContentsAsDownloadableFile(
JSON.stringify(tags.data, null, " "), "tags-"+(tags.data.id ?? layer?.id ?? "")+".json"
JSON.stringify(tags.data, null, " "),
"tags-" + (tags.data.id ?? layer?.id ?? "") + ".json"
)
}
</script>
<section>
<Searchbar value={search} placeholder={Translations.T("Search a key")}></Searchbar>
<button class="as-link" on:click={() => downloadAsJson()}>
Download as JSON
</button>
<Searchbar value={search} placeholder={Translations.T("Search a key")} />
<button class="as-link" on:click={() => downloadAsJson()}>Download as JSON</button>
<table class="zebra-table break-all">
<tr>
<th>Key</th>
@ -57,7 +56,9 @@
<th colspan="2">Normal tags</th>
</tr>
{#each $tagKeys as key}
{#if !allCalculatedTags.has(key) && ($search?.length === 0 || key.toLowerCase().indexOf($search.toLowerCase()) >= 0)}
{#if !allCalculatedTags.has(key) && ($search?.length === 0 || key
.toLowerCase()
.indexOf($search.toLowerCase()) >= 0)}
<tr>
<td>{key}</td>
<td style="width: 75%">

View file

@ -82,7 +82,8 @@
<span class="flex flex-col p-2">
{#if currentStep === "reason" && moveWizardState.reasons.length > 1}
{#each moveWizardState.reasons as reasonSpec}
<button class="flex justify-start"
<button
class="flex justify-start"
on:click={() => {
reason.setData(reasonSpec)
currentStep = "pick_location"
@ -112,7 +113,7 @@
</div>
{#if $reason.includeSearch}
<!-- TODO -->
<!-- TODO -->
{/if}
<div class="flex flex-wrap">
@ -124,7 +125,12 @@
<button
class="primary w-full"
on:click={() => {
moveWizardState.moveFeature(newLocation.data, snappedTo.data, reason.data, featureToMove)
moveWizardState.moveFeature(
newLocation.data,
snappedTo.data,
reason.data,
featureToMove
)
currentStep = "moved"
}}
>

View file

@ -48,7 +48,12 @@ export class MoveWizardState {
* @param layer
* @param state
*/
constructor(id: string, options: MoveConfig, layer: LayerConfig, state: SpecialVisualizationState) {
constructor(
id: string,
options: MoveConfig,
layer: LayerConfig,
state: SpecialVisualizationState
) {
this.layer = layer
this._state = state
this.featureToMoveId = id
@ -91,11 +96,16 @@ export class MoveWizardState {
}
const tags = this._state.featureProperties.getStore(this.featureToMoveId).data
const matchingPresets = this.layer.presets.filter(preset => preset.preciseInput.snapToLayers && new And(preset.tags).matchesProperties(tags))
const matchingPreset = matchingPresets.flatMap(pr => pr.preciseInput?.snapToLayers)
const matchingPresets = this.layer.presets.filter(
(preset) =>
preset.preciseInput.snapToLayers && new And(preset.tags).matchesProperties(tags)
)
const matchingPreset = matchingPresets.flatMap((pr) => pr.preciseInput?.snapToLayers)
for (const layerId of matchingPreset) {
const snapOntoLayer = this._state.theme.getLayer(layerId)
const text = <Translation> t.reasons.reasonSnapTo.PartialSubsTr("name", snapOntoLayer.snapName)
const text = <Translation>(
t.reasons.reasonSnapTo.PartialSubsTr("name", snapOntoLayer.snapName)
)
reasons.push({
text,
invitingText: text,
@ -112,7 +122,6 @@ export class MoveWizardState {
})
}
return reasons
}
@ -120,21 +129,23 @@ export class MoveWizardState {
loc: { lon: number; lat: number },
snappedTo: WayId,
reason: MoveReason,
featureToMove: Feature<Point>,
featureToMove: Feature<Point>
) {
const state = this._state
if(snappedTo !== undefined){
if (snappedTo !== undefined) {
this.moveDisallowedReason.set(Translations.t.move.partOfAWay)
}
await state.changes.applyAction(
new ChangeLocationAction(state,
new ChangeLocationAction(
state,
featureToMove.properties.id,
[loc.lon, loc.lat],
snappedTo,
{
reason: reason.changesetCommentValue,
theme: state.theme.id,
}),
}
)
)
featureToMove.properties._lat = loc.lat
featureToMove.properties._lon = loc.lon
@ -153,8 +164,8 @@ export class MoveWizardState {
{
changeType: "relocated",
theme: state.theme.id,
},
),
}
)
)
}

View file

@ -95,7 +95,6 @@
</div>
{:else}
<TitledPanel>
<Tr slot="title" t={Translations.t.notes.createNoteTitle} />
{#if !$isDisplayed}
@ -107,57 +106,52 @@
<Tr slot="message" t={Translations.t.notes.noteLayerDoEnable} />
</SubtleButton>
{:else if $hasFilter}
<!-- ...but a filter is set ...-->
<div class="alert">
<Tr t={Translations.t.notes.noteLayerHasFilters} />
</div>
<SubtleButton on:click={() => notelayer.disableAllFilters()}>
<Layers class="mr-4 h-8 w-8" />
<Tr slot="message" t={Translations.t.notes.disableAllNoteFilters} />
</SubtleButton>
<!-- ...but a filter is set ...-->
<div class="alert">
<Tr t={Translations.t.notes.noteLayerHasFilters} />
</div>
<SubtleButton on:click={() => notelayer.disableAllFilters()}>
<Layers class="mr-4 h-8 w-8" />
<Tr slot="message" t={Translations.t.notes.disableAllNoteFilters} />
</SubtleButton>
{:else}
<!-- The layer with notes is displayed without filters, so we can add a note without worrying for duplicates -->
<div class="h-full flex flex-col justify-between">
<div class="flex h-full flex-col justify-between">
<form class="flex flex-col rounded-sm p-2" on:submit|preventDefault={uploadNote}>
<label class="neutral-label">
<Tr t={Translations.t.notes.createNoteIntro} />
<div class="w-full p-1">
<ValidatedInput autofocus={true} type="text" value={comment} />
</div>
</label>
<form
class="flex flex-col rounded-sm p-2"
on:submit|preventDefault={uploadNote}
>
<label class="neutral-label">
<Tr t={Translations.t.notes.createNoteIntro} />
<div class="w-full p-1">
<ValidatedInput autofocus={true} type="text" value={comment} />
</div>
</label>
<LoginToggle {state}>
<span slot="loading"><!--empty: don't show a loading message--></span>
<div slot="not-logged-in" class="alert">
<Tr t={Translations.t.notes.warnAnonymous} />
</div>
</LoginToggle>
<LoginToggle {state}>
<span slot="loading"><!--empty: don't show a loading message--></span>
<div slot="not-logged-in" class="alert">
<Tr t={Translations.t.notes.warnAnonymous} />
</div>
</LoginToggle>
{#if $comment?.length >= 3}
<NextButton on:click={uploadNote} clss="self-end primary">
<AddSmall slot="image" class="mr-4 h-8 w-8" />
<Tr t={Translations.t.notes.createNote} />
</NextButton>
{:else}
<div class="alert">
<Tr t={Translations.t.notes.textNeeded} />
</div>
{/if}
</form>
{#if $comment?.length >= 3}
<NextButton on:click={uploadNote} clss="self-end primary">
<AddSmall slot="image" class="mr-4 h-8 w-8" />
<Tr t={Translations.t.notes.createNote} />
</NextButton>
{:else}
<div class="alert">
<Tr t={Translations.t.notes.textNeeded} />
</div>
{/if}
</form>
<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" />
</div>
</NewPointLocationInput>
<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" />
</div>
</NewPointLocationInput>
</div>
</div>
</div>
{/if}
</TitledPanel>
{/if}

View file

@ -28,8 +28,8 @@
let userinfo = Stores.FromPromise(
Utils.downloadJsonCached<{ user: { img: { href: string } } }>(
"https://api.openstreetmap.org/api/0.6/user/" + comment.uid,
24 * 60 * 60 * 1000,
),
24 * 60 * 60 * 1000
)
)
const htmlElement = document.createElement("div")
@ -44,23 +44,27 @@
})
.filter((link) => !link.startsWith("https://wiki.openstreetmap.org/wiki/File:"))
const attributedImages = AllImageProviders.loadImagesFrom(images)
/**
* Class of the little icons indicating 'opened', 'comment' and 'resolved'
*/
export let iconClass = "shrink-0 w-6 mr-3 my-2 "
</script>
<div class="flex flex-col my-2 border-gray-500 border-b" class:border-interactive={comment.highlighted}>
<div
class="my-2 flex flex-col border-b border-gray-500"
class:border-interactive={comment.highlighted}
>
<div class="flex items-center">
<!-- Action icon, e.g. 'created', 'commented', 'closed' -->
{#if $userinfo?.user?.img?.href}
<img alt="avatar" aria-hidden="true" src={$userinfo?.user?.img?.href} class="rounded-full w-10 h-10 mr-3" />
<img
alt="avatar"
aria-hidden="true"
src={$userinfo?.user?.img?.href}
class="mr-3 h-10 w-10 rounded-full"
/>
{:else if comment.action === "opened" || comment.action === "reopened"}
<Note class={iconClass} />
{:else if comment.action === "closed"}
@ -74,7 +78,10 @@
</div>
{#if $attributedImages?.length > 0}
<div class="flex justify-center w-full space-x-4 overflow-x-auto" style="scroll-snap-type: x proximity">
<div
class="flex w-full justify-center space-x-4 overflow-x-auto"
style="scroll-snap-type: x proximity"
>
{#each $attributedImages as image (image.id)}
<AttributedImage
{state}
@ -82,14 +89,12 @@
imgClass="max-h-64 w-auto sm:h-32 md:h-64"
previewedImage={state.previewedImage}
attributionFormat="minimal"
>
</AttributedImage>
{/each}
/>
{/each}
</div>
{/if}
<div class="flex justify-end items-center subtle py-2">
<div class="subtle flex items-center justify-end py-2">
<!-- commenter info -->
<span class="mr-2">
{#if comment.user === undefined}
@ -99,6 +104,5 @@
{/if}
{comment.date}
</span>
</div>
</div>

View file

@ -1,8 +1,6 @@
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
export default class NoteCommentElement {
/**
* Adds the comment to the _visualisation_ of the given note; doesn't _actually_ upload
* @param txt

View file

@ -46,7 +46,7 @@
<ul>
{#each $trs as mapping}
<li>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} {noIcons}/>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} {noIcons} />
</li>
{/each}
</ul>

View file

@ -48,7 +48,7 @@
}
let htmlElem: HTMLDivElement
function enableEditMode(){
function enableEditMode() {
editMode = true
// EditMode switched to true yet the answer is already known, so the person wants to make a change
// Make sure that the question is in the scrollview!

View file

@ -51,7 +51,7 @@
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
let isKnown = tags.mapD(tags => config.GetRenderValue(tags) !== undefined)
let isKnown = tags.mapD((tags) => config.GetRenderValue(tags) !== undefined)
let matchesEmpty = config.GetRenderValue({}) !== undefined
// Will be bound if a freeform is available
@ -67,7 +67,7 @@
/**
* IF set: we can remove the current answer by deleting all those keys
*/
let settableKeys = tags.mapD(tags => config.removeToSetUnknown(layer, tags))
let settableKeys = tags.mapD((tags) => config.removeToSetUnknown(layer, tags))
let unknownModal = new UIEventSource(false)
let searchTerm: UIEventSource<string> = new UIEventSource("")
@ -91,7 +91,7 @@
return !m.hideInAnswer.matchesProperties(tgs)
})
selectedMapping = mappings?.findIndex(
(mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs),
(mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs)
)
if (selectedMapping < 0) {
selectedMapping = undefined
@ -199,7 +199,7 @@
if (freeformValue?.length > 0) {
selectedMapping = config.mappings.length
}
}),
})
)
$: {
@ -217,7 +217,7 @@
$freeformInput,
selectedMapping,
checkedMappings,
tags.data,
tags.data
)
if (featureSwitchIsDebugging?.data) {
console.log(
@ -229,7 +229,7 @@
currentTags: tags.data,
},
" --> ",
selectedTags,
selectedTags
)
}
} catch (e) {
@ -251,7 +251,7 @@
selectedTags = new And([...selectedTags.and, ...extraTagsArray])
} else {
console.error(
"selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags),
"selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags)
)
}
}
@ -320,12 +320,12 @@
onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount
}),
})
)
}
function clearAnswer() {
const tagsToSet = settableKeys.data.map(k => new Tag(k, ""))
const tagsToSet = settableKeys.data.map((k) => new Tag(k, ""))
const change = new ChangeTagAction(tags.data.id, new And(tagsToSet), tags.data, {
theme: tags.data["_orig_theme"] ?? state.theme.id,
changeType: "answer",
@ -517,15 +517,18 @@
</div>
{/if}
<Popup shown={unknownModal}>
<h2 slot="header">
<Tr t={Translations.t.unknown.title} />
</h2>
<Tr t={Translations.t.unknown.explanation} />
<If condition={state.userRelatedState.showTags.map(v => v === "yes" || v === "full" || v === "always")}>
<If
condition={state.userRelatedState.showTags.map(
(v) => v === "yes" || v === "full" || v === "always"
)}
>
<div class="subtle">
<Tr t={Translations.t.unknown.removedKeys}/>
<Tr t={Translations.t.unknown.removedKeys} />
{#each $settableKeys as key}
<code>
<del>
@ -535,30 +538,35 @@
{/each}
</div>
</If>
<div class="flex justify-end w-full" slot="footer">
<div class="flex w-full justify-end" slot="footer">
<button on:click={() => unknownModal.set(false)}>
<Tr t={Translations.t.unknown.keep} />
</button>
<button class="primary" on:click={() => {unknownModal.set(false); clearAnswer()}}>
<button
class="primary"
on:click={() => {
unknownModal.set(false)
clearAnswer()
}}
>
<Tr t={Translations.t.unknown.clear} />
</button>
</div>
</Popup>
<div
class="sticky bottom-0 flex justify-between flex-wrap interactive"
class="interactive sticky bottom-0 flex flex-wrap justify-between"
style="z-index: 11"
>
{#if $settableKeys && $isKnown && !matchesEmpty }
{#if $settableKeys && $isKnown && !matchesEmpty}
<button class="as-link small text-sm" on:click={() => unknownModal.set(true)}>
<Tr t={Translations.t.unknown.markUnknown} />
</button>
{/if}
<div class="flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap self-end flex-grow mt-4 mb-2">
<div
class="mt-4 mb-2 flex flex-grow flex-wrap-reverse items-stretch justify-end self-end sm:flex-nowrap"
>
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel" />
<slot name="save-button" {selectedTags}>
@ -574,23 +582,24 @@
<button
on:click={() => onSave()}
class={twJoin(
selectedTags === undefined ? "disabled" : "button-shadow",
"primary"
)}
selectedTags === undefined ? "disabled" : "button-shadow",
"primary"
)}
>
<Tr t={Translations.t.general.save} />
</button>
{/if}
</slot>
</div>
</div>
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
<span class="flex flex-wrap justify-between">
<TagHint {state} tags={selectedTags} currentProperties={$tags} />
<span class="flex flex-wrap">
{#if $featureSwitchIsTesting}
<div class="alert" style="padding: 0; margin: 0; margin-right: 0.5rem">Testmode &nbsp;</div>
<div class="alert" style="padding: 0; margin: 0; margin-right: 0.5rem">
Testmode &nbsp;
</div>
{/if}
{#if $featureSwitchIsTesting || $featureSwitchIsDebugging}
<a class="small" on:click={() => console.log("Configuration is ", config)}>

View file

@ -29,45 +29,43 @@
<Loading />
{:else}
<div class="flex flex-col">
{#if $reviews?.length > 0}
<div class="flex flex-col gap-y-1" on:keypress={(e) => console.log("Got keypress", e)}>
{#each $reviews as review (review.sub)}
<SingleReview {review} showSub={true} {state} />
{/each}
</div>
{:else}
<Tr t={t.your_reviews_empty} />
{/if}
{#if $allReviews?.length > $reviews?.length}
{#if $allReviews?.length - $reviews?.length === 1}
<Tr t={t.non_place_review} />
{#if $reviews?.length > 0}
<div class="flex flex-col gap-y-1" on:keypress={(e) => console.log("Got keypress", e)}>
{#each $reviews as review (review.sub)}
<SingleReview {review} showSub={true} {state} />
{/each}
</div>
{:else}
<Tr t={t.non_place_reviews.Subs({ n: $allReviews?.length - $reviews?.length })} />
<Tr t={t.your_reviews_empty} />
{/if}
{#if $allReviews?.length > $reviews?.length}
{#if $allReviews?.length - $reviews?.length === 1}
<Tr t={t.non_place_review} />
{:else}
<Tr t={t.non_place_reviews.Subs({ n: $allReviews?.length - $reviews?.length })} />
{/if}
<a
target="_blank"
class="link-underline"
rel="noopener nofollow"
href={`https://mangrove.reviews/list?kid=${encodeURIComponent($kid)}`}
>
<Tr t={t.see_all} />
</a>
{/if}
<a
target="_blank"
class="link-underline"
rel="noopener nofollow"
href={`https://mangrove.reviews/list?kid=${encodeURIComponent($kid)}`}
href="https://github.com/pietervdvn/MapComplete/issues/1782"
target="_blank"
rel="noopener noreferrer"
>
<Tr t={t.see_all} />
<Tr t={t.reviews_bug} />
</a>
{/if}
<a
class="link-underline"
href="https://github.com/pietervdvn/MapComplete/issues/1782"
target="_blank"
rel="noopener noreferrer"
>
<Tr t={t.reviews_bug} />
</a>
</div>
</div>
{/if}
<div class="flex justify-end">
<Mangrove_logo class="h-6 w-6 shrink-0 p-1" />
<Tr cls="text-sm subtle" t={t.attribution} />
</div>
</LoginToggle>

View file

@ -5,10 +5,9 @@
import FilterToggle from "./FilterToggle.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
export let activeFilter: ActiveFilter[]
let { control, filter } = activeFilter[0]
let option = control.map(c => filter.options[c] ?? filter.options[0])
let option = control.map((c) => filter.options[c] ?? filter.options[0])
let loading = false
function clear() {
@ -24,14 +23,15 @@
export let state: SpecialVisualizationState
let debug = state.featureSwitches.featureSwitchIsDebugging
</script>
{#if loading}
<Loading />
{:else }
{:else}
<FilterToggle on:click={() => clear()}>
<FilterOption option={$option} />
{#if $debug}
<span class="subtle">
({activeFilter.map(af => af.layer.id).join(", ")})
({activeFilter.map((af) => af.layer.id).join(", ")})
</span>
{/if}
</FilterToggle>

View file

@ -15,17 +15,20 @@
import Locale from "../i18n/Locale"
export let activeFilters: ( FilterSearchResult & ActiveFilter)[]
export let activeFilters: (FilterSearchResult & ActiveFilter)[]
let language = Locale.language
let mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language)
$:mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language)
$: mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language)
export let state: SpecialVisualizationState
let loading = false
const t =Translations.t.general.search
const t = Translations.t.general.search
let activeLayers: Store<FilteredLayer[]> = state.layerState.activeLayers.mapD(l => l.filter(l => l.layerDef.isNormal()))
let nonactiveLayers: Store<FilteredLayer[]> = state.layerState.nonactiveLayers.mapD(l => l.filter(l => l.layerDef.isNormal()))
let activeLayers: Store<FilteredLayer[]> = state.layerState.activeLayers.mapD((l) =>
l.filter((l) => l.layerDef.isNormal())
)
let nonactiveLayers: Store<FilteredLayer[]> = state.layerState.nonactiveLayers.mapD((l) =>
l.filter((l) => l.layerDef.isNormal())
)
function enableAllLayers() {
for (const flayer of $nonactiveLayers) {
@ -52,20 +55,23 @@
{#if mergedActiveFilters.length > 0 || $nonactiveLayers.length > 0}
<SidebarUnit>
<div class="flex justify-between">
<h3><Tr t={t.activeFilters}/></h3>
<h3><Tr t={t.activeFilters} /></h3>
<button class="as-link subtle self-end" on:click={() => clear()} style="margin-right: 0.75rem">
<Tr t={t.clearFilters}/>
<button
class="as-link subtle self-end"
on:click={() => clear()}
style="margin-right: 0.75rem"
>
<Tr t={t.clearFilters} />
</button>
</div>
{#if loading}
<Loading />
{:else}
<div class="flex flex-wrap gap-x-1 gap-y-2 overflow-x-hidden overflow-y-auto">
<div class="flex flex-wrap gap-x-1 gap-y-2 overflow-y-auto overflow-x-hidden">
{#if $activeLayers.length === 1}
<FilterToggle on:click={() => enableAllLayers()}>
<div class="w-8 h-8 p-1">
<div class="h-8 w-8 p-1">
<ToSvelte construct={$activeLayers[0].layerDef.defaultIcon()} />
</div>
<b>
@ -75,7 +81,7 @@
{:else if $nonactiveLayers.length > 0}
{#each $nonactiveLayers as nonActive (nonActive.layerDef.id)}
<FilterToggle on:click={() => nonActive.isDisplayed.set(true)}>
<div class="w-8 h-8 p-1">
<div class="h-8 w-8 p-1">
<ToSvelte construct={nonActive.layerDef.defaultIcon()} />
</div>
<del class="block-ruby">
@ -85,10 +91,9 @@
{/each}
{/if}
{#each mergedActiveFilters as activeFilter (activeFilter)}
<div>
<ActiveFilterSvelte {activeFilter} {state}/>
<ActiveFilterSvelte {activeFilter} {state} />
</div>
{/each}
</div>

View file

@ -3,7 +3,7 @@
import Tr from "../Base/Tr.svelte"
import Icon from "../Map/Icon.svelte"
export let option : FilterConfigOption
export let option: FilterConfigOption
</script>
<Icon icon={option.icon ?? option.emoji} clss="w-5 h-5" emojiHeight="14px" />

View file

@ -10,11 +10,10 @@
export let entry: FilterSearchResult[] | LayerConfig
let asFilter: FilterSearchResult[]
let asLayer: LayerConfig
if(Array.isArray(entry)){
asFilter = entry
}else{
if (Array.isArray(entry)) {
asFilter = entry
} else {
asLayer = <LayerConfig>entry
}
export let state: SpecialVisualizationState
@ -33,6 +32,7 @@
}, 25)
}
</script>
<button on:click={() => apply()} class:disabled={loading}>
{#if loading}
<Loading />
@ -40,18 +40,22 @@
<div class="flex flex-col items-start">
<div class="flex items-center gap-x-1">
{#if asLayer}
<div class="w-8 h-8 p-1">
<div class="h-8 w-8 p-1">
<ToSvelte construct={asLayer.defaultIcon()} />
</div>
<b>
<Tr t={asLayer.name} />
</b>
{:else}
<Icon icon={asFilter[0].option.icon ?? asFilter[0].option.emoji} clss="w-4 h-4" emojiHeight="14px" />
<Icon
icon={asFilter[0].option.icon ?? asFilter[0].option.emoji}
clss="w-4 h-4"
emojiHeight="14px"
/>
<Tr cls="whitespace-nowrap" t={asFilter[0].option.question} />
{#if $debug}
<span class="subtle">({asFilter.map(f => f.layer.id).join(", ")})</span>
{/if}
<span class="subtle">({asFilter.map((f) => f.layer.id).join(", ")})</span>
{/if}
{/if}
</div>
</div>

View file

@ -18,32 +18,40 @@
let activeLayers = state.layerState.activeLayers
let filterResults = state.searchState.filterSuggestions
let filtersMerged = filterResults.map(filters => FilterSearch.mergeSemiIdenticalLayers(filters, Locale.language.data), [Locale.language])
let filtersMerged = filterResults.map(
(filters) => FilterSearch.mergeSemiIdenticalLayers(filters, Locale.language.data),
[Locale.language]
)
let layerResults = state.searchState.layerSuggestions.map(layers => {
const nowActive = activeLayers.data.filter(al => al.layerDef.isNormal())
if (nowActive.length === 1) {
const shownInActiveFiltersView = nowActive[0]
layers = layers.filter(l => l.id !== shownInActiveFiltersView.layerDef.id)
}
return layers
}, [activeLayers])
let layerResults = state.searchState.layerSuggestions.map(
(layers) => {
const nowActive = activeLayers.data.filter((al) => al.layerDef.isNormal())
if (nowActive.length === 1) {
const shownInActiveFiltersView = nowActive[0]
layers = layers.filter((l) => l.id !== shownInActiveFiltersView.layerDef.id)
}
return layers
},
[activeLayers]
)
let filterResultsClipped: Store<{
clipped: (FilterSearchResult[] | LayerConfig)[],
clipped: (FilterSearchResult[] | LayerConfig)[]
rest?: (FilterSearchResult[] | LayerConfig)[]
}> = filtersMerged.mapD(filters => {
let layers = layerResults.data
const ls: (FilterSearchResult[] | LayerConfig)[] = [].concat(layers, filters)
if (ls.length <= 6) {
return { clipped: ls }
}
return { clipped: ls.slice(0, 4), rest: ls.slice(4) }
}, [layerResults, activeLayers, Locale.language])
}> = filtersMerged.mapD(
(filters) => {
let layers = layerResults.data
const ls: (FilterSearchResult[] | LayerConfig)[] = [].concat(layers, filters)
if (ls.length <= 6) {
return { clipped: ls }
}
return { clipped: ls.slice(0, 4), rest: ls.slice(4) }
},
[layerResults, activeLayers, Locale.language]
)
</script>
{#if $searchTerm.length > 0 && ($filterResults.length > 0 || $layerResults.length > 0)}
<SidebarUnit>
<h3>
<Tr t={Translations.t.general.search.pickFilter} />
</h3>
@ -55,10 +63,13 @@
</div>
{#if $filtersMerged.length + $layerResults.length > $filterResultsClipped.clipped.length}
<AccordionSingle noBorder>
<div class="flex justify-end text-sm subtle" slot="header">
<Tr t={Translations.t.general.search.nMoreFilters.Subs(
{n: $filtersMerged.length + $layerResults.length - $filterResultsClipped.clipped.length}
)}/>
<div class="subtle flex justify-end text-sm" slot="header">
<Tr
t={Translations.t.general.search.nMoreFilters.Subs({
n:
$filtersMerged.length + $layerResults.length - $filterResultsClipped.clipped.length,
})}
/>
</div>
<div class="flex flex-wrap overflow-y-auto">
{#each $filterResultsClipped.rest as filterResult (filterResult)}

View file

@ -1,9 +1,10 @@
<script lang="ts">
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
</script>
<div class="badge button-unstyled w-fit">
<slot/>
<button on:click>
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
</button>
<slot />
<button on:click>
<XMarkIcon class="h-5 w-5 pl-1" color="gray" />
</button>
</div>

View file

@ -22,14 +22,17 @@
if (entry.feature?.properties?.id) {
layer = state.theme.getMatchingLayer(entry.feature.properties)
tags = state.featureProperties.getStore(entry.feature.properties.id)
descriptionTr = layer?.tagRenderings?.find(tr => tr.labels.indexOf("description") >= 0)
descriptionTr = layer?.tagRenderings?.find((tr) => tr.labels.indexOf("description") >= 0)
}
let distance = state.mapProperties.location.mapD(l => GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat]))
let bearing = state.mapProperties.location.mapD(l => GeoOperations.bearing([l.lon, l.lat], [entry.lon, entry.lat]))
let distance = state.mapProperties.location.mapD((l) =>
GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat])
)
let bearing = state.mapProperties.location.mapD((l) =>
GeoOperations.bearing([l.lon, l.lat], [entry.lon, entry.lat])
)
let mapRotation = state.mapProperties.rotation
let inView = state.mapProperties.bounds.mapD(bounds => bounds.contains([entry.lon, entry.lat]))
let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat]))
function select() {
if (entry.boundingbox) {
@ -38,10 +41,14 @@
new BBox([
[lon0, lat0],
[lon1, lat1],
]).pad(0.01),
]).pad(0.01)
)
} else {
state.mapProperties.flyTo(entry.lon, entry.lat, GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17)
state.mapProperties.flyTo(
entry.lon,
entry.lat,
GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17
)
}
if (entry.feature?.properties?.id) {
state.selectedElement.set(entry.feature)
@ -51,28 +58,43 @@
}
</script>
<button class="unstyled w-full link-no-underline searchresult" on:click={() => select() }>
<div class="p-2 flex items-center w-full gap-y-2">
<button class="unstyled link-no-underline searchresult w-full" on:click={() => select()}>
<div class="flex w-full items-center gap-y-2 p-2">
{#if layer}
<div class="h-6">
<ToSvelte construct={() => layer.defaultIcon(entry.feature.properties)?.SetClass("w-6 h-6")} />
<ToSvelte
construct={() => layer.defaultIcon(entry.feature.properties)?.SetClass("w-6 h-6")}
/>
</div>
{:else if entry.category}
<Icon icon={GeocodingUtils.categoryToIcon[entry.category]} clss="w-6 h-6 shrink-0" color="#aaa" />
<Icon
icon={GeocodingUtils.categoryToIcon[entry.category]}
clss="w-6 h-6 shrink-0"
color="#aaa"
/>
{/if}
<div class="flex flex-col items-start pl-2 w-full">
<div class="flex flex-wrap gap-x-2 justify-between w-full">
<div class="flex w-full flex-col items-start pl-2">
<div class="flex w-full flex-wrap justify-between gap-x-2">
<b class="nowrap">
{#if layer && $tags?.id}
<TagRenderingAnswer config={layer.title} selectedElement={entry.feature} {state} {tags} {layer} />
<TagRenderingAnswer
config={layer.title}
selectedElement={entry.feature}
{state}
{tags}
{layer}
/>
{:else}
{entry.display_name ?? entry.osm_id}
{/if}
</b>
{#if $distance > 50}
<div class="flex gap-x-1 items-center">
<div class="flex items-center gap-x-1">
{#if $bearing && !$inView}
<ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} />
<ArrowUp
class="h-4 w-4 shrink-0"
style={`transform: rotate(${$bearing - $mapRotation}deg)`}
/>
{/if}
{#if $distance}
{GeoOperations.distanceToHuman($distance)}
@ -81,21 +103,26 @@
{/if}
</div>
<div class="flex flex-wrap gap-x-2">
{#if descriptionTr && tags}
<TagRenderingAnswer defaultSize="subtle" noIcons={true} config={descriptionTr} {tags} {state}
selectedElement={entry.feature} {layer} />
<TagRenderingAnswer
defaultSize="subtle"
noIcons={true}
config={descriptionTr}
{tags}
{state}
selectedElement={entry.feature}
{layer}
/>
{/if}
{#if descriptionTr && tags && entry.description}
{/if}
{#if entry.description}
<div class="subtle flex justify-between w-full">
<div class="subtle flex w-full justify-between">
{entry.description}
</div>
{/if}
</div>
</div>
</div>
</button>

View file

@ -20,14 +20,12 @@
let results = state.searchState.suggestions
let isSearching = state.searchState.suggestionsSearchRunning
let recentlySeen: Store<GeocodeResult[]> = state.userRelatedState.recentlyVisitedSearch.value
const t = Translations.t.general.search
const t = Translations.t.general.search
</script>
{#if $searchTerm.length > 0}
<SidebarUnit>
<h3><Tr t={t.locations}/></h3>
<h3><Tr t={t.locations} /></h3>
{#if $results?.length > 0}
{#each $results as entry (entry)}
@ -36,7 +34,7 @@ const t = Translations.t.general.search
{/if}
{#if $isSearching}
<div class="flex justify-center m-4 my-8">
<div class="m-4 my-8 flex justify-center">
<Loading>
<Tr t={t.searching} />
</Loading>
@ -45,26 +43,28 @@ const t = Translations.t.general.search
{#if !$isSearching && $results.length === 0}
<b class="flex justify-center p-4">
<Tr t={t.nothingFor.Subs({term: "<i>"+$searchTerm+"</i>"})} />
<Tr t={t.nothingFor.Subs({ term: "<i>" + $searchTerm + "</i>" })} />
</b>
{/if}
</SidebarUnit>
{:else if $recentlySeen?.length > 0}
<SidebarUnit>
<div class="flex justify-between">
<h3 class="m-2">
<Tr t={t.recents} />
</h3>
<DotMenu>
<button on:click={() => {state.userRelatedState.recentlyVisitedSearch.clear()}}>
<button
on:click={() => {
state.userRelatedState.recentlyVisitedSearch.clear()
}}
>
<TrashIcon />
<Tr t={t.deleteSearchHistory}/>
<Tr t={t.deleteSearchHistory} />
</button>
<button on:click={() => state.guistate.openUsersettings("sync-visited-locations")}>
<CogIcon />
<Tr t={t.editSearchSyncSettings}/>
<Tr t={t.editSearchSyncSettings} />
</button>
</DotMenu>
</div>

View file

@ -12,23 +12,33 @@
import type { FilterSearchResult } from "../../Logic/Search/FilterSearch"
export let state: ThemeViewState
let activeFilters: Store<(ActiveFilter & FilterSearchResult)[]> = state.layerState.activeFilters.map(fs => fs.filter(f =>
(f.filter.options[0].fields.length === 0) &&
Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0)
.map(af => {
const index = <number> af.control.data
const r : FilterSearchResult & ActiveFilter = { ...af, index, option: af.filter.options[index] }
return r
}))
let activeFilters: Store<(ActiveFilter & FilterSearchResult)[]> =
state.layerState.activeFilters.map((fs) =>
fs
.filter(
(f) =>
f.filter.options[0].fields.length === 0 &&
Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0
)
.map((af) => {
const index = <number>af.control.data
const r: FilterSearchResult & ActiveFilter = {
...af,
index,
option: af.filter.options[index],
}
return r
})
)
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
let searchTerm = state.searchState.searchTerm
</script>
<div class="p-4 low-interaction flex gap-y-2 flex-col">
<div class="low-interaction flex flex-col gap-y-2 p-4">
<ActiveFilters {state} activeFilters={$activeFilters} />
{#if $searchTerm.length === 0 && $activeFilters.length === 0 }
<div class="p-8 items-center text-center">
{#if $searchTerm.length === 0 && $activeFilters.length === 0}
<div class="items-center p-8 text-center">
<b>
<Tr t={Translations.t.general.search.instructions} />
</b>

View file

@ -8,10 +8,12 @@
export let entry: MinimalThemeInformation
let otherTheme = entry
</script>
{#if entry}
<a href={ThemeSearch.createUrlFor(otherTheme)}
class="flex items-center p-2 w-full gap-y-2 rounded-xl searchresult">
{#if entry}
<a
href={ThemeSearch.createUrlFor(otherTheme)}
class="searchresult flex w-full items-center gap-y-2 rounded-xl p-2"
>
<Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" />
<div class="flex flex-col">
<b>

View file

@ -14,17 +14,18 @@
export let state: SpecialVisualizationState
let searchTerm = state.searchState.searchTerm
let recentThemes = state.userRelatedState.recentlyVisitedThemes.value.map(themes => themes.filter(th => th !== state.theme.id).slice(0, 6))
let recentThemes = state.userRelatedState.recentlyVisitedThemes.value.map((themes) =>
themes.filter((th) => th !== state.theme.id).slice(0, 6)
)
let themeResults = state.searchState.themeSuggestions
const t =Translations.t.general.search
const t = Translations.t.general.search
</script>
{#if $themeResults.length > 0}
<SidebarUnit>
<h3>
<Tr t={t.otherMaps}/>
<Tr t={t.otherMaps} />
</h3>
{#each $themeResults as entry (entry.id)}
<ThemeResult {entry} />
@ -35,23 +36,26 @@
{#if $searchTerm.length === 0 && $recentThemes?.length > 0}
<SidebarUnit>
<div class="flex w-full justify-between">
<h3 class="m-2">
<Tr t={t.recentThemes} />
</h3>
<DotMenu>
<button on:click={() => {state.userRelatedState.recentlyVisitedThemes.clear()}}>
<button
on:click={() => {
state.userRelatedState.recentlyVisitedThemes.clear()
}}
>
<TrashIcon />
<Tr t={t.deleteThemeHistory}/>
<Tr t={t.deleteThemeHistory} />
</button>
<button on:click={() => state.guistate.openUsersettings("sync-visited-themes")}>
<CogIcon />
<Tr t={t.editThemeSync}/>
<Tr t={t.editThemeSync} />
</button>
</DotMenu>
</div>
{#each $recentThemes as themeId (themeId)}
<ThemeResult entry={ ThemeSearch.officialThemesById.get(themeId)} />
<ThemeResult entry={ThemeSearch.officialThemesById.get(themeId)} />
{/each}
</SidebarUnit>
{/if}

View file

@ -1,7 +1,11 @@
import { Store, UIEventSource } from "../Logic/UIEventSource"
import BaseUIElement from "./BaseUIElement"
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
import {
FeatureSource,
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { Changes } from "../Logic/Osm/Changes"
import { ExportableMap, MapProperties } from "../Models/MapProperties"
@ -80,14 +84,13 @@ export interface SpecialVisualizationState {
readonly previewedImage: UIEventSource<ProvidedImage>
readonly nearbyImageSearcher: CombinedFetcher
readonly geolocation: GeoLocationHandler
readonly geocodedImages : UIEventSource<Feature[]>
readonly geocodedImages: UIEventSource<Feature[]>
readonly searchState: SearchState
getMatchingLayer(properties: Record<string, string>);
getMatchingLayer(properties: Record<string, string>)
showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer
reportError(message: string | Error | XMLHttpRequest, extramessage?: string): Promise<void>
}
export interface SpecialVisualization {
@ -122,7 +125,7 @@ export interface SpecialVisualization {
export type RenderingSpecification =
| string
| {
func: SpecialVisualization
args: string[]
style: string
}
func: SpecialVisualization
args: string[]
style: string
}

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,7 @@ class StatsticsForOverviewFile extends Combine {
new Title("Filters"),
new SvelteUIElement(Filterview, { filteredLayer }),
])
filteredLayer.currentFilter.addCallbackAndRun(tf => {
filteredLayer.currentFilter.addCallbackAndRun((tf) => {
console.log("Filters are", tf)
})
const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([])

View file

@ -62,7 +62,7 @@
return "offline"
}
}),
message: osmApi
message: osmApi,
})
}
@ -90,7 +90,7 @@
}
const files: string[] = s["success"]["allFiles"]
return "Contains " + (files.length ?? "no") + " files"
})
}),
})
}
{
@ -106,8 +106,7 @@
return "degraded"
}
}),
message: simpleMessage(testDownload(Constants.panoramax.url + "/api"))
message: simpleMessage(testDownload(Constants.panoramax.url + "/api")),
})
}
{
@ -123,7 +122,7 @@
return "degraded"
}
}),
message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip"))
message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip")),
})
}
@ -142,7 +141,7 @@
}
return "degraded"
}),
message: simpleMessage(status)
message: simpleMessage(status),
})
}
@ -161,7 +160,7 @@
}
return "online"
}),
message: simpleMessage(status)
message: simpleMessage(status),
})
}
@ -200,7 +199,7 @@
const json = JSON.stringify(s["success"], null, " ")
return "Database is " + Math.floor(timediffDays) + " days out of sync\n\n" + json
})
}),
})
}
@ -219,7 +218,7 @@
}
return "degraded"
}),
message: status.map((s) => JSON.stringify(s))
message: status.map((s) => JSON.stringify(s)),
})
}
@ -229,7 +228,7 @@
services.push({
name: s,
message: simpleMessage(status),
status: status.mapD(s => {
status: status.mapD((s) => {
if (s["error"]) {
return "offline"
}
@ -238,7 +237,7 @@
return "online"
}
return "degraded"
})
}),
})
}
@ -247,7 +246,7 @@
const status = testDownload(s + "/api/?q=Brugge")
services.push({
name: s,
status: status.mapD(s => {
status: status.mapD((s) => {
if (s["error"]) {
return "offline"
}
@ -257,7 +256,7 @@
}
return "degraded"
}),
message: simpleMessage(status)
message: simpleMessage(status),
})
}
@ -283,7 +282,7 @@
return "online"
}),
message: simpleMessage(status)
message: simpleMessage(status),
})
}
}
@ -296,7 +295,7 @@
return "online"
}
return "offline"
})
}),
})
}

View file

@ -44,8 +44,8 @@
let indexInToAdd = 0
for (let i = 0; i < newPath.length; i++) {
if(newPath[i] === toAdd[indexInToAdd]){
indexInToAdd ++
if (newPath[i] === toAdd[indexInToAdd]) {
indexInToAdd++
}
}

View file

@ -5,7 +5,7 @@ import {
Conversion,
ConversionMessage,
DesugaringContext,
Pipe
Pipe,
} from "../../Models/ThemeConfig/Conversion/Conversion"
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
import { PrevalidateTheme, ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation"
@ -98,7 +98,6 @@ export abstract class EditJsonState<T> {
public startSavingUpdates(enabled = true) {
this.sendingUpdates = enabled
if (!this.server.isDirect) {
this.register(
["credits"],
this.osmConnection.userDetails.mapD((u) => u.name),
@ -177,10 +176,10 @@ export abstract class EditJsonState<T> {
path,
type: "translation",
hints: {
typehint: "translation"
typehint: "translation",
},
required: origConfig.required ?? false,
description: origConfig.description ?? "A translatable object"
description: origConfig.description ?? "A translatable object",
}
}
@ -332,7 +331,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
public readonly imageUploadManager = {
getCountsFor() {
return 0
}
},
}
public readonly theme: { getMatchingLayer: (key: any) => LayerConfig }
public readonly featureSwitches: {
@ -348,8 +347,8 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
properties: this.testTags.data,
geometry: {
type: "Point",
coordinates: [3.21, 51.2]
}
coordinates: [3.21, 51.2],
},
}
constructor(
@ -366,10 +365,10 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
} catch (e) {
return undefined
}
}
},
}
this.featureSwitches = {
featureSwitchIsDebugging: new UIEventSource<boolean>(true)
featureSwitchIsDebugging: new UIEventSource<boolean>(true),
}
this.addMissingTagRenderingIds()
@ -459,7 +458,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
const state: DesugaringContext = {
tagRenderings: sharedQuestions,
sharedLayers: layers,
tagRenderingOrder: []
tagRenderingOrder: [],
}
const prepare = this.buildValidation(state)
const context = ConversionContext.construct([], ["prepare"])
@ -536,7 +535,7 @@ export class EditThemeState extends EditJsonState<ThemeConfigJson> {
const state: DesugaringContext = {
tagRenderings: sharedQuestions,
sharedLayers: layers,
tagRenderingOrder: []
tagRenderingOrder: [],
}
const prepare = this.buildValidation(state)
const context = ConversionContext.construct([], ["prepare"])

View file

@ -5,9 +5,7 @@
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
import ShowConversionMessage from "./ShowConversionMessage.svelte"
import Markdown from "../Base/Markdown.svelte"
import type {
QuestionableTagRenderingConfigJson
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import CollapsedTagRenderingPreview from "./CollapsedTagRenderingPreview.svelte"
import { Accordion } from "flowbite-svelte"
import { Utils } from "../../Utils"
@ -17,7 +15,6 @@
let schema: ConfigMeta = state.getSchema(path)[0]
console.log("SBA got schema", schema, "for path", path)
let title = schema?.path?.at(-1)
let singular = title
if (title?.endsWith("s")) {
@ -69,6 +66,7 @@
currentValue.ping()
}
</script>
{#if schema !== undefined}
<div class="pl-2">
<h3>{schema.path.at(-1)}</h3>
@ -97,15 +95,16 @@
<button
class="h-fit w-fit rounded-full border border-black p-1"
on:click={() => {
del(i)
}}
del(i)
}}
>
<TrashIcon class="h-4 w-4" />
</button>
</div>
{/each}
{:else}
<Accordion> <!-- The CollapsedTagRenderingPreview contains the accordeon items -->
<Accordion>
<!-- The CollapsedTagRenderingPreview contains the accordeon items -->
{#each $currentValue as value, i}
<CollapsedTagRenderingPreview
{state}
@ -125,8 +124,8 @@
{#if path.length === 1 && path[0] === "tagRenderings"}
<button
on:click={() => {
createItem("images")
}}
createItem("images")
}}
>
Add a builtin tagRendering
</button>

View file

@ -32,14 +32,14 @@
return type.some((t) => mightBeBoolean(t))
}
function mightBeTag(){
function mightBeTag() {
const t = schema.type
if(!Array.isArray(t)){
if (!Array.isArray(t)) {
return false
}
const hasAnd = t.some(obj => obj["$ref"] === "#/definitions/{and:TagConfigJson[];}")
const hasOr = t.some(obj => obj["$ref"] === "#/definitions/{or:TagConfigJson[];}")
const hasString = t.some(obj => obj["type"] === "string")
const hasAnd = t.some((obj) => obj["$ref"] === "#/definitions/{and:TagConfigJson[];}")
const hasOr = t.some((obj) => obj["$ref"] === "#/definitions/{or:TagConfigJson[];}")
const hasString = t.some((obj) => obj["type"] === "string")
return hasAnd && hasOr && hasString
}

View file

@ -31,7 +31,11 @@
import Add from "../assets/svg/Add.svelte"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import Hash from "../Logic/Web/Hash"
const directEntry = QueryParameters.GetBooleanQueryParameter("direct",false,"If set, write directly into the theme files")
const directEntry = QueryParameters.GetBooleanQueryParameter(
"direct",
false,
"If set, write directly into the theme files"
)
export let studioUrl =
window.location.hostname === "127.0.0.2" || directEntry.data
@ -60,7 +64,10 @@
)
expertMode.addCallbackAndRunD((expert) => console.log("Expert mode is", expert))
const createdBy = osmConnection.userDetails.data.name
const uid = osmConnection.userDetails.map((ud) => directEntry.data ? null : ud?.uid, [directEntry])
const uid = osmConnection.userDetails.map(
(ud) => (directEntry.data ? null : ud?.uid),
[directEntry]
)
const studio = new StudioServer(studioUrl, uid, directEntry.data)
let layersWithErr = studio.fetchOverview()
@ -288,9 +295,11 @@
<div class="flex justify-between">
<Checkbox selected={expertMode}>Enable more options (expert mode)</Checkbox>
<span class="subtle">MapComplete version {version}</span>
<div>{$uid} {studioUrl}
<div>
{$uid}
{studioUrl}
{#if $directEntry}
<b>direct</b>
<b>direct</b>
{/if}
</div>
</div>
@ -375,7 +384,7 @@
</div>
{:else if state === "loading"}
<div class="h-8 w-8">
<Loading >Fetching information from {studioUrl}</Loading>
<Loading>Fetching information from {studioUrl}</Loading>
</div>
{:else if state === "editing_layer"}
<EditLayer state={editLayerState} {backToStudio}>

View file

@ -1,3 +1,2 @@
<script lang="ts">
</script>

View file

@ -52,7 +52,6 @@
export let state: ThemeViewState
let theme = state.theme
let maplibremap: UIEventSource<MlMap> = state.map
let state_selectedElement = state.selectedElement
@ -82,7 +81,7 @@
selectedElement.setData(undefined)
return
}
if(!selectedElement.data){
if (!selectedElement.data) {
// The store for this component doesn't have value right now, so we can simply set it
selectedElement.set(value)
return
@ -100,7 +99,6 @@
state.mapProperties.installCustomKeyboardHandler(viewport)
let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => {
if (element.properties.id.startsWith("current_view")) {
return currentViewLayer
@ -126,7 +124,6 @@
})
)
debug.addCallbackAndRun((dbg) => {
if (dbg) {
document.body.classList.add("debug")
@ -135,7 +132,6 @@
}
})
function updateViewport() {
const rect = viewport.data?.getBoundingClientRect()
if (!rect) {
@ -149,7 +145,7 @@
const bottomRight = mlmap.unproject([rect.right, rect.bottom])
const bbox = new BBox([
[topLeft.lng, topLeft.lat],
[bottomRight.lng, bottomRight.lat]
[bottomRight.lng, bottomRight.lat],
])
state.visualFeedbackViewportBounds.setData(bbox)
}
@ -172,7 +168,6 @@
const animation = mlmap.keyboard?.keydown(e)
animation?.cameraAnimation(mlmap)
}
</script>
<main>
@ -207,7 +202,6 @@
</div>
{/if}
<div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen">
<!-- bottom controls -->
<div class="flex w-full items-end justify-between px-4">
@ -317,28 +311,27 @@
</If>
</div>
</div>
</div>
<DrawerRight shown={state.searchState.showSearchDrawer}>
<SearchResults {state} />
</DrawerRight>
<!-- Top components -->
<div class="pointer-events-none absolute top-0 left-0 w-full z-4">
<div class="z-4 pointer-events-none absolute top-0 left-0 w-full">
<div
id="top-bar"
class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap">
class="bg-black-light-transparent pointer-events-auto flex flex-wrap items-center justify-between px-4 py-1"
>
<!-- Top bar with tools -->
<div class="flex items-center">
<MapControlButton
cls="m-0.5 p-0.5 sm:p-1"
arialabel={Translations.t.general.labels.menu}
on:click={() => {console.log("Opening...."); state.guistate.pageStates.menu.setData(true)}}
on:click={() => {
console.log("Opening....")
state.guistate.pageStates.menu.setData(true)
}}
on:keydown={forwardEventToMap}
>
<MenuIcon class="h-6 w-6 cursor-pointer" />
@ -348,9 +341,7 @@
on:click={() => state.guistate.pageStates.about_theme.set(true)}
on:keydown={forwardEventToMap}
>
<div
class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 mr-2"
>
<div class="m-0.5 mx-1 mr-2 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1">
<Marker icons={theme.icon} size="h-6 w-6 shrink-0 mr-0.5 sm:mr-1 md:mr-2" />
<b class="mr-1">
<Tr t={theme.title} />
@ -366,24 +357,30 @@
{/if}
<If condition={state.featureSwitches.featureSwitchSearch}>
<div class="flex items-center flex-grow justify-end">
<div class="flex flex-grow items-center justify-end">
<div class="w-full sm:w-64">
<Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused} />
<Searchbar
value={state.searchState.searchTerm}
isFocused={state.searchState.searchIsFocused}
/>
</div>
<MapControlButton on:keydown={forwardEventToMap} on:click={() =>{
if(searchOpened.data){
searchOpened.set(false)
}else{
state.searchState.searchIsFocused.set(true)
}
}}>
<ChevronRight class="w-7 h-7 p-0 m-0 transition-all"
style={"rotate: " + ($searchOpened ? "0deg" : "180deg" ) } />
<MapControlButton
on:keydown={forwardEventToMap}
on:click={() => {
if (searchOpened.data) {
searchOpened.set(false)
} else {
state.searchState.searchIsFocused.set(true)
}
}}
>
<ChevronRight
class="m-0 h-7 w-7 p-0 transition-all"
style={"rotate: " + ($searchOpened ? "0deg" : "180deg")}
/>
</MapControlButton>
</div>
</If>
</div>
<div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2">
@ -394,7 +391,6 @@
</div>
{/if}
</If>
</div>
<div class="float-left m-1 flex flex-col sm:mt-2">
@ -434,7 +430,6 @@
</div>
</div>
<DrawerLeft shown={state.guistate.pageStates.menu}>
<div class="h-screen overflow-y-auto">
<MenuDrawer onlyLink={true} {state} />
@ -451,15 +446,16 @@
id="drawer-right"
width="w-full md:w-6/12 lg:w-5/12 xl:w-4/12"
rightOffset="inset-y-0 right-0"
transitionParams={ {
x: 640,
duration: slideDuration,
easing: linear
}}
transitionParams={{
x: 640,
duration: slideDuration,
easing: linear,
}}
divClass="overflow-y-auto z-50 "
hidden={$selectedElement === undefined}
on:close={() => { state.selectedElement.setData(undefined)
}}
on:close={() => {
state.selectedElement.setData(undefined)
}}
>
<div slot="close-button" />
<SelectedElementPanel {state} selected={$state_selectedElement} />
@ -494,5 +490,4 @@
{/if}
<MenuDrawer onlyLink={false} {state} />
</main>

View file

@ -417,7 +417,7 @@ export class TypedTranslation<T extends Record<string, any>> extends Translation
key: string,
replaceWith: Translation
): TypedTranslation<Omit<T, K>> {
if(replaceWith === undefined){
if (replaceWith === undefined) {
return this
}
const newTranslations: Record<string, string> = {}