forked from MapComplete/MapComplete
Feature: allow to move and snap to a layer, fix #2120
This commit is contained in:
parent
eb89427bfc
commit
fdedb75954
34 changed files with 824 additions and 301 deletions
|
@ -18,6 +18,7 @@
|
|||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import type { WayId } from "../../Models/OsmFeature"
|
||||
|
||||
/**
|
||||
* An advanced location input, which has support to:
|
||||
|
@ -45,11 +46,16 @@
|
|||
}
|
||||
export let snapToLayers: string[] | undefined = undefined
|
||||
export let targetLayer: LayerConfig | undefined = undefined
|
||||
/**
|
||||
* If a 'targetLayer' is given, objects of this layer will be shown as well to avoid duplicates
|
||||
* If you want to hide some of them, blacklist them here
|
||||
*/
|
||||
export let dontShow: string[] = []
|
||||
export let maxSnapDistance: number = undefined
|
||||
export let presetProperties: Tag[] = []
|
||||
let presetPropertiesUnpacked = TagUtils.KVtoProperties(presetProperties)
|
||||
|
||||
export let snappedTo: UIEventSource<string | undefined>
|
||||
export let snappedTo: UIEventSource<WayId | undefined>
|
||||
|
||||
let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
|
||||
lon: number
|
||||
|
@ -57,7 +63,7 @@
|
|||
}>(undefined)
|
||||
|
||||
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let initialMapProperties: Partial<MapProperties> & { location } = {
|
||||
export let mapProperties: Partial<MapProperties> & { location } = {
|
||||
zoom: new UIEventSource<number>(19),
|
||||
maxbounds: new UIEventSource(undefined),
|
||||
/*If no snapping needed: the value is simply the map location;
|
||||
|
@ -77,8 +83,11 @@
|
|||
|
||||
if (targetLayer) {
|
||||
// Show already existing items
|
||||
const featuresForLayer = state.perLayer.get(targetLayer.id)
|
||||
let featuresForLayer: FeatureSource = state.perLayer.get(targetLayer.id)
|
||||
if (featuresForLayer) {
|
||||
if (dontShow) {
|
||||
featuresForLayer = new StaticFeatureSource(featuresForLayer.features.map(feats => feats.filter(f => dontShow.indexOf(f.properties.id) < 0)))
|
||||
}
|
||||
new ShowDataLayer(map, {
|
||||
layer: targetLayer,
|
||||
features: featuresForLayer,
|
||||
|
@ -104,13 +113,13 @@
|
|||
const snappedLocation = new SnappingFeatureSource(
|
||||
new FeatureSourceMerger(...Utils.NoNull(snapSources)),
|
||||
// We snap to the (constantly updating) map location
|
||||
initialMapProperties.location,
|
||||
mapProperties.location,
|
||||
{
|
||||
maxDistance: maxSnapDistance ?? 15,
|
||||
allowUnsnapped: true,
|
||||
snappedTo,
|
||||
snapLocation: value,
|
||||
}
|
||||
},
|
||||
)
|
||||
const withCorrectedAttributes = new StaticFeatureSource(
|
||||
snappedLocation.features.mapD((feats) =>
|
||||
|
@ -124,8 +133,8 @@
|
|||
...f,
|
||||
properties,
|
||||
}
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
// The actual point to be created, snapped at the new location
|
||||
new ShowDataLayer(map, {
|
||||
|
@ -139,7 +148,7 @@
|
|||
<LocationInput
|
||||
{map}
|
||||
on:click
|
||||
mapProperties={initialMapProperties}
|
||||
{mapProperties}
|
||||
value={preciseLocation}
|
||||
initialCoordinate={coordinate}
|
||||
maxDistanceInMeters={50}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
import Relocation from "../../assets/svg/Relocation.svelte"
|
||||
import LockClosed from "@babeard/svelte-heroicons/solid/LockClosed"
|
||||
import Key from "@babeard/svelte-heroicons/solid/Key"
|
||||
import Snap from "../../assets/svg/Snap.svelte"
|
||||
|
||||
/**
|
||||
* Renders a single icon.
|
||||
|
@ -152,6 +153,8 @@
|
|||
<LockClosed class={clss} {color} />
|
||||
{:else if icon === "key"}
|
||||
<Key class={clss} {color} />
|
||||
{:else if icon === "snap"}
|
||||
<Snap class={clss} />
|
||||
{:else if Utils.isEmoji(icon)}
|
||||
<span style={`font-size: ${emojiHeight}; line-height: ${emojiHeight}`}>
|
||||
{icon}
|
||||
|
|
|
@ -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 Geosearch from "../BigComponents/Geosearch.svelte"
|
||||
import If from "../Base/If.svelte"
|
||||
|
@ -21,6 +20,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
|
||||
|
||||
|
@ -36,20 +37,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])
|
||||
}
|
||||
|
@ -57,8 +60,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" />
|
||||
|
@ -81,7 +84,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"
|
||||
|
@ -93,10 +96,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} />
|
||||
|
@ -116,7 +125,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"
|
||||
}}
|
||||
>
|
||||
|
@ -155,5 +164,5 @@
|
|||
</span>
|
||||
</AccordionSingle>
|
||||
{/if}
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
</LoginToggle>
|
||||
{/if}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1995,35 +1995,8 @@ export default class SpecialVisualizations {
|
|||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
const translation = tagSource.map((tags) => {
|
||||
const presets = state.layout.getMatchingLayer(tags)?.presets
|
||||
if(!presets){
|
||||
return undefined
|
||||
}
|
||||
const matchingPresets = presets
|
||||
.filter((pr) => pr.description !== undefined)
|
||||
.filter((pr) => new And(pr.tags).matchesProperties(tags))
|
||||
let mostShadowed = matchingPresets[0]
|
||||
let mostShadowedTags = new And(mostShadowed.tags)
|
||||
for (let i = 1; i < matchingPresets.length; i++) {
|
||||
const pr = matchingPresets[i]
|
||||
const prTags = new And(pr.tags)
|
||||
if (mostShadowedTags.shadows(prTags)) {
|
||||
if (!prTags.shadows(mostShadowedTags)) {
|
||||
// We have a new most shadowed item
|
||||
mostShadowed = pr
|
||||
mostShadowedTags = prTags
|
||||
} else {
|
||||
// Both shadow each other: abort
|
||||
mostShadowed = undefined
|
||||
break
|
||||
}
|
||||
} else if (!prTags.shadows(mostShadowedTags)) {
|
||||
// The new contender does not win, but it might defeat the current contender
|
||||
mostShadowed = undefined
|
||||
break
|
||||
}
|
||||
}
|
||||
return mostShadowed?.description ?? matchingPresets[0]?.description
|
||||
const layer = state.layout.getMatchingLayer(tags)
|
||||
return layer?.getMostMatchingPreset(tags)?.description
|
||||
})
|
||||
return new VariableUiElement(translation)
|
||||
}
|
||||
|
|
|
@ -417,6 +417,9 @@ export class TypedTranslation<T extends Record<string, any>> extends Translation
|
|||
key: string,
|
||||
replaceWith: Translation
|
||||
): TypedTranslation<Omit<T, K>> {
|
||||
if(replaceWith === undefined){
|
||||
return this
|
||||
}
|
||||
const newTranslations: Record<string, string> = {}
|
||||
const toSearch = "{" + key + "}"
|
||||
const missingLanguages = new Set<string>(Object.keys(this.translations))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue