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
				
			
		| 
						 | 
				
			
			@ -408,6 +408,12 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
 | 
			
		|||
            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")) {
 | 
			
		||||
            json.tagRenderings.push({
 | 
			
		||||
                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)))
 | 
			
		||||
 | 
			
		||||
        if (!usedSpecialFunctions.has("all_tags")) {
 | 
			
		||||
| 
						 | 
				
			
			@ -893,7 +907,7 @@ class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> {
 | 
			
		|||
        const pr = json.pointRendering?.[0]
 | 
			
		||||
        if (pr) {
 | 
			
		||||
            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" })
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@
 | 
			
		|||
    dialogClass += " h-full-child"
 | 
			
		||||
  }
 | 
			
		||||
  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"
 | 
			
		||||
  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 { FixedUiElement } from "../Base/FixedUiElement"
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
    public static initList(): SpecialVisualization[] {
 | 
			
		||||
        return [
 | 
			
		||||
| 
						 | 
				
			
			@ -348,6 +365,7 @@ export class UISpecialVisualisations {
 | 
			
		|||
            new IfNothingKnown(),
 | 
			
		||||
            new ShareLinkViz(),
 | 
			
		||||
            new AddNewPointVis(),
 | 
			
		||||
            new CreateCopyVis(),
 | 
			
		||||
            new Translated(),
 | 
			
		||||
            new BracedVis(),
 | 
			
		||||
            new TitleVis(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue