Refactoring of import button, various improvements
This commit is contained in:
parent
cabbdf96db
commit
a095af4f18
17 changed files with 527 additions and 328 deletions
|
@ -7,17 +7,11 @@ import Translations from "../i18n/Translations";
|
|||
import Constants from "../../Models/Constants";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
import Loading from "../Base/Loading";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import {Changes} from "../../Logic/Osm/Changes";
|
||||
import {ElementStorage} from "../../Logic/ElementStorage";
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
|
||||
import Lazy from "../Base/Lazy";
|
||||
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
|
||||
import Img from "../Base/Img";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import SpecialVisualizations from "../SpecialVisualizations";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
@ -28,68 +22,29 @@ import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
|||
import AllKnownLayers from "../../Customizations/AllKnownLayers";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||
import CreateWayWithPointReuseAction, {MergePointConfig} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import OsmChangeAction, {OsmCreateAction} from "../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
|
||||
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
|
||||
import {DefaultGuiState} from "../DefaultGuiState";
|
||||
import {PresetInfo} from "../BigComponents/SimpleAddUI";
|
||||
|
||||
|
||||
export interface ImportButtonState {
|
||||
description?: Translation;
|
||||
image: () => BaseUIElement,
|
||||
message: string | BaseUIElement,
|
||||
originalTags: UIEventSource<any>,
|
||||
newTags: UIEventSource<Tag[]>,
|
||||
targetLayer: FilteredLayer,
|
||||
feature: any,
|
||||
minZoom: number,
|
||||
state: {
|
||||
backgroundLayer: UIEventSource<BaseLayer>;
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>;
|
||||
featureSwitchUserbadge: UIEventSource<boolean>;
|
||||
featurePipeline: FeaturePipeline;
|
||||
allElements: ElementStorage;
|
||||
selectedElement: UIEventSource<any>;
|
||||
layoutToUse: LayoutConfig,
|
||||
osmConnection: OsmConnection,
|
||||
changes: Changes,
|
||||
locationControl: UIEventSource<{ zoom: number }>
|
||||
},
|
||||
guiState: { filterViewIsOpened: UIEventSource<boolean> },
|
||||
|
||||
/**
|
||||
* SnapSettings for newly imported points
|
||||
*/
|
||||
snapSettings?: {
|
||||
snapToLayers: string[],
|
||||
snapToLayersMaxDist?: number
|
||||
},
|
||||
/**
|
||||
* Settings if an imported feature must be conflated with an already existing feature
|
||||
*/
|
||||
conflationSettings?: {
|
||||
conflateWayId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings for newly created points which are part of a way: when to snap to already existing points?
|
||||
*/
|
||||
mergeConfigs: MergePointConfig[]
|
||||
}
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import {And} from "../../Logic/Tags/And";
|
||||
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||
import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction";
|
||||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
|
||||
|
||||
abstract class AbstractImportButton implements SpecialVisualizations {
|
||||
public readonly funcName: string
|
||||
public readonly docs: string
|
||||
public readonly args: { name: string, defaultValue?: string, doc: string }[]
|
||||
private readonly showRemovedTags: boolean;
|
||||
|
||||
constructor(funcName: string, docsIntro: string, extraArgs: { name: string, doc: string, defaultValue?: string }[]) {
|
||||
constructor(funcName: string, docsIntro: string, extraArgs: { name: string, doc: string, defaultValue?: string }[], showRemovedTags = true) {
|
||||
this.funcName = funcName
|
||||
this.showRemovedTags = showRemovedTags;
|
||||
|
||||
this.docs = `${docsIntro}
|
||||
|
||||
|
@ -102,9 +57,7 @@ The argument \`tags\` of the import button takes a \`;\`-seperated list of tags
|
|||
|
||||
${Utils.Special_visualizations_tagsToApplyHelpText}
|
||||
${Utils.special_visualizations_importRequirementDocs}
|
||||
|
||||
`
|
||||
|
||||
this.args = [
|
||||
{
|
||||
name: "targetLayer",
|
||||
|
@ -128,11 +81,16 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
|
||||
};
|
||||
|
||||
abstract constructElement(state: FeaturePipelineState, args: { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string },
|
||||
tagSource: UIEventSource<any>, guiState: DefaultGuiState): BaseUIElement;
|
||||
abstract constructElement(state: FeaturePipelineState,
|
||||
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, newTags: UIEventSource<any>, targetLayer: string },
|
||||
tagSource: UIEventSource<any>,
|
||||
guiState: DefaultGuiState,
|
||||
feature: any,
|
||||
onCancelClicked: () => void): BaseUIElement;
|
||||
|
||||
|
||||
constr(state, tagSource, argsRaw, guiState) {
|
||||
|
||||
const self = this;
|
||||
/**
|
||||
* Some generic import button pre-validation is implemented here:
|
||||
* - Are we logged in?
|
||||
|
@ -144,7 +102,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
|
||||
const t = Translations.t.general.add.import;
|
||||
const t0 = Translations.t.general.add;
|
||||
const args = this.parseArgs(argsRaw)
|
||||
const args = this.parseArgs(argsRaw, tagSource)
|
||||
|
||||
{
|
||||
// Some initial validation
|
||||
|
@ -171,26 +129,22 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
const id = tagSource.data.id;
|
||||
const feature = state.allElements.ContainingFeatures.get(id)
|
||||
|
||||
|
||||
/**** THe actual panel showing the import guiding map ****/
|
||||
const importGuidingPanel = this.constructElement(state, args, tagSource, guiState)
|
||||
|
||||
// Explanation of the tags that will be applied onto the imported/conflated object
|
||||
const newTags = SpecialVisualizations.generateTagsToApply(args.tags, tagSource)
|
||||
const appliedTags = new Toggle(
|
||||
new VariableUiElement(
|
||||
newTags.map(tgs => {
|
||||
const parts = []
|
||||
for (const tag of tgs) {
|
||||
parts.push(tag.key + "=" + tag.value)
|
||||
}
|
||||
const txt = parts.join(" & ")
|
||||
return t0.presetInfo.Subs({tags: txt}).SetClass("subtle")
|
||||
})), undefined,
|
||||
const filteredTags = tgs.filter(tg => self.showRemovedTags || (tg.value ?? "") !== "")
|
||||
const asText = new And(filteredTags)
|
||||
.asHumanString(true, true, {})
|
||||
|
||||
return t0.presetInfo.Subs({tags: asText}).SetClass("subtle");
|
||||
})),
|
||||
undefined,
|
||||
state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt)
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
const importClicked = new UIEventSource(false);
|
||||
inviteToImportButton.onClick(() => {
|
||||
|
@ -207,15 +161,17 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
|
||||
const isImported = tagSource.map(tags => tags._imported === "yes")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**** THe actual panel showing the import guiding map ****/
|
||||
const importGuidingPanel = this.constructElement(state, args, tagSource, guiState, feature, () => importClicked.setData(false))
|
||||
|
||||
|
||||
const importFlow = new Toggle(
|
||||
new Toggle(
|
||||
new Loading(t0.stillLoading),
|
||||
new Combine([importGuidingPanel, appliedTags]).SetClass("flex flex-col"),
|
||||
state.featurePipeline.runningQuery
|
||||
) ,
|
||||
new Loading(t0.stillLoading),
|
||||
new Combine([importGuidingPanel, appliedTags]).SetClass("flex flex-col"),
|
||||
state.featurePipeline.runningQuery
|
||||
),
|
||||
inviteToImportButton,
|
||||
importClicked
|
||||
);
|
||||
|
@ -239,12 +195,16 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
|
||||
}
|
||||
|
||||
private parseArgs(argsRaw: string[]): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string } {
|
||||
return Utils.ParseVisArgs(this.args, argsRaw)
|
||||
private parseArgs(argsRaw: string[], originalFeatureTags: UIEventSource<any>): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string, newTags: UIEventSource<Tag[]> } {
|
||||
const baseArgs = Utils.ParseVisArgs(this.args, argsRaw)
|
||||
if (originalFeatureTags !== undefined) {
|
||||
baseArgs["newTags"] = SpecialVisualizations.generateTagsToApply(baseArgs.tags, originalFeatureTags)
|
||||
}
|
||||
return baseArgs
|
||||
}
|
||||
|
||||
getLayerDependencies(argsRaw: string[]) {
|
||||
const args = this.parseArgs(argsRaw)
|
||||
const args = this.parseArgs(argsRaw, undefined)
|
||||
|
||||
const dependsOnLayers: string[] = []
|
||||
|
||||
|
@ -261,181 +221,31 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
|
||||
|
||||
protected abstract canBeImported(feature: any)
|
||||
}
|
||||
|
||||
|
||||
export class ImportButtonSpecialViz extends AbstractImportButton {
|
||||
|
||||
constructor() {
|
||||
super("import_button",
|
||||
"This button will copy the data from an external dataset into OpenStreetMap",
|
||||
[{
|
||||
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: "If the imported object is a point, the maximum distance that this point will be moved to snap onto a way in an already existing layer (in meters)",
|
||||
defaultValue: "5"
|
||||
}]
|
||||
)
|
||||
}
|
||||
|
||||
canBeImported(feature: any) {
|
||||
const type = feature.geometry.type
|
||||
return type === "Point" || type === "LineString" || type === "Polygon"
|
||||
}
|
||||
|
||||
constructElement(state, args,
|
||||
tagSource,
|
||||
guiState): BaseUIElement {
|
||||
|
||||
let snapSettings = undefined
|
||||
{
|
||||
// Configure the snapsettings (if applicable)
|
||||
const snapToLayers = args.snap_onto_layers?.trim()?.split(";")?.filter(s => s !== "")
|
||||
const snapToLayersMaxDist = Number(args.max_snap_distance ?? 5)
|
||||
if (snapToLayers.length > 0) {
|
||||
snapSettings = {
|
||||
snapToLayers,
|
||||
snapToLayersMaxDist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const o =
|
||||
{
|
||||
state, guiState, image: img,
|
||||
feature, newTags, message, minZoom: 18,
|
||||
originalTags: tagSource,
|
||||
targetLayer,
|
||||
snapSettings,
|
||||
conflationSettings: undefined,
|
||||
mergeConfigs: undefined
|
||||
}
|
||||
|
||||
return ImportButton.createConfirmPanel(o, isImported, importClicked),
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default class ImportButton {
|
||||
|
||||
public static createConfirmPanel(o: ImportButtonState,
|
||||
isImported: UIEventSource<boolean>,
|
||||
importClicked: UIEventSource<boolean>) {
|
||||
const geometry = o.feature.geometry
|
||||
if (geometry.type === "Point") {
|
||||
return new Lazy(() => ImportButton.createConfirmPanelForPoint(o, isImported, importClicked))
|
||||
}
|
||||
|
||||
|
||||
if (geometry.type === "Polygon" && geometry.coordinates.length > 1) {
|
||||
return new Lazy(() => ImportButton.createConfirmForMultiPolygon(o, isImported, importClicked))
|
||||
}
|
||||
|
||||
if (geometry.type === "Polygon" || geometry.type == "LineString") {
|
||||
return new Lazy(() => ImportButton.createConfirmForWay(o, isImported, importClicked))
|
||||
}
|
||||
console.error("Invalid type to import", geometry.type)
|
||||
return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert")
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static createConfirmForMultiPolygon(o: ImportButtonState,
|
||||
isImported: UIEventSource<boolean>,
|
||||
importClicked: UIEventSource<boolean>): BaseUIElement {
|
||||
if (o.conflationSettings !== undefined) {
|
||||
return new FixedUiElement("Conflating multipolygons is not supported").SetClass("alert")
|
||||
|
||||
}
|
||||
|
||||
// For every single linear ring, we create a new way
|
||||
const createRings: (OsmChangeAction & { getPreview(): Promise<FeatureSource> })[] = []
|
||||
|
||||
for (const coordinateRing of o.feature.geometry.coordinates) {
|
||||
createRings.push(new CreateWayWithPointReuseAction(
|
||||
// The individual way doesn't receive any tags
|
||||
[],
|
||||
coordinateRing,
|
||||
// @ts-ignore
|
||||
o.state,
|
||||
o.mergeConfigs
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
return new FixedUiElement("Multipolygon! Here we come").SetClass("alert")
|
||||
}
|
||||
|
||||
public static createConfirmForWay(o: ImportButtonState,
|
||||
isImported: UIEventSource<boolean>,
|
||||
importClicked: UIEventSource<boolean>): BaseUIElement {
|
||||
|
||||
protected createConfirmPanelForWay(
|
||||
state: FeaturePipelineState,
|
||||
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<Tag[]>, targetLayer: string },
|
||||
feature: any,
|
||||
originalFeatureTags: UIEventSource<any>,
|
||||
action: (OsmChangeAction & { getPreview(): Promise<FeatureSource>, newElementId?: string }),
|
||||
onCancel: () => void): BaseUIElement {
|
||||
const self = this;
|
||||
const confirmationMap = Minimap.createMiniMap({
|
||||
allowMoving: false,
|
||||
background: o.state.backgroundLayer
|
||||
background: state.backgroundLayer
|
||||
})
|
||||
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
|
||||
|
||||
const relevantFeatures = Utils.NoNull([o.feature, o.state.allElements?.ContainingFeatures?.get(o.conflationSettings?.conflateWayId)])
|
||||
// SHow all relevant data - including (eventually) the way of which the geometry will be replaced
|
||||
new ShowDataMultiLayer({
|
||||
leafletMap: confirmationMap.leafletMap,
|
||||
enablePopups: false,
|
||||
zoomToFeatures: true,
|
||||
features: new StaticFeatureSource(relevantFeatures, false),
|
||||
allElements: o.state.allElements,
|
||||
layers: o.state.filteredLayers
|
||||
features: new StaticFeatureSource([feature], false),
|
||||
allElements: state.allElements,
|
||||
layers: state.filteredLayers
|
||||
})
|
||||
|
||||
let action: OsmChangeAction & { getPreview(): Promise<FeatureSource> }
|
||||
|
||||
const changes = o.state.changes
|
||||
let confirm: () => Promise<string>
|
||||
if (o.conflationSettings !== undefined) {
|
||||
// Conflate the way
|
||||
action = new ReplaceGeometryAction(
|
||||
o.state,
|
||||
o.feature,
|
||||
o.conflationSettings.conflateWayId,
|
||||
{
|
||||
theme: o.state.layoutToUse.id,
|
||||
newTags: o.newTags.data
|
||||
}
|
||||
)
|
||||
|
||||
confirm = async () => {
|
||||
changes.applyAction(action)
|
||||
return o.feature.properties.id
|
||||
}
|
||||
|
||||
} else {
|
||||
// Upload the way to OSM
|
||||
const geom = o.feature.geometry
|
||||
let coordinates: [number, number][]
|
||||
if (geom.type === "LineString") {
|
||||
coordinates = geom.coordinates
|
||||
} else if (geom.type === "Polygon") {
|
||||
coordinates = geom.coordinates[0]
|
||||
}
|
||||
|
||||
action = new CreateWayWithPointReuseAction(
|
||||
o.newTags.data,
|
||||
coordinates,
|
||||
// @ts-ignore
|
||||
o.state,
|
||||
o.mergeConfigs
|
||||
)
|
||||
|
||||
|
||||
confirm = async () => {
|
||||
changes.applyAction(action)
|
||||
return action.mainObjectId
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
action.getPreview().then(changePreview => {
|
||||
new ShowDataLayer({
|
||||
|
@ -443,89 +253,328 @@ export default class ImportButton {
|
|||
enablePopups: false,
|
||||
zoomToFeatures: false,
|
||||
features: changePreview,
|
||||
allElements: o.state.allElements,
|
||||
allElements: state.allElements,
|
||||
layerToShow: AllKnownLayers.sharedLayers.get("conflation")
|
||||
})
|
||||
})
|
||||
|
||||
const tagsExplanation = new VariableUiElement(o.newTags.map(tagsToApply => {
|
||||
const tagsStr = tagsToApply.map(t => t.asHumanString(false, true)).join("&");
|
||||
const tagsExplanation = new VariableUiElement(args.newTags.map(tagsToApply => {
|
||||
const filteredTags = tagsToApply.filter(t => self.showRemovedTags || (t.value ?? "") !== "")
|
||||
const tagsStr = new And(filteredTags).asHumanString(false, true, {})
|
||||
return Translations.t.general.add.import.importTags.Subs({tags: tagsStr});
|
||||
}
|
||||
)).SetClass("subtle")
|
||||
|
||||
const confirmButton = new SubtleButton(o.image(), new Combine([o.message, tagsExplanation]).SetClass("flex flex-col"))
|
||||
const confirmButton = new SubtleButton(new Img(args.icon), new Combine([args.text, tagsExplanation]).SetClass("flex flex-col"))
|
||||
confirmButton.onClick(async () => {
|
||||
{
|
||||
if (isImported.data) {
|
||||
return
|
||||
}
|
||||
o.originalTags.data["_imported"] = "yes"
|
||||
o.originalTags.ping() // will set isImported as per its definition
|
||||
|
||||
const idToSelect = await confirm()
|
||||
|
||||
o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get(idToSelect))
|
||||
originalFeatureTags.data["_imported"] = "yes"
|
||||
originalFeatureTags.ping() // will set isImported as per its definition
|
||||
state.changes.applyAction(action)
|
||||
state.selectedElement.setData(state.allElements.ContainingFeatures.get(action.newElementId ?? action.mainObjectId))
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
const cancel = new SubtleButton(Svg.close_ui(), Translations.t.general.cancel).onClick(() => importClicked.setData(false))
|
||||
|
||||
|
||||
const cancel = new SubtleButton(Svg.close_ui(), Translations.t.general.cancel).onClick(onCancel)
|
||||
return new Combine([confirmationMap, confirmButton, cancel]).SetClass("flex flex-col")
|
||||
}
|
||||
}
|
||||
|
||||
public static createConfirmPanelForPoint(
|
||||
o: ImportButtonState,
|
||||
isImported: UIEventSource<boolean>,
|
||||
importClicked: UIEventSource<boolean>): BaseUIElement {
|
||||
export class ConflateButton extends AbstractImportButton {
|
||||
|
||||
constructor() {
|
||||
super("conflate_button", "This button will modify the geometry of an existing OSM way to match the specified geometry. This can conflate OSM-ways with LineStrings and Polygons (only simple polygons with one single ring). An attempt is made to move points with special values to a decent new location (e.g. entrances)",
|
||||
[{
|
||||
name: "way_to_conflate",
|
||||
doc: "The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag"
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
protected canBeImported(feature: any) {
|
||||
return feature.geometry.type === "LineString" || (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
|
||||
}
|
||||
|
||||
constructElement(state: FeaturePipelineState,
|
||||
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<Tag[]>; targetLayer: string },
|
||||
tagSource: UIEventSource<any>, guiState: DefaultGuiState, feature: any, onCancelClicked: () => void): BaseUIElement {
|
||||
|
||||
const nodesMustMatch = args.snap_onto_layers?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
|
||||
|
||||
const mergeConfigs = []
|
||||
if (nodesMustMatch !== undefined && nodesMustMatch.length > 0) {
|
||||
const mergeConfig: MergePointConfig = {
|
||||
mode: args["point_move_mode"] == "move_osm" ? "move_osm_point" : "reuse_osm_point",
|
||||
ifMatches: new And(nodesMustMatch),
|
||||
withinRangeOfM: Number(args.max_snap_distance)
|
||||
}
|
||||
mergeConfigs.push(mergeConfig)
|
||||
}
|
||||
|
||||
|
||||
const key = args["way_to_conflate"]
|
||||
const wayToConflate = tagSource.data[key]
|
||||
const action = new ReplaceGeometryAction(
|
||||
state,
|
||||
feature,
|
||||
wayToConflate,
|
||||
{
|
||||
theme: state.layoutToUse.id,
|
||||
newTags: args.newTags.data
|
||||
}
|
||||
)
|
||||
|
||||
return this.createConfirmPanelForWay(
|
||||
state,
|
||||
args,
|
||||
feature,
|
||||
tagSource,
|
||||
action,
|
||||
onCancelClicked
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ImportWayButton extends AbstractImportButton {
|
||||
|
||||
constructor() {
|
||||
super("import_way_button",
|
||||
"This button will copy the data from an external dataset into OpenStreetMap",
|
||||
[
|
||||
{
|
||||
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: "5"
|
||||
},
|
||||
{
|
||||
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: "1"
|
||||
},{
|
||||
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"
|
||||
}],
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
canBeImported(feature: any) {
|
||||
const type = feature.geometry.type
|
||||
return type === "LineString" || type === "Polygon"
|
||||
}
|
||||
|
||||
getLayerDependencies(argsRaw: string[]): string[] {
|
||||
const deps = super.getLayerDependencies(argsRaw);
|
||||
deps.push("type_node")
|
||||
return deps
|
||||
}
|
||||
|
||||
constructElement(state, args,
|
||||
originalFeatureTags,
|
||||
guiState,
|
||||
feature,
|
||||
onCancel): 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")
|
||||
}
|
||||
|
||||
|
||||
// Upload the way to OSM
|
||||
const geom = feature.geometry
|
||||
let coordinates: [number, number][]
|
||||
if (geom.type === "LineString") {
|
||||
coordinates = geom.coordinates
|
||||
} else if (geom.type === "Polygon") {
|
||||
coordinates = geom.coordinates[0]
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
let action: OsmCreateAction & { getPreview(): Promise<FeatureSource> };
|
||||
|
||||
const coors = feature.geometry.coordinates
|
||||
if (feature.geometry.type === "Polygon" && coors.length > 1) {
|
||||
const outer = coors[0]
|
||||
const inner = [...coors]
|
||||
inner.splice(0, 1)
|
||||
action = new CreateMultiPolygonWithPointReuseAction(
|
||||
args.newTags.data,
|
||||
outer,
|
||||
inner,
|
||||
state,
|
||||
mergeConfigs,
|
||||
"import"
|
||||
)
|
||||
} else {
|
||||
|
||||
action = new CreateWayWithPointReuseAction(
|
||||
args.newTags.data,
|
||||
coordinates,
|
||||
state,
|
||||
mergeConfigs
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return this.createConfirmPanelForWay(
|
||||
state,
|
||||
args,
|
||||
feature,
|
||||
originalFeatureTags,
|
||||
action,
|
||||
onCancel
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class ImportPointButton extends AbstractImportButton {
|
||||
|
||||
constructor() {
|
||||
super("import_button",
|
||||
"This button will copy the point from an external dataset into OpenStreetMap",
|
||||
[{
|
||||
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"
|
||||
}],
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
canBeImported(feature: any) {
|
||||
return feature.geometry.type === "Point"
|
||||
}
|
||||
|
||||
getLayerDependencies(argsRaw: string[]): string[] {
|
||||
const deps = super.getLayerDependencies(argsRaw);
|
||||
const layerSnap = argsRaw["snap_onto_layers"] ?? ""
|
||||
if (layerSnap === "") {
|
||||
return deps
|
||||
}
|
||||
|
||||
deps.push(...layerSnap.split(";"))
|
||||
return deps
|
||||
}
|
||||
|
||||
private static createConfirmPanelForPoint(
|
||||
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<any>, targetLayer: string },
|
||||
state: FeaturePipelineState,
|
||||
guiState: DefaultGuiState,
|
||||
originalFeatureTags: UIEventSource<any>,
|
||||
feature: any,
|
||||
onCancel: () => void): BaseUIElement {
|
||||
|
||||
async function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) {
|
||||
|
||||
if (isImported.data) {
|
||||
return
|
||||
}
|
||||
o.originalTags.data["_imported"] = "yes"
|
||||
o.originalTags.ping() // will set isImported as per its definition
|
||||
originalFeatureTags.data["_imported"] = "yes"
|
||||
originalFeatureTags.ping() // will set isImported as per its definition
|
||||
let snapOnto: OsmObject = undefined
|
||||
if (snapOntoWayId !== undefined) {
|
||||
snapOnto = await OsmObject.DownloadObjectAsync(snapOntoWayId)
|
||||
}
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
|
||||
theme: o.state.layoutToUse.id,
|
||||
theme: state.layoutToUse.id,
|
||||
changeType: "import",
|
||||
snapOnto: <OsmWay>snapOnto
|
||||
})
|
||||
|
||||
await o.state.changes.applyAction(newElementAction)
|
||||
o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get(
|
||||
await state.changes.applyAction(newElementAction)
|
||||
state.selectedElement.setData(state.allElements.ContainingFeatures.get(
|
||||
newElementAction.newElementId
|
||||
))
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
importClicked.setData(false)
|
||||
}
|
||||
|
||||
const presetInfo = <PresetInfo>{
|
||||
tags: o.newTags.data,
|
||||
icon: o.image,
|
||||
description: o.description,
|
||||
layerToAddTo: o.targetLayer,
|
||||
name: o.message,
|
||||
title: o.message,
|
||||
tags: args.newTags.data,
|
||||
icon: () => new Img(args.icon),
|
||||
description: Translations.WT(args.text),
|
||||
layerToAddTo: state.filteredLayers.data.filter(l => l.layerDef.id === args.targetLayer)[0],
|
||||
name: args.text,
|
||||
title: Translations.WT(args.text),
|
||||
preciseInput: {
|
||||
snapToLayers: o.snapSettings?.snapToLayers,
|
||||
maxSnapDistance: o.snapSettings?.snapToLayersMaxDist
|
||||
snapToLayers: args.snap_onto_layers?.split(";"),
|
||||
maxSnapDistance: Number(args.max_snap_distance)
|
||||
}
|
||||
}
|
||||
|
||||
const [lon, lat] = o.feature.geometry.coordinates
|
||||
return new ConfirmLocationOfPoint(o.state, o.guiState.filterViewIsOpened, presetInfo, Translations.W(o.message), {
|
||||
const [lon, lat] = feature.geometry.coordinates
|
||||
return new ConfirmLocationOfPoint(state, guiState.filterViewIsOpened, presetInfo, Translations.W(args.text), {
|
||||
lon,
|
||||
lat
|
||||
}, confirm, cancel)
|
||||
}, confirm, onCancel)
|
||||
|
||||
}
|
||||
|
||||
constructElement(state, args,
|
||||
originalFeatureTags,
|
||||
guiState,
|
||||
feature,
|
||||
onCancel): BaseUIElement {
|
||||
|
||||
|
||||
const geometry = feature.geometry
|
||||
|
||||
if (geometry.type === "Point") {
|
||||
return new Lazy(() => ImportPointButton.createConfirmPanelForPoint(
|
||||
args,
|
||||
state,
|
||||
guiState,
|
||||
originalFeatureTags,
|
||||
feature,
|
||||
onCancel
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
console.error("Invalid type to import", geometry.type)
|
||||
return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert")
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue