Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-09-05 17:34:13 +02:00
commit 423618847b
334 changed files with 9307 additions and 6025 deletions

View file

@ -55,7 +55,7 @@
for (const preset of layer.presets) {
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
if(preset.preciseInput.snapToLayers){
if (preset.preciseInput.snapToLayers) {
tags["_referencing_ways"] = '["way/-1"]'
}

View file

@ -10,7 +10,6 @@
import type { MapProperties } from "../../Models/MapProperties"
import type { Feature, Point } from "geojson"
import { GeoOperations } from "../../Logic/GeoOperations"
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
import OpenBackgroundSelectorButton from "../BigComponents/OpenBackgroundSelectorButton.svelte"
import If from "../Base/If.svelte"
import Constants from "../../Models/Constants"
@ -19,6 +18,8 @@
import ChevronLeft from "@babeard/svelte-heroicons/solid/ChevronLeft"
import ThemeViewState from "../../Models/ThemeViewState"
import Icon from "../Map/Icon.svelte"
import NewPointLocationInput from "../BigComponents/NewPointLocationInput.svelte"
import type { WayId } from "../../Models/OsmFeature"
export let state: ThemeViewState
@ -34,20 +35,22 @@
let newLocation = new UIEventSource<{ lon: number; lat: number }>(undefined)
function initMapProperties() {
let snappedTo = new UIEventSource<WayId | undefined>(undefined)
function initMapProperties(reason: MoveReason) {
return <any>{
allowMoving: new UIEventSource(true),
allowRotating: new UIEventSource(false),
allowZooming: new UIEventSource(true),
bounds: new UIEventSource(undefined),
location: new UIEventSource({ lon, lat }),
minzoom: new UIEventSource($reason.minZoom),
minzoom: new UIEventSource(reason.minZoom),
rasterLayer: state.mapProperties.rasterLayer,
zoom: new UIEventSource($reason?.startZoom ?? 16),
zoom: new UIEventSource(reason?.startZoom ?? 16),
}
}
let moveWizardState = new MoveWizardState(id, layer.allowMove, state)
let moveWizardState = new MoveWizardState(id, layer.allowMove, layer, state)
if (moveWizardState.reasons.length === 1) {
reason.setData(moveWizardState.reasons[0])
}
@ -55,8 +58,8 @@
let currentMapProperties: MapProperties = undefined
</script>
<LoginToggle {state}>
{#if moveWizardState.reasons.length > 0}
{#if moveWizardState.reasons.length > 0}
<LoginToggle {state}>
{#if $notAllowed}
<div class="m-2 flex rounded-lg bg-gray-200 p-2">
<Move_not_allowed class="m-2 h-8 w-8" />
@ -79,7 +82,7 @@
<span class="flex flex-col p-2">
{#if currentStep === "reason" && moveWizardState.reasons.length > 1}
{#each moveWizardState.reasons as reasonSpec}
<button
<button class="flex justify-start"
on:click={() => {
reason.setData(reasonSpec)
currentStep = "pick_location"
@ -91,10 +94,16 @@
{/each}
{:else if currentStep === "pick_location" || currentStep === "reason"}
<div class="relative h-64 w-full">
<LocationInput
mapProperties={(currentMapProperties = initMapProperties())}
<NewPointLocationInput
mapProperties={(currentMapProperties = initMapProperties($reason))}
value={newLocation}
initialCoordinate={{ lon, lat }}
{state}
coordinate={{ lon, lat }}
{snappedTo}
maxSnapDistance={$reason.maxSnapDistance ?? 5}
snapToLayers={$reason.snapTo}
targetLayer={layer}
dontShow={[id]}
/>
<div class="absolute bottom-0 left-0">
<OpenBackgroundSelectorButton {state} />
@ -114,7 +123,7 @@
<button
class="primary w-full"
on:click={() => {
moveWizardState.moveFeature(newLocation.data, reason.data, featureToMove)
moveWizardState.moveFeature(newLocation.data, snappedTo.data, reason.data, featureToMove)
currentStep = "moved"
}}
>
@ -153,5 +162,5 @@
</span>
</AccordionSingle>
{/if}
{/if}
</LoginToggle>
</LoginToggle>
{/if}

View file

@ -12,6 +12,8 @@ import { Feature, Point } from "geojson"
import SvelteUIElement from "../Base/SvelteUIElement"
import Relocation from "../../assets/svg/Relocation.svelte"
import Location from "../../assets/svg/Location.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { WayId } from "../../Models/OsmFeature"
export interface MoveReason {
text: Translation | string
@ -24,25 +26,40 @@ export interface MoveReason {
startZoom: number
minZoom: number
eraseAddressFields: false | boolean
/**
* Snap to these layers
*/
snapTo?: string[]
maxSnapDistance?: number
}
export class MoveWizardState {
public readonly reasons: ReadonlyArray<MoveReason>
public readonly moveDisallowedReason = new UIEventSource<Translation>(undefined)
private readonly layer: LayerConfig
private readonly _state: SpecialVisualizationState
private readonly featureToMoveId: string
constructor(id: string, options: MoveConfig, state: SpecialVisualizationState) {
/**
* Initialize the movestate for the feature of the given ID
* @param id of the feature that should be moved
* @param options
* @param layer
* @param state
*/
constructor(id: string, options: MoveConfig, layer: LayerConfig, state: SpecialVisualizationState) {
this.layer = layer
this._state = state
this.reasons = MoveWizardState.initReasons(options)
this.featureToMoveId = id
this.reasons = this.initReasons(options)
if (this.reasons.length > 0) {
this.checkIsAllowed(id)
}
}
private static initReasons(options: MoveConfig): MoveReason[] {
private initReasons(options: MoveConfig): MoveReason[] {
const t = Translations.t.move
const reasons: MoveReason[] = []
if (options.enableRelocation) {
reasons.push({
@ -72,20 +89,52 @@ export class MoveWizardState {
eraseAddressFields: false,
})
}
const tags = this._state.featureProperties.getStore(this.featureToMoveId).data
const matchingPresets = this.layer.presets.filter(preset => preset.preciseInput.snapToLayers && new And(preset.tags).matchesProperties(tags))
const matchingPreset = matchingPresets.flatMap(pr => pr.preciseInput?.snapToLayers)
for (const layerId of matchingPreset) {
const snapOntoLayer = this._state.layout.getLayer(layerId)
const text = <Translation> t.reasons.reasonSnapTo.PartialSubsTr("name", snapOntoLayer.snapName)
reasons.push({
text,
invitingText: text,
icon: "snap",
changesetCommentValue: "snap",
lockBounds: true,
includeSearch: false,
background: "photo",
startZoom: 19,
minZoom: 16,
eraseAddressFields: false,
snapTo: [snapOntoLayer.id],
maxSnapDistance: 5,
})
}
return reasons
}
public async moveFeature(
loc: { lon: number; lat: number },
snappedTo: WayId,
reason: MoveReason,
featureToMove: Feature<Point>
featureToMove: Feature<Point>,
) {
const state = this._state
if(snappedTo !== undefined){
this.moveDisallowedReason.set(Translations.t.move.partOfAWay)
}
await state.changes.applyAction(
new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], {
reason: reason.changesetCommentValue,
theme: state.layout.id,
})
new ChangeLocationAction(state,
featureToMove.properties.id,
[loc.lon, loc.lat],
snappedTo,
{
reason: reason.changesetCommentValue,
theme: state.layout.id,
}),
)
featureToMove.properties._lat = loc.lat
featureToMove.properties._lon = loc.lon
@ -104,8 +153,8 @@ export class MoveWizardState {
{
changeType: "relocated",
theme: state.layout.id,
}
)
},
),
)
}

View file

@ -79,7 +79,6 @@
>([])
async function calculateQuestions() {
console.log("Applying questions to ask")
const qta = questionsToAsk.data
firstQuestion.setData(undefined)
//allQuestionsToAsk.setData([])

View file

@ -104,7 +104,6 @@
{state}
{layer}
on:saved={() => (editMode = false)}
allowDeleteOfFreeform={true}
>
<button
slot="cancel"

View file

@ -33,6 +33,9 @@
import Markdown from "../../Base/Markdown.svelte"
import { Utils } from "../../../Utils"
import type { UploadableTag } from "../../../Logic/Tags/TagTypes"
import { Modal } from "flowbite-svelte"
import Popup from "../../Base/Popup.svelte"
import If from "../../Base/If.svelte"
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
@ -43,13 +46,13 @@
export let selectedTags: UploadableTag = undefined
export let extraTags: UIEventSource<Record<string, string>> = new UIEventSource({})
export let allowDeleteOfFreeform: boolean = true
export let clss = "interactive border-interactive"
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
let isKnown = tags.mapD(tags => config.GetRenderValue(tags) !== undefined)
let matchesEmpty = config.GetRenderValue({}) !== undefined
// Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
@ -61,6 +64,12 @@
*/
let checkedMappings: boolean[]
/**
* IF set: we can remove the current answer by deleting all those keys
*/
let settableKeys = tags.mapD(tags => config.removeToSetUnknown(layer, tags))
let unknownModal = new UIEventSource(false)
let searchTerm: UIEventSource<string> = new UIEventSource("")
let dispatch = createEventDispatcher<{
@ -82,7 +91,7 @@
return !m.hideInAnswer.matchesProperties(tgs)
})
selectedMapping = mappings?.findIndex(
(mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs)
(mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs),
)
if (selectedMapping < 0) {
selectedMapping = undefined
@ -144,7 +153,6 @@
let usedKeys: string[] = Utils.Dedup(config.usedTags().flatMap((t) => t.usedKeys()))
let keysToDeleteOnUnknown = config.settableKeys()
/**
* The 'minimalTags' is a subset of the tags of the feature, only containing the values relevant for this object.
* The main goal is to be stable and only 'ping' when an actual change is relevant
@ -191,13 +199,12 @@
if (freeformValue?.length > 0) {
selectedMapping = config.mappings.length
}
})
}),
)
$: {
if (
config.freeform?.key &&
allowDeleteOfFreeform &&
!$freeformInput &&
!$freeformInputUnvalidated &&
!checkedMappings?.some((m) => m) &&
@ -210,7 +217,7 @@
$freeformInput,
selectedMapping,
checkedMappings,
tags.data
tags.data,
)
if (featureSwitchIsDebugging?.data) {
console.log(
@ -222,7 +229,7 @@
currentTags: tags.data,
},
" --> ",
selectedTags
selectedTags,
)
}
} catch (e) {
@ -244,7 +251,7 @@
selectedTags = new And([...selectedTags.and, ...extraTagsArray])
} else {
console.error(
"selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags)
"selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags),
)
}
}
@ -313,9 +320,24 @@
onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount
})
}),
)
}
function clearAnswer() {
const tagsToSet = settableKeys.data.map(k => new Tag(k, ""))
const change = new ChangeTagAction(tags.data.id, new And(tagsToSet), tags.data, {
theme: tags.data["_orig_theme"] ?? state.layout.id,
changeType: "answer",
})
freeformInput.set(undefined)
selectedMapping = undefined
selectedTags = undefined
change
.CreateChangeDescriptions()
.then((changes) => state.changes.applyChanges(changes))
.catch(console.error)
}
</script>
{#if question !== undefined}
@ -324,7 +346,7 @@
class="relative flex flex-col overflow-y-auto px-2"
style="max-height: 75vh"
on:submit|preventDefault={() => {
/*onSave(); This submit is not needed and triggers to early, causing bugs: see #1808*/
/*onSave(); This submit is not needed and triggers too early, causing bugs: see #1808*/
}}
>
<fieldset>
@ -386,7 +408,7 @@
/>
{:else if config.mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
<div class="flex flex-col no-bold">
<div class="no-bold flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
<TagRenderingMappingInput
@ -401,7 +423,7 @@
>
<input
type="radio"
class="self-center mr-1"
class="mr-1 self-center"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={i}
@ -413,7 +435,7 @@
<label class="flex gap-x-1">
<input
type="radio"
class="self-center mr-1"
class="mr-1 self-center"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={config.mappings?.length}
@ -436,7 +458,7 @@
</div>
{:else if config.mappings !== undefined && config.multiAnswer}
<!-- Multiple answers can be chosen: checkboxes -->
<div class="flex flex-col no-bold">
<div class="no-bold flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<TagRenderingMappingInput
{mapping}
@ -450,7 +472,7 @@
>
<input
type="checkbox"
class="self-center mr-1"
class="mr-1 self-center"
name={"mappings-checkbox-" + config.id + "-" + i}
bind:checked={checkedMappings[i]}
on:keypress={(e) => onInputKeypress(e)}
@ -461,7 +483,7 @@
<label class="flex gap-x-1">
<input
type="checkbox"
class="self-center mr-1"
class="mr-1 self-center"
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}
on:keypress={(e) => onInputKeypress(e)}
@ -494,36 +516,74 @@
<Tr t={$feedback} />
</div>
{/if}
<!--{#if keysToDeleteOnUnknown?.some(k => !! $tags[k])}
Mark as unknown (delete {keysToDeleteOnUnknown?.filter(k => !! $tags[k]).join(";")})
{/if}-->
<Popup shown={unknownModal}>
<h2 slot="header">
<Tr t={Translations.t.unknown.title} />
</h2>
<Tr t={Translations.t.unknown.explanation} />
<If condition={state.userRelatedState.showTags.map(v => v === "yes" || v === "full" || v === "always")}>
<div class="subtle">
<Tr t={Translations.t.unknown.removedKeys}/>
{#each $settableKeys as key}
<code>
<del>
{key}
</del>
</code>
{/each}
</div>
</If>
<div class="flex justify-end w-full" slot="footer">
<button on:click={() => unknownModal.set(false)}>
<Tr t={Translations.t.unknown.keep} />
</button>
<button class="primary" on:click={() => {unknownModal.set(false); clearAnswer()}}>
<Tr t={Translations.t.unknown.clear} />
</button>
</div>
</Popup>
<div
class="sticky bottom-0 flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap"
class="sticky bottom-0 flex justify-between flex-wrap"
style="z-index: 11"
>
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel" />
<slot name="save-button" {selectedTags}>
{#if config.freeform?.key && allowDeleteOfFreeform && !checkedMappings?.some((m) => m) && !$freeformInput && !$freeformInputUnvalidated && $tags[config.freeform.key]}
<button
class="primary flex"
on:click|stopPropagation|preventDefault={() => onSave()}
>
<TrashIcon class="h-6 w-6 text-red-500" />
<Tr t={Translations.t.general.eraseValue} />
</button>
{:else}
<button
on:click={() => onSave()}
class={twJoin(
{#if $settableKeys && $isKnown && !matchesEmpty }
<button class="as-link small text-sm" on:click={() => unknownModal.set(true)}>
<Tr t={Translations.t.unknown.markUnknown} />
</button>
{/if}
<div class="flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap self-end flex-grow">
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel" />
<slot name="save-button" {selectedTags}>
{#if config.freeform?.key && !checkedMappings?.some((m) => m) && !$freeformInput && !$freeformInputUnvalidated && $tags[config.freeform.key]}
<button
class="primary flex"
on:click|stopPropagation|preventDefault={() => onSave()}
>
<TrashIcon class="h-6 w-6 text-red-500" />
<Tr t={Translations.t.general.eraseValue} />
</button>
{:else}
<button
on:click={() => onSave()}
class={twJoin(
selectedTags === undefined ? "disabled" : "button-shadow",
"primary"
)}
>
<Tr t={Translations.t.general.save} />
</button>
{/if}
</slot>
>
<Tr t={Translations.t.general.save} />
</button>
{/if}
</slot>
</div>
</div>
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
<span class="flex flex-wrap justify-between">

View file

@ -28,8 +28,6 @@
export let selectedTags: UploadableTag = undefined
export let extraTags: UIEventSource<Record<string, string>> = new UIEventSource({})
export let allowDeleteOfFreeform: boolean = true
let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
</script>
@ -40,7 +38,6 @@
{selectedElement}
{layer}
{selectedTags}
{allowDeleteOfFreeform}
{extraTags}
>
<slot name="cancel" slot="cancel" />