forked from MapComplete/MapComplete
Feature: add 'copy-button' capabilities
This commit is contained in:
parent
3d442d0558
commit
2f89f9203d
6 changed files with 204 additions and 10 deletions
|
@ -23,6 +23,13 @@
|
||||||
"intro": "Get in touch with other people to get to know them, learn from them, …",
|
"intro": "Get in touch with other people to get to know them, learn from them, …",
|
||||||
"title": "Get in touch with others"
|
"title": "Get in touch with others"
|
||||||
},
|
},
|
||||||
|
"copy": {
|
||||||
|
"button": "Create a copy",
|
||||||
|
"confirm": "Create copy at the specified location",
|
||||||
|
"intro": "Creating a copy will create a new POI on the map with the same properties as the current object.",
|
||||||
|
"loading": "Creating a copy...",
|
||||||
|
"title": "Create a copy"
|
||||||
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"cannotBeDeleted": "This feature can not be deleted",
|
"cannotBeDeleted": "This feature can not be deleted",
|
||||||
|
|
|
@ -1766,6 +1766,10 @@ input[type="range"].range-lg::-moz-range-thumb {
|
||||||
height: 0.875rem;
|
height: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-3\/4 {
|
||||||
|
height: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
.h-32 {
|
.h-32 {
|
||||||
height: 8rem;
|
height: 8rem;
|
||||||
}
|
}
|
||||||
|
@ -8420,10 +8424,6 @@ svg.apply-fill path {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm\:grow-0 {
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:grid-cols-2 {
|
.sm\:grid-cols-2 {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
@ -8436,10 +8436,6 @@ svg.apply-fill path {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm\:justify-start {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:gap-y-3 {
|
.sm\:gap-y-3 {
|
||||||
row-gap: 0.75rem;
|
row-gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -408,6 +408,12 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
||||||
delete json.allowSplit
|
delete json.allowSplit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json.allowCopy && !usedSpecialFunctions.has("create_copy")) {
|
||||||
|
json.tagRenderings.push({
|
||||||
|
id: "create_copy",
|
||||||
|
render: { "*": "{create_copy()}" },
|
||||||
|
})
|
||||||
|
}
|
||||||
if (json.allowMove && !usedSpecialFunctions.has("move_button")) {
|
if (json.allowMove && !usedSpecialFunctions.has("move_button")) {
|
||||||
json.tagRenderings.push({
|
json.tagRenderings.push({
|
||||||
id: "move-button",
|
id: "move-button",
|
||||||
|
@ -421,6 +427,14 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const spacerIndex = json.tagRenderings.findIndex(item => item?.["id"] === "spacer")
|
||||||
|
if (spacerIndex >= 0) {
|
||||||
|
json.tagRenderings.splice(spacerIndex, 1)
|
||||||
|
}
|
||||||
|
json.tagRenderings.push({
|
||||||
|
id: "spacer",
|
||||||
|
render: { "*": "<div class='m-4'/>" },
|
||||||
|
})
|
||||||
json.tagRenderings.push(...this._addedByDefault.filter((tr) => !allIds.has(tr.id)))
|
json.tagRenderings.push(...this._addedByDefault.filter((tr) => !allIds.has(tr.id)))
|
||||||
|
|
||||||
if (!usedSpecialFunctions.has("all_tags")) {
|
if (!usedSpecialFunctions.has("all_tags")) {
|
||||||
|
@ -893,7 +907,7 @@ class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> {
|
||||||
const pr = json.pointRendering?.[0]
|
const pr = json.pointRendering?.[0]
|
||||||
if (pr) {
|
if (pr) {
|
||||||
pr.iconBadges ??= []
|
pr.iconBadges ??= []
|
||||||
if (!pr.iconBadges.some((ti) => ti.if === "_favourite=yes")) {
|
if (!pr.iconBadges.some((ti) => ti["if"] === "_favourite=yes")) {
|
||||||
pr.iconBadges.push({ if: "_favourite=yes", then: "circle:white;heart:red" })
|
pr.iconBadges.push({ if: "_favourite=yes", then: "circle:white;heart:red" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
dialogClass += " h-full-child"
|
dialogClass += " h-full-child"
|
||||||
}
|
}
|
||||||
let bodyClass =
|
let bodyClass =
|
||||||
bodyPadding + " h-full space-y-4 flex-1 overflow-y-auto overscroll-contain background-normal"
|
bodyPadding + " h-full max-h-leave-room space-y-4 flex-1 overflow-y-auto overscroll-contain background-normal"
|
||||||
|
|
||||||
let headerClass = "flex justify-between items-center p-2 px-4 md:px-5 rounded-t-lg"
|
let headerClass = "flex justify-between items-center p-2 px-4 md:px-5 rounded-t-lg"
|
||||||
if (!$$slots.header) {
|
if (!$$slots.header) {
|
||||||
|
|
159
src/UI/Popup/AddNewPoint/CreateCopy.svelte
Normal file
159
src/UI/Popup/AddNewPoint/CreateCopy.svelte
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* A special visualisation element which allows to create a full copy (including all tags) of a node
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Popup from "../../Base/Popup.svelte"
|
||||||
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||||
|
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||||
|
import type { Feature } from "geojson"
|
||||||
|
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"
|
||||||
|
import NextButton from "../../Base/NextButton.svelte"
|
||||||
|
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||||
|
import type { WayId } from "../../../Models/OsmFeature"
|
||||||
|
import { Tag } from "../../../Logic/Tags/Tag"
|
||||||
|
import { twJoin } from "tailwind-merge"
|
||||||
|
import Translations from "../../i18n/Translations"
|
||||||
|
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||||
|
import Tr from "../../Base/Tr.svelte"
|
||||||
|
import ThemeViewState from "../../../Models/ThemeViewState"
|
||||||
|
import TagExplanation from "../TagExplanation.svelte"
|
||||||
|
import { And } from "../../../Logic/Tags/And"
|
||||||
|
import Loading from "../../Base/Loading.svelte"
|
||||||
|
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
|
||||||
|
import DocumentDuplicate from "@babeard/svelte-heroicons/solid/DocumentDuplicate"
|
||||||
|
|
||||||
|
export let state: ThemeViewState
|
||||||
|
export let layer: LayerConfig
|
||||||
|
|
||||||
|
export let tags: UIEventSource<Record<string, string>>
|
||||||
|
export let feature: Feature
|
||||||
|
let c = GeoOperations.centerpointCoordinates(feature)
|
||||||
|
let coordinate: { lon: number; lat: number } = { lat: c[1], lon: c[0] }
|
||||||
|
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
|
||||||
|
|
||||||
|
let snappedToObject: UIEventSource<WayId> = new UIEventSource<WayId>(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
|
||||||
|
|
||||||
|
const forbiddenKeys = new Set(["id", "timestamp", "user", "changeset", "version", "uid"])
|
||||||
|
let asTags = tags.map(tgs => Object.keys(tgs).filter(k => !k.startsWith("_") && !forbiddenKeys.has(k)).map(k => new Tag(k, tgs[k])))
|
||||||
|
let showPopup: UIEventSource<boolean> = new UIEventSource(false)
|
||||||
|
|
||||||
|
const showTags = state.userRelatedState.showTagsB
|
||||||
|
let creatingCopy = new UIEventSource(false)
|
||||||
|
|
||||||
|
const t = Translations.t.copy
|
||||||
|
|
||||||
|
async function createCopy() {
|
||||||
|
creatingCopy.set(true)
|
||||||
|
const tags: Tag[] = asTags.data
|
||||||
|
const location: { lon: number; lat: number } = preciseCoordinate.data
|
||||||
|
const newElementAction = new CreateNewNodeAction(
|
||||||
|
tags, location.lat, location.lon, {
|
||||||
|
theme: state.theme?.id ?? "unkown",
|
||||||
|
changeType: "copy",
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
if (!tagsStore) {
|
||||||
|
console.error("Bug: no tagsStore found for", newId)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Set some metainfo
|
||||||
|
const properties = tagsStore.data
|
||||||
|
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")
|
||||||
|
creatingCopy.set(false)
|
||||||
|
showPopup.set(false)
|
||||||
|
state.selectedElement.setData(feature)
|
||||||
|
tagsStore.ping()
|
||||||
|
state.mapProperties.location.setData(location)
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Popup shown={showPopup} fullscreen>
|
||||||
|
<Tr slot="header" t={t.title} />
|
||||||
|
|
||||||
|
{#if $creatingCopy}
|
||||||
|
<div class="h-full flex flex-col justify-center">
|
||||||
|
<div class="h-fit flex justify-center">
|
||||||
|
<Loading>
|
||||||
|
<Tr t={t.loading} />
|
||||||
|
|
||||||
|
</Loading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
|
||||||
|
<div class="h-full flex flex-col gap-y-4 justify-between pb-4">
|
||||||
|
<Tr t={t.intro} />
|
||||||
|
|
||||||
|
<div class="relative" style="height: calc(100% - 7rem);">
|
||||||
|
<NewPointLocationInput
|
||||||
|
on:click={() => {
|
||||||
|
preciseInputIsTapped = true
|
||||||
|
}}
|
||||||
|
value={preciseCoordinate}
|
||||||
|
snappedTo={snappedToObject}
|
||||||
|
{state}
|
||||||
|
{coordinate}
|
||||||
|
targetLayer={layer}
|
||||||
|
presetProperties={$asTags}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class={twJoin(
|
||||||
|
!preciseInputIsTapped && "hidden",
|
||||||
|
"absolute top-0 flex w-full justify-center p-12"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<!-- This is an _extra_ button that appears when the map is tapped - see usertest 2023-01-07 -->
|
||||||
|
<NextButton on:click={() => createCopy()} clss="primary w-fit">
|
||||||
|
<div class="flex w-full justify-end gap-x-2">
|
||||||
|
<Tr t={t.confirm} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</NextButton>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 left-0 p-4">
|
||||||
|
<OpenBackgroundSelectorButton {state} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full justify-end">
|
||||||
|
<NextButton clss="primary shrink-0" on:click={() => createCopy()}>
|
||||||
|
<DocumentDuplicate class="w-8 mx-2" />
|
||||||
|
<Tr t={t.confirm} />
|
||||||
|
|
||||||
|
</NextButton>
|
||||||
|
</div>
|
||||||
|
{#if showTags}
|
||||||
|
<div class="subtle">
|
||||||
|
<TagExplanation tagsFilter={new And($asTags)} linkToWiki />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</Popup>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
|
||||||
|
<button on:click={() => showPopup.set(true)}>
|
||||||
|
<DocumentDuplicate class="w-4" />
|
||||||
|
<Tr t={t.button} />
|
||||||
|
</button>
|
||||||
|
</div>
|
|
@ -18,6 +18,7 @@ import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
import { Translation } from "../i18n/Translation"
|
import { Translation } from "../i18n/Translation"
|
||||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||||
import { default as FeatureTitle } from "../Popup/Title.svelte"
|
import { default as FeatureTitle } from "../Popup/Title.svelte"
|
||||||
|
import CreateCopy from "../Popup/AddNewPoint/CreateCopy.svelte"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thin wrapper around QuestionBox.svelte to include it into the special Visualisations
|
* Thin wrapper around QuestionBox.svelte to include it into the special Visualisations
|
||||||
|
@ -336,6 +337,22 @@ class BracedVis extends SpecialVisualization {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCopyVis extends SpecialVisualizationSvelte {
|
||||||
|
group= "UI"
|
||||||
|
funcName="create_copy"
|
||||||
|
docs = "Allow to create a copy of the current element"
|
||||||
|
args = []
|
||||||
|
constr(state: SpecialVisualizationState, tags: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): SvelteUIElement {
|
||||||
|
|
||||||
|
try{
|
||||||
|
console.log(">>> create_copy invoked")
|
||||||
|
return new SvelteUIElement(CreateCopy, {state, tags, argument, feature, layer} )
|
||||||
|
}catch (e) {
|
||||||
|
console.error(">>> failed",e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class UISpecialVisualisations {
|
export class UISpecialVisualisations {
|
||||||
public static initList(): SpecialVisualization[] {
|
public static initList(): SpecialVisualization[] {
|
||||||
return [
|
return [
|
||||||
|
@ -348,6 +365,7 @@ export class UISpecialVisualisations {
|
||||||
new IfNothingKnown(),
|
new IfNothingKnown(),
|
||||||
new ShareLinkViz(),
|
new ShareLinkViz(),
|
||||||
new AddNewPointVis(),
|
new AddNewPointVis(),
|
||||||
|
new CreateCopyVis(),
|
||||||
new Translated(),
|
new Translated(),
|
||||||
new BracedVis(),
|
new BracedVis(),
|
||||||
new TitleVis(),
|
new TitleVis(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue