forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
477ef56e00
202 changed files with 5785 additions and 2386 deletions
|
@ -1,12 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource.js"
|
||||
import type { Writable } from "svelte/store"
|
||||
|
||||
/**
|
||||
* For some stupid reason, it is very hard to bind inputs
|
||||
*/
|
||||
export let selected: UIEventSource<boolean>
|
||||
export let selected: Writable<boolean>
|
||||
let _c: boolean = selected.data ?? true
|
||||
$: selected.setData(_c)
|
||||
$: selected.set(_c)
|
||||
</script>
|
||||
|
||||
<input type="checkbox" bind:checked={_c} />
|
||||
<label class="no-image-background flex gap-1">
|
||||
<input bind:checked={_c} type="checkbox" />
|
||||
<slot />
|
||||
</label>
|
||||
|
|
|
@ -1,40 +1,48 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
export let accept: string
|
||||
export let multiple: boolean = true
|
||||
|
||||
export let accept: string;
|
||||
export let multiple: boolean = true;
|
||||
|
||||
const dispatcher = createEventDispatcher<{ submit: FileList }>();
|
||||
export let cls: string = "";
|
||||
let drawAttention = false;
|
||||
let inputElement: HTMLInputElement;
|
||||
let id = Math.random() * 1000000000 + "";
|
||||
const dispatcher = createEventDispatcher<{ submit: FileList }>()
|
||||
export let cls: string = ""
|
||||
let drawAttention = false
|
||||
let inputElement: HTMLInputElement
|
||||
let id = Math.random() * 1000000000 + ""
|
||||
</script>
|
||||
|
||||
<form>
|
||||
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput"+id}>
|
||||
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput" + id}>
|
||||
<slot />
|
||||
|
||||
</label>
|
||||
<input {accept} bind:this={inputElement} class="hidden" id={"fileinput" + id} {multiple} name="file-input"
|
||||
on:change|preventDefault={() => {
|
||||
drawAttention = false;
|
||||
dispatcher("submit", inputElement.files)}}
|
||||
|
||||
on:dragend={ () => {drawAttention = false}}
|
||||
on:dragover|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Dragging over!")
|
||||
drawAttention = true
|
||||
e.dataTransfer.drop = "copy"
|
||||
}}
|
||||
on:dragstart={ () => {drawAttention = false}}
|
||||
on:drop|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Got a 'drop'")
|
||||
drawAttention = false
|
||||
dispatcher("submit", e.dataTransfer.files)
|
||||
}}
|
||||
type="file"
|
||||
>
|
||||
<input
|
||||
{accept}
|
||||
bind:this={inputElement}
|
||||
class="hidden"
|
||||
id={"fileinput" + id}
|
||||
{multiple}
|
||||
name="file-input"
|
||||
on:change|preventDefault={() => {
|
||||
drawAttention = false
|
||||
dispatcher("submit", inputElement.files)
|
||||
}}
|
||||
on:dragend={() => {
|
||||
drawAttention = false
|
||||
}}
|
||||
on:dragover|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Dragging over!")
|
||||
drawAttention = true
|
||||
e.dataTransfer.drop = "copy"
|
||||
}}
|
||||
on:dragstart={() => {
|
||||
drawAttention = false
|
||||
}}
|
||||
on:drop|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Got a 'drop'")
|
||||
drawAttention = false
|
||||
dispatcher("submit", e.dataTransfer.files)
|
||||
}}
|
||||
type="file"
|
||||
/>
|
||||
</form>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/**
|
||||
* Given an HTML string, properly shows this
|
||||
*/
|
||||
import { Utils } from "../../Utils";
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export let src: string
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import ToSvelte from "./ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import ToSvelte from "./ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export let cls : string = undefined
|
||||
export let cls: string = undefined
|
||||
</script>
|
||||
|
||||
<div class={twMerge( "flex p-1 pl-2", cls)}>
|
||||
<div class={twMerge("flex p-1 pl-2", cls)}>
|
||||
<div class="min-w-6 h-6 w-6 animate-spin self-center">
|
||||
<ToSvelte construct={Svg.loading_svg()} />
|
||||
</div>
|
||||
|
|
|
@ -55,8 +55,7 @@
|
|||
|
||||
{#if filteredLayer.layerDef.name}
|
||||
<div bind:this={mainElem} class="mb-1.5">
|
||||
<label class="no-image-background flex gap-1">
|
||||
<Checkbox selected={isDisplayed} />
|
||||
<Checkbox selected={isDisplayed}>
|
||||
<If condition={filteredLayer.isDisplayed}>
|
||||
<ToSvelte
|
||||
construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background")}
|
||||
|
@ -75,7 +74,7 @@
|
|||
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
</Checkbox>
|
||||
|
||||
{#if $isDisplayed && filteredLayer.layerDef.filters?.length > 0}
|
||||
<div id="subfilters" class="ml-4 flex flex-col gap-y-1">
|
||||
|
@ -83,10 +82,9 @@
|
|||
<div>
|
||||
<!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with searchable fields -->
|
||||
{#if filter.options.length === 1 && filter.options[0].fields.length === 0}
|
||||
<label>
|
||||
<Checkbox selected={getBooleanStateFor(filter)} />
|
||||
<Checkbox selected={getBooleanStateFor(filter)}>
|
||||
{filter.options[0].question}
|
||||
</label>
|
||||
</Checkbox>
|
||||
{/if}
|
||||
|
||||
{#if filter.options.length === 1 && filter.options[0].fields.length > 0}
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
>
|
||||
{#each layer.titleIcons as titleIconConfig}
|
||||
{#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ..._metatags, ..._tags } ) ?? true) && titleIconConfig.IsKnown(_tags)}
|
||||
<div class="flex h-8 w-8 items-center">
|
||||
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
|
||||
<TagRenderingAnswer
|
||||
config={titleIconConfig}
|
||||
{tags}
|
||||
|
|
|
@ -1,44 +1,45 @@
|
|||
<script lang="ts">
|
||||
import Translations from "../i18n/Translations";
|
||||
import Svg from "../../Svg";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import NextButton from "../Base/NextButton.svelte";
|
||||
import Geosearch from "./Geosearch.svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { twJoin } from "tailwind-merge";
|
||||
import { Utils } from "../../Utils";
|
||||
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState";
|
||||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Geosearch from "./Geosearch.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import { Utils } from "../../Utils"
|
||||
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState"
|
||||
|
||||
/**
|
||||
* The theme introduction panel
|
||||
*/
|
||||
export let state: ThemeViewState;
|
||||
let layout = state.layout;
|
||||
let selectedElement = state.selectedElement;
|
||||
let selectedLayer = state.selectedLayer;
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
let selectedElement = state.selectedElement
|
||||
let selectedLayer = state.selectedLayer
|
||||
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined);
|
||||
let searchEnabled = false;
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
let searchEnabled = false
|
||||
|
||||
let geopermission: Store<GeolocationPermissionState> = state.geolocation.geolocationState.permission;
|
||||
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation;
|
||||
let geopermission: Store<GeolocationPermissionState> =
|
||||
state.geolocation.geolocationState.permission
|
||||
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation
|
||||
|
||||
geopermission.addCallback(perm => console.log(">>>> Permission", perm));
|
||||
geopermission.addCallback((perm) => console.log(">>>> Permission", perm))
|
||||
|
||||
function jumpToCurrentLocation() {
|
||||
const glstate = state.geolocation.geolocationState;
|
||||
const glstate = state.geolocation.geolocationState
|
||||
if (glstate.currentGPSLocation.data !== undefined) {
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data;
|
||||
state.guistate.themeIsOpened.setData(false);
|
||||
const coor = { lon: c.longitude, lat: c.latitude };
|
||||
state.mapProperties.location.setData(coor);
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
|
||||
state.guistate.themeIsOpened.setData(false)
|
||||
const coor = { lon: c.longitude, lat: c.latitude }
|
||||
state.mapProperties.location.setData(coor)
|
||||
}
|
||||
if (glstate.permission.data !== "granted") {
|
||||
glstate.requestPermission();
|
||||
return;
|
||||
glstate.requestPermission()
|
||||
return
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -69,22 +70,29 @@
|
|||
</button>
|
||||
<!-- No geolocation granted - we don't show the button -->
|
||||
{:else if $geopermission === "requested"}
|
||||
<button class="flex w-full items-center gap-x-2 disabled" on:click={jumpToCurrentLocation}>
|
||||
<button class="disabled flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
|
||||
<!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
|
||||
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetClass("animate-spin")} />
|
||||
<ToSvelte
|
||||
construct={Svg.crosshair_svg()
|
||||
.SetClass("w-8 h-8")
|
||||
.SetStyle("animation: 3s linear 0s infinite normal none running spin;")}
|
||||
/>
|
||||
<Tr t={Translations.t.general.waitingForGeopermission} />
|
||||
</button>
|
||||
{:else if $geopermission === "denied"}
|
||||
<button class="flex w-full items-center gap-x-2 disabled">
|
||||
<button class="disabled flex w-full items-center gap-x-2">
|
||||
<ToSvelte construct={Svg.location_refused_svg().SetClass("w-8 h-8")} />
|
||||
<Tr t={Translations.t.general.geopermissionDenied} />
|
||||
</button>
|
||||
{:else }
|
||||
<button class="flex w-full items-center gap-x-2 disabled">
|
||||
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetClass("animate-spin")} />
|
||||
{:else}
|
||||
<button class="disabled flex w-full items-center gap-x-2">
|
||||
<ToSvelte
|
||||
construct={Svg.crosshair_svg()
|
||||
.SetClass("w-8 h-8")
|
||||
.SetStyle("animation: 3s linear 0s infinite normal none running spin;")}
|
||||
/>
|
||||
<Tr t={Translations.t.general.waitingForLocation} />
|
||||
</button>
|
||||
|
||||
{/if}
|
||||
|
||||
<div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2">
|
||||
|
|
|
@ -1,63 +1,63 @@
|
|||
<script lang="ts">/**
|
||||
* Shows an 'upload'-button which will start the upload for this feature
|
||||
*/
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Shows an 'upload'-button which will start the upload for this feature
|
||||
*/
|
||||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import UploadingImageCounter from "./UploadingImageCounter.svelte";
|
||||
import FileSelector from "../Base/FileSelector.svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import UploadingImageCounter from "./UploadingImageCounter.svelte"
|
||||
import FileSelector from "../Base/FileSelector.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
export let tags: Store<OsmTags>;
|
||||
/**
|
||||
* Image to show in the button
|
||||
* NOT the image to upload!
|
||||
*/
|
||||
export let image: string = undefined;
|
||||
if (image === "") {
|
||||
image = undefined;
|
||||
}
|
||||
export let labelText: string = undefined;
|
||||
const t = Translations.t.image;
|
||||
export let tags: Store<OsmTags>
|
||||
/**
|
||||
* Image to show in the button
|
||||
* NOT the image to upload!
|
||||
*/
|
||||
export let image: string = undefined
|
||||
if (image === "") {
|
||||
image = undefined
|
||||
}
|
||||
export let labelText: string = undefined
|
||||
const t = Translations.t.image
|
||||
|
||||
let licenseStore = state.userRelatedState.imageLicense;
|
||||
let licenseStore = state.userRelatedState.imageLicense
|
||||
|
||||
function handleFiles(files: FileList) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files.item(i);
|
||||
console.log("Got file", file.name)
|
||||
try {
|
||||
state.imageUploadManager.uploadImageAndApply(file, tags);
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
function handleFiles(files: FileList) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files.item(i)
|
||||
console.log("Got file", file.name)
|
||||
try {
|
||||
state.imageUploadManager.uploadImageAndApply(file, tags)
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<LoginToggle {state}>
|
||||
|
||||
<Tr slot="not-logged-in" t={t.pleaseLogin} />
|
||||
<div class="flex flex-col">
|
||||
|
||||
<UploadingImageCounter {state} {tags} />
|
||||
<FileSelector accept="image/*" cls="button border-2 text-2xl" multiple={true}
|
||||
on:submit={e => handleFiles(e.detail)}>
|
||||
<FileSelector
|
||||
accept="image/*"
|
||||
cls="button border-2 text-2xl"
|
||||
multiple={true}
|
||||
on:submit={(e) => handleFiles(e.detail)}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
|
||||
{#if image !== undefined}
|
||||
<img src={image} />
|
||||
{:else}
|
||||
<ToSvelte construct={ Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl ")} />
|
||||
<ToSvelte construct={Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl ")} />
|
||||
{/if}
|
||||
{#if labelText}
|
||||
{labelText}
|
||||
|
@ -68,10 +68,14 @@ function handleFiles(files: FileList) {
|
|||
</FileSelector>
|
||||
<div class="text-sm">
|
||||
<Tr t={t.respectPrivacy} />
|
||||
<a class="cursor-pointer" on:click={() => {state.guistate.openUsersettings("picture-license")}}>
|
||||
<Tr t={t.currentLicense.Subs({license: $licenseStore})} />
|
||||
<a
|
||||
class="cursor-pointer"
|
||||
on:click={() => {
|
||||
state.guistate.openUsersettings("picture-license")
|
||||
}}
|
||||
>
|
||||
<Tr t={t.currentLicense.Subs({ license: $licenseStore })} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</LoginToggle>
|
||||
|
|
|
@ -1,32 +1,28 @@
|
|||
<script lang="ts">/**
|
||||
* Shows information about how much images are uploaded for the given feature
|
||||
*/
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Shows information about how much images are uploaded for the given feature
|
||||
*/
|
||||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
export let tags: Store<OsmTags>;
|
||||
const featureId = tags.data.id;
|
||||
const {
|
||||
uploadStarted,
|
||||
uploadFinished,
|
||||
retried,
|
||||
failed
|
||||
} = state.imageUploadManager.getCountsFor(featureId);
|
||||
const t = Translations.t.image;
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: Store<OsmTags>
|
||||
const featureId = tags.data.id
|
||||
const { uploadStarted, uploadFinished, retried, failed } =
|
||||
state.imageUploadManager.getCountsFor(featureId)
|
||||
const t = Translations.t.image
|
||||
</script>
|
||||
|
||||
{#if $uploadStarted == 1}
|
||||
{#if $uploadFinished == 1 }
|
||||
{#if $uploadFinished == 1}
|
||||
<Tr cls="thanks" t={t.upload.one.done} />
|
||||
{:else if $failed == 1}
|
||||
<div class="flex flex-col alert">
|
||||
<div class="alert flex flex-col">
|
||||
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||
<Tr t={t.upload.failReasons} />
|
||||
<Tr t={t.upload.failReasonsAdvanced} />
|
||||
|
@ -35,30 +31,34 @@ const t = Translations.t.image;
|
|||
<Loading cls="alert">
|
||||
<Tr t={t.upload.one.retrying} />
|
||||
</Loading>
|
||||
{:else }
|
||||
{:else}
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.one.uploading} />
|
||||
</Loading>
|
||||
{/if}
|
||||
{:else if $uploadStarted > 1}
|
||||
{#if ($uploadFinished + $failed) == $uploadStarted && $uploadFinished > 0}
|
||||
<Tr cls="thanks" t={t.upload.multiple.done.Subs({count: $uploadFinished})} />
|
||||
{#if $uploadFinished + $failed == $uploadStarted && $uploadFinished > 0}
|
||||
<Tr cls="thanks" t={t.upload.multiple.done.Subs({ count: $uploadFinished })} />
|
||||
{:else if $uploadFinished == 0}
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.multiple.uploading.Subs({count: $uploadStarted})} />
|
||||
<Tr t={t.upload.multiple.uploading.Subs({ count: $uploadStarted })} />
|
||||
</Loading>
|
||||
{:else if $uploadFinished > 0}
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.multiple.partiallyDone.Subs({count: $uploadStarted - $uploadFinished, done: $uploadFinished})} />
|
||||
<Tr
|
||||
t={t.upload.multiple.partiallyDone.Subs({
|
||||
count: $uploadStarted - $uploadFinished,
|
||||
done: $uploadFinished,
|
||||
})}
|
||||
/>
|
||||
</Loading>
|
||||
{/if}
|
||||
{#if $failed > 0}
|
||||
<div class="flex flex-col alert">
|
||||
<div class="alert flex flex-col">
|
||||
{#if failed === 1}
|
||||
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||
{:else}
|
||||
<Tr cls="self-center" t={t.upload.multiple.someFailed.Subs({count: $failed})} />
|
||||
|
||||
<Tr cls="self-center" t={t.upload.multiple.someFailed.Subs({ count: $failed })} />
|
||||
{/if}
|
||||
<Tr t={t.upload.failReasons} />
|
||||
<Tr t={t.upload.failReasonsAdvanced} />
|
||||
|
|
|
@ -432,7 +432,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
)
|
||||
}
|
||||
await this.awaitStyleIsLoaded()
|
||||
if(this._currentRasterLayer !== background?.id){
|
||||
if (this._currentRasterLayer !== background?.id) {
|
||||
this.removeCurrentLayer(map)
|
||||
}
|
||||
this._currentRasterLayer = background?.id
|
||||
|
@ -459,10 +459,10 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
map.rotateTo(0, { duration: 0 })
|
||||
map.setPitch(0)
|
||||
map.dragRotate.disable()
|
||||
map.touchZoomRotate.disableRotation();
|
||||
map.touchZoomRotate.disableRotation()
|
||||
} else {
|
||||
map.dragRotate.enable()
|
||||
map.touchZoomRotate.enableRotation();
|
||||
map.touchZoomRotate.enableRotation()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,76 +1,79 @@
|
|||
<script lang="ts">
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import PlantNetSpeciesList from "./PlantNetSpeciesList.svelte";
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet";
|
||||
import PlantNet from "../../Logic/Web/PlantNet";
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid";
|
||||
import BackButton from "../Base/BackButton.svelte";
|
||||
import NextButton from "../Base/NextButton.svelte";
|
||||
import WikipediaPanel from "../Wikipedia/WikipediaPanel.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import PlantNetSpeciesList from "./PlantNetSpeciesList.svelte"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet"
|
||||
import PlantNet from "../../Logic/Web/PlantNet"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import BackButton from "../Base/BackButton.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import WikipediaPanel from "../Wikipedia/WikipediaPanel.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
|
||||
/**
|
||||
* The main entry point for the plantnet wizard
|
||||
*/
|
||||
const t = Translations.t.plantDetection;
|
||||
|
||||
const t = Translations.t.plantDetection
|
||||
|
||||
/**
|
||||
* All the URLs pointing to images of the selected feature.
|
||||
* We need to feed them into Plantnet when applicable
|
||||
*/
|
||||
export let imageUrls: Store<string[]>;
|
||||
export let onConfirm: (wikidataId: string) => void;
|
||||
const dispatch = createEventDispatcher<{ selected: string }>();
|
||||
let collapsedMode = true;
|
||||
let options: UIEventSource<PlantNetSpeciesMatch[]> = new UIEventSource<PlantNetSpeciesMatch[]>(undefined);
|
||||
export let imageUrls: Store<string[]>
|
||||
export let onConfirm: (wikidataId: string) => void
|
||||
const dispatch = createEventDispatcher<{ selected: string }>()
|
||||
let collapsedMode = true
|
||||
let options: UIEventSource<PlantNetSpeciesMatch[]> = new UIEventSource<PlantNetSpeciesMatch[]>(
|
||||
undefined
|
||||
)
|
||||
|
||||
let error: string = undefined;
|
||||
let error: string = undefined
|
||||
|
||||
/**
|
||||
* The Wikidata-id of the species to apply
|
||||
*/
|
||||
let selectedOption: string;
|
||||
let selectedOption: string
|
||||
|
||||
let done = false;
|
||||
let done = false
|
||||
|
||||
function speciesSelected(species: PlantNetSpeciesMatch) {
|
||||
console.log("Selected:", species);
|
||||
selectedOption = species;
|
||||
console.log("Selected:", species)
|
||||
selectedOption = species
|
||||
}
|
||||
|
||||
async function detectSpecies() {
|
||||
collapsedMode = false;
|
||||
collapsedMode = false
|
||||
|
||||
try {
|
||||
|
||||
const result = await PlantNet.query(imageUrls.data.slice(0, 5));
|
||||
options.set(result.results.filter(r => r.score > 0.005).slice(0, 8));
|
||||
const result = await PlantNet.query(imageUrls.data.slice(0, 5))
|
||||
options.set(result.results.filter((r) => r.score > 0.005).slice(0, 8))
|
||||
} catch (e) {
|
||||
error = e;
|
||||
error = e
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
|
||||
{#if collapsedMode}
|
||||
<button class="w-full" on:click={detectSpecies}>
|
||||
<Tr t={t.button} />
|
||||
</button>
|
||||
{:else if $error !== undefined}
|
||||
<Tr cls="alert" t={t.error.Subs({error})} />
|
||||
<Tr cls="alert" t={t.error.Subs({ error })} />
|
||||
{:else if $imageUrls.length === 0}
|
||||
<!-- No urls are available, show the explanation instead-->
|
||||
<div class=" border-region p-2 mb-1 relative">
|
||||
<XCircleIcon class="absolute top-0 right-0 w-8 h-8 m-4 cursor-pointer"
|
||||
on:click={() => {collapsedMode = true}}></XCircleIcon>
|
||||
<div class=" border-region relative mb-1 p-2">
|
||||
<XCircleIcon
|
||||
class="absolute top-0 right-0 m-4 h-8 w-8 cursor-pointer"
|
||||
on:click={() => {
|
||||
collapsedMode = true
|
||||
}}
|
||||
/>
|
||||
<Tr t={t.takeImages} />
|
||||
<Tr t={ t.howTo.intro} />
|
||||
<Tr t={t.howTo.intro} />
|
||||
<ul>
|
||||
<li>
|
||||
<Tr t={t.howTo.li0} />
|
||||
|
@ -87,23 +90,39 @@
|
|||
</ul>
|
||||
</div>
|
||||
{:else if selectedOption === undefined}
|
||||
<PlantNetSpeciesList {options} numberOfImages={$imageUrls.length}
|
||||
on:selected={(species) => speciesSelected(species.detail)}>
|
||||
<XCircleIcon slot="upper-right" class="w-8 h-8 m-4 cursor-pointer"
|
||||
on:click={() => {collapsedMode = true}}></XCircleIcon>
|
||||
|
||||
<PlantNetSpeciesList
|
||||
{options}
|
||||
numberOfImages={$imageUrls.length}
|
||||
on:selected={(species) => speciesSelected(species.detail)}
|
||||
>
|
||||
<XCircleIcon
|
||||
slot="upper-right"
|
||||
class="m-4 h-8 w-8 cursor-pointer"
|
||||
on:click={() => {
|
||||
collapsedMode = true
|
||||
}}
|
||||
/>
|
||||
</PlantNetSpeciesList>
|
||||
{:else if !done}
|
||||
<div class="flex flex-col border-interactive">
|
||||
<div class="border-interactive flex flex-col">
|
||||
<div class="m-2">
|
||||
|
||||
<WikipediaPanel wikiIds={new ImmutableStore([selectedOption])} />
|
||||
</div>
|
||||
<div class="flex flex-col items-stretch">
|
||||
<BackButton on:click={() => {selectedOption = undefined}}>
|
||||
<BackButton
|
||||
on:click={() => {
|
||||
selectedOption = undefined
|
||||
}}
|
||||
>
|
||||
<Tr t={t.back} />
|
||||
</BackButton>
|
||||
<NextButton clss="primary" on:click={() => { done = true; onConfirm(selectedOption); }} >
|
||||
<NextButton
|
||||
clss="primary"
|
||||
on:click={() => {
|
||||
done = true
|
||||
onConfirm(selectedOption)
|
||||
}}
|
||||
>
|
||||
<Tr t={t.confirm} />
|
||||
</NextButton>
|
||||
</div>
|
||||
|
@ -111,13 +130,21 @@
|
|||
{:else}
|
||||
<!-- done ! -->
|
||||
<Tr t={t.done} cls="thanks w-full" />
|
||||
<BackButton imageClass="w-6 h-6 shrink-0" clss="p-1 m-0" on:click={() => {done = false; selectedOption = undefined}}>
|
||||
<BackButton
|
||||
imageClass="w-6 h-6 shrink-0"
|
||||
clss="p-1 m-0"
|
||||
on:click={() => {
|
||||
done = false
|
||||
selectedOption = undefined
|
||||
}}
|
||||
>
|
||||
<Tr t={t.tryAgain} />
|
||||
</BackButton>
|
||||
{/if}
|
||||
<div class="flex p-2 low-interaction rounded-xl self-end">
|
||||
<ToSvelte construct={Svg.plantnet_logo_svg().SetClass("w-8 h-8 p-1 mr-1 bg-white rounded-full")} />
|
||||
<div class="low-interaction flex self-end rounded-xl p-2">
|
||||
<ToSvelte
|
||||
construct={Svg.plantnet_logo_svg().SetClass("w-8 h-8 p-1 mr-1 bg-white rounded-full")}
|
||||
/>
|
||||
<Tr t={t.poweredByPlantnet} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,29 +1,28 @@
|
|||
<script lang="ts">/**
|
||||
* Show the list of options to choose from
|
||||
*/
|
||||
import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
import SpeciesButton from "./SpeciesButton.svelte";
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Show the list of options to choose from
|
||||
*/
|
||||
import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import SpeciesButton from "./SpeciesButton.svelte"
|
||||
|
||||
const t = Translations.t.plantDetection;
|
||||
|
||||
export let options: Store<PlantNetSpeciesMatch[]>;
|
||||
export let numberOfImages: number;
|
||||
const t = Translations.t.plantDetection
|
||||
|
||||
export let options: Store<PlantNetSpeciesMatch[]>
|
||||
export let numberOfImages: number
|
||||
</script>
|
||||
|
||||
{#if $options === undefined}
|
||||
<Loading>
|
||||
<Tr t={t.querying.Subs({length: numberOfImages})} />
|
||||
<Tr t={t.querying.Subs({ length: numberOfImages })} />
|
||||
</Loading>
|
||||
{:else}
|
||||
<div class="low-interaction border-interactive flex p-2 flex-col relative">
|
||||
<div class="absolute top-0 right-0" >
|
||||
|
||||
<slot name="upper-right"/>
|
||||
<div class="low-interaction border-interactive relative flex flex-col p-2">
|
||||
<div class="absolute top-0 right-0">
|
||||
<slot name="upper-right" />
|
||||
</div>
|
||||
<h3>
|
||||
<Tr t={t.overviewTitle} />
|
||||
|
@ -31,7 +30,7 @@ export let numberOfImages: number;
|
|||
<Tr t={t.overviewIntro} />
|
||||
<Tr cls="font-bold" t={t.overviewVerify} />
|
||||
{#each $options as species}
|
||||
<SpeciesButton {species} on:selected/>
|
||||
{/each}
|
||||
<SpeciesButton {species} on:selected />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,54 +1,61 @@
|
|||
<script lang="ts">/**
|
||||
* A button to select a single species
|
||||
*/
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import Wikidata from "../../Logic/Web/Wikidata";
|
||||
import NextButton from "../Base/NextButton.svelte";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
<script lang="ts">
|
||||
/**
|
||||
* A button to select a single species
|
||||
*/
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Wikidata from "../../Logic/Web/Wikidata"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
|
||||
export let species: PlantNetSpeciesMatch;
|
||||
let wikidata = UIEventSource.FromPromise(
|
||||
Wikidata.Sparql<{ species }>(
|
||||
["?species", "?speciesLabel"],
|
||||
["?species wdt:P846 \"" + species.gbif.id + "\""]
|
||||
export let species: PlantNetSpeciesMatch
|
||||
let wikidata = UIEventSource.FromPromise(
|
||||
Wikidata.Sparql<{ species }>(
|
||||
["?species", "?speciesLabel"],
|
||||
['?species wdt:P846 "' + species.gbif.id + '"']
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const dispatch = createEventDispatcher<{ selected: string /* wikidata-id*/ }>();
|
||||
const t = Translations.t.plantDetection;
|
||||
const dispatch = createEventDispatcher<{ selected: string /* wikidata-id*/ }>()
|
||||
const t = Translations.t.plantDetection
|
||||
|
||||
|
||||
/**
|
||||
* PlantNet give us a GBIF-id, but we want the Wikidata-id instead.
|
||||
* We look this up in wikidata
|
||||
*/
|
||||
const wikidataId: Store<string> = UIEventSource.FromPromise(
|
||||
Wikidata.Sparql<{ species }>(
|
||||
["?species", "?speciesLabel"],
|
||||
["?species wdt:P846 \"" + species.gbif.id + "\""]
|
||||
)
|
||||
).mapD(wd => wd[0]?.species?.value);
|
||||
/**
|
||||
* PlantNet give us a GBIF-id, but we want the Wikidata-id instead.
|
||||
* We look this up in wikidata
|
||||
*/
|
||||
const wikidataId: Store<string> = UIEventSource.FromPromise(
|
||||
Wikidata.Sparql<{ species }>(
|
||||
["?species", "?speciesLabel"],
|
||||
['?species wdt:P846 "' + species.gbif.id + '"']
|
||||
)
|
||||
).mapD((wd) => wd[0]?.species?.value)
|
||||
</script>
|
||||
|
||||
<NextButton on:click={() => dispatch("selected", $wikidataId)}>
|
||||
{#if $wikidata === undefined}
|
||||
<Loading>
|
||||
<Tr t={ t.loadingWikidata.Subs({
|
||||
species: species.species.scientificNameWithoutAuthor,
|
||||
})} />
|
||||
<Tr
|
||||
t={t.loadingWikidata.Subs({
|
||||
species: species.species.scientificNameWithoutAuthor,
|
||||
})}
|
||||
/>
|
||||
</Loading>
|
||||
{:else}
|
||||
<ToSvelte construct={() => new WikidataPreviewBox(wikidataId,
|
||||
{ imageStyle: "max-width: 8rem; width: unset; height: 8rem",
|
||||
extraItems: [t.matchPercentage
|
||||
.Subs({ match: Math.round(species.score * 100) })
|
||||
.SetClass("thanks w-fit self-center")]
|
||||
}).SetClass("w-full")}></ToSvelte>
|
||||
<ToSvelte
|
||||
construct={() =>
|
||||
new WikidataPreviewBox(wikidataId, {
|
||||
imageStyle: "max-width: 8rem; width: unset; height: 8rem",
|
||||
extraItems: [
|
||||
t.matchPercentage
|
||||
.Subs({ match: Math.round(species.score * 100) })
|
||||
.SetClass("thanks w-fit self-center"),
|
||||
],
|
||||
}).SetClass("w-full")}
|
||||
/>
|
||||
{/if}
|
||||
</NextButton>
|
||||
|
|
|
@ -3,109 +3,109 @@
|
|||
* This component ties together all the steps that are needed to create a new point.
|
||||
* There are many subcomponents which help with that
|
||||
*/
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import PresetList from "./PresetList.svelte";
|
||||
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import SubtleButton from "../../Base/SubtleButton.svelte";
|
||||
import FromHtml from "../../Base/FromHtml.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import TagHint from "../TagHint.svelte";
|
||||
import { And } from "../../../Logic/Tags/And.js";
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte";
|
||||
import Constants from "../../../Models/Constants.js";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import LoginButton from "../../Base/LoginButton.svelte";
|
||||
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte";
|
||||
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||
import { OsmWay } from "../../../Logic/Osm/OsmObject";
|
||||
import { Tag } from "../../../Logic/Tags/Tag";
|
||||
import type { WayId } from "../../../Models/OsmFeature";
|
||||
import Loading from "../../Base/Loading.svelte";
|
||||
import type { GlobalFilter } from "../../../Models/GlobalFilter";
|
||||
import { onDestroy } from "svelte";
|
||||
import NextButton from "../../Base/NextButton.svelte";
|
||||
import BackButton from "../../Base/BackButton.svelte";
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||
import Svg from "../../../Svg";
|
||||
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte";
|
||||
import { twJoin } from "tailwind-merge";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import PresetList from "./PresetList.svelte"
|
||||
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import SubtleButton from "../../Base/SubtleButton.svelte"
|
||||
import FromHtml from "../../Base/FromHtml.svelte"
|
||||
import Translations from "../../i18n/Translations.js"
|
||||
import TagHint from "../TagHint.svelte"
|
||||
import { And } from "../../../Logic/Tags/And.js"
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte"
|
||||
import Constants from "../../../Models/Constants.js"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import LoginButton from "../../Base/LoginButton.svelte"
|
||||
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"
|
||||
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
|
||||
import { OsmWay } from "../../../Logic/Osm/OsmObject"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
import type { WayId } from "../../../Models/OsmFeature"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import type { GlobalFilter } from "../../../Models/GlobalFilter"
|
||||
import { onDestroy } from "svelte"
|
||||
import NextButton from "../../Base/NextButton.svelte"
|
||||
import BackButton from "../../Base/BackButton.svelte"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import Svg from "../../../Svg"
|
||||
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
|
||||
export let coordinate: { lon: number; lat: number };
|
||||
export let state: SpecialVisualizationState;
|
||||
export let coordinate: { lon: number; lat: number }
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
let selectedPreset: {
|
||||
preset: PresetConfig
|
||||
layer: LayerConfig
|
||||
icon: string
|
||||
tags: Record<string, string>
|
||||
} = undefined;
|
||||
let checkedOfGlobalFilters: number = 0;
|
||||
let confirmedCategory = false;
|
||||
} = undefined
|
||||
let checkedOfGlobalFilters: number = 0
|
||||
let confirmedCategory = false
|
||||
$: if (selectedPreset === undefined) {
|
||||
confirmedCategory = false;
|
||||
creating = false;
|
||||
checkedOfGlobalFilters = 0;
|
||||
confirmedCategory = false
|
||||
creating = false
|
||||
checkedOfGlobalFilters = 0
|
||||
}
|
||||
|
||||
let flayer: FilteredLayer = undefined;
|
||||
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined;
|
||||
let layerHasFilters: Store<boolean> | undefined = undefined;
|
||||
let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters;
|
||||
let _globalFilter: GlobalFilter[] = [];
|
||||
let flayer: FilteredLayer = undefined
|
||||
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined
|
||||
let layerHasFilters: Store<boolean> | undefined = undefined
|
||||
let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters
|
||||
let _globalFilter: GlobalFilter[] = []
|
||||
onDestroy(
|
||||
globalFilter.addCallbackAndRun((globalFilter) => {
|
||||
console.log("Global filters are", globalFilter);
|
||||
_globalFilter = globalFilter ?? [];
|
||||
console.log("Global filters are", globalFilter)
|
||||
_globalFilter = globalFilter ?? []
|
||||
})
|
||||
);
|
||||
)
|
||||
$: {
|
||||
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id);
|
||||
layerIsDisplayed = flayer?.isDisplayed;
|
||||
layerHasFilters = flayer?.hasFilter;
|
||||
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id)
|
||||
layerIsDisplayed = flayer?.isDisplayed
|
||||
layerHasFilters = flayer?.hasFilter
|
||||
}
|
||||
const t = Translations.t.general.add;
|
||||
const t = Translations.t.general.add
|
||||
|
||||
const zoom = state.mapProperties.zoom;
|
||||
const zoom = state.mapProperties.zoom
|
||||
|
||||
const isLoading = state.dataIsLoading;
|
||||
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined);
|
||||
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined);
|
||||
const isLoading = state.dataIsLoading
|
||||
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
|
||||
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
|
||||
// Small helper variable: if the map is tapped, we should let the 'Next'-button grab some attention as users have to click _that_ to continue, not the map
|
||||
let preciseInputIsTapped = false;
|
||||
let preciseInputIsTapped = false
|
||||
|
||||
let creating = false;
|
||||
let creating = false
|
||||
|
||||
/**
|
||||
* Call when the user should restart the flow by clicking on the map, e.g. because they disabled filters.
|
||||
* Will delete the lastclick-location
|
||||
*/
|
||||
function abort() {
|
||||
state.selectedElement.setData(undefined);
|
||||
state.selectedElement.setData(undefined)
|
||||
// When aborted, we force the contributors to place the pin _again_
|
||||
// This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map
|
||||
state.lastClickObject.features.setData([]);
|
||||
preciseInputIsTapped = false;
|
||||
state.lastClickObject.features.setData([])
|
||||
preciseInputIsTapped = false
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
creating = true;
|
||||
const location: { lon: number; lat: number } = preciseCoordinate.data;
|
||||
const snapTo: WayId | undefined = <WayId>snappedToObject.data;
|
||||
creating = true
|
||||
const location: { lon: number; lat: number } = preciseCoordinate.data
|
||||
const snapTo: WayId | undefined = <WayId>snappedToObject.data
|
||||
const tags: Tag[] = selectedPreset.preset.tags.concat(
|
||||
..._globalFilter.map((f) => f?.onNewPoint?.tags ?? [])
|
||||
);
|
||||
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags);
|
||||
)
|
||||
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags)
|
||||
|
||||
let snapToWay: undefined | OsmWay = undefined;
|
||||
if (snapTo !== undefined) {
|
||||
const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0);
|
||||
let snapToWay: undefined | OsmWay = undefined
|
||||
if (snapTo !== undefined && snapTo !== null) {
|
||||
const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0)
|
||||
if (downloaded !== "deleted") {
|
||||
snapToWay = downloaded;
|
||||
snapToWay = downloaded
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,42 +113,44 @@
|
|||
theme: state.layout?.id ?? "unkown",
|
||||
changeType: "create",
|
||||
snapOnto: snapToWay,
|
||||
reusePointWithinMeters: 1
|
||||
});
|
||||
await state.changes.applyAction(newElementAction);
|
||||
state.newFeatures.features.ping();
|
||||
reusePointWithinMeters: 1,
|
||||
})
|
||||
await state.changes.applyAction(newElementAction)
|
||||
state.newFeatures.features.ping()
|
||||
// The 'changes' should have created a new point, which added this into the 'featureProperties'
|
||||
const newId = newElementAction.newElementId;
|
||||
console.log("Applied pending changes, fetching store for", newId);
|
||||
const tagsStore = state.featureProperties.getStore(newId);
|
||||
const newId = newElementAction.newElementId
|
||||
console.log("Applied pending changes, fetching store for", newId)
|
||||
const tagsStore = state.featureProperties.getStore(newId)
|
||||
if (!tagsStore) {
|
||||
console.error("Bug: no tagsStore found for", newId);
|
||||
console.error("Bug: no tagsStore found for", newId)
|
||||
}
|
||||
{
|
||||
// Set some metainfo
|
||||
const properties = tagsStore.data;
|
||||
const properties = tagsStore.data
|
||||
if (snapTo) {
|
||||
// metatags (starting with underscore) are not uploaded, so we can safely mark this
|
||||
delete properties["_referencing_ways"];
|
||||
properties["_referencing_ways"] = `["${snapTo}"]`;
|
||||
delete properties["_referencing_ways"]
|
||||
properties["_referencing_ways"] = `["${snapTo}"]`
|
||||
}
|
||||
properties["_backend"] = state.osmConnection.Backend();
|
||||
properties["_last_edit:timestamp"] = new Date().toISOString();
|
||||
const userdetails = state.osmConnection.userDetails.data;
|
||||
properties["_last_edit:contributor"] = userdetails.name;
|
||||
properties["_last_edit:uid"] = "" + userdetails.uid;
|
||||
tagsStore.ping();
|
||||
properties["_backend"] = state.osmConnection.Backend()
|
||||
properties["_last_edit:timestamp"] = new Date().toISOString()
|
||||
const userdetails = state.osmConnection.userDetails.data
|
||||
properties["_last_edit:contributor"] = userdetails.name
|
||||
properties["_last_edit:uid"] = "" + userdetails.uid
|
||||
tagsStore.ping()
|
||||
}
|
||||
const feature = state.indexedFeatures.featuresById.data.get(newId);
|
||||
console.log("Selecting feature", feature, "and opening their popup");
|
||||
abort();
|
||||
state.selectedLayer.setData(selectedPreset.layer);
|
||||
state.selectedElement.setData(feature);
|
||||
tagsStore.ping();
|
||||
const feature = state.indexedFeatures.featuresById.data.get(newId)
|
||||
console.log("Selecting feature", feature, "and opening their popup")
|
||||
abort()
|
||||
state.selectedLayer.setData(selectedPreset.layer)
|
||||
state.selectedElement.setData(feature)
|
||||
tagsStore.ping()
|
||||
}
|
||||
|
||||
function confirmSync() {
|
||||
confirm().then(_ => console.debug("New point successfully handled")).catch(e => console.error("Handling the new point went wrong due to", e));
|
||||
confirm()
|
||||
.then((_) => console.debug("New point successfully handled"))
|
||||
.catch((e) => console.error("Handling the new point went wrong due to", e))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
state.newFeatures.features.data.push(feature)
|
||||
state.newFeatures.features.ping()
|
||||
state.selectedElement?.setData(feature)
|
||||
if(state.featureProperties.trackFeature){
|
||||
if (state.featureProperties.trackFeature) {
|
||||
state.featureProperties.trackFeature(feature)
|
||||
}
|
||||
comment.setData("")
|
||||
|
|
|
@ -23,18 +23,20 @@
|
|||
export let feature: Feature
|
||||
export let layer: LayerConfig
|
||||
|
||||
export let linkable = true;
|
||||
let isLinked = Object.values(tags.data).some(v => image.pictureUrl === v);
|
||||
export let linkable = true
|
||||
let isLinked = Object.values(tags.data).some((v) => image.pictureUrl === v)
|
||||
|
||||
const t = Translations.t.image.nearby;
|
||||
const c = [lon, lat];
|
||||
const t = Translations.t.image.nearby
|
||||
const c = [lon, lat]
|
||||
let attributedImage = new AttributedImage({
|
||||
url: image.thumbUrl ?? image.pictureUrl,
|
||||
provider: AllImageProviders.byName(image.provider),
|
||||
date: new Date(image.date)
|
||||
});
|
||||
let distance = Math.round(GeoOperations.distanceBetween([image.coordinates.lng, image.coordinates.lat], c));
|
||||
|
||||
date: new Date(image.date),
|
||||
})
|
||||
let distance = Math.round(
|
||||
GeoOperations.distanceBetween([image.coordinates.lng, image.coordinates.lat], c)
|
||||
)
|
||||
|
||||
$: {
|
||||
const currentTags = tags.data
|
||||
const key = Object.keys(image.osmTags)[0]
|
||||
|
|
46
src/UI/Reviews/AllReviews.svelte
Normal file
46
src/UI/Reviews/AllReviews.svelte
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
||||
import SingleReview from "./SingleReview.svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import ReviewForm from "./ReviewForm.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
|
||||
/**
|
||||
* An element showing all reviews
|
||||
*/
|
||||
export let reviews: FeatureReviews
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig
|
||||
let average = reviews.average
|
||||
let _reviews = []
|
||||
reviews.reviews.addCallbackAndRunD((r) => {
|
||||
_reviews = Utils.NoNull(r)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="border-2 border-dashed border-gray-300">
|
||||
{#if _reviews.length > 1}
|
||||
<StarsBar score={$average} />
|
||||
{/if}
|
||||
{#if _reviews.length > 0}
|
||||
{#each _reviews as review}
|
||||
<SingleReview {review} />
|
||||
{/each}
|
||||
{:else}
|
||||
<Tr t={Translations.t.reviews.no_reviews_yet} />
|
||||
{/if}
|
||||
<div class="flex justify-end">
|
||||
<ToSvelte construct={Svg.mangrove_logo_svg().SetClass("w-12 h-12")} />
|
||||
<Tr t={Translations.t.reviews.attribution} />
|
||||
</div>
|
||||
</div>
|
|
@ -1,56 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import SingleReview from "./SingleReview"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Img from "../Base/Img"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Link from "../Base/Link"
|
||||
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
||||
|
||||
/**
|
||||
* Shows the reviews and scoring base on mangrove.reviews
|
||||
* The middle element is some other component shown in the middle, e.g. the review input element
|
||||
*/
|
||||
export default class ReviewElement extends VariableUiElement {
|
||||
constructor(reviews: FeatureReviews, middleElement: BaseUIElement) {
|
||||
super(
|
||||
reviews.reviews.map(
|
||||
(revs) => {
|
||||
const elements = []
|
||||
revs.sort((a, b) => b.iat - a.iat) // Sort with most recent first
|
||||
const avg =
|
||||
revs.map((review) => review.rating).reduce((a, b) => a + b, 0) / revs.length
|
||||
elements.push(
|
||||
new Combine([
|
||||
SingleReview.GenStars(avg),
|
||||
new Link(
|
||||
revs.length === 1
|
||||
? Translations.t.reviews.title_singular.Clone()
|
||||
: Translations.t.reviews.title.Subs({
|
||||
count: "" + revs.length,
|
||||
}),
|
||||
`https://mangrove.reviews/search?sub=${encodeURIComponent(
|
||||
reviews.subjectUri.data
|
||||
)}`,
|
||||
true
|
||||
),
|
||||
]).SetClass("font-2xl flex justify-between items-center pl-2 pr-2")
|
||||
)
|
||||
|
||||
elements.push(middleElement)
|
||||
|
||||
elements.push(...revs.map((review) => new SingleReview(review)))
|
||||
elements.push(
|
||||
new Combine([
|
||||
Translations.t.reviews.attribution.Clone(),
|
||||
new Img("./assets/mangrove_logo.png"),
|
||||
]).SetClass("review-attribution")
|
||||
)
|
||||
|
||||
return new Combine(elements).SetClass("block")
|
||||
},
|
||||
[reviews.subjectUri]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
106
src/UI/Reviews/ReviewForm.svelte
Normal file
106
src/UI/Reviews/ReviewForm.svelte
Normal file
|
@ -0,0 +1,106 @@
|
|||
<script lang="ts">
|
||||
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Checkbox from "../Base/Checkbox.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import If from "../Base/If.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig
|
||||
/**
|
||||
* The form to create a new review.
|
||||
* This is multi-stepped.
|
||||
*/
|
||||
export let reviews: FeatureReviews
|
||||
|
||||
let score = 0
|
||||
let confirmedScore = undefined
|
||||
let isAffiliated = new UIEventSource(false)
|
||||
let opinion = new UIEventSource<string>(undefined)
|
||||
|
||||
const t = Translations.t.reviews
|
||||
|
||||
let _state: "ask" | "saving" | "done" = "ask"
|
||||
|
||||
const connection = state.osmConnection
|
||||
|
||||
async function save() {
|
||||
_state = "saving"
|
||||
let nickname = undefined
|
||||
if (connection.isLoggedIn.data) {
|
||||
nickname = connection.userDetails.data.name
|
||||
}
|
||||
const review: Omit<Review, "sub"> = {
|
||||
rating: confirmedScore,
|
||||
opinion: opinion.data,
|
||||
metadata: { nickname, is_affiliated: isAffiliated.data },
|
||||
}
|
||||
if (state.featureSwitchIsTesting.data) {
|
||||
console.log("Testing - not actually saving review", review)
|
||||
await Utils.waitFor(1000)
|
||||
} else {
|
||||
await reviews.createReview(review)
|
||||
}
|
||||
_state = "done"
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if _state === "done"}
|
||||
<Tr cls="thanks w-full" t={t.saved} />
|
||||
{:else if _state === "saving"}
|
||||
<Loading>
|
||||
<Tr t={t.saving_review} />
|
||||
</Loading>
|
||||
{:else}
|
||||
<div class="interactive border-interactive p-1">
|
||||
<div class="font-bold">
|
||||
<SpecialTranslation {feature} {layer} {state} t={Translations.t.reviews.question} {tags} />
|
||||
</div>
|
||||
<StarsBar
|
||||
on:click={(e) => {
|
||||
confirmedScore = e.detail.score
|
||||
}}
|
||||
on:hover={(e) => {
|
||||
score = e.detail.score
|
||||
}}
|
||||
on:mouseout={(e) => {
|
||||
score = null
|
||||
}}
|
||||
score={score ?? confirmedScore ?? 0}
|
||||
starSize="w-8 h-8"
|
||||
/>
|
||||
|
||||
{#if confirmedScore !== undefined}
|
||||
<Tr cls="font-bold mt-2" t={t.question_opinion} />
|
||||
<textarea bind:value={$opinion} inputmode="text" rows="3" class="mb-1 w-full" />
|
||||
<Checkbox selected={isAffiliated}>
|
||||
<div class="flex flex-col">
|
||||
<Tr t={t.i_am_affiliated} />
|
||||
<Tr cls="subtle" t={t.i_am_affiliated_explanation} />
|
||||
</div>
|
||||
</Checkbox>
|
||||
<div class="flex w-full flex-wrap items-center justify-between">
|
||||
<If condition={state.osmConnection.isLoggedIn}>
|
||||
<Tr t={t.reviewing_as.Subs({ nickname: state.osmConnection.userDetails.data.name })} />
|
||||
<Tr slot="else" t={t.reviewing_as_anonymous} />
|
||||
</If>
|
||||
<button class="primary" on:click={save}>
|
||||
<Tr t={t.save} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Tr cls="subtle mt-4" t={t.tos} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
|
@ -1,101 +0,0 @@
|
|||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { TextField } from "../Input/TextField"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Combine from "../Base/Combine"
|
||||
import Svg from "../../Svg"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { CheckBox } from "../Input/Checkboxes"
|
||||
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
|
||||
export default class ReviewForm extends LoginToggle {
|
||||
constructor(
|
||||
onSave: (r: Omit<Review, "sub">) => Promise<void>,
|
||||
state: {
|
||||
readonly osmConnection: OsmConnection
|
||||
readonly featureSwitchUserbadge: Store<boolean>
|
||||
}
|
||||
) {
|
||||
/* made_by_user: new UIEventSource<boolean>(true),
|
||||
rating: undefined,
|
||||
comment: undefined,
|
||||
author: osmConnection.userDetails.data.name,
|
||||
affiliated: false,
|
||||
date: new Date(),*/
|
||||
const commentForm = new TextField({
|
||||
placeholder: Translations.t.reviews.write_a_comment.Clone(),
|
||||
htmlType: "area",
|
||||
textAreaRows: 5,
|
||||
})
|
||||
|
||||
const rating = new UIEventSource<number>(undefined)
|
||||
const isAffiliated = new CheckBox(Translations.t.reviews.i_am_affiliated)
|
||||
const reviewMade = new UIEventSource(false)
|
||||
|
||||
const postingAs = new Combine([
|
||||
Translations.t.reviews.posting_as.Clone(),
|
||||
new VariableUiElement(
|
||||
state.osmConnection.userDetails.map((ud: UserDetails) => ud.name)
|
||||
).SetClass("review-author"),
|
||||
]).SetStyle("display:flex;flex-direction: column;align-items: flex-end;margin-left: auto;")
|
||||
|
||||
const saveButton = new Toggle(
|
||||
Translations.t.reviews.no_rating.SetClass("block alert"),
|
||||
new SubtleButton(Svg.confirm_svg(), Translations.t.reviews.save)
|
||||
.OnClickWithLoading(
|
||||
Translations.t.reviews.saving_review.SetClass("alert"),
|
||||
async () => {
|
||||
const review: Omit<Review, "sub"> = {
|
||||
rating: rating.data,
|
||||
opinion: commentForm.GetValue().data,
|
||||
metadata: { nickname: state.osmConnection.userDetails.data.name },
|
||||
}
|
||||
await onSave(review)
|
||||
}
|
||||
)
|
||||
.SetClass("break-normal"),
|
||||
rating.map((r) => r === undefined, [commentForm.GetValue()])
|
||||
)
|
||||
|
||||
const stars = []
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
stars.push(
|
||||
new VariableUiElement(
|
||||
rating.map((score) => {
|
||||
if (score === undefined) {
|
||||
return Svg.star_outline.replace(/#000000/g, "#ccc")
|
||||
}
|
||||
return score < i * 20 ? Svg.star_outline : Svg.star
|
||||
})
|
||||
).onClick(() => {
|
||||
rating.setData(i * 20)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const form = new Combine([
|
||||
new Combine([new Combine(stars).SetClass("review-form-rating"), postingAs]).SetClass(
|
||||
"flex"
|
||||
),
|
||||
commentForm,
|
||||
new Combine([isAffiliated, saveButton]),
|
||||
Translations.t.reviews.tos.Clone().SetClass("subtle"),
|
||||
])
|
||||
.SetClass("flex flex-col p-4")
|
||||
.SetStyle(
|
||||
"border-radius: 1em;" +
|
||||
" background-color: var(--subtle-detail-color);" +
|
||||
" color: var(--subtle-detail-color-contrast);" +
|
||||
" border: 2px solid var(--subtle-detail-color-contrast)"
|
||||
)
|
||||
|
||||
super(
|
||||
new Toggle(Translations.t.reviews.saved.Clone().SetClass("thanks"), form, reviewMade),
|
||||
Translations.t.reviews.plz_login,
|
||||
state
|
||||
)
|
||||
}
|
||||
}
|
38
src/UI/Reviews/SingleReview.svelte
Normal file
38
src/UI/Reviews/SingleReview.svelte
Normal file
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let review: Review & { madeByLoggedInUser: Store<boolean> }
|
||||
let name = review.metadata.nickname
|
||||
name ??= (review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "").trim()
|
||||
if (name.length === 0) {
|
||||
name = "Anonymous"
|
||||
}
|
||||
let d = new Date()
|
||||
d.setTime(review.iat * 1000)
|
||||
let date = d.toDateString()
|
||||
let byLoggedInUser = review.madeByLoggedInUser
|
||||
</script>
|
||||
|
||||
<div class={"low-interaction rounded-lg p-1 px-2 " + ($byLoggedInUser ? "border-interactive" : "")}>
|
||||
<div class="flex items-center justify-between">
|
||||
<StarsBar score={review.rating} />
|
||||
<div class="flex flex-wrap space-x-2">
|
||||
<div class="font-bold">
|
||||
{name}
|
||||
</div>
|
||||
<span class="subtle">
|
||||
{date}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if review.opinion}
|
||||
{review.opinion}
|
||||
{/if}
|
||||
{#if review.metadata.is_affiliated}
|
||||
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />
|
||||
{/if}
|
||||
</div>
|
|
@ -1,64 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Utils } from "../../Utils"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Img from "../Base/Img"
|
||||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
|
||||
export default class SingleReview extends Combine {
|
||||
constructor(review: Review & { madeByLoggedInUser: Store<boolean> }) {
|
||||
const date = new Date(review.iat * 1000)
|
||||
const reviewAuthor =
|
||||
review.metadata.nickname ??
|
||||
(review.metadata.given_name ?? "") + (review.metadata.family_name ?? "")
|
||||
const authorElement = new FixedUiElement(reviewAuthor).SetClass("font-bold")
|
||||
|
||||
super([
|
||||
new Combine([SingleReview.GenStars(review.rating)]),
|
||||
new FixedUiElement(review.opinion),
|
||||
new Combine([
|
||||
new Combine([
|
||||
authorElement,
|
||||
review.metadata.is_affiliated
|
||||
? Translations.t.reviews.affiliated_reviewer_warning
|
||||
: "",
|
||||
]).SetStyle("margin-right: 0.5em"),
|
||||
new FixedUiElement(
|
||||
`${date.getFullYear()}-${Utils.TwoDigits(
|
||||
date.getMonth() + 1
|
||||
)}-${Utils.TwoDigits(date.getDate())} ${Utils.TwoDigits(
|
||||
date.getHours()
|
||||
)}:${Utils.TwoDigits(date.getMinutes())}`
|
||||
).SetClass("subtle"),
|
||||
]).SetClass("flex mb-4 justify-end"),
|
||||
])
|
||||
this.SetClass("block p-2 m-4 rounded-xl subtle-background review-element")
|
||||
review.madeByLoggedInUser.addCallbackAndRun((madeByUser) => {
|
||||
if (madeByUser) {
|
||||
authorElement.SetClass("thanks")
|
||||
} else {
|
||||
authorElement.RemoveClass("thanks")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public static GenStars(rating: number): BaseUIElement {
|
||||
if (rating === undefined) {
|
||||
return Translations.t.reviews.no_rating
|
||||
}
|
||||
if (rating < 10) {
|
||||
rating = 10
|
||||
}
|
||||
const scoreTen = Math.round(rating / 10)
|
||||
return new Combine([
|
||||
...Utils.TimesT(scoreTen / 2, (_) =>
|
||||
new Img("./assets/svg/star.svg").SetClass("'h-8 w-8 md:h-12")
|
||||
),
|
||||
scoreTen % 2 == 1
|
||||
? new Img("./assets/svg/star_half.svg").SetClass("h-8 w-8 md:h-12")
|
||||
: undefined,
|
||||
]).SetClass("flex w-max")
|
||||
}
|
||||
}
|
32
src/UI/Reviews/StarElement.svelte
Normal file
32
src/UI/Reviews/StarElement.svelte
Normal file
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let score: number
|
||||
export let cutoff: number
|
||||
export let starSize = "w-h h-4"
|
||||
|
||||
let dispatch = createEventDispatcher<{ hover: { score: number } }>()
|
||||
let container: HTMLElement
|
||||
|
||||
function getScore(e: MouseEvent): number {
|
||||
const x = e.clientX - e.target.getBoundingClientRect().x
|
||||
const w = container.getClientRects()[0]?.width
|
||||
return x / w < 0.5 ? cutoff - 10 : cutoff
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={container}
|
||||
on:click={(e) => dispatch("click", { score: getScore(e) })}
|
||||
on:mousemove={(e) => dispatch("hover", { score: getScore(e) })}
|
||||
>
|
||||
{#if score >= cutoff}
|
||||
<ToSvelte construct={Svg.star_svg().SetClass(starSize)} />
|
||||
{:else if score + 10 >= cutoff}
|
||||
<ToSvelte construct={Svg.star_half_svg().SetClass(starSize)} />
|
||||
{:else}
|
||||
<ToSvelte construct={Svg.star_outline_svg().SetClass(starSize)} />
|
||||
{/if}
|
||||
</div>
|
21
src/UI/Reviews/StarsBar.svelte
Normal file
21
src/UI/Reviews/StarsBar.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import StarElement from "./StarElement.svelte"
|
||||
|
||||
/**
|
||||
* Number between 0 and 100. Every 10 points, another half star is added
|
||||
*/
|
||||
export let score: number
|
||||
let dispatch = createEventDispatcher<{ hover: number; click: number }>()
|
||||
|
||||
let cutoffs = [20, 40, 60, 80, 100]
|
||||
export let starSize = "w-h h-4"
|
||||
</script>
|
||||
|
||||
{#if score !== undefined}
|
||||
<div class="flex" on:mouseout>
|
||||
{#each cutoffs as cutoff}
|
||||
<StarElement {score} {cutoff} {starSize} on:hover on:click />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
10
src/UI/Reviews/StarsBarIcon.svelte
Normal file
10
src/UI/Reviews/StarsBarIcon.svelte
Normal file
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
|
||||
export let score: Store<number>
|
||||
</script>
|
||||
|
||||
{#if $score !== undefined && $score !== null}
|
||||
<StarsBar score={$score} />
|
||||
{/if}
|
|
@ -27,8 +27,6 @@ import { Utils } from "../Utils"
|
|||
import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"
|
||||
import { Translation } from "./i18n/Translation"
|
||||
import Translations from "./i18n/Translations"
|
||||
import ReviewForm from "./Reviews/ReviewForm"
|
||||
import ReviewElement from "./Reviews/ReviewElement"
|
||||
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
|
||||
import { SubtleButton } from "./Base/SubtleButton"
|
||||
import Svg from "../Svg"
|
||||
|
@ -74,6 +72,9 @@ import Constants from "../Models/Constants"
|
|||
import { MangroveReviews } from "mangrove-reviews-typescript"
|
||||
import Wikipedia from "../Logic/Web/Wikipedia"
|
||||
import NearbyImagesSearch from "../Logic/Web/NearbyImagesSearch"
|
||||
import AllReviews from "./Reviews/AllReviews.svelte"
|
||||
import StarsBarIcon from "./Reviews/StarsBarIcon.svelte"
|
||||
import ReviewForm from "./Reviews/ReviewForm.svelte"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
@ -648,7 +649,75 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
},
|
||||
{
|
||||
funcName: "reviews",
|
||||
funcName: "rating",
|
||||
docs: "Shows stars which represent the avarage rating on mangrove.reviews",
|
||||
needsUrls: [MangroveReviews.ORIGINAL_API],
|
||||
args: [
|
||||
{
|
||||
name: "subjectKey",
|
||||
defaultValue: "name",
|
||||
doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>",
|
||||
},
|
||||
{
|
||||
name: "fallback",
|
||||
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args, feature, layer) => {
|
||||
const nameKey = args[0] ?? "name"
|
||||
let fallbackName = args[1]
|
||||
const reviews = FeatureReviews.construct(
|
||||
feature,
|
||||
tags,
|
||||
state.userRelatedState.mangroveIdentity,
|
||||
{
|
||||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
}
|
||||
)
|
||||
return new SvelteUIElement(StarsBarIcon, {
|
||||
score: reviews.average,
|
||||
reviews,
|
||||
state,
|
||||
tags,
|
||||
feature,
|
||||
layer,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
funcName: "create_review",
|
||||
docs: "Invites the contributor to leave a review. Somewhat small UI-element until interacted",
|
||||
needsUrls: [MangroveReviews.ORIGINAL_API],
|
||||
args: [
|
||||
{
|
||||
name: "subjectKey",
|
||||
defaultValue: "name",
|
||||
doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>",
|
||||
},
|
||||
{
|
||||
name: "fallback",
|
||||
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args, feature, layer) => {
|
||||
const nameKey = args[0] ?? "name"
|
||||
let fallbackName = args[1]
|
||||
const reviews = FeatureReviews.construct(
|
||||
feature,
|
||||
tags,
|
||||
state.userRelatedState.mangroveIdentity,
|
||||
{
|
||||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
}
|
||||
)
|
||||
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer })
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "list_reviews",
|
||||
docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten",
|
||||
needsUrls: [MangroveReviews.ORIGINAL_API],
|
||||
example:
|
||||
|
@ -664,10 +733,10 @@ export default class SpecialVisualizations {
|
|||
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args, feature) => {
|
||||
constr: (state, tags, args, feature, layer) => {
|
||||
const nameKey = args[0] ?? "name"
|
||||
let fallbackName = args[1]
|
||||
const mangrove = FeatureReviews.construct(
|
||||
const reviews = FeatureReviews.construct(
|
||||
feature,
|
||||
tags,
|
||||
state.userRelatedState.mangroveIdentity,
|
||||
|
@ -676,9 +745,7 @@ export default class SpecialVisualizations {
|
|||
fallbackName,
|
||||
}
|
||||
)
|
||||
|
||||
const form = new ReviewForm((r) => mangrove.createReview(r), state)
|
||||
return new ReviewElement(mangrove, form)
|
||||
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<div class="border-interactive interactive">
|
||||
Highly interactive area (mostly: active question)
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex">
|
||||
<button class="primary">
|
||||
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
|
||||
|
|
|
@ -33,22 +33,22 @@
|
|||
<Tr t={Translations.t.general.wikipedia.loading} />
|
||||
</Loading>
|
||||
{:else}
|
||||
<span class="wikipedia-article">
|
||||
<FromHtml src={$wikipediaDetails.firstParagraph} />
|
||||
<Disclosure let:open>
|
||||
<DisclosureButton>
|
||||
<span class="flex">
|
||||
<ChevronRightIcon
|
||||
style={(open ? "transform: rotate(90deg); " : "") +
|
||||
" transition: all .25s linear; width: 1.5rem; height: 1.5rem"}
|
||||
/>
|
||||
<Tr t={Translations.t.general.wikipedia.readMore}/>
|
||||
</span>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel>
|
||||
<FromHtml src={$wikipediaDetails.restOfArticle} />
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</span>
|
||||
<span class="wikipedia-article">
|
||||
<FromHtml src={$wikipediaDetails.firstParagraph} />
|
||||
<Disclosure let:open>
|
||||
<DisclosureButton>
|
||||
<span class="flex">
|
||||
<ChevronRightIcon
|
||||
style={(open ? "transform: rotate(90deg); " : "") +
|
||||
" transition: all .25s linear; width: 1.5rem; height: 1.5rem"}
|
||||
/>
|
||||
<Tr t={Translations.t.general.wikipedia.readMore} />
|
||||
</span>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel>
|
||||
<FromHtml src={$wikipediaDetails.restOfArticle} />
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
export let wikiIds: Store<string[]>
|
||||
let wikipediaStores: Store<Store<FullWikipediaDetails>[]> = Locale.language.bind((language) =>
|
||||
wikiIds?.map((wikiIds) => wikiIds?.map((id) => Wikipedia.fetchArticleAndWikidata(id, language)))
|
||||
);
|
||||
)
|
||||
let _wikipediaStores
|
||||
onDestroy(
|
||||
wikipediaStores.addCallbackAndRunD((wikipediaStores) => {
|
||||
|
|
|
@ -244,7 +244,7 @@ export class Translation extends BaseUIElement {
|
|||
continue
|
||||
}
|
||||
let txt = this.translations[lng]
|
||||
txt = txt.replace(/(\.|<br\/>|<br>).*/, "")
|
||||
txt = txt.replace(/(\.|<br\/>|<br>|。).*/, "")
|
||||
txt = Utils.EllipsesAfter(txt, 255)
|
||||
tr[lng] = txt.trim()
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue