Refactoring: fix import buttons (WIP)

This commit is contained in:
Pieter Vander Vennet 2023-05-30 02:52:22 +02:00
parent 52b54d8b08
commit 8f942f0163
28 changed files with 970 additions and 779 deletions

View file

@ -0,0 +1,93 @@
<script lang="ts">
/**
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
* They show some default components
*/
import ImportFlow from "./ImportFlow";
import LoginToggle from "../../Base/LoginToggle.svelte";
import BackButton from "../../Base/BackButton.svelte";
import Translations from "../../i18n/Translations";
import Tr from "../../Base/Tr.svelte";
import NextButton from "../../Base/NextButton.svelte";
import {createEventDispatcher} from "svelte";
import Loading from "../../Base/Loading.svelte";
import {And} from "../../../Logic/Tags/And";
import TagHint from "../TagHint.svelte";
import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
import {Store} from "../../../Logic/UIEventSource";
export let importFlow: ImportFlow
let state = importFlow.state
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
const isLoading = state.dataIsLoading
const dispatch = createEventDispatcher<{ confirm }>()
const canBeImported = importFlow.canBeImported()
const tags : Store<TagsFilter> = importFlow.tagsToApply.map(tags => new And(tags))
const isDisplayed = importFlow.targetLayer.isDisplayed
const hasFilter = importFlow.targetLayer.appliedFilters
</script>
<LoginToggle {state}>
{#if currentFlowStep === "start"}
<NextButton clss="primary w-full" on:click={() => currentFlowStep = "confirm"}>
<slot name="start-flow-text">
{#if importFlow?.args?.icon}
<img class="w-8 h-8" src={importFlow.args.icon}/>
{/if}
{importFlow.args.text}
</slot>
</NextButton>
{:else if $canBeImported !== true && $canBeImported !== undefined}
<Tr cls="alert w-full flex justify-center" t={$canBeImported.error}/>
{#if $canBeImported.extraHelp}
<Tr t={$canBeImported.extraHelp}/>
{/if}
{:else if $isLoading}
<Loading>
<Tr t={Translations.t.general.add.stillLoading}/>
</Loading>
{:else if currentFlowStep === "confirm"}
<div class="h-full w-full flex flex-col">
<div class="w-full h-full">
<slot name="map"/>
</div>
<div class="flex flex-col-reverse md:flex-row">
<BackButton clss="w-full" on:click={() => currentFlowStep = "start"}>
<Tr t={Translations.t.general.back}/>
</BackButton>
<NextButton clss="primary w-full" on:click={() => {
currentFlowStep = "imported"
dispatch("confirm")
}}>
<span slot="image">
{#if importFlow.args.icon}
<img src={importFlow.args.icon}>
{/if}
</span>
<slot name="confirm-text">
{importFlow.args.text}
</slot>
</NextButton>
</div>
<div class="subtle">
<TagHint embedIn={str => Translations.t.general.add.import.importTags.Subs({tags: str})} {state}
tags={$tags}/>
</div>
</div>
{:else if currentFlowStep === "importing"}
<Loading/>
{:else if currentFlowStep === "imported"}
<div class="thanks w-full p-4">
<Tr t={Translations.t.general.add.import.hasBeenImported}/>
</div>
{/if}
</LoginToggle>

View file

@ -0,0 +1,199 @@
import {SpecialVisualizationState} from "../../SpecialVisualization";
import {Utils} from "../../../Utils";
import {Store, UIEventSource} from "../../../Logic/UIEventSource";
import {Tag} from "../../../Logic/Tags/Tag";
import TagApplyButton from "../TagApplyButton";
import {PointImportFlowArguments} from "./PointImportFlowState";
import {Translation} from "../../i18n/Translation";
import Translations from "../../i18n/Translations";
import {OsmConnection} from "../../../Logic/Osm/OsmConnection";
import FilteredLayer from "../../../Models/FilteredLayer";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import {LayerConfigJson} from "../../../Models/ThemeConfig/Json/LayerConfigJson";
import conflation_json from "../../../assets/layers/conflation/conflation.json";
export interface ImportFlowArguments {
readonly text: string
readonly tags: string
readonly targetLayer: string
readonly icon?: string
}
export class ImportFlowUtils {
public static importedIds = new Set<string>()
public static readonly conflationLayer = new LayerConfig(
<LayerConfigJson>conflation_json,
"all_known_layers",
true
)
public static readonly documentationGeneral = `\n\n\nNote that the contributor must zoom to at least zoomlevel 18 to be able to use this functionality.
It is only functional in official themes, but can be tested in unoffical themes.
#### Specifying which tags to copy or add
The argument \`tags\` of the import button takes a \`;\`-seperated list of tags to add (or the name of a property which contains a JSON-list of properties).
${Utils.Special_visualizations_tagsToApplyHelpText}
${Utils.special_visualizations_importRequirementDocs}
`
public static generalArguments = [{
name: "targetLayer",
doc: "The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements",
required: true,
},
{
name: "tags",
doc: "The tags to add onto the new object - see specification above. If this is a key (a single word occuring in the properties of the object), the corresponding value is taken and expanded instead",
required: true,
},
{
name: "text",
doc: "The text to show on the button",
defaultValue: "Import this data into OpenStreetMap",
},
{
name: "icon",
doc: "A nice icon to show in the button",
defaultValue: "./assets/svg/addSmall.svg",
},]
/**
* Given the tagsstore of the point which represents the challenge, creates a new store with tags that should be applied onto the newly created point,
*/
public static getTagsToApply(
originalFeatureTags: UIEventSource<any>,
args: { tags: string }
): Store<Tag[]> {
if (originalFeatureTags === undefined) {
return undefined
}
let newTags: Store<Tag[]>
const tags = args.tags
if (
tags.indexOf(" ") < 0 &&
tags.indexOf(";") < 0 &&
originalFeatureTags.data[tags] !== undefined
) {
// This is a property to expand...
const items: string = originalFeatureTags.data[tags]
console.debug(
"The import button is using tags from properties[" +
tags +
"] of this object, namely ",
items
)
newTags = TagApplyButton.generateTagsToApply(items, originalFeatureTags)
} else {
newTags = TagApplyButton.generateTagsToApply(tags, originalFeatureTags)
}
return newTags
}
/**
* Lists:
* - targetLayer
*
* Others (e.g.: snapOnto-layers) are not to be handled here
* @param argsRaw
*/
public static getLayerDependencies(argsRaw: string[]) {
const args: ImportFlowArguments = <any>Utils.ParseVisArgs(ImportFlowUtils.generalArguments, argsRaw)
return [args.targetLayer]
}
public static getLayerDependenciesWithSnapOnto(argSpec: {
name: string,
defaultValue?: string
}[], argsRaw: string[]): string[] {
const deps = ImportFlowUtils.getLayerDependencies(argsRaw)
const argsParsed: PointImportFlowArguments = <any>Utils.ParseVisArgs(argSpec, argsRaw)
const snapOntoLayers = argsParsed.snap_onto_layers?.split(";")?.map(l => l.trim()) ?? []
deps.push(...snapOntoLayers)
return deps
}
public static buildTagSpec(args: ImportFlowArguments, tagSource: Store<Record<string, string>>): Store<string> {
let tagSpec = args.tags
return tagSource.mapD(tags => {
if (
tagSpec.indexOf(" ") < 0 &&
tagSpec.indexOf(";") < 0 &&
tags[args.tags] !== undefined
) {
// This is probably a key
tagSpec = tags[args.tags]
console.debug(
"The import button is using tags from properties[" +
args.tags +
"] of this object, namely ",
tagSpec
)
}
return tagSpec
})
}
}
/**
* The ImportFlow dictates some aspects of the import flow, e.g. what type of map should be shown and, in the case of a preview map, what layers that should be added.
*
* This class works together closely with ImportFlow.svelte
*/
export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
public readonly state: SpecialVisualizationState;
public readonly args: ArgT;
public readonly targetLayer: FilteredLayer;
public readonly tagsToApply: Store<Tag[]>
constructor(state: SpecialVisualizationState, args: ArgT, tagsToApply: Store<Tag[]>) {
this.state = state;
this.args = args;
this.tagsToApply = tagsToApply;
this.targetLayer = state.layerState.filteredLayers.get(args.targetLayer)
}
/**
* Constructs a store that contains either 'true' or gives a translation with the reason why it cannot be imported
*/
public canBeImported(): Store<true | { error: Translation, extraHelp?: Translation }> {
const state = this.state
return state.featureSwitchIsTesting.map(isTesting => {
const t = Translations.t.general.add.import
const usesTestUrl = this.state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url
if (!state.layout.official && !(isTesting || usesTestUrl)) {
// Unofficial theme - imports not allowed
return {
error: t.officialThemesOnly,
extraHelp: t.howToTest
}
}
if (this.targetLayer === undefined) {
const e = `Target layer not defined: error in import button for theme: ${this.state.layout.id}: layer ${this.args.targetLayer} not found`
console.error(e)
return {error: new Translation({"*": e})}
}
if (state.mapProperties.zoom.data < 18) {
return {error: t.zoomInMore}
}
if(state.dataIsLoading.data){
return {error: Translations.t.general.add.stillLoading}
}
return undefined
}, [state.mapProperties.zoom, state.dataIsLoading])
}
}

View file

@ -0,0 +1,66 @@
import {Feature, Point} from "geojson";
import {UIEventSource} from "../../../Logic/UIEventSource";
import {SpecialVisualization, SpecialVisualizationState} from "../../SpecialVisualization";
import BaseUIElement from "../../BaseUIElement";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import SvelteUIElement from "../../Base/SvelteUIElement";
import PointImportFlow from "./PointImportFlow.svelte";
import {PointImportFlowArguments, PointImportFlowState} from "./PointImportFlowState";
import {Utils} from "../../../Utils";
import {ImportFlowUtils} from "./ImportFlow";
import Translations from "../../i18n/Translations";
/**
* The wrapper to make the special visualisation for the PointImportFlow
*/
export class ImportPointButtonViz implements SpecialVisualization {
public readonly funcName: string
public readonly docs: string | BaseUIElement
public readonly example?: string
public readonly args: { name: string; defaultValue?: string; doc: string }[]
constructor() {
this.funcName = "import_button"
this.docs = "This button will copy the point from an external dataset into OpenStreetMap" + ImportFlowUtils.documentationGeneral
this.args =
[...ImportFlowUtils.generalArguments,
{
name: "snap_onto_layers",
doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list",
},
{
name: "max_snap_distance",
doc: "The maximum distance that the imported point will be moved to snap onto a way in an already existing layer (in meters). This is previewed to the contributor, similar to the 'add new point'-action of MapComplete",
defaultValue: "5",
},
{
name: "note_id",
doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'",
},
{
name: "maproulette_id",
doc: "The property name of the maproulette_id - this is probably `mr_taskId`. If given, the maproulette challenge will be marked as fixed. Only use this if part of a maproulette-layer.",
},
]
}
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
if (feature.geometry.type !== "Point") {
return Translations.t.general.add.import.wrongType.SetClass("alert")
}
const baseArgs: PointImportFlowArguments = <any> Utils.ParseVisArgs(this.args, argument)
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource , baseArgs)
const importFlow = new PointImportFlowState(state, <Feature<Point>> feature, baseArgs, tagsToApply)
return new SvelteUIElement(
PointImportFlow, {
importFlow
}
)
}
getLayerDependencies(argsRaw: string[]): string[] {
return ImportFlowUtils.getLayerDependenciesWithSnapOnto(this.args, argsRaw)
}
}

View file

@ -0,0 +1,62 @@
<script lang="ts">
import ImportFlow from "./ImportFlow.svelte";
import {PointImportFlowState} from "./PointImportFlowState";
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import {UIEventSource} from "../../../Logic/UIEventSource";
import MapControlButton from "../../Base/MapControlButton.svelte";
import {Square3Stack3dIcon} from "@babeard/svelte-heroicons/solid";
export let importFlow: PointImportFlowState
const state = importFlow.state;
const args = importFlow.args
// The following variables are used for the map
const targetLayer: LayerConfig = state.layout.layers.find(l => l.id === args.targetLayer)
const snapToLayers: string[] | undefined = args.snap_onto_layers?.split(",")?.map(l => l.trim()) ?? []
const maxSnapDistance: number = Number(args.max_snap_distance ?? 25) ?? 25;
const snappedTo: UIEventSource<string | undefined> = new UIEventSource<string | undefined>(undefined);
const startCoordinate = {
lon: importFlow.startCoordinate[0],
lat: importFlow.startCoordinate[1]
}
const value: UIEventSource<{ lon: number, lat: number }> = new UIEventSource<{ lon: number; lat: number }>(
startCoordinate
);
async function onConfirm(): Promise<void> {
const importedId = await importFlow.onConfirm(
value.data,
snappedTo.data
)
state.selectedLayer.setData(targetLayer)
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(importedId))
}
</script>
<ImportFlow {importFlow} on:confirm={onConfirm }>
<div class="relative" slot="map">
<div class="h-32">
<NewPointLocationInput coordinate={startCoordinate}
{maxSnapDistance}
{snapToLayers}
{snappedTo}
{state}
{targetLayer}
{value}
/>
</div>
<MapControlButton on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)} cls="absolute bottom-0">
<Square3Stack3dIcon class="w-6 h-6"/>
</MapControlButton>
</div>
</ImportFlow>

View file

@ -0,0 +1,98 @@
import ImportFlow, {ImportFlowArguments} from "./ImportFlow";
import {SpecialVisualizationState} from "../../SpecialVisualization";
import {Store, UIEventSource} from "../../../Logic/UIEventSource";
import {OsmObject, OsmWay} from "../../../Logic/Osm/OsmObject";
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction";
import {Feature, Point} from "geojson";
import Maproulette from "../../../Logic/Maproulette";
import {GeoOperations} from "../../../Logic/GeoOperations";
import {Tag} from "../../../Logic/Tags/Tag";
export interface PointImportFlowArguments extends ImportFlowArguments {
max_snap_distance?: string
snap_onto_layers?: string
icon?: string
targetLayer: string
note_id?: string
maproulette_id?: string
}
export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
public readonly startCoordinate: [number, number]
private readonly _originalFeature: Feature<Point>;
private readonly _originalFeatureTags: UIEventSource<Record<string, string>>
constructor(state: SpecialVisualizationState, originalFeature: Feature<Point>, args: PointImportFlowArguments, tagsToApply: Store<Tag[]>) {
super(state, args, tagsToApply);
this._originalFeature = originalFeature;
this._originalFeatureTags = state.featureProperties.getStore(originalFeature.properties.id)
this.startCoordinate = GeoOperations.centerpointCoordinates(originalFeature)
}
/**
* Creates a new point on OSM, closes (if applicable) the OSM-note or the MapRoulette-challenge
*
* Gives back the id of the newly created element
*/
async onConfirm(
location: { lat: number; lon: number },
snapOntoWayId: string
): Promise<string> {
const tags = this.tagsToApply.data
const originalFeatureTags = this._originalFeatureTags
originalFeatureTags.data["_imported"] = "yes"
originalFeatureTags.ping() // will set isImported as per its definition
let snapOnto: OsmObject | "deleted" = undefined
if (snapOntoWayId !== undefined) {
snapOnto = await this.state.osmObjectDownloader.DownloadObjectAsync(snapOntoWayId)
}
if (snapOnto === "deleted") {
snapOnto = undefined
}
let specialMotivation = undefined
let note_id = this.args.note_id
if (note_id !== undefined && isNaN(Number(note_id))) {
note_id = originalFeatureTags.data[this.args.note_id]
specialMotivation = "source: https://osm.org/note/" + note_id
}
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
theme: this.state.layout.id,
changeType: "import",
snapOnto: <OsmWay>snapOnto,
specialMotivation: specialMotivation,
})
await this.state.changes.applyAction(newElementAction)
this.state.selectedElement.setData(
this.state.indexedFeatures.featuresById.data.get(newElementAction.newElementId)
)
if (note_id !== undefined) {
await this.state.osmConnection.closeNote(note_id, "imported")
originalFeatureTags.data["closed_at"] = new Date().toISOString()
originalFeatureTags.ping()
}
let maproulette_id = originalFeatureTags.data[this.args.maproulette_id]
if (maproulette_id !== undefined) {
if (this.state.featureSwitchIsTesting.data) {
console.log(
"Not marking maproulette task " +
maproulette_id +
" as fixed, because we are in testing mode"
)
} else {
console.log("Marking maproulette task as fixed")
await Maproulette.singleton.closeTask(Number(maproulette_id))
originalFeatureTags.data["mr_taskStatus"] = "Fixed"
originalFeatureTags.ping()
}
}
this.state.mapProperties.location.setData(location)
return newElementAction.newElementId
}
}

View file

@ -0,0 +1,111 @@
import {SpecialVisualization, SpecialVisualizationState} from "../../SpecialVisualization";
import {AutoAction} from "../AutoApplyButton";
import {Feature, LineString, Polygon} from "geojson";
import {UIEventSource} from "../../../Logic/UIEventSource";
import BaseUIElement from "../../BaseUIElement";
import {ImportFlowUtils} from "./ImportFlow";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import SvelteUIElement from "../../Base/SvelteUIElement";
import {FixedUiElement} from "../../Base/FixedUiElement";
import WayImportFlow from "./WayImportFlow.svelte";
import WayImportFlowState, {WayImportFlowArguments} from "./WayImportFlowState";
import {Utils} from "../../../Utils";
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "../../../Logic/Osm/Changes";
import {IndexedFeatureSource} from "../../../Logic/FeatureSource/FeatureSource";
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
/**
* Wrapper around 'WayImportFlow' to make it a special visualisation
*/
export default class WayImportButtonViz implements AutoAction, SpecialVisualization {
public readonly funcName: string
public readonly docs: string | BaseUIElement
public readonly example?: string
public readonly args: { name: string; defaultValue?: string; doc: string }[]
public readonly supportsAutoAction = true
public readonly needsNodeDatabase = true
constructor() {
this.funcName = "import_way_button"
this.docs = "This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" + ImportFlowUtils.documentationGeneral
this.args = [
...ImportFlowUtils.generalArguments,
{
name: "snap_to_point_if",
doc: "Points with the given tags will be snapped to or moved",
},
{
name: "max_snap_distance",
doc: "If the imported object is a LineString or (Multi)Polygon, already existing OSM-points will be reused to construct the geometry of the newly imported way",
defaultValue: "0.05",
},
{
name: "move_osm_point_if",
doc: "Moves the OSM-point to the newly imported point if these conditions are met",
},
{
name: "max_move_distance",
doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m",
defaultValue: "0.05",
},
{
name: "snap_onto_layers",
doc: "If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead",
},
{
name: "snap_to_layer_max_distance",
doc: "Distance to distort the geometry to snap to this layer",
defaultValue: "0.1",
},
]
}
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
const geometry = feature.geometry
if (!(geometry.type == "LineString" || geometry.type === "Polygon")) {
console.error("Invalid type to import", geometry.type)
return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert")
}
const args: WayImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
const importFlow = new WayImportFlowState(state, <Feature<LineString | Polygon>>feature, args, tagsToApply, tagSource)
return new SvelteUIElement(WayImportFlow, {
importFlow
})
}
public async applyActionOn(feature: Feature, state: {
layout: LayoutConfig;
changes: Changes;
indexedFeatures: IndexedFeatureSource,
fullNodeDatabase: FullNodeDatabaseSource
}, tagSource: UIEventSource<any>, argument: string[]): Promise<void> {
{
// Small safety check to prevent duplicate imports
const id = tagSource.data.id
if (ImportFlowUtils.importedIds.has(id)) {
return
}
ImportFlowUtils.importedIds.add(id)
}
if(feature.geometry.type !== "LineString" && feature.geometry.type !== "Polygon"){
return
}
const args: WayImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
const mergeConfigs = WayImportFlowState.GetMergeConfig(args, tagsToApply)
const action = WayImportFlowState.CreateAction(<Feature<LineString | Polygon >>feature, args, state, tagsToApply, mergeConfigs)
await state.changes.applyAction(action)
}
getLayerDependencies(args: string[]){
return ImportFlowUtils.getLayerDependenciesWithSnapOnto(this.args, args)
}
}

View file

@ -0,0 +1,58 @@
<script lang="ts">
import WayImportFlowState from "./WayImportFlowState";
import ImportFlow from "./ImportFlow.svelte";
import MapControlButton from "../../Base/MapControlButton.svelte";
import {Square3Stack3dIcon} from "@babeard/svelte-heroicons/solid";
import {UIEventSource} from "../../../Logic/UIEventSource";
import {Map as MlMap} from "maplibre-gl"
import {MapLibreAdaptor} from "../../Map/MapLibreAdaptor";
import MaplibreMap from "../../Map/MaplibreMap.svelte";
import ShowDataLayer from "../../Map/ShowDataLayer";
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource";
import {ImportFlowUtils} from "./ImportFlow";
import {GeoOperations} from "../../../Logic/GeoOperations";
export let importFlow: WayImportFlowState
const state = importFlow.state
const map = new UIEventSource<MlMap>(undefined)
const [lon, lat] = GeoOperations.centerpointCoordinates(importFlow.originalFeature)
const mla = new MapLibreAdaptor(map, {
allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting),
allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting),
rasterLayer : state.mapProperties.rasterLayer,
location: new UIEventSource<{lon: number; lat: number}>({lon, lat}),
zoom: new UIEventSource<number>(18)
})
// Show all relevant data - including (eventually) the way of which the geometry will be replaced
ShowDataLayer.showMultipleLayers(
map,
new StaticFeatureSource([importFlow.originalFeature]),
state.layout.layers,
{zoomToFeatures: false}
)
importFlow.GetPreview().then(features => {
new ShowDataLayer(map, {
zoomToFeatures: false,
features,
layer: ImportFlowUtils.conflationLayer,
})
})
</script>
<ImportFlow {importFlow} on:confirm={() => importFlow.onConfirm()}>
<div slot="map" class="relative">
<div class="h-32">
<MaplibreMap {map}/>
</div>
<MapControlButton on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)} cls="absolute bottom-0">
<Square3Stack3dIcon class="w-6 h-6"/>
</MapControlButton>
</div>
</ImportFlow>

View file

@ -0,0 +1,129 @@
import ImportFlow, {ImportFlowArguments} from "./ImportFlow";
import {SpecialVisualizationState} from "../../SpecialVisualization";
import {Feature, LineString, Polygon} from "geojson";
import {Store, UIEventSource} from "../../../Logic/UIEventSource";
import {Tag} from "../../../Logic/Tags/Tag";
import {And} from "../../../Logic/Tags/And";
import CreateWayWithPointReuseAction, {
MergePointConfig
} from "../../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
import {TagUtils} from "../../../Logic/Tags/TagUtils";
import {OsmCreateAction} from "../../../Logic/Osm/Actions/OsmChangeAction";
import {FeatureSource, IndexedFeatureSource} from "../../../Logic/FeatureSource/FeatureSource";
import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction";
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "../../../Logic/Osm/Changes";
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
export interface WayImportFlowArguments extends ImportFlowArguments {
max_snap_distance: string
snap_onto_layers: string,
snap_to_layer_max_distance: string,
max_move_distance: string,
move_osm_point_if,
snap_to_point_if
}
export default class WayImportFlowState extends ImportFlow<WayImportFlowArguments> {
public readonly originalFeature: Feature<LineString | Polygon>;
private readonly action: OsmCreateAction & { getPreview?(): Promise<FeatureSource>; }
private readonly _originalFeatureTags: UIEventSource<Record<string, string>>;
constructor(state: SpecialVisualizationState, originalFeature: Feature<LineString | Polygon>, args: WayImportFlowArguments, tagsToApply: Store<Tag[]>, originalFeatureTags: UIEventSource<Record<string, string>>) {
super(state, args, tagsToApply);
this.originalFeature = originalFeature;
this._originalFeatureTags = originalFeatureTags;
const mergeConfigs = WayImportFlowState.GetMergeConfig(args, tagsToApply)
this.action = WayImportFlowState.CreateAction(originalFeature, args, state, tagsToApply, mergeConfigs)
}
public static CreateAction(
feature: Feature<LineString | Polygon>,
args: WayImportFlowArguments,
state: {
layout: LayoutConfig;
changes: Changes;
indexedFeatures: IndexedFeatureSource,
fullNodeDatabase?: FullNodeDatabaseSource
},
tagsToApply: Store<Tag[]>,
mergeConfigs: MergePointConfig[]
): OsmCreateAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string } {
if (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length > 1) {
const coors = (<Polygon>feature.geometry).coordinates
const outer = coors[0]
const inner = [...coors]
inner.splice(0, 1)
return new CreateMultiPolygonWithPointReuseAction(
tagsToApply.data,
outer,
inner,
state,
mergeConfigs,
"import"
)
} else if (feature.geometry.type === "Polygon") {
const coors = feature.geometry.coordinates
const outer = coors[0]
return new CreateWayWithPointReuseAction(tagsToApply.data, outer, state, mergeConfigs)
} else if (feature.geometry.type === "LineString") {
const coors = feature.geometry.coordinates
return new CreateWayWithPointReuseAction(tagsToApply.data, coors, state, mergeConfigs)
} else {
throw "Unsupported type"
}
}
public static GetMergeConfig(args: WayImportFlowArguments, newTags: Store<Tag[]>): MergePointConfig[] {
const nodesMustMatch = args.snap_to_point_if
?.split(";")
?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
const mergeConfigs = []
if (nodesMustMatch !== undefined && nodesMustMatch.length > 0) {
const mergeConfig: MergePointConfig = {
mode: "reuse_osm_point",
ifMatches: new And(nodesMustMatch),
withinRangeOfM: Number(args.max_snap_distance),
}
mergeConfigs.push(mergeConfig)
}
const moveOsmPointIfTags = args["move_osm_point_if"]
?.split(";")
?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
if (nodesMustMatch !== undefined && moveOsmPointIfTags.length > 0) {
const moveDistance = Math.min(20, Number(args["max_move_distance"]))
const mergeConfig: MergePointConfig = {
mode: "move_osm_point",
ifMatches: new And(moveOsmPointIfTags),
withinRangeOfM: moveDistance,
}
mergeConfigs.push(mergeConfig)
}
return mergeConfigs
}
public async onConfirm() {
const originalFeatureTags = this._originalFeatureTags
originalFeatureTags.data["_imported"] = "yes"
originalFeatureTags.ping() // will set isImported as per its definition
const action = this.action
await this.state.changes.applyAction(action)
const newId = action.newElementId ?? action.mainObjectId
this.state.selectedLayer.setData(this.targetLayer.layerDef)
this.state.selectedElement.setData(this.state.indexedFeatures.featuresById.data.get(newId))
}
public GetPreview(): undefined | Promise<FeatureSource> {
if (!this.action?.getPreview) {
return undefined
}
return this.action.getPreview()
}
}