forked from MapComplete/MapComplete
refactoring: fix basic flow to add a new point
This commit is contained in:
parent
52a0810ea9
commit
0241f89d3d
109 changed files with 1931 additions and 1446 deletions
239
UI/Popup/AddNewPoint/AddNewPoint.svelte
Normal file
239
UI/Popup/AddNewPoint/AddNewPoint.svelte
Normal file
|
@ -0,0 +1,239 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* This component ties together all the steps that are needed to create a new point.
|
||||
* There are many subcomponents which help with that
|
||||
*/
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import PresetList from "./PresetList.svelte";
|
||||
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import SubtleButton from "../../Base/SubtleButton.svelte";
|
||||
import FromHtml from "../../Base/FromHtml.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import TagHint from "../TagHint.svelte";
|
||||
import { And } from "../../../Logic/Tags/And.js";
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte";
|
||||
import Constants from "../../../Models/Constants.js";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import LoginButton from "../../Base/LoginButton.svelte";
|
||||
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte";
|
||||
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||
import { OsmObject } from "../../../Logic/Osm/OsmObject";
|
||||
import { Tag } from "../../../Logic/Tags/Tag";
|
||||
import type { WayId } from "../../../Models/OsmFeature";
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils";
|
||||
import Loading from "../../Base/Loading.svelte";
|
||||
|
||||
export let coordinate: { lon: number, lat: number };
|
||||
export let state: SpecialVisualizationState;
|
||||
|
||||
let selectedPreset: { preset: PresetConfig, layer: LayerConfig, icon: string, tags: Record<string, string> } = undefined;
|
||||
|
||||
let confirmedCategory = false;
|
||||
$: if (selectedPreset === undefined) {
|
||||
confirmedCategory = false;
|
||||
creating = false
|
||||
}
|
||||
|
||||
let flayer: FilteredLayer = undefined;
|
||||
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined;
|
||||
let layerHasFilters: Store<boolean> | undefined = undefined;
|
||||
|
||||
$:{
|
||||
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id);
|
||||
layerIsDisplayed = flayer?.isDisplayed;
|
||||
layerHasFilters = flayer?.hasFilter;
|
||||
}
|
||||
const t = Translations.t.general.add;
|
||||
|
||||
const zoom = state.mapProperties.zoom;
|
||||
|
||||
let preciseCoordinate: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(undefined);
|
||||
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined);
|
||||
|
||||
let creating = false;
|
||||
|
||||
/**
|
||||
* Call when the user should restart the flow by clicking on the map, e.g. because they disabled filters.
|
||||
* Will delete the lastclick-location
|
||||
*/
|
||||
function abort() {
|
||||
state.selectedElement.setData(undefined);
|
||||
// When aborted, we force the contributors to place the pin _again_
|
||||
// This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map
|
||||
state.lastClickObject.features.setData([]);
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
creating = true;
|
||||
const location: { lon: number; lat: number } = preciseCoordinate.data;
|
||||
const snapTo: WayId | undefined = <WayId>snappedToObject.data;
|
||||
const tags: Tag[] = selectedPreset.preset.tags;
|
||||
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags);
|
||||
|
||||
const snapToWay = snapTo === undefined ? undefined : await OsmObject.DownloadObjectAsync(snapTo, 0);
|
||||
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
|
||||
theme: state.layout?.id ?? "unkown",
|
||||
changeType: "create",
|
||||
snapOnto: snapToWay
|
||||
});
|
||||
await state.changes.applyAction(newElementAction);
|
||||
const newId = newElementAction.newElementId;
|
||||
state.newFeatures.features.data.push({
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: newId,
|
||||
...TagUtils.KVtoProperties(tags)
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [location.lon, location.lat]
|
||||
}
|
||||
});
|
||||
state.newFeatures.features.ping();
|
||||
console.log("New features:", state.newFeatures.features.data )
|
||||
{
|
||||
// Set some metainfo
|
||||
const tagsStore = state.featureProperties.getStore(newId);
|
||||
const properties = tagsStore.data;
|
||||
if (snapTo) {
|
||||
// metatags (starting with underscore) are not uploaded, so we can safely mark this
|
||||
properties["_referencing_ways"] = `["${snapTo}"]`;
|
||||
}
|
||||
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);
|
||||
abort();
|
||||
state.selectedElement.setData(feature);
|
||||
state.selectedLayer.setData(selectedPreset.layer);
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
|
||||
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
|
||||
</LoginButton>
|
||||
|
||||
{#if $zoom < Constants.minZoomLevelToAddNewPoint}
|
||||
<div class="alert">
|
||||
<Tr t={Translations.t.general.add.zoomInFurther}></Tr>
|
||||
</div>
|
||||
{:else if selectedPreset === undefined}
|
||||
<!-- First, select the correct preset -->
|
||||
<PresetList {state} on:select={event => {selectedPreset = event.detail}}></PresetList>
|
||||
|
||||
|
||||
{:else if !$layerIsDisplayed}
|
||||
<!-- Check that the layer is enabled, so that we don't add a duplicate -->
|
||||
<div class="alert flex justify-center items-center">
|
||||
<EyeOffIcon class="w-8" />
|
||||
<Tr t={Translations.t.general.add.layerNotEnabled
|
||||
.Subs({ layer: selectedPreset.layer.name })
|
||||
} />
|
||||
</div>
|
||||
|
||||
<SubtleButton on:click={() => {
|
||||
layerIsDisplayed.setData(true)
|
||||
abort()
|
||||
}}>
|
||||
<EyeIcon slot="image" class="w-8" />
|
||||
<Tr slot="message" t={Translations.t.general.add.enableLayer.Subs({name: selectedPreset.layer.name})} />
|
||||
</SubtleButton>
|
||||
<SubtleButton on:click={() => {
|
||||
abort()
|
||||
state.guistate.openFilterView(selectedPreset.layer) } }>
|
||||
<img src="./assets/svg/layers.svg" slot="image" class="w-6">
|
||||
<Tr slot="message" t={Translations.t.general.add.openLayerControl}></Tr>
|
||||
</SubtleButton>
|
||||
|
||||
|
||||
{:else if $layerHasFilters}
|
||||
<!-- Some filters are enabled. The feature to add might already be mapped, but hiddne -->
|
||||
<div class="alert flex justify-center items-center">
|
||||
<EyeOffIcon class="w-8" />
|
||||
<Tr t={Translations.t.general.add.disableFiltersExplanation} />
|
||||
</div>
|
||||
|
||||
<SubtleButton on:click={() => {
|
||||
abort()
|
||||
const flayer = state.layerState.filteredLayers.get(selectedPreset.layer.id)
|
||||
flayer.disableAllFilters()
|
||||
}
|
||||
}>
|
||||
<EyeOffIcon class="w-8" />
|
||||
<Tr slot="message" t={Translations.t.general.add.disableFilters}></Tr>
|
||||
</SubtleButton>
|
||||
|
||||
|
||||
<SubtleButton on:click={() => {
|
||||
abort()
|
||||
state.guistate.openFilterView(selectedPreset.layer)
|
||||
}
|
||||
}>
|
||||
<img src="./assets/svg/layers.svg" slot="image" class="w-6">
|
||||
<Tr slot="message" t={Translations.t.general.add.openLayerControl}></Tr>
|
||||
</SubtleButton>
|
||||
|
||||
{:else if !confirmedCategory }
|
||||
<!-- Second, confirm the category -->
|
||||
<Tr t={Translations.t.general.add.confirmIntro.Subs({title: selectedPreset.preset.title})}></Tr>
|
||||
|
||||
|
||||
{#if selectedPreset.preset.description}
|
||||
<Tr t={selectedPreset.preset.description} />
|
||||
{/if}
|
||||
|
||||
{#if selectedPreset.preset.exampleImages}
|
||||
<h4>
|
||||
{#if selectedPreset.preset.exampleImages.length == 1}
|
||||
<Tr t={Translations.t.general.example} />
|
||||
{:else}
|
||||
<Tr t={Translations.t.general.examples } />
|
||||
{/if}
|
||||
</h4>
|
||||
<span class="flex flex-wrap items-stretch">
|
||||
{#each selectedPreset.preset.exampleImages as src}
|
||||
<img {src} class="h-64 m-1 w-auto rounded-lg">
|
||||
{/each}
|
||||
</span>
|
||||
{/if}
|
||||
<TagHint embedIn={tags => t.presetInfo.Subs({tags})} osmConnection={state.osmConnection}
|
||||
tags={new And(selectedPreset.preset.tags)}></TagHint>
|
||||
|
||||
|
||||
<SubtleButton on:click={() => confirmedCategory = true}>
|
||||
<div slot="image" class="relative">
|
||||
<FromHtml src={selectedPreset.icon}></FromHtml>
|
||||
<img class="absolute bottom-0 right-0 w-4 h-4" src="./assets/svg/confirm.svg">
|
||||
</div>
|
||||
<div slot="message">
|
||||
<Tr t={selectedPreset.text}></Tr>
|
||||
</div>
|
||||
</SubtleButton>
|
||||
<SubtleButton on:click={() => selectedPreset = undefined}>
|
||||
<img src="./assets/svg/back.svg" class="w-8 h-8" slot="image">
|
||||
<div slot="message">
|
||||
<Tr t={t.backToSelect} />
|
||||
</div>
|
||||
</SubtleButton>
|
||||
{:else if !creating}
|
||||
<NewPointLocationInput value={preciseCoordinate} snappedTo={snappedToObject} {state} {coordinate}
|
||||
targetLayer={selectedPreset.layer}
|
||||
snapToLayers={selectedPreset.preset.preciseInput.snapToLayers}></NewPointLocationInput>
|
||||
<SubtleButton on:click={confirm}>
|
||||
<span slot="message">Confirm location</span>
|
||||
</SubtleButton>
|
||||
{:else}
|
||||
<Loading>Creating point...</Loading>
|
||||
{/if}
|
||||
</LoginToggle>
|
88
UI/Popup/AddNewPoint/PresetList.svelte
Normal file
88
UI/Popup/AddNewPoint/PresetList.svelte
Normal file
|
@ -0,0 +1,88 @@
|
|||
<script lang="ts">
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import SubtleButton from "../../Base/SubtleButton.svelte";
|
||||
import { Translation } from "../../i18n/Translation";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import { ImmutableStore } from "../../../Logic/UIEventSource";
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import FromHtml from "../../Base/FromHtml.svelte";
|
||||
|
||||
/**
|
||||
* This component lists all the presets and allows the user to select one
|
||||
*/
|
||||
export let state: SpecialVisualizationState;
|
||||
let layout: LayoutConfig = state.layout;
|
||||
let presets: {
|
||||
preset: PresetConfig,
|
||||
layer: LayerConfig,
|
||||
text: Translation,
|
||||
icon: string,
|
||||
tags: Record<string, string>
|
||||
}[] = [];
|
||||
|
||||
for (const layer of layout.layers) {
|
||||
const flayer = state.layerState.filteredLayers.get(layer.id);
|
||||
if (flayer.isDisplayed.data === false) {
|
||||
// The layer is not displayed...
|
||||
if (!state.featureSwitches.featureSwitchFilter.data) {
|
||||
// ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
|
||||
continue;
|
||||
}
|
||||
|
||||
if (layer.name === undefined) {
|
||||
// this layer can never be toggled on in any case, so we skip the presets
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const preset of layer.presets) {
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
|
||||
const icon: string =
|
||||
layer.mapRendering[0]
|
||||
.RenderIcon(new ImmutableStore<any>(tags), false)
|
||||
.html.SetClass("w-12 h-12 block relative")
|
||||
.ConstructElement().innerHTML;
|
||||
|
||||
const description = preset.description?.FirstSentence();
|
||||
|
||||
const simplified = {
|
||||
preset,
|
||||
layer,
|
||||
icon,
|
||||
description,
|
||||
tags,
|
||||
text: Translations.t.general.add.addNew.Subs({ category: preset.title }, preset.title["context"])
|
||||
};
|
||||
presets.push(simplified);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: {preset: PresetConfig, layer: LayerConfig, icon: string, tags: Record<string, string>} }>();
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Tr t={Translations.t.general.add.intro} />
|
||||
{#each presets as preset}
|
||||
<SubtleButton on:click={() => dispatch("select", preset)}>
|
||||
<FromHtml slot="image" src={preset.icon}></FromHtml>
|
||||
<div slot="message">
|
||||
|
||||
<b>
|
||||
<Tr t={preset.text} />
|
||||
</b>
|
||||
{#if preset.description}
|
||||
<Tr t={preset.description}/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</SubtleButton>
|
||||
{/each}
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue