forked from MapComplete/MapComplete
merge develop
This commit is contained in:
commit
3e4708b0b9
506 changed files with 7945 additions and 74587 deletions
|
|
@ -8,24 +8,16 @@
|
|||
import Logo from "../assets/svg/Logo.svelte"
|
||||
import Tr from "./Base/Tr.svelte"
|
||||
import LoginToggle from "./Base/LoginToggle.svelte"
|
||||
import Pencil from "../assets/svg/Pencil.svelte"
|
||||
import Constants from "../Models/Constants"
|
||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../Logic/UIEventSource"
|
||||
import ThemesList from "./BigComponents/ThemesList.svelte"
|
||||
import { MinimalThemeInformation } from "../Models/ThemeConfig/ThemeConfig"
|
||||
import Eye from "../assets/svg/Eye.svelte"
|
||||
import LoginButton from "./Base/LoginButton.svelte"
|
||||
import Mastodon from "../assets/svg/Mastodon.svelte"
|
||||
import Liberapay from "../assets/svg/Liberapay.svelte"
|
||||
import Bug from "../assets/svg/Bug.svelte"
|
||||
import { Utils } from "../Utils"
|
||||
import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp"
|
||||
import Searchbar from "./Base/Searchbar.svelte"
|
||||
import ThemeSearch, { ThemeSearchIndex } from "../Logic/Search/ThemeSearch"
|
||||
import SearchUtils from "../Logic/Search/SearchUtils"
|
||||
import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight"
|
||||
import { AndroidPolyfill } from "../Logic/Web/AndroidPolyfill"
|
||||
import Forgejo from "../assets/svg/Forgejo.svelte"
|
||||
import Locale from "./i18n/Locale"
|
||||
import DrawerLeft from "./Base/DrawerLeft.svelte"
|
||||
import MenuDrawer from "./BigComponents/MenuDrawer.svelte"
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@
|
|||
import Hotkeys from "../Base/Hotkeys"
|
||||
import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp"
|
||||
import ArrowTopRightOnSquare from "@babeard/svelte-heroicons/mini/ArrowTopRightOnSquare"
|
||||
import { PhotoIcon } from "@babeard/svelte-heroicons/outline"
|
||||
import ImageUploadQueue from "../../Logic/ImageProviders/ImageUploadQueue"
|
||||
import QueuedImagesView from "../Image/QueuedImagesView.svelte"
|
||||
|
||||
export let state: {
|
||||
favourites: FavouritesFeatureSource
|
||||
|
|
@ -97,6 +100,8 @@
|
|||
}
|
||||
})
|
||||
let isAndroid = AndroidPolyfill.inAndroid
|
||||
let nrOfFailedImages = ImageUploadQueue.singleton.imagesInQueue
|
||||
let failedImagesOpen = pg.failedImages
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
@ -156,6 +161,16 @@
|
|||
/>
|
||||
</Page>
|
||||
|
||||
{#if $nrOfFailedImages.length > 0 || $failedImagesOpen}
|
||||
<Page {onlyLink} shown={pg.failedImages} bodyPadding="p-0 pb-4">
|
||||
<svelte:fragment slot="header">
|
||||
<PhotoIcon />
|
||||
<Tr t={Translations.t.imageQueue.menu.Subs({count: $nrOfFailedImages.length})} />
|
||||
</svelte:fragment>
|
||||
<QueuedImagesView {state} />
|
||||
</Page>
|
||||
{/if}
|
||||
|
||||
<LoginToggle {state} silentFail>
|
||||
{#if state.favourites}
|
||||
<Page {onlyLink} shown={pg.favourites}>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
{#if $isTesting || $isDebugging}
|
||||
<a
|
||||
class="subtle"
|
||||
href="https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Layers/{layer.id}.md"
|
||||
href="https://source.mapcomplete.org/MapComplete/MapComplete/src/branch/develop/Docs/Layers/{layer.id}.md"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener "
|
||||
>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
? "flex flex-wrap items-center justify-center gap-x-2"
|
||||
: "theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3"}
|
||||
>
|
||||
{#each Utils.NoNull(themes) as theme (theme.id)}
|
||||
{#each Utils.DedupOnId(Utils.NoNull(themes)) as theme (theme.id)}
|
||||
<ThemeButton {theme} {state} iconOnly={onlyIcons}>
|
||||
{#if $search && hasSelection && themes?.[0] === theme}
|
||||
<span class="thanks hidden-on-mobile" aria-hidden="true">
|
||||
|
|
|
|||
65
src/UI/Image/QueuedImage.svelte
Normal file
65
src/UI/Image/QueuedImage.svelte
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<script lang="ts">
|
||||
import type { ImageUploadArguments } from "../../Logic/ImageProviders/ImageUploadQueue"
|
||||
import ImageUploadQueue from "../../Logic/ImageProviders/ImageUploadQueue"
|
||||
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import Popup from "../Base/Popup.svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Page from "../Base/Page.svelte"
|
||||
import BackButton from "../Base/BackButton.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
let queue = ImageUploadQueue.singleton
|
||||
export let imageArguments: ImageUploadArguments
|
||||
let confirmDelete = new UIEventSource(false)
|
||||
|
||||
|
||||
function del() {
|
||||
queue.delete(imageArguments)
|
||||
}
|
||||
|
||||
const t = Translations.t
|
||||
let src = undefined
|
||||
try{
|
||||
|
||||
src = URL.createObjectURL(imageArguments.blob)
|
||||
}catch (e) {
|
||||
console.error("Could not create an ObjectURL for blob", imageArguments.blob)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="low-interaction rounded border-interactive w-fit p-2 m-1 flex flex-col">
|
||||
|
||||
<img class="max-w-64 w-auto max-h-64 w-auto" {src} />
|
||||
{imageArguments.featureId} {imageArguments.layoutId}
|
||||
<button class="as-link self-end" on:click={() => {confirmDelete.set(true)}}>
|
||||
<TrashIcon class="w-4" />
|
||||
<Tr t={t.imageQueue.delete} />
|
||||
</button>
|
||||
<Popup shown={confirmDelete} dismissable={true}>
|
||||
<Page shown={confirmDelete}>
|
||||
<svelte:fragment slot="header">
|
||||
<TrashIcon class="w-8 m-1" />
|
||||
<Tr t={t.imageQueue.confirmDeleteTitle} />
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="flex flex-col ">
|
||||
|
||||
<div class="flex justify-center">
|
||||
<img class="max-w-128 w-auto max-h-128 w-auto" src={URL.createObjectURL(imageArguments.blob)} />
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<BackButton clss="w-full" on:click={() => confirmDelete.set(false)}>
|
||||
<Tr t={t.general.back} />
|
||||
</BackButton>
|
||||
<button on:click={() => del()} class="primary w-full">
|
||||
|
||||
<TrashIcon class="w-8 m-1" />
|
||||
<Tr t={t.imageQueue.confirmDelete} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</Popup>
|
||||
</div>
|
||||
42
src/UI/Image/QueuedImagesView.svelte
Normal file
42
src/UI/Image/QueuedImagesView.svelte
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<script lang="ts">
|
||||
import QueuedImage from "./QueuedImage.svelte"
|
||||
import { ArrowPathIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { WithImageState } from "../../Models/ThemeViewState/WithImageState"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { ImageUploadArguments } from "../../Logic/ImageProviders/ImageUploadQueue"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import UploadingImageCounter from "./UploadingImageCounter.svelte"
|
||||
export let state: WithImageState
|
||||
let queued: Store<ImageUploadArguments[]> = state.imageUploadManager.queuedArgs
|
||||
let isUploading = state.imageUploadManager.isUploading
|
||||
const t = Translations.t
|
||||
const q = t.imageQueue
|
||||
</script>
|
||||
|
||||
<div class="m-4 flex flex-col">
|
||||
{#if $queued.length === 0}
|
||||
<Tr t={q.noFailedImages} />
|
||||
{:else}
|
||||
<div>
|
||||
<Tr t={q.intro} />
|
||||
</div>
|
||||
|
||||
<UploadingImageCounter {state}/>
|
||||
|
||||
{#if $isUploading}
|
||||
<Loading />
|
||||
{:else}
|
||||
<button class="primary" on:click={() => state.imageUploadManager.uploadQueue()}>
|
||||
<ArrowPathIcon class="w-8 h-8 m-1" />
|
||||
<Tr t={q.retryAll} />
|
||||
</button>
|
||||
{/if}
|
||||
<div class="flex flex-wrap">
|
||||
{#each $queued as i (i.date + i.featureId)}
|
||||
<QueuedImage imageArguments={i} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
import LoginButton from "../Base/LoginButton.svelte"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import NoteCommentElement from "../Popup/Notes/NoteCommentElement"
|
||||
import type { Feature } from "geojson"
|
||||
import Camera from "@babeard/svelte-heroicons/mini/Camera"
|
||||
|
||||
|
|
@ -22,7 +21,6 @@
|
|||
|
||||
export let tags: UIEventSource<OsmTags>
|
||||
export let targetKey: string = undefined
|
||||
export let layer: LayerConfig
|
||||
export let noBlur: boolean = false
|
||||
export let feature: Feature
|
||||
/**
|
||||
|
|
@ -38,7 +36,7 @@
|
|||
|
||||
let errors = new UIEventSource<Translation[]>([])
|
||||
|
||||
async function handleFiles(files: FileList, ignoreGps: boolean = false) {
|
||||
async function handleFiles(files: FileList, ignoreGPS: boolean = false) {
|
||||
const errs = []
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files.item(i)
|
||||
|
|
@ -49,29 +47,7 @@
|
|||
errs.push(canBeUploaded.error)
|
||||
continue
|
||||
}
|
||||
|
||||
if (layer?.id === "note") {
|
||||
const uploadResult = await state?.imageUploadManager.uploadImageWithLicense(
|
||||
tags.data.id,
|
||||
state.osmConnection.userDetails.data?.name ?? "Anonymous",
|
||||
file,
|
||||
"image",
|
||||
noBlur,
|
||||
feature,
|
||||
ignoreGps
|
||||
)
|
||||
if (!uploadResult) {
|
||||
return
|
||||
}
|
||||
const url = uploadResult.absoluteUrl
|
||||
await state.osmConnection.addCommentToNote(tags.data.id, url)
|
||||
NoteCommentElement.addCommentTo(url, <UIEventSource<OsmTags>>tags, {
|
||||
osmConnection: state.osmConnection,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await state?.imageUploadManager?.uploadImageAndApply(file, tags, targetKey, noBlur, feature)
|
||||
await state?.imageUploadManager?.uploadImageAndApply(file, tags, targetKey, noBlur, feature, { ignoreGPS })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
state.reportError(e, "Could not upload image")
|
||||
|
|
@ -98,7 +74,7 @@
|
|||
<Tr t={error} cls="alert" />
|
||||
{/each}
|
||||
<FileSelector
|
||||
accept="image/*"
|
||||
accept=".jpg,.jpeg,image/jpeg"
|
||||
capture="environment"
|
||||
cls="button border-2 flex flex-col"
|
||||
multiple={true}
|
||||
|
|
|
|||
|
|
@ -7,76 +7,69 @@
|
|||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
import type { NoteId, OsmTags, OsmId } from "../../Models/OsmFeature"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import UploadFailedMessage from "./UploadFailedMessage.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: Store<OsmTags> = undefined
|
||||
export let featureId = tags?.data?.id
|
||||
export let featureId: OsmId | NoteId | "*" = tags?.data?.id ?? "*"
|
||||
if (featureId === undefined) {
|
||||
throw "No tags or featureID given"
|
||||
}
|
||||
export let showThankYou: boolean = true
|
||||
const { uploadStarted, uploadFinished, retried, failed } =
|
||||
state.imageUploadManager.getCountsFor(featureId)
|
||||
|
||||
/*
|
||||
Number of images uploaded succesfully
|
||||
*/
|
||||
function getCount(input: Store<string[]>): Store<number> {
|
||||
if (featureId == "*") {
|
||||
return input.map(inp => inp.length)
|
||||
}
|
||||
return input.map(success => success.filter(item => item === featureId).length)
|
||||
}
|
||||
|
||||
let successfull = getCount(state.imageUploadManager.successfull)
|
||||
/* Number of failed uploads */
|
||||
let failed = getCount(state.imageUploadManager.fails)
|
||||
|
||||
let pending = getCount(state.imageUploadManager.queued)
|
||||
const t = Translations.t.image
|
||||
const debugging = state.featureSwitches.featureSwitchIsDebugging
|
||||
let dismissed = 0
|
||||
failed.addCallbackAndRun(failed => {
|
||||
dismissed = Math.min(failed, dismissed)
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $debugging}
|
||||
<div class="low-interaction">
|
||||
Started {$uploadStarted} Done {$uploadFinished} Retry {$retried} Err {$failed}
|
||||
Pending {$pending} Done {$successfull} Err {$failed}
|
||||
</div>
|
||||
{/if}
|
||||
{#if dismissed === $uploadStarted}
|
||||
<!-- We don't show anything as we ignore this number of failed items-->
|
||||
{:else if $uploadStarted === 1}
|
||||
{#if $uploadFinished === 1}
|
||||
{#if showThankYou}
|
||||
<Tr cls="thanks" t={t.upload.one.done} />
|
||||
{/if}
|
||||
{:else if $failed === 1}
|
||||
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} />
|
||||
{:else if $retried === 1}
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
<Tr t={t.upload.one.retrying} />
|
||||
</Loading>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
|
||||
{#if $pending - $failed > 0}
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
{#if $pending - $failed === 1}
|
||||
<Tr t={t.upload.one.uploading} />
|
||||
</Loading>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if $uploadStarted > 1}
|
||||
{#if $uploadFinished + $failed === $uploadStarted}
|
||||
{#if $uploadFinished === 0}
|
||||
<!-- pass -->
|
||||
{:else if showThankYou}
|
||||
<Tr cls="thanks" t={t.upload.multiple.done.Subs({ count: $uploadFinished })} />
|
||||
{/if}
|
||||
{:else if $uploadFinished === 0}
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.multiple.uploading.Subs({ count: $uploadStarted })} />
|
||||
{:else if $pending - $failed > 1}
|
||||
<Tr t={t.upload.multiple.uploading.Subs({count: $pending})} />
|
||||
{/if}
|
||||
</Loading>
|
||||
{:else if $uploadFinished > 0}
|
||||
<Loading cls="alert">
|
||||
<Tr
|
||||
t={t.upload.multiple.partiallyDone.Subs({
|
||||
count: $uploadStarted - $uploadFinished,
|
||||
done: $uploadFinished,
|
||||
})}
|
||||
/>
|
||||
</Loading>
|
||||
{/if}
|
||||
{#if $failed > 0}
|
||||
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $failed > dismissed}
|
||||
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} />
|
||||
{/if}
|
||||
|
||||
{#if showThankYou}
|
||||
{#if $successfull === 1}
|
||||
<Tr cls="thanks" t={t.upload.one.done} />
|
||||
{:else if $successfull > 1}
|
||||
<Tr cls="thanks" t={t.upload.multiple.done.Subs({count: $successfull})} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export class TextField extends InputElement<string> {
|
|||
self.value.setData(val)
|
||||
}
|
||||
// Setting the value might cause the value to be set again. We keep the distance _to the end_ stable, as phone number formatting might cause the start to change
|
||||
// See https://github.com/pietervdvn/MapComplete/issues/103
|
||||
// See https://source.mapcomplete.org/MapComplete/MapComplete/issues/103
|
||||
// We reread the field value - it might have changed!
|
||||
|
||||
// @ts-ignore
|
||||
|
|
|
|||
100
src/UI/InputElement/Helpers/DistanceInput.svelte
Normal file
100
src/UI/InputElement/Helpers/DistanceInput.svelte
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<script lang="ts">
|
||||
|
||||
/**
|
||||
* Used to quickly calculate a distance by dragging a map (and selecting start- and endpoints)
|
||||
*/
|
||||
|
||||
import LocationInput from "./LocationInput.svelte"
|
||||
import { UIEventSource, Store } from "../../../Logic/UIEventSource"
|
||||
import type { MapProperties } from "../../../Models/MapProperties"
|
||||
import ThemeViewState from "../../../Models/ThemeViewState"
|
||||
import type { Feature } from "geojson"
|
||||
import type { RasterLayerPolygon } from "../../../Models/RasterLayers"
|
||||
import { RasterLayerUtils } from "../../../Models/RasterLayers"
|
||||
import { eliCategory } from "../../../Models/RasterLayerProperties"
|
||||
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import ShowDataLayer from "../../Map/ShowDataLayer"
|
||||
import * as conflation from "../../../../assets/layers/conflation/conflation.json"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
|
||||
export let value: UIEventSource<number>
|
||||
export let feature: Feature
|
||||
export let args: { background?: string, zoom?: number }
|
||||
export let state: ThemeViewState = undefined
|
||||
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
|
||||
let center = GeoOperations.centerpointCoordinates(feature)
|
||||
export let initialCoordinate: { lon: number, lat: number } = { lon: center[0], lat: center[1] }
|
||||
let mapLocation: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(initialCoordinate)
|
||||
let bg = args?.background
|
||||
let rasterLayer = state?.mapProperties.rasterLayer
|
||||
if (bg !== undefined) {
|
||||
if (eliCategory.indexOf(bg) >= 0) {
|
||||
const availableLayers = state.availableLayers.store.data
|
||||
const startLayer: RasterLayerPolygon = RasterLayerUtils.SelectBestLayerAccordingTo(availableLayers, bg)
|
||||
rasterLayer = new UIEventSource(startLayer)
|
||||
state?.mapProperties.rasterLayer.addCallbackD(layer => rasterLayer.set(layer))
|
||||
}
|
||||
|
||||
}
|
||||
let mapProperties: Partial<MapProperties> = {
|
||||
rasterLayer: rasterLayer,
|
||||
location: mapLocation,
|
||||
zoom: new UIEventSource(args?.zoom ?? 18)
|
||||
}
|
||||
|
||||
let start: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(undefined)
|
||||
|
||||
function selectStart() {
|
||||
start.set(mapLocation.data)
|
||||
}
|
||||
|
||||
let lengthFeature: Store<Feature[]> = start.map(start => {
|
||||
if (!start) {
|
||||
return []
|
||||
}
|
||||
// A bit of a double task: calculate the actual value _and_ the map rendering
|
||||
const end = mapLocation.data
|
||||
const distance = GeoOperations.distanceBetween([start.lon, start.lat], [end.lon, end.lat])
|
||||
value.set(distance.toFixed(2))
|
||||
|
||||
|
||||
return <Feature[]>[
|
||||
|
||||
{
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: "distance_line_" + distance,
|
||||
distance: "" + distance
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [[start.lon, start.lat], [end.lon, end.lat]]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}, [mapLocation])
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: new LayerConfig(conflation),
|
||||
features: new StaticFeatureSource(lengthFeature)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<div class="relative w-full h-64">
|
||||
<LocationInput value={mapLocation} {mapProperties} {map} />
|
||||
<div class="absolute bottom-0 left-0 p-4">
|
||||
<OpenBackgroundSelectorButton {state} {map} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="primary" on:click={() => selectStart()}>
|
||||
<Tr t={Translations.t.input_helpers.distance.setFirst} />
|
||||
</button>
|
||||
|
|
@ -164,7 +164,6 @@
|
|||
|
||||
/**
|
||||
* Determines 'top' and 'height-attributes, returns a CSS-string'
|
||||
* @param oh
|
||||
*/
|
||||
function rangeStyle(oh: OpeningHour, totalHeight: number): string {
|
||||
const top = ((oh.startHour + oh.startMinutes / 60) * totalHeight) / 24
|
||||
|
|
|
|||
|
|
@ -41,4 +41,5 @@
|
|||
</button>
|
||||
</Popup>
|
||||
<button on:click={() => expanded.set(true)}>Pick opening hours</button>
|
||||
|
||||
<PublicHolidaySelector value={state.phSelectorValue} />
|
||||
|
|
|
|||
|
|
@ -19,12 +19,13 @@
|
|||
import SlopeInput from "./Helpers/SlopeInput.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import WikidataInputHelper from "./WikidataInputHelper.svelte"
|
||||
import DistanceInput from "./Helpers/DistanceInput.svelte"
|
||||
|
||||
export let type: ValidatorType
|
||||
export let value: UIEventSource<string | object>
|
||||
|
||||
export let feature: Feature = undefined
|
||||
export let args: (string | number | boolean)[] = undefined
|
||||
export let args: (string | number | boolean)[] | any = undefined
|
||||
export let state: SpecialVisualizationState = undefined
|
||||
</script>
|
||||
|
||||
|
|
@ -52,6 +53,8 @@
|
|||
<SlopeInput {value} {feature} {state} />
|
||||
{:else if type === "wikidata"}
|
||||
<WikidataInputHelper {value} {feature} {state} {args} />
|
||||
{:else if type === "distance"}
|
||||
<DistanceInput {value} {state} {feature} {args} />
|
||||
{:else}
|
||||
<slot name="fallback" />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export interface InputHelperProperties {
|
|||
export default class InputHelpers {
|
||||
public static hideInputField: string[] = ["translation", "simple_tag", "tag"]
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
/**
|
||||
* Constructs a mapProperties-object for the given properties.
|
||||
* Assumes that the first helper-args contains the desired zoom-level
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export abstract class Validator {
|
|||
* */
|
||||
public readonly explanation: string
|
||||
/**
|
||||
* What HTML-inputmode to use
|
||||
* What HTML-inputmode to use?
|
||||
* Note: some inputHelpers will completely hide the default text field. This is kept in InputHelpers.hideInputField
|
||||
*/
|
||||
public readonly inputmode?:
|
||||
| "none"
|
||||
|
|
@ -81,4 +82,14 @@ export abstract class Validator {
|
|||
public reformat(s: string, _?: () => string): string {
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the helper arguments are correct.
|
||||
* This is called while preparing the themes.
|
||||
* Returns 'undefined' if everything is fine, or feedback if an error is detected
|
||||
* @param args the args for the input helper
|
||||
*/
|
||||
public validateArguments(args: string): undefined | string {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import TextValidator from "./Validators/TextValidator"
|
|||
import DateValidator from "./Validators/DateValidator"
|
||||
import NatValidator from "./Validators/NatValidator"
|
||||
import IntValidator from "./Validators/IntValidator"
|
||||
import LengthValidator from "./Validators/LengthValidator"
|
||||
import DistanceValidator from "./Validators/DistanceValidator"
|
||||
import DirectionValidator from "./Validators/DirectionValidator"
|
||||
import WikidataValidator from "./Validators/WikidataValidator"
|
||||
import PNatValidator from "./Validators/PNatValidator"
|
||||
|
|
@ -33,36 +33,35 @@ export type ValidatorType = (typeof Validators.availableTypes)[number]
|
|||
|
||||
export default class Validators {
|
||||
public static readonly availableTypes = [
|
||||
"string",
|
||||
"text",
|
||||
"date",
|
||||
"nat",
|
||||
"int",
|
||||
"distance",
|
||||
"direction",
|
||||
"wikidata",
|
||||
"pnat",
|
||||
"float",
|
||||
"pfloat",
|
||||
"email",
|
||||
"url",
|
||||
"phone",
|
||||
"opening_hours",
|
||||
"color",
|
||||
"image",
|
||||
"simple_tag",
|
||||
"key",
|
||||
"translation",
|
||||
"icon",
|
||||
"fediverse",
|
||||
"tag",
|
||||
"fediverse",
|
||||
"id",
|
||||
"slope",
|
||||
"velopark",
|
||||
"nsi",
|
||||
"currency",
|
||||
"date",
|
||||
"direction",
|
||||
"distance",
|
||||
"email",
|
||||
"fediverse",
|
||||
"float",
|
||||
"icon",
|
||||
"id",
|
||||
"image",
|
||||
"int",
|
||||
"key",
|
||||
"nat",
|
||||
"nsi",
|
||||
"opening_hours",
|
||||
"pfloat",
|
||||
"phone",
|
||||
"pnat",
|
||||
"regex",
|
||||
"simple_tag",
|
||||
"slope",
|
||||
"string",
|
||||
"tag",
|
||||
"text",
|
||||
"translation",
|
||||
"url",
|
||||
"velopark",
|
||||
"wikidata"
|
||||
] as const
|
||||
|
||||
public static readonly AllValidators: ReadonlyArray<Validator> = [
|
||||
|
|
@ -71,7 +70,7 @@ export default class Validators {
|
|||
new DateValidator(),
|
||||
new NatValidator(),
|
||||
new IntValidator(),
|
||||
new LengthValidator(),
|
||||
new DistanceValidator(),
|
||||
new DirectionValidator(),
|
||||
new WikidataValidator(),
|
||||
new PNatValidator(),
|
||||
|
|
@ -94,7 +93,7 @@ export default class Validators {
|
|||
new VeloparkValidator(),
|
||||
new NameSuggestionIndexValidator(),
|
||||
new CurrencyValidator(),
|
||||
new RegexValidator(),
|
||||
new RegexValidator()
|
||||
]
|
||||
|
||||
private static _byType = Validators._byTypeConstructor()
|
||||
|
|
|
|||
55
src/UI/InputElement/Validators/DistanceValidator.ts
Normal file
55
src/UI/InputElement/Validators/DistanceValidator.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Validator } from "../Validator"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { eliCategory } from "../../../Models/RasterLayerProperties"
|
||||
|
||||
export default class DistanceValidator extends Validator {
|
||||
private readonly docs: string = [
|
||||
"#### Helper-arguments",
|
||||
"Options are:",
|
||||
["````json",
|
||||
" \"background\": \"some_background_id or category, e.g. 'map'\"",
|
||||
" \"zoom\": 20 # initial zoom level of the map",
|
||||
"}",
|
||||
"```"].join("\n")
|
||||
].join("\n\n")
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
"distance",
|
||||
"A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `[\"21\", \"map,photo\"]",
|
||||
"decimal"
|
||||
)
|
||||
}
|
||||
|
||||
isValid = (str) => {
|
||||
const t = Number(str)
|
||||
return !isNaN(t)
|
||||
}
|
||||
|
||||
validateArguments(args: any): undefined | string {
|
||||
if (args === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (typeof args !== "object" || Array.isArray(args)) {
|
||||
return "Expected an object of type `{background?: string, zoom?: number}`"
|
||||
}
|
||||
|
||||
const optionalKeys = ["background", "zoom"]
|
||||
const keys = Object.keys(args).filter(k => optionalKeys.indexOf(k) < 0)
|
||||
if (keys.length > 0) {
|
||||
return "Unknown key " + keys.join("; ") + "; use " + optionalKeys.join("; ") + " instead"
|
||||
}
|
||||
const bg = args["background"]
|
||||
if (bg && eliCategory.indexOf(bg) < 0) {
|
||||
return "The given background layer is not a recognized ELI-type. Perhaps you meant one of " +
|
||||
Utils.sortedByLevenshteinDistance(bg, eliCategory, x => x).slice(0, 5)
|
||||
}
|
||||
if (typeof args["zoom"] !== "number") {
|
||||
return "zoom must be a number, got a " + typeof args["zoom"]
|
||||
}
|
||||
if (typeof args["zoom"] !== "number" || args["zoom"] <= 1 || args["zoom"] > 25) {
|
||||
return "zoom must be a number between 2 and 25, got " + args["zoom"]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { Validator } from "../Validator"
|
|||
import { ValidatorType } from "../Validators"
|
||||
|
||||
export default class FloatValidator extends Validator {
|
||||
inputmode: "decimal" = "decimal"
|
||||
inputmode: "decimal" = "decimal" as const
|
||||
|
||||
constructor(name?: ValidatorType, explanation?: string) {
|
||||
super(name ?? "float", explanation ?? "A decimal number", "decimal")
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import { Validator } from "../Validator"
|
||||
|
||||
export default class LengthValidator extends Validator {
|
||||
constructor() {
|
||||
super(
|
||||
"distance",
|
||||
'A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]',
|
||||
"decimal"
|
||||
)
|
||||
}
|
||||
|
||||
isValid = (str) => {
|
||||
const t = Number(str)
|
||||
return !isNaN(t)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +1,14 @@
|
|||
import Title from "../../Base/Title"
|
||||
import Combine from "../../Base/Combine"
|
||||
import { Validator } from "../Validator"
|
||||
import Table from "../../Base/Table"
|
||||
|
||||
export default class NameSuggestionIndexValidator extends Validator {
|
||||
constructor() {
|
||||
super(
|
||||
"nsi",
|
||||
new Combine([
|
||||
"Gives a list of possible suggestions for a brand or operator tag.",
|
||||
new Title("Helper arguments"),
|
||||
new Table(
|
||||
["name", "doc"],
|
||||
[
|
||||
[
|
||||
"options",
|
||||
new Combine([
|
||||
"A JSON-object of type `{ main: string, key: string }`. ",
|
||||
new Table(
|
||||
["subarg", "doc"],
|
||||
[
|
||||
[
|
||||
"main",
|
||||
"The main tag to give suggestions for, e.g. `amenity=restaurant`.",
|
||||
],
|
||||
[
|
||||
"addExtraTags",
|
||||
"Extra tags to add to the suggestions, e.g. `nobrand=yes`.",
|
||||
],
|
||||
]
|
||||
),
|
||||
]),
|
||||
],
|
||||
]
|
||||
),
|
||||
])
|
||||
"Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
|
||||
)
|
||||
}
|
||||
|
||||
validateArguments(args: string): string | undefined {
|
||||
return "No arguments needed"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ 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"],
|
||||
|
|
@ -22,7 +22,7 @@ export default class OpeningHoursValidator extends Validator {
|
|||
],
|
||||
]
|
||||
),
|
||||
"### Example usage",
|
||||
"#### Example usage",
|
||||
"To add a conditional (based on time) access restriction:\n\n```\n" +
|
||||
`
|
||||
"freeform": {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import Combine from "../../Base/Combine"
|
||||
import Wikidata, { WikidataResponse } from "../../../Logic/Web/Wikidata"
|
||||
import { Validator } from "../Validator"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Title from "../../Base/Title"
|
||||
import Table from "../../Base/Table"
|
||||
import MarkdownUtils from "../../../Utils/MarkdownUtils"
|
||||
|
||||
export default class WikidataValidator extends Validator {
|
||||
public static readonly _searchCache = new Map<string, Promise<WikidataResponse[]>>()
|
||||
|
||||
public static docs = [
|
||||
"### Helper arguments",
|
||||
private static docs = [
|
||||
"#### Helper arguments",
|
||||
MarkdownUtils.table(
|
||||
["name", "doc"],
|
||||
[
|
||||
|
|
@ -25,7 +22,7 @@ export default class WikidataValidator extends Validator {
|
|||
],
|
||||
]
|
||||
),
|
||||
"#### Suboptions",
|
||||
"##### Suboptions",
|
||||
MarkdownUtils.table(
|
||||
["subarg", "doc"],
|
||||
[
|
||||
|
|
@ -50,7 +47,7 @@ export default class WikidataValidator extends Validator {
|
|||
),
|
||||
].join("\n\n")
|
||||
private static readonly docsExampleUsage: string =
|
||||
"### Example usage\n\n" +
|
||||
"#### 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
|
||||
|
|
@ -113,7 +110,7 @@ Another example is to search for species and trees:
|
|||
return !str.split(";").some((str) => Wikidata.ExtractKey(str) === undefined)
|
||||
}
|
||||
|
||||
getFeedback(s: string, _?: () => string): Translation | undefined {
|
||||
getFeedback(s: string): Translation | undefined {
|
||||
const t = Translations.t.validation.wikidata
|
||||
if (s === "") {
|
||||
return t.empty
|
||||
|
|
|
|||
|
|
@ -352,7 +352,7 @@ class LineRenderingLayer {
|
|||
// After waiting 'till the map has loaded, the data might have changed already
|
||||
// As such, we only now read the features from the featureSource and compare with the previously set data
|
||||
const features = featureSource.data
|
||||
if (features.length === 0) {
|
||||
if (!features || features.length === 0) {
|
||||
// This is a very ugly workaround for https://source.mapcomplete.org/MapComplete/MapComplete/issues/2312,
|
||||
// but I couldn't find the root cause
|
||||
return
|
||||
|
|
|
|||
|
|
@ -859,12 +859,23 @@ This list will be sorted
|
|||
return ranges
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* const oh = {weekday: 0, startHour: 18, startMinutes: 0, endHour: 20, endMinutes: 30}
|
||||
* OH.isSame(oh, oh) // => true
|
||||
* const ohEndMidnights = {...oh, endMinutes: 0, endHour: 24}
|
||||
* OH.isSame(oh, ohEndMidnights) // => false
|
||||
* OH.isSame(ohEndMidnights, ohEndMidnights) // => true
|
||||
* const ohEndMidnightsAlt = {...ohEndMidnights, endMinutes: 0, endHour: 0}
|
||||
* OH.isSame(ohEndMidnightsAlt, ohEndMidnights) // => true
|
||||
*
|
||||
*/
|
||||
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.endHour % 24 === b.endHour % 24 &&
|
||||
a.endMinutes === b.endMinutes
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
/**
|
||||
* A single opening hours range, shown on top of the OH-picker table
|
||||
*/
|
||||
import { Utils } from "../../Utils"
|
||||
import Combine from "../Base/Combine"
|
||||
import { OH, OpeningHour } from "./OpeningHours"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import Delete_icon from "../../assets/svg/Delete_icon.svelte"
|
||||
|
||||
export default class OpeningHoursRange extends BaseUIElement {
|
||||
private _oh: OpeningHour
|
||||
|
||||
private readonly _onDelete: () => void
|
||||
|
||||
constructor(oh: OpeningHour, onDelete: () => void) {
|
||||
super()
|
||||
this._oh = oh
|
||||
this._onDelete = onDelete
|
||||
this.SetClass("oh-timerange")
|
||||
}
|
||||
|
||||
InnerConstructElement(): HTMLElement {
|
||||
const height = this.getHeight()
|
||||
const oh = this._oh
|
||||
const startTime = new FixedUiElement(
|
||||
Utils.TwoDigits(oh.startHour) + ":" + Utils.TwoDigits(oh.startMinutes)
|
||||
)
|
||||
const endTime = new FixedUiElement(
|
||||
Utils.TwoDigits(oh.endHour) + ":" + Utils.TwoDigits(oh.endMinutes)
|
||||
)
|
||||
|
||||
const deleteRange = new SvelteUIElement(Delete_icon)
|
||||
.SetClass("rounded-full w-6 h-6 block bg-black pointer-events-auto ")
|
||||
.onClick(() => {
|
||||
this._onDelete()
|
||||
})
|
||||
|
||||
let content: BaseUIElement
|
||||
if (height > 3) {
|
||||
content = new Combine([startTime, deleteRange, endTime]).SetClass(
|
||||
"flex flex-col h-full justify-between"
|
||||
)
|
||||
} else {
|
||||
content = new Combine([deleteRange])
|
||||
.SetClass("flex flex-col h-full")
|
||||
.SetStyle("flex-content: center; overflow-x: unset;")
|
||||
}
|
||||
|
||||
const el = new Combine([content]).ConstructElement()
|
||||
|
||||
el.style.top = `${(100 * OH.startTime(oh)) / 24}%`
|
||||
el.style.height = `${(100 * this.getHeight()) / 24}%`
|
||||
return el
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the relative height, in number of hours to display
|
||||
* Range: ]0 - 24]
|
||||
*/
|
||||
private getHeight(): number {
|
||||
const oh = this._oh
|
||||
|
||||
let endhour = oh.endHour
|
||||
if (oh.endHour == 0 && oh.endMinutes == 0) {
|
||||
endhour = 24
|
||||
}
|
||||
return endhour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import Toggle from "../Input/Toggle"
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Table from "../Base/Table"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Loading from "../Base/Loading"
|
||||
import opening_hours from "opening_hours"
|
||||
import Locale from "../i18n/Locale"
|
||||
|
|
@ -95,7 +94,7 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
rangeStart: Date
|
||||
): BaseUIElement {
|
||||
const isWeekstable: boolean = oh.isWeekStable()
|
||||
let [changeHours, changeHourText] = OH.allChangeMoments(ranges)
|
||||
const [changeHours, changeHourText] = OH.allChangeMoments(ranges)
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
|
|
@ -299,10 +298,6 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
}
|
||||
return Translations.t.general.opening_hours.closed_permanently.Clone()
|
||||
}
|
||||
const willOpenAt = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(
|
||||
opensAtDate.getHours(),
|
||||
opensAtDate.getMinutes()
|
||||
)}`
|
||||
return Translations.t.general.opening_hours.closed_until.Subs({
|
||||
date: opensAtDate.toLocaleString(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
import SelectedElementView from "../BigComponents/SelectedElementView.svelte"
|
||||
import TagRenderingAnswer from "./TagRendering/TagRenderingAnswer.svelte"
|
||||
import TagRenderingEditableDynamic from "./TagRendering/TagRenderingEditableDynamic.svelte"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
|
|
@ -18,7 +17,10 @@
|
|||
export let layer: LayerConfig
|
||||
|
||||
let headerTr = layer.tagRenderings.find((tr) => tr.id === header)
|
||||
let trgs: TagRenderingConfig[] = []
|
||||
if (headerTr === undefined) {
|
||||
console.error("Header tagRendering with ID", header, "was not found")
|
||||
}
|
||||
let tagRenderings: TagRenderingConfig[] = []
|
||||
let seenIds = new Set<string>()
|
||||
for (const label of labels) {
|
||||
for (const tr of layer.tagRenderings) {
|
||||
|
|
@ -26,18 +28,30 @@
|
|||
continue
|
||||
}
|
||||
if (label === tr.id || tr.labels.some((l) => l === label)) {
|
||||
trgs.push(tr)
|
||||
tagRenderings.push(tr)
|
||||
seenIds.add(tr.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<AccordionSingle>
|
||||
{#if tagRenderings.length > 0}
|
||||
<div class="mb-8">
|
||||
|
||||
<AccordionSingle>
|
||||
<div slot="header">
|
||||
<TagRenderingAnswer {tags} {layer} config={headerTr} {state} {selectedElement} />
|
||||
{#if headerTr}
|
||||
<TagRenderingAnswer {tags} {layer} config={headerTr} {state} {selectedElement} />
|
||||
{:else}
|
||||
{header}
|
||||
{/if}
|
||||
</div>
|
||||
{#each trgs as config (config.id)}
|
||||
<TagRenderingEditableDynamic {tags} {config} {state} {selectedElement} {layer} />
|
||||
{#each tagRenderings as config (config.id)}
|
||||
{#if config.IsKnown($tags) && (config.condition === undefined || config.condition.matchesProperties($tags))}
|
||||
<TagRenderingEditableDynamic {tags} {config} {state} {selectedElement} {layer} />
|
||||
{/if}
|
||||
{/each}
|
||||
</AccordionSingle>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import DelayedComponent from "../Base/DelayedComponent.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default class NoteCommentElement {
|
|||
*/
|
||||
public static addCommentTo(
|
||||
txt: string,
|
||||
tags: UIEventSource<any>,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
state: { osmConnection: { userDetails: Store<{ name: string; uid: number }> } }
|
||||
) {
|
||||
const comments: any[] = JSON.parse(tags.data["comments"])
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@
|
|||
const _onlyForLabels = new Set(onlyForLabels)
|
||||
/**
|
||||
* If set, only questions _not_ having these labels will be shown.
|
||||
* This is used for a partial questionbox
|
||||
* This is used for a partial questionbox. If both 'onlyFor' and 'notFor' are set, questions must accept both
|
||||
*/
|
||||
export let notForLabels: string[] | undefined = undefined
|
||||
const _notForLabels = new Set(notForLabels)
|
||||
let showAllQuestionsAtOnce: Store<boolean> =
|
||||
export let showAllQuestionsAtOnce: Store<boolean> =
|
||||
state.userRelatedState?.showAllQuestionsAtOnce ?? new ImmutableStore(false)
|
||||
|
||||
function allowed(labels: string[]) {
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
return true
|
||||
}
|
||||
|
||||
const baseQuestions = (layer?.tagRenderings ?? [])?.filter(
|
||||
let baseQuestions = (layer?.tagRenderings ?? [])?.filter(
|
||||
(tr) => allowed(tr.labels) && tr.question !== undefined
|
||||
)
|
||||
|
||||
|
|
@ -119,7 +119,6 @@
|
|||
}, 50)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $loginEnabled}
|
||||
<div
|
||||
bind:this={questionboxElem}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@
|
|||
export let id: string = undefined
|
||||
|
||||
if (config === undefined) {
|
||||
throw "Config is undefined in tagRenderingAnswer"
|
||||
console.error("TagRenderingAnswer: Config is undefined")
|
||||
throw ("Config is undefined in tagRenderingAnswer")
|
||||
}
|
||||
let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD((tags) =>
|
||||
Utils.NoNull(config?.GetRenderValues(tags))
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@
|
|||
export let getCountry = () => "?"
|
||||
|
||||
onMount(() => {
|
||||
console.log("Setting selected unit based on country", getCountry(), upstreamValue.data)
|
||||
console.log("Setting selected unit based on country", getCountry(), "and upstream value:", upstreamValue.data)
|
||||
if (upstreamValue.data === undefined || upstreamValue.data === "") {
|
||||
// Init the selected unit
|
||||
let denomination: Denomination = unit.getDefaultDenomination(getCountry)
|
||||
console.log("Found denom", denomination.canonical)
|
||||
console.log("Found denom", denomination.canonical, "available denominations are:", unit.denominations.map(denom => denom.canonical))
|
||||
selectedUnit.setData(denomination.canonical)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
{/if}
|
||||
<a
|
||||
class="link-underline"
|
||||
href="https://github.com/pietervdvn/MapComplete/issues/1782"
|
||||
href="https://source.mapcomplete.org/MapComplete/MapComplete/issues/1782"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,21 @@
|
|||
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { MinimalThemeInformation } from "../../Models/ThemeConfig/ThemeConfig"
|
||||
|
||||
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) => {
|
||||
const recent = themes.filter((th) => th !== state.theme.id).slice(0, 6)
|
||||
const deduped: MinimalThemeInformation[] = []
|
||||
for (const theme of recent) {
|
||||
if (deduped.some(th => th.id === theme.id)) {
|
||||
continue
|
||||
}
|
||||
deduped.push(theme)
|
||||
}
|
||||
return deduped
|
||||
}
|
||||
)
|
||||
let themeResults = state.searchState.themeSuggestions
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export class ImageVisualisations {
|
|||
args: [
|
||||
{
|
||||
name: "image_key",
|
||||
defaultValue: AllImageProviders.defaultKeys.join(","),
|
||||
defaultValue: AllImageProviders.defaultKeys.join(";"),
|
||||
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ",
|
||||
},
|
||||
],
|
||||
|
|
@ -73,7 +73,7 @@ export class ImageVisualisations {
|
|||
constr: (state, tags, args) => {
|
||||
let imagePrefixes: string[] = undefined
|
||||
if (args.length > 0) {
|
||||
imagePrefixes = [].concat(...args.map((a) => a.split(",")))
|
||||
imagePrefixes = [].concat(...args.map((a) => a.split(";")))
|
||||
}
|
||||
const images = AllImageProviders.loadImagesFor(tags, imagePrefixes)
|
||||
const estimated = tags.mapD((tags) =>
|
||||
|
|
@ -89,8 +89,9 @@ export class ImageVisualisations {
|
|||
needsUrls: [Imgur.apiUrl, ...Imgur.supportingUrls],
|
||||
args: [
|
||||
{
|
||||
name: "image-key",
|
||||
name: "image_key",
|
||||
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
|
||||
defaultValue: "panoramax",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
SpecialVisualization,
|
||||
SpecialVisualizationState,
|
||||
SpecialVisualizationSvelte,
|
||||
} from "../SpecialVisualization"
|
||||
import { SpecialVisualization, SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Feature } from "geojson"
|
||||
|
|
@ -136,10 +132,10 @@ export class NoteVisualisations {
|
|||
group: "notes",
|
||||
needsUrls: [Imgur.apiUrl, ...Imgur.supportingUrls],
|
||||
|
||||
constr: (state, tags, args, feature, layer) => {
|
||||
constr: (state, tags, args, feature) => {
|
||||
const id = tags.data[args[0] ?? "id"]
|
||||
tags = state.featureProperties.getStore(id)
|
||||
return new SvelteUIElement(UploadImage, { state, tags, layer, feature })
|
||||
return new SvelteUIElement(UploadImage, { state, tags, feature })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import LoginButton from "../Base/LoginButton.svelte"
|
|||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import OrientationDebugPanel from "../Debug/OrientationDebugPanel.svelte"
|
||||
import AllTagsPanel from "../Popup/AllTagsPanel/AllTagsPanel.svelte"
|
||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import ClearCaches from "../Popup/ClearCaches.svelte"
|
||||
import Locale from "../i18n/Locale"
|
||||
import LanguageUtils from "../../Utils/LanguageUtils"
|
||||
import LanguagePicker from "../InputElement/LanguagePicker.svelte"
|
||||
import PendingChangesIndicator from "../BigComponents/PendingChangesIndicator.svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export class SettingsVisualisations {
|
||||
public static initList(): SpecialVisualizationSvelte[] {
|
||||
|
|
@ -79,14 +80,23 @@ export class SettingsVisualisations {
|
|||
group: "settings",
|
||||
docs: "Shows the current state of storage",
|
||||
args: [],
|
||||
constr(state: SpecialVisualizationState): SvelteUIElement {
|
||||
constr: function(state: SpecialVisualizationState): SvelteUIElement {
|
||||
const data = {}
|
||||
for (const key in localStorage) {
|
||||
data[key] = localStorage[key]
|
||||
}
|
||||
const tags = new ImmutableStore(data)
|
||||
const tags = new UIEventSource(data)
|
||||
|
||||
navigator.storage.estimate().then(estimate => {
|
||||
data["__usage:current:bytes"] = estimate.usage
|
||||
data["__usage:current:human"] = Utils.toHumanByteSize(estimate.usage)
|
||||
data["__usage:quota:bytes"] = estimate.quota
|
||||
data["__usage:quota:human"] = Utils.toHumanByteSize(estimate.quota)
|
||||
tags.ping()
|
||||
|
||||
})
|
||||
return new SvelteUIElement(AllTagsPanel, { state, tags })
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
funcName: "clear_caches",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Questionbox from "../Popup/TagRendering/Questionbox.svelte"
|
||||
|
|
@ -29,8 +29,13 @@ class QuestionViz implements SpecialVisualizationSvelte {
|
|||
},
|
||||
{
|
||||
name: "blacklisted-labels",
|
||||
doc: "One or more ';'-separated labels of questions which should _not_ be included. Default: 'hidden'",
|
||||
doc: "One or more ';'-separated labels of questions which should _not_ be included. Note that the questionbox which is added by default will blacklist 'hidden'"
|
||||
},
|
||||
{
|
||||
name: "show_all",
|
||||
default: "user-preference",
|
||||
doc: "Either `no`, `yes` or `user-preference`. Indicates if all questions should be shown at once"
|
||||
}
|
||||
]
|
||||
svelteBased = true
|
||||
group: "default"
|
||||
|
|
@ -50,8 +55,12 @@ class QuestionViz implements SpecialVisualizationSvelte {
|
|||
?.split(";")
|
||||
?.map((s) => s.trim())
|
||||
?.filter((s) => s !== "")
|
||||
if (blacklist.length === 0) {
|
||||
blacklist.push("hidden")
|
||||
const showAll = args[2]
|
||||
let showAllQuestionsAtOnce: Store<boolean> = state.userRelatedState?.showAllQuestionsAtOnce
|
||||
if (showAll === "yes") {
|
||||
showAllQuestionsAtOnce = new ImmutableStore(true)
|
||||
} else if (showAll === "no") {
|
||||
showAllQuestionsAtOnce = new ImmutableStore(false)
|
||||
}
|
||||
return new SvelteUIElement(Questionbox, {
|
||||
layer,
|
||||
|
|
@ -60,6 +69,7 @@ class QuestionViz implements SpecialVisualizationSvelte {
|
|||
state,
|
||||
onlyForLabels: labels,
|
||||
notForLabels: blacklist,
|
||||
showAllQuestionsAtOnce
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@
|
|||
import { AccordionItem } from "flowbite-svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import DefaultIcon from "../Map/DefaultIcon.svelte"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
|
||||
export let layer: LayerConfig
|
||||
export let state: ThemeViewState
|
||||
let bbox = state.mapProperties.bounds
|
||||
let elements: Feature[] = state.perLayer.get(layer.id).GetFeaturesWithin($bbox)
|
||||
$: elements = state.perLayer.get(layer.id).GetFeaturesWithin($bbox)
|
||||
let elements: Store<Feature[]> = bbox.mapD(bbox => state.perLayer.get(layer.id).GetFeaturesWithin(bbox))
|
||||
|
||||
let trs = layer.tagRenderings.filter((tr) => tr.question)
|
||||
</script>
|
||||
|
|
@ -31,19 +31,19 @@
|
|||
<DefaultIcon {layer} />
|
||||
</div>
|
||||
<Tr t={layer.name} />
|
||||
({elements.length} elements in view)
|
||||
({$elements.length} elements in view)
|
||||
</div>
|
||||
|
||||
{#if elements === undefined}
|
||||
{#if $elements === undefined}
|
||||
<Loading />
|
||||
{:else if elements.length === 0}
|
||||
{:else if $elements.length === 0}
|
||||
No features in view
|
||||
{:else}
|
||||
<div class="flex w-full flex-wrap gap-x-4 gap-y-4">
|
||||
{#each trs as tr}
|
||||
<ToSvelte
|
||||
construct={() =>
|
||||
new TagRenderingChart(elements, tr, {
|
||||
new TagRenderingChart($elements, tr, {
|
||||
chartclasses: "w-full self-center",
|
||||
includeTitle: true,
|
||||
}).SetClass(tr.multiAnswer ? "w-128" : "w-96")}
|
||||
|
|
|
|||
|
|
@ -429,6 +429,11 @@
|
|||
enableTrafficLight.addCallbackAndRunD((_) => {
|
||||
setTrafficLight(all.data)
|
||||
})
|
||||
|
||||
let now = Math.round(new Date().getTime() / 1000)
|
||||
let twoDaysAgo = now - 2 * 24 * 60 * 60
|
||||
let lastHour = now - 60 * 60
|
||||
|
||||
</script>
|
||||
|
||||
<h1>MapComplete status indicators</h1>
|
||||
|
|
@ -450,6 +455,18 @@
|
|||
<ServiceIndicator {service} />
|
||||
{/each}
|
||||
|
||||
<h3>Panoramax & OSM.fr Blurring service</h3>
|
||||
<a href="https://status.thibaultmol.link/status/panoramax">Panoramax.MapComplete.org status page</a>
|
||||
<a href="https://munin.openstreetmap.fr/osm37.openstreetmap.fr/blur.vm.openstreetmap.fr/index.html#system"
|
||||
target="_blank" rel="noopener">
|
||||
See more statistics for the blurring service
|
||||
</a>
|
||||
<img
|
||||
style="width: 80rem"
|
||||
src={`https://munin.openstreetmap.fr/munin-cgi/munin-cgi-graph/osm37.openstreetmap.fr/blur.vm.openstreetmap.fr/nvidia_gpu_power-pinpoint=${twoDaysAgo},${now}.png?&lower_limit=&upper_limit=&size_x=800&size_y=400`} />
|
||||
<img
|
||||
style="width: 80rem"
|
||||
src={`https://munin.openstreetmap.fr/munin-cgi/munin-cgi-graph/osm37.openstreetmap.fr/blur.vm.openstreetmap.fr/nvidia_gpu_power-pinpoint=${lastHour},${now}.png?&lower_limit=&upper_limit=&size_x=800&size_y=400`} />
|
||||
<button
|
||||
on:click={() => {
|
||||
fetch(Constants.ErrorReportServer, {
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@
|
|||
</div>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md"
|
||||
href="https://source.mapcomplete.org/MapComplete/MapComplete/src/branch/develop/Docs/Tags_format.md"
|
||||
>
|
||||
<QuestionMarkCircle class="h-6 w-6" />
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@
|
|||
Someone might be able to help you
|
||||
</li>
|
||||
<li>
|
||||
File <a href="https://github.com/pietervdvn/MapComplete/issues">an issue</a>
|
||||
File <a href="https://source.mapcomplete.org/MapComplete/MapComplete/issues">an issue</a>
|
||||
</li>
|
||||
<li>
|
||||
Contact the devs via <a href="mailto:info@posteo.net">email</a>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue