forked from MapComplete/MapComplete
chore: automated housekeeping...
This commit is contained in:
parent
c9ce29f206
commit
40e894df8b
294 changed files with 14209 additions and 4192 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,5 @@
|
|||
osmConnection: OsmConnection
|
||||
}
|
||||
|
||||
|
||||
let customThemes
|
||||
</script>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,5 @@
|
|||
</div>
|
||||
|
||||
<slot />
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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} `}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
])
|
||||
})
|
||||
)*/
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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-->
|
||||
|
|
|
|||
|
|
@ -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%">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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 </div>
|
||||
<div class="alert" style="padding: 0; margin: 0; margin-right: 0.5rem">
|
||||
Testmode
|
||||
</div>
|
||||
{/if}
|
||||
{#if $featureSwitchIsTesting || $featureSwitchIsDebugging}
|
||||
<a class="small" on:click={() => console.log("Configuration is ", config)}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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[] }[]>([])
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
})
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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++
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
<script lang="ts">
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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> = {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue