forked from MapComplete/MapComplete
Refactoring: fix import buttons (WIP)
This commit is contained in:
parent
52b54d8b08
commit
8f942f0163
28 changed files with 970 additions and 779 deletions
93
UI/Popup/ImportButtons/ImportFlow.svelte
Normal file
93
UI/Popup/ImportButtons/ImportFlow.svelte
Normal 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>
|
199
UI/Popup/ImportButtons/ImportFlow.ts
Normal file
199
UI/Popup/ImportButtons/ImportFlow.ts
Normal 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])
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
66
UI/Popup/ImportButtons/ImportPointButtonViz.ts
Normal file
66
UI/Popup/ImportButtons/ImportPointButtonViz.ts
Normal 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)
|
||||
}
|
||||
}
|
62
UI/Popup/ImportButtons/PointImportFlow.svelte
Normal file
62
UI/Popup/ImportButtons/PointImportFlow.svelte
Normal 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>
|
98
UI/Popup/ImportButtons/PointImportFlowState.ts
Normal file
98
UI/Popup/ImportButtons/PointImportFlowState.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
111
UI/Popup/ImportButtons/WayImportButtonViz.ts
Normal file
111
UI/Popup/ImportButtons/WayImportButtonViz.ts
Normal 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)
|
||||
}
|
||||
}
|
58
UI/Popup/ImportButtons/WayImportFlow.svelte
Normal file
58
UI/Popup/ImportButtons/WayImportFlow.svelte
Normal 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>
|
129
UI/Popup/ImportButtons/WayImportFlowState.ts
Normal file
129
UI/Popup/ImportButtons/WayImportFlowState.ts
Normal 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()
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue