Merge develop

This commit is contained in:
Pieter Vander Vennet 2023-10-17 16:57:49 +02:00
commit 553ee6d5aa
165 changed files with 6745 additions and 1720 deletions

View file

@ -5,23 +5,11 @@ import { Utils } from "../../Utils"
import { Feature } from "geojson"
export default class PendingChangesUploader {
private lastChange: Date
constructor(changes: Changes, selectedFeature: UIEventSource<Feature>) {
const self = this
this.lastChange = new Date()
changes.pendingChanges.addCallback(() => {
self.lastChange = new Date()
changes.pendingChanges.stabilized(Constants.updateTimeoutSec * 1000).addCallback(() => changes.flushChanges("Flushing changes due to timeout"))
window.setTimeout(() => {
const diff = (new Date().getTime() - self.lastChange.getTime()) / 1000
if (Constants.updateTimeoutSec >= diff - 1) {
changes.flushChanges("Flushing changes due to timeout")
}
}, Constants.updateTimeoutSec * 1000)
})
selectedFeature.stabilized(10000).addCallback((feature) => {
selectedFeature.stabilized(1000).addCallback((feature) => {
if (feature === undefined) {
// The popup got closed - we flush
changes.flushChanges("Flushing changes due to popup closed")

View file

@ -97,7 +97,7 @@ export class ImageUploadManager {
console.log("Upload done, creating ")
const action = await this.uploadImageWithLicense(featureId, title, description, file)
if (!isNaN(Number(featureId))) {
// THis is a map note
// This is a map note
const url = action._url
await this._osmConnection.addCommentToNote(featureId, url)
NoteCommentElement.addCommentTo(url, <UIEventSource<any>>tagsStore, {
@ -151,9 +151,13 @@ export class ImageUploadManager {
}
private increaseCountFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
const counter = this.getCounterFor(collection, key)
counter.setData(counter.data + 1)
const global = this.getCounterFor(collection, "*")
global.setData(counter.data + 1)
{
const counter = this.getCounterFor(collection, key)
counter.setData(counter.data + 1)
}
{
const global = this.getCounterFor(collection, "*")
global.setData(global.data + 1)
}
}
}

View file

@ -26,6 +26,7 @@ export class Changes {
public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined)
public readonly backend: string
public readonly isUploading = new UIEventSource(false)
public readonly errors = new UIEventSource<string[]>([], "upload-errors")
private readonly historicalUserLocations?: FeatureSource
private _nextId: number = -1 // Newly assigned ID's are negative
private readonly previouslyCreated: OsmObject[] = []
@ -128,8 +129,11 @@ export class Changes {
const csNumber = await this.flushChangesAsync()
this.isUploading.setData(false)
console.log("Changes flushed. Your changeset is " + csNumber)
this.errors.setData([])
} catch (e) {
this.isUploading.setData(false)
this.errors.data.push(e)
this.errors.ping()
console.error("Flushing changes failed due to", e)
}
}
@ -415,6 +419,8 @@ export class Changes {
id,
" dropping it from the changes (" + e + ")"
)
this.errors.data.push(e)
this.errors.ping()
return undefined
}
})
@ -572,9 +578,15 @@ export class Changes {
openChangeset.data
)
return await self.flushSelectChanges(pendingChanges, openChangeset)
const result = await self.flushSelectChanges(pendingChanges, openChangeset)
if(result){
this.errors.setData([])
}
return result
} catch (e) {
console.error("Could not upload some changes:", e)
this.errors.data.push(e)
this.errors.ping()
return false
}
})
@ -589,6 +601,8 @@ export class Changes {
"Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those",
e
)
this.errors.data.push(e)
this.errors.ping()
self.pendingChanges.setData([])
} finally {
self.isUploading.setData(false)

View file

@ -72,7 +72,7 @@ export class OsmPreferences {
let i = 0
while (str !== "") {
if (str === undefined || str === "undefined") {
throw "Long pref became undefined?"
throw "Got 'undefined' or a literal string containing 'undefined' for a long preference with name "+key
}
if (i > 100) {
throw "This long preference is getting very long... "

View file

@ -119,6 +119,12 @@ export class GeoLocationState {
return
}
if(navigator.permissions === undefined && navigator.geolocation !== undefined){
// This is probably safari - we just start watching right away
this.startWatching()
return
}
this.permission.setData("requested")
try {
const status = await navigator?.permissions?.query({ name: "geolocation" })

View file

@ -39,6 +39,7 @@ export default class UserRelatedState {
public readonly installedUserThemes: Store<string[]>
public readonly showAllQuestionsAtOnce: UIEventSource<boolean>
public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
public readonly showCrosshair: UIEventSource<"yes" | undefined>
public readonly fixateNorth: UIEventSource<undefined | "yes">
public readonly homeLocation: FeatureSource
public readonly language: UIEventSource<string>
@ -102,6 +103,7 @@ export default class UserRelatedState {
)
this.language = this.osmConnection.GetPreference("language")
this.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags")
this.showCrosshair = <UIEventSource<any>>this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = <UIEventSource<"yes">>this.osmConnection.GetPreference("fixate-north")
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove")

View file

@ -6,7 +6,7 @@ import { AuthConfig } from "../Logic/Osm/AuthConfig"
export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number]
export default class Constants {
public static vNumber = packagefile.version
public static vNumber : string = packagefile.version
/**
* API key for Maproulette
*
@ -63,7 +63,7 @@ export default class Constants {
* Used by 'PendingChangesUploader', which waits this amount of seconds to upload changes.
* (Note that pendingChanges might upload sooner if the popup is closed or similar)
*/
static updateTimeoutSec: number = 30
static updateTimeoutSec: number = 15
/**
* If the contributor has their GPS location enabled and makes a change,
* the points visited less then `nearbyVisitTime`-seconds ago will be inspected.

View file

@ -9,7 +9,7 @@
</script>
<div
class="absolute top-0 right-0 h-screen w-full overflow-auto drop-shadow-2xl md:w-6/12 lg:w-5/12 xl:w-4/12"
class="absolute top-0 right-0 h-screen w-full overflow-y-auto drop-shadow-2xl md:w-6/12 lg:w-5/12 xl:w-4/12"
style="max-width: 100vw; max-height: 100vh"
>
<div class="normal-background m-0 flex flex-col">

View file

@ -29,16 +29,25 @@
* The start coordinate
*/
export let coordinate: { lon: number; lat: number }
export let snapToLayers: string[] | undefined
export let targetLayer: LayerConfig
export let maxSnapDistance: number = undefined
export let snappedTo: UIEventSource<string | undefined>
/**
* The center of the map at all times
* If undefined at the beginning, 'coordinate' will be used
*/
export let value: UIEventSource<{ lon: number; lat: number }>
if (value.data === undefined) {
value.setData(coordinate)
}
if(coordinate === undefined){
coordinate = value.data
}
export let snapToLayers: string[] | undefined
export let targetLayer: LayerConfig | undefined
export let maxSnapDistance: number = undefined
export let snappedTo: UIEventSource<string | undefined>
let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
lon: number
@ -66,12 +75,14 @@
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
}
const featuresForLayer = state.perLayer.get(targetLayer.id)
if (featuresForLayer) {
new ShowDataLayer(map, {
layer: targetLayer,
features: featuresForLayer,
})
if(targetLayer){
const featuresForLayer = state.perLayer.get(targetLayer.id)
if (featuresForLayer) {
new ShowDataLayer(map, {
layer: targetLayer,
features: featuresForLayer,
})
}
}
if (snapToLayers?.length > 0) {
@ -114,4 +125,8 @@
value={preciseLocation}
initialCoordinate={coordinate}
maxDistanceInMeters="50"
/>
>
<slot name="image" slot="image">
<img class="h-full max-h-24" src="./assets/svg/move-arrows.svg" />
</slot>
</LocationInput>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { Store } from "../../Logic/UIEventSource"
import { Changes } from "../../Logic/Osm/Changes"
import Loading from "../Base/Loading.svelte"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
export let state: SpecialVisualizationState
const changes: Changes = state.changes
const isUploading: Store<boolean> = changes.isUploading
const pendingChangesCount: Store<number> = changes.pendingChanges.map(ls => ls.length)
const errors = changes.errors
</script>
<div class="flex flex-col pointer-events-auto" on:click={() => changes.flushChanges("Pending changes indicator clicked")}>
{#if $isUploading}
<Loading>
<Tr cls="thx" t={Translations.t.general.uploadingChanges} />
</Loading>
{:else if $pendingChangesCount === 1}
<Tr cls="alert" t={Translations.t.general.uploadPendingSingle} />
{:else if $pendingChangesCount > 1}
<Tr cls="alert" t={Translations.t.general.uploadPending.Subs({count: $pendingChangesCount})} />
{/if}
{#each $errors as error}
<Tr cls="alert" t={Translations.t.general.uploadError.Subs({error})} />
{/each}
</div>

View file

@ -8,13 +8,26 @@
import Tr from "../Base/Tr.svelte"
import SubtleLink from "../Base/SubtleLink.svelte"
import Translations from "../i18n/Translations"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
export let theme: LayoutInformation
export let isCustom: boolean = false
export let userDetails: UIEventSource<UserDetails>
export let state: { layoutToUse?: { id: string }; osmConnection: OsmConnection }
export let selected: boolean = false
let unlockedPersonal = LocalStorageSource.GetParsed("unlocked_personal_theme", false)
userDetails.addCallbackAndRunD(userDetails => {
if(!userDetails.loggedIn){
return
}
if(userDetails.csCount > Constants.userJourney.personalLayoutUnlock){
unlockedPersonal.setData(true)
}
return true
})
$: title = new Translation(
theme.title,
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
@ -72,7 +85,7 @@
let href = createUrl(theme, isCustom, state)
</script>
{#if theme.id !== personal.id || $userDetails.csCount > Constants.userJourney.personalLayoutUnlock}
{#if theme.id !== personal.id || $unlockedPersonal}
<SubtleLink href={$href} options={{ extraClasses: "w-full" }}>
<img slot="image" src={theme.icon} class="mx-4 block h-11 w-11" alt="" />
<span class="flex flex-col overflow-hidden text-ellipsis">

View file

@ -1,33 +1,38 @@
<script lang="ts">
/**
* Shows information about how much images are uploaded for the given feature
*/
/**
* Shows information about how much images are uploaded for the given feature
*
* Either pass in a store with tags or a featureId.
*/
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"
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
export let state: SpecialVisualizationState
export let tags: Store<OsmTags>
export let featureId = tags.data.id
export let showThankYou: boolean = true
const { uploadStarted, uploadFinished, retried, failed } =
state.imageUploadManager.getCountsFor(featureId)
const t = Translations.t.image
</script>
{#if $uploadStarted == 1}
{#if $uploadFinished == 1}
<Tr cls="thanks" t={t.upload.one.done} />
{:else if $failed == 1}
{#if $uploadStarted === 1}
{#if $uploadFinished === 1}
{#if showThankYou}
<Tr cls="thanks" t={t.upload.one.done} />
{/if}
{:else if $failed === 1}
<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} />
</div>
{:else if $retried == 1}
{:else if $retried === 1}
<Loading cls="alert">
<Tr t={t.upload.one.retrying} />
</Loading>
@ -37,9 +42,11 @@
</Loading>
{/if}
{:else if $uploadStarted > 1}
{#if $uploadFinished + $failed == $uploadStarted && $uploadFinished > 0}
<Tr cls="thanks" t={t.upload.multiple.done.Subs({ count: $uploadFinished })} />
{:else if $uploadFinished == 0}
{#if $uploadFinished + $failed === $uploadStarted && $uploadFinished > 0}
{#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 })} />
</Loading>

View file

@ -89,7 +89,9 @@
<div
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center p-8 opacity-50"
>
<img class="h-full max-h-24" src="./assets/svg/move-arrows.svg" />
<slot name="image">
<img class="h-full max-h-24" src="./assets/svg/move-arrows.svg" />
</slot>
</div>
<DragInvitation hideSignal={mla.location} />

View file

@ -0,0 +1,11 @@
<script lang="ts">/**
* Opens the 'Opening hours input' in another top level window
*/
import { UIEventSource } from "../../../Logic/UIEventSource"
import ToSvelte from "../../Base/ToSvelte.svelte"
import OpeningHoursInput from "../../OpeningHours/OpeningHoursInput"
export let value: UIEventSource<string>
</script>
<ToSvelte construct={new OpeningHoursInput(value)}></ToSvelte>

View file

@ -3,13 +3,17 @@ import { UIEventSource } from "../../Logic/UIEventSource"
import { MapProperties } from "../../Models/MapProperties"
import BaseUIElement from "../BaseUIElement"
import OpeningHoursInput from "../OpeningHours/OpeningHoursInput"
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"
import Wikidata from "../../Logic/Web/Wikidata"
import { Utils } from "../../Utils"
import Locale from "../i18n/Locale"
import { Feature } from "geojson"
import { GeoOperations } from "../../Logic/GeoOperations"
import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"
import SvelteUIElement from "../Base/SvelteUIElement"
import DirectionInput from "./Helpers/DirectionInput.svelte"
import DateInput from "./Helpers/DateInput.svelte"
import ColorInput from "./Helpers/ColorInput.svelte"
export interface InputHelperProperties {
/**
@ -46,8 +50,14 @@ export default class InputHelpers {
>
>
> = {
// TODO: remake in svelte,move selection logic to 'inputHelper.svelte'
opening_hours: (value) => new OpeningHoursInput(value),
direction: (value, properties) =>
new SvelteUIElement(DirectionInput, {
value,
mapProperties: InputHelpers.constructMapProperties(properties),
}),
date: (value) => new SvelteUIElement(DateInput, { value }),
color: (value) => new SvelteUIElement(ColorInput, { value }),
opening_hours: (value) => new SvelteUIElement(OpeningHoursInput, { value }),
wikidata: InputHelpers.constructWikidataHelper,
} as const

64
src/UI/Leaderboard.svelte Normal file
View file

@ -0,0 +1,64 @@
<script lang="ts">
import { Utils } from "../Utils"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import Loading from "./Base/Loading.svelte"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
const osmConnection = new OsmConnection({
attemptLogin: true
})
let loggedInContributor: Store<string> = osmConnection.userDetails.map(ud => ud.name)
export let source = "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/picture-leaderboard.json"
let data: Store<undefined | {
leaderboard: {
rank: number,
name: string,
account: string,
nrOfImages: number
}[],
median: number,
totalAuthors: number,
byLicense: {
license: string, total: number, authors: string[]
},
date: string
}> = UIEventSource.FromPromise(Utils.downloadJsonCached(source))
</script>
<h1>Contributed images with MapComplete: leaderboard</h1>
{#if $data}
<table>
<tr>
<th>Rank</th>
<th>Contributor</th>
<th>Number of images contributed</th>
</tr>
{#each $data.leaderboard as contributor}
<tr>
<td>
{contributor.rank}
</td>
<td>
{#if $loggedInContributor === contributor.name}
<a class="thanks" href="{contributor.account}">{contributor.name}</a>
{:else}
<a href="{contributor.account}">{contributor.name}</a>
{/if}
</td>
<td>
<b>{contributor.nrOfImages}</b> total images
</td>
</tr>
{/each}
</table>
Statistics generated on {$data.date}
{:else}
<Loading />
{/if}
<div>
Logged in as {$loggedInContributor}
</div>

View file

@ -162,16 +162,16 @@
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
</LoginButton>
{#if $isLoading}
<div class="alert">
<Loading>
<Tr t={Translations.t.general.add.stillLoading} />
</Loading>
</div>
{:else if $zoom < Constants.minZoomLevelToAddNewPoint}
{#if $zoom < Constants.minZoomLevelToAddNewPoint}
<div class="alert">
<Tr t={Translations.t.general.add.zoomInFurther} />
</div>
{:else if $isLoading}
<div class="alert">
<Loading>
<Tr t={Translations.t.general.add.stillLoading} />
</Loading>
</div>
{:else if selectedPreset === undefined}
<!-- First, select the correct preset -->
<PresetList

View file

@ -30,6 +30,7 @@
if (flayer.isDisplayed.data === false) {
// The layer is not displayed...
if (!state.featureSwitches.featureSwitchFilter.data) {
console.log("Not showing presets for layer", flayer.layerDef.id, "as not displayed and featureSwitchFilter.data is set",state.featureSwitches.featureSwitchFilter.data)
// ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
continue
}

View file

@ -2,47 +2,50 @@
/**
* UIcomponent to create a new note at the given location
*/
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import ValidatedInput from "../InputElement/ValidatedInput.svelte"
import SubtleButton from "../Base/SubtleButton.svelte"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations.js"
import type { Feature, Point } from "geojson"
import LoginToggle from "../Base/LoginToggle.svelte"
import FilteredLayer from "../../Models/FilteredLayer"
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { UIEventSource } from "../../Logic/UIEventSource";
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource";
import ValidatedInput from "../InputElement/ValidatedInput.svelte";
import SubtleButton from "../Base/SubtleButton.svelte";
import Tr from "../Base/Tr.svelte";
import Translations from "../i18n/Translations.js";
import type { Feature, Point } from "geojson";
import LoginToggle from "../Base/LoginToggle.svelte";
import FilteredLayer from "../../Models/FilteredLayer";
import NewPointLocationInput from "../BigComponents/NewPointLocationInput.svelte";
import ToSvelte from "../Base/ToSvelte.svelte";
import Svg from "../../Svg";
export let coordinate: { lon: number; lat: number }
export let state: SpecialVisualizationState
export let coordinate: UIEventSource<{ lon: number; lat: number }>;
export let state: SpecialVisualizationState;
let comment: UIEventSource<string> = LocalStorageSource.Get("note-text")
let created = false
let comment: UIEventSource<string> = LocalStorageSource.Get("note-text");
let created = false;
let notelayer: FilteredLayer = state.layerState.filteredLayers.get("note")
let notelayer: FilteredLayer = state.layerState.filteredLayers.get("note");
let hasFilter = notelayer?.hasFilter
let isDisplayed = notelayer?.isDisplayed
let hasFilter = notelayer?.hasFilter;
let isDisplayed = notelayer?.isDisplayed;
function enableNoteLayer() {
state.guistate.closeAll()
isDisplayed.setData(true)
state.guistate.closeAll();
isDisplayed.setData(true);
}
async function uploadNote() {
let txt = comment.data
let txt = comment.data;
if (txt === undefined || txt === "") {
return
return;
}
const loc = coordinate
txt += "\n\n #MapComplete #" + state?.layout?.id
const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt)
console.log("Created a note, got id", id)
const loc = coordinate.data;
txt += "\n\n #MapComplete #" + state?.layout?.id;
const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt);
console.log("Created a note, got id", id);
const feature = <Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [loc.lon, loc.lat],
coordinates: [loc.lon, loc.lat]
},
properties: {
id: "" + id.id,
@ -53,22 +56,22 @@
text: txt,
html: txt,
user: state.osmConnection?.userDetails?.data?.name,
uid: state.osmConnection?.userDetails?.data?.uid,
},
]),
},
}
uid: state.osmConnection?.userDetails?.data?.uid
}
])
}
};
// Normally, the 'Changes' will generate the new element. The 'notes' are an exception to this
state.newFeatures.features.data.push(feature)
state.newFeatures.features.ping()
state.selectedElement?.setData(feature)
state.newFeatures.features.data.push(feature);
state.newFeatures.features.ping();
state.selectedElement?.setData(feature);
if (state.featureProperties.trackFeature) {
state.featureProperties.trackFeature(feature)
state.featureProperties.trackFeature(feature);
}
comment.setData("")
created = true
state.selectedElement.setData(feature)
state.selectedLayer.setData(state.layerState.filteredLayers.get("note"))
comment.setData("");
created = true;
state.selectedElement.setData(feature);
state.selectedLayer.setData(state.layerState.filteredLayers.get("note"));
}
</script>
@ -106,6 +109,15 @@
<ValidatedInput type="text" value={comment} />
</div>
<div class="w-full h-56">
<NewPointLocationInput value={coordinate} {state} >
<div class="h-20 w-full pb-10" slot="image">
<ToSvelte construct={Svg.note_svg().SetClass("h-10 w-full")}/>
</div>
</NewPointLocationInput>
</div>
<LoginToggle {state}>
<span slot="loading"><!--empty: don't show a loading message--></span>
<div slot="not-logged-in" class="alert">

View file

@ -4,6 +4,7 @@
import { Translation } from "../i18n/Translation"
import Tr from "../Base/Tr.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import Translations from "../i18n/Translations"
/**
* A 'TagHint' will show the given tags in a human readable form.
@ -25,7 +26,7 @@
{#if !userDetails || $userDetails.loggedIn}
<div>
{#if tags === undefined}
<slot name="no-tags">No tags</slot>
<slot name="no-tags"><Tr cls="subtle" t={Translations.t.general.noTagsSelected}></Tr></slot>
{:else if embedIn === undefined}
<FromHtml src={tagsExplanation} />
{:else}

View file

@ -178,8 +178,10 @@
</script>
{#if config.question !== undefined}
<div class="interactive border-interactive flex flex-col p-1 px-2">
<div class="flex justify-between">
<div class="interactive border-interactive flex flex-col p-1 px-2 relative overflow-y-auto" style="max-height: 85vh">
<div class="sticky top-0" style="z-index: 11">
<div class="flex justify-between sticky top-0 interactive">
<span class="font-bold">
<SpecialTranslation t={config.question} {tags} {state} {layer} feature={selectedElement} />
</span>
@ -197,9 +199,10 @@
/>
</div>
{/if}
</div>
{#if config.mappings?.length >= 8}
<div class="flex w-full">
<div class="flex w-full sticky">
<img src="./assets/svg/search.svg" class="h-6 w-6" />
<input type="text" bind:value={$searchTerm} class="w-full" />
</div>
@ -314,7 +317,7 @@
<Tr t={$feedback} />
</div>
{/if}
<div class="flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap">
<div class="flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap sticky bottom-0 interactive" style="z-index: 11">
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel" />
<slot name="save-button" {selectedTags}>

View file

@ -40,7 +40,7 @@
<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} />
<ToSvelte construct={Svg.mangrove_logo_svg().SetClass("w-12 h-12 shrink-0 p-1 ")} />
<Tr cls="text-sm subtle" t={Translations.t.reviews.attribution} />
</div>
</div>

View file

@ -563,7 +563,10 @@ export default class SpecialVisualizations {
feature: Feature
): BaseUIElement {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(CreateNewNote, { state, coordinate: { lon, lat } })
return new SvelteUIElement(CreateNewNote, {
state,
coordinate: new UIEventSource({ lon, lat }),
})
},
},
new CloseNoteButton(),

View file

@ -1,114 +1,115 @@
<script lang="ts">
import { Store, UIEventSource } from "../Logic/UIEventSource";
import { Map as MlMap } from "maplibre-gl";
import MaplibreMap from "./Map/MaplibreMap.svelte";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import MapControlButton from "./Base/MapControlButton.svelte";
import ToSvelte from "./Base/ToSvelte.svelte";
import If from "./Base/If.svelte";
import { GeolocationControl } from "./BigComponents/GeolocationControl";
import type { Feature } from "geojson";
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import Filterview from "./BigComponents/Filterview.svelte";
import ThemeViewState from "../Models/ThemeViewState";
import type { MapProperties } from "../Models/MapProperties";
import Geosearch from "./BigComponents/Geosearch.svelte";
import Translations from "./i18n/Translations";
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import { Store, UIEventSource } from "../Logic/UIEventSource"
import { Map as MlMap } from "maplibre-gl"
import MaplibreMap from "./Map/MaplibreMap.svelte"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import MapControlButton from "./Base/MapControlButton.svelte"
import ToSvelte from "./Base/ToSvelte.svelte"
import If from "./Base/If.svelte"
import { GeolocationControl } from "./BigComponents/GeolocationControl"
import type { Feature } from "geojson"
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import Filterview from "./BigComponents/Filterview.svelte"
import ThemeViewState from "../Models/ThemeViewState"
import type { MapProperties } from "../Models/MapProperties"
import Geosearch from "./BigComponents/Geosearch.svelte"
import Translations from "./i18n/Translations"
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import Tr from "./Base/Tr.svelte"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
import FloatOver from "./Base/FloatOver.svelte"
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
import Constants from "../Models/Constants"
import TabbedGroup from "./Base/TabbedGroup.svelte"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LoginToggle from "./Base/LoginToggle.svelte"
import LoginButton from "./Base/LoginButton.svelte"
import CopyrightPanel from "./BigComponents/CopyrightPanel"
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
import ModalRight from "./Base/ModalRight.svelte"
import { Utils } from "../Utils"
import Hotkeys from "./Base/Hotkeys"
import { VariableUiElement } from "./Base/VariableUIElement"
import SvelteUIElement from "./Base/SvelteUIElement"
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
import LevelSelector from "./BigComponents/LevelSelector.svelte"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
import Svg from "../Svg"
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
import type { RasterLayerPolygon } from "../Models/RasterLayers"
import { AvailableRasterLayers } from "../Models/RasterLayers"
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
import IfHidden from "./Base/IfHidden.svelte"
import { onDestroy } from "svelte"
import { OpenJosm } from "./BigComponents/OpenJosm"
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
import StateIndicator from "./BigComponents/StateIndicator.svelte"
import LanguagePicker from "./LanguagePicker"
import Locale from "./i18n/Locale"
import ShareScreen from "./BigComponents/ShareScreen.svelte"
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
import Tr from "./Base/Tr.svelte";
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
import FloatOver from "./Base/FloatOver.svelte";
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
import Constants from "../Models/Constants";
import TabbedGroup from "./Base/TabbedGroup.svelte";
import UserRelatedState from "../Logic/State/UserRelatedState";
import LoginToggle from "./Base/LoginToggle.svelte";
import LoginButton from "./Base/LoginButton.svelte";
import CopyrightPanel from "./BigComponents/CopyrightPanel";
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
import ModalRight from "./Base/ModalRight.svelte";
import { Utils } from "../Utils";
import Hotkeys from "./Base/Hotkeys";
import { VariableUiElement } from "./Base/VariableUIElement";
import SvelteUIElement from "./Base/SvelteUIElement";
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
import LevelSelector from "./BigComponents/LevelSelector.svelte";
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
import Svg from "../Svg";
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
import type { RasterLayerPolygon } from "../Models/RasterLayers";
import { AvailableRasterLayers } from "../Models/RasterLayers";
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
import IfHidden from "./Base/IfHidden.svelte";
import { onDestroy } from "svelte";
import { OpenJosm } from "./BigComponents/OpenJosm";
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
import StateIndicator from "./BigComponents/StateIndicator.svelte";
import LanguagePicker from "./LanguagePicker";
import Locale from "./i18n/Locale";
import ShareScreen from "./BigComponents/ShareScreen.svelte";
export let state: ThemeViewState
let layout = state.layout
export let state: ThemeViewState;
let layout = state.layout;
let maplibremap: UIEventSource<MlMap> = state.map;
let selectedElement: UIEventSource<Feature> = state.selectedElement;
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer;
let maplibremap: UIEventSource<MlMap> = state.map
let selectedElement: UIEventSource<Feature> = state.selectedElement
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
const selectedElementView = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data;
const layer = selectedLayer.data
if (selectedElement === undefined || layer === undefined) {
return undefined;
return undefined
}
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
return undefined;
return undefined
}
const tags = state.featureProperties.getStore(selectedElement.properties.id);
return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags });
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags })
},
[selectedLayer]
);
[selectedLayer],
)
const selectedElementTitle = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data;
const layer = selectedLayer.data
if (selectedElement === undefined || layer === undefined) {
return undefined;
return undefined
}
const tags = state.featureProperties.getStore(selectedElement.properties.id);
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags });
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags })
},
[selectedLayer]
);
[selectedLayer],
)
let mapproperties: MapProperties = state.mapProperties;
let featureSwitches: FeatureSwitchState = state.featureSwitches;
let availableLayers = state.availableLayers;
let userdetails = state.osmConnection.userDetails;
let currentViewLayer = layout.layers.find((l) => l.id === "current_view");
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer;
let mapproperties: MapProperties = state.mapProperties
let featureSwitches: FeatureSwitchState = state.featureSwitches
let availableLayers = state.availableLayers
let userdetails = state.osmConnection.userDetails
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
let rasterLayerName =
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name;
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name;
})
);
rasterLayerName = l.properties.name
}),
)
</script>
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
@ -154,6 +155,8 @@
<ToSvelte
construct={() => new ExtraLinkButton(state, layout.extraLink).SetClass("pointer-events-auto")}
/>
<UploadingImageCounter {state} featureId="*" showThankYou={false}/>
<PendingChangesIndicator {state}/>
<If condition={state.featureSwitchIsTesting}>
<div class="alert w-fit">Testmode</div>
</If>
@ -233,6 +236,17 @@
</div>
</div>
<LoginToggle ignoreLoading={true} {state }>
<If condition={state.userRelatedState.showCrosshair.map(s => s === "yes")}>
<If condition={state.mapProperties.zoom.map(z => z >= 17)}>
<div class="absolute top-0 left-0 flex items-center justify-center pointer-events-none w-full h-full">
<ToSvelte construct={Svg.cross_svg()} />
</div>
</If>
</If>
</LoginToggle>
<If
condition={selectedElementView.map(
(v) =>
@ -257,6 +271,7 @@
</ModalRight>
</If>
<If
condition={selectedElementView.map(
(v) =>
@ -364,7 +379,8 @@
<!-- Menu page -->
<FloatOver on:close={() => state.guistate.menuIsOpened.setData(false) }>
<span slot="close-button"><!-- Hide the default close button --></span>
<TabbedGroup condition1={featureSwitches.featureSwitchEnableLogin} condition2={state.featureSwitches. featureSwitchCommunityIndex}
<TabbedGroup condition1={featureSwitches.featureSwitchEnableLogin}
condition2={state.featureSwitches. featureSwitchCommunityIndex}
tab={state.guistate.menuViewTabIndex}>
<div slot="post-tablist">
<XCircleIcon
@ -464,3 +480,5 @@
</TabbedGroup>
</FloatOver>
</If>

View file

@ -1,5 +1,17 @@
{
"layers": [
{
"name": "OpenStreetMap Carto",
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"category": "osmbasedmap",
"id": "osm.carto",
"type": "raster",
"max_zoom": 19,
"attribution": {
"text": "OpenStreetMap",
"url": "https://osm.org/copyright"
}
},
{
"name": "Americana",
"url": "https://zelonewolf.github.io/openstreetmap-americana/style.json",

View file

@ -407,7 +407,6 @@ select:hover {
.subtle {
/* For all information that is not important for 99% of the users */
color: #999;
font-size: medium;
font-weight: normal;
}

4
src/leaderboard.ts Normal file
View file

@ -0,0 +1,4 @@
import SvelteUIElement from "./UI/Base/SvelteUIElement"
import Leaderboard from "./UI/Leaderboard.svelte"
new SvelteUIElement(Leaderboard, {}).AttachTo("main")