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
|
@ -1,56 +1,26 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"
|
||||
import Loading from "../Base/Loading"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"
|
||||
import Img from "../Base/Img"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Svg from "../../Svg"
|
||||
import { Utils } from "../../Utils"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import CreateWayWithPointReuseAction, {
|
||||
MergePointConfig,
|
||||
} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"
|
||||
import OsmChangeAction, { OsmCreateAction } from "../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
|
||||
import { PresetInfo } from "../BigComponents/SimpleAddUI"
|
||||
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"
|
||||
import TagApplyButton from "./TagApplyButton"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import conflation_json from "../../assets/layers/conflation/conflation.json"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { LoginToggle } from "./LoginButton"
|
||||
import { AutoAction } from "./AutoApplyButton"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Maproulette from "../../Logic/Maproulette"
|
||||
import { Feature, Point } from "geojson"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
import {FixedUiElement} from "../Base/FixedUiElement"
|
||||
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import {And} from "../../Logic/Tags/And"
|
||||
import {Tag} from "../../Logic/Tags/Tag"
|
||||
import {SpecialVisualization, SpecialVisualizationState} from "../SpecialVisualization"
|
||||
import {Feature} from "geojson"
|
||||
import {ImportFlowArguments, ImportFlowUtils} from "./ImportButtons/ImportFlow";
|
||||
import {MergePointConfig} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* A helper class for the various import-flows.
|
||||
* An import-flow always starts with a 'Import this'-button. Upon click, a custom confirmation panel is provided
|
||||
*/
|
||||
abstract class AbstractImportButton implements SpecialVisualization {
|
||||
protected static importedIds = new Set<string>()
|
||||
export abstract class AbstractImportButton implements SpecialVisualization {
|
||||
|
||||
public readonly funcName: string
|
||||
public readonly docs: string
|
||||
public readonly args: { name: string; defaultValue?: string; doc: string }[]
|
||||
|
@ -66,18 +36,8 @@ abstract class AbstractImportButton implements SpecialVisualization {
|
|||
this.funcName = funcName
|
||||
this.showRemovedTags = options?.showRemovedTags ?? true
|
||||
this.cannotBeImportedMessage = options?.cannotBeImportedMessage
|
||||
this.docs = `${docsIntro}
|
||||
this.docs = `${docsIntro}${ImportFlowUtils.documentationGeneral}`
|
||||
|
||||
Note 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}
|
||||
`
|
||||
this.args = [
|
||||
{
|
||||
name: "targetLayer",
|
||||
|
@ -105,15 +65,8 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
|
||||
abstract constructElement(
|
||||
state: SpecialVisualizationState,
|
||||
args: {
|
||||
max_snap_distance: string
|
||||
snap_onto_layers: string
|
||||
icon: string
|
||||
text: string
|
||||
tags: string
|
||||
newTags: UIEventSource<any>
|
||||
targetLayer: string
|
||||
},
|
||||
args: ImportFlowArguments,
|
||||
newTags: Store<Tag[]>,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
feature: Feature,
|
||||
onCancelClicked: () => void
|
||||
|
@ -124,246 +77,14 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argsRaw: string[]
|
||||
) {
|
||||
/**
|
||||
* Some generic import button pre-validation is implemented here:
|
||||
* - Are we logged in?
|
||||
* - Did the user zoom in enough?
|
||||
* ...
|
||||
*
|
||||
* The actual import flow (showing the conflation map, special cases) are handled in 'constructElement'
|
||||
*/
|
||||
|
||||
const t = Translations.t.general.add.import
|
||||
const t0 = Translations.t.general.add
|
||||
const args = this.parseArgs(argsRaw, tagSource)
|
||||
|
||||
{
|
||||
// Some initial validation
|
||||
if (
|
||||
!state.layout.official &&
|
||||
!(
|
||||
state.featureSwitchIsTesting.data ||
|
||||
state.osmConnection._oauth_config.url ===
|
||||
OsmConnection.oauth_configs["osm-test"].url
|
||||
)
|
||||
) {
|
||||
return new Combine([t.officialThemesOnly.SetClass("alert"), t.howToTest])
|
||||
}
|
||||
const targetLayer: FilteredLayer = state.layerState.filteredLayers.get(args.targetLayer)
|
||||
if (targetLayer === undefined) {
|
||||
const e = `Target layer not defined: error in import button for theme: ${state.layout.id}: layer ${args.targetLayer} not found`
|
||||
console.error(e)
|
||||
return new FixedUiElement(e).SetClass("alert")
|
||||
}
|
||||
}
|
||||
|
||||
let img: BaseUIElement
|
||||
if (args.icon !== undefined && args.icon !== "") {
|
||||
img = new Img(args.icon)
|
||||
} else {
|
||||
img = Svg.add_svg()
|
||||
}
|
||||
const inviteToImportButton = new SubtleButton(img, args.text)
|
||||
|
||||
const id = tagSource.data.id
|
||||
const feature = state.indexedFeatures.featuresById.data.get(id)
|
||||
|
||||
// Explanation of the tags that will be applied onto the imported/conflated object
|
||||
|
||||
let tagSpec = args.tags
|
||||
if (
|
||||
tagSpec.indexOf(" ") < 0 &&
|
||||
tagSpec.indexOf(";") < 0 &&
|
||||
tagSource.data[args.tags] !== undefined
|
||||
) {
|
||||
// This is probably a key
|
||||
tagSpec = tagSource.data[args.tags]
|
||||
console.debug(
|
||||
"The import button is using tags from properties[" +
|
||||
args.tags +
|
||||
"] of this object, namely ",
|
||||
tagSpec
|
||||
)
|
||||
}
|
||||
|
||||
const importClicked = new UIEventSource(false)
|
||||
inviteToImportButton.onClick(() => {
|
||||
importClicked.setData(true)
|
||||
})
|
||||
|
||||
const pleaseLoginButton = new Toggle(
|
||||
t0.pleaseLogin
|
||||
.onClick(() => state.osmConnection.AttemptLogin())
|
||||
.SetClass("login-button-friendly"),
|
||||
undefined,
|
||||
state.featureSwitchUserbadge
|
||||
)
|
||||
|
||||
const isImported = tagSource.map((tags) => {
|
||||
AbstractImportButton.importedIds.add(tags.id)
|
||||
return tags._imported === "yes"
|
||||
})
|
||||
|
||||
/**** The actual panel showing the import guiding map ****/
|
||||
const importGuidingPanel = this.constructElement(state, args, tagSource, feature, () =>
|
||||
importClicked.setData(false)
|
||||
)
|
||||
|
||||
const importFlow = new Toggle(
|
||||
new Toggle(new Loading(t0.stillLoading), importGuidingPanel, state.dataIsLoading),
|
||||
inviteToImportButton,
|
||||
importClicked
|
||||
)
|
||||
|
||||
return new Toggle(
|
||||
new LoginToggle(
|
||||
new Toggle(
|
||||
new Toggle(t.hasBeenImported, importFlow, isImported),
|
||||
t.zoomInMore.SetClass("alert block"),
|
||||
state.mapProperties.zoom.map((zoom) => zoom >= 18)
|
||||
),
|
||||
pleaseLoginButton,
|
||||
state
|
||||
),
|
||||
this.cannotBeImportedMessage ?? t.wrongType,
|
||||
new UIEventSource(this.canBeImported(feature))
|
||||
)
|
||||
return new FixedUiElement("Deprecated")
|
||||
}
|
||||
|
||||
getLayerDependencies(argsRaw: string[]) {
|
||||
const args = this.parseArgs(argsRaw, undefined)
|
||||
|
||||
const dependsOnLayers: string[] = []
|
||||
|
||||
// The target layer
|
||||
dependsOnLayers.push(args.targetLayer)
|
||||
|
||||
const snapOntoLayers = args.snap_onto_layers?.trim() ?? ""
|
||||
if (snapOntoLayers !== "") {
|
||||
dependsOnLayers.push(...snapOntoLayers.split(";"))
|
||||
}
|
||||
|
||||
return dependsOnLayers
|
||||
}
|
||||
|
||||
protected abstract canBeImported(feature: any)
|
||||
|
||||
private static readonly conflationLayer = new LayerConfig(
|
||||
<LayerConfigJson>conflation_json,
|
||||
"all_known_layers",
|
||||
true
|
||||
)
|
||||
protected createConfirmPanelForWay(
|
||||
state: SpecialVisualizationState,
|
||||
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 map = new UIEventSource(undefined)
|
||||
new MapLibreAdaptor(map, {
|
||||
allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting),
|
||||
allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting),
|
||||
rasterLayer: state.mapProperties.rasterLayer,
|
||||
})
|
||||
const confirmationMap = new SvelteUIElement(MaplibreMap, { map })
|
||||
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
|
||||
|
||||
ShowDataLayer.showMultipleLayers(
|
||||
map,
|
||||
new StaticFeatureSource([feature]),
|
||||
state.layout.layers,
|
||||
{ zoomToFeatures: true }
|
||||
)
|
||||
// Show all relevant data - including (eventually) the way of which the geometry will be replaced
|
||||
|
||||
action.getPreview().then((changePreview) => {
|
||||
new ShowDataLayer(map, {
|
||||
zoomToFeatures: false,
|
||||
features: changePreview,
|
||||
layer: AbstractImportButton.conflationLayer,
|
||||
})
|
||||
})
|
||||
|
||||
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(
|
||||
new Img(args.icon),
|
||||
new Combine([args.text, tagsExplanation]).SetClass("flex flex-col")
|
||||
)
|
||||
confirmButton.onClick(async () => {
|
||||
{
|
||||
originalFeatureTags.data["_imported"] = "yes"
|
||||
originalFeatureTags.ping() // will set isImported as per its definition
|
||||
await state.changes.applyAction(action)
|
||||
const newId = action.newElementId ?? action.mainObjectId
|
||||
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(newId))
|
||||
}
|
||||
})
|
||||
|
||||
const cancel = new SubtleButton(Svg.close_svg(), Translations.t.general.cancel).onClick(
|
||||
onCancel
|
||||
)
|
||||
return new Combine([confirmationMap, confirmButton, cancel]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
protected 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) {
|
||||
const tags = baseArgs.tags
|
||||
if (
|
||||
tags.indexOf(" ") < 0 &&
|
||||
tags.indexOf(";") < 0 &&
|
||||
originalFeatureTags.data[tags] !== undefined
|
||||
) {
|
||||
// This might be 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
|
||||
)
|
||||
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(items, originalFeatureTags)
|
||||
} else {
|
||||
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags)
|
||||
}
|
||||
}
|
||||
return baseArgs
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflateButton extends AbstractImportButton {
|
||||
needsNodeDatabase = true
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
"conflate_button",
|
||||
|
@ -432,377 +153,10 @@ export class ConflateButton extends AbstractImportButton {
|
|||
)
|
||||
}
|
||||
|
||||
protected canBeImported(feature: any) {
|
||||
protected canBeImported(feature: Feature) {
|
||||
return (
|
||||
feature.geometry.type === "LineString" ||
|
||||
(feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class ImportWayButton extends AbstractImportButton implements AutoAction {
|
||||
public readonly supportsAutoAction = true
|
||||
|
||||
needsNodeDatabase = true
|
||||
|
||||
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: "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",
|
||||
},
|
||||
],
|
||||
{ showRemovedTags: false }
|
||||
)
|
||||
}
|
||||
|
||||
private static CreateAction(
|
||||
feature,
|
||||
args: {
|
||||
max_snap_distance: string
|
||||
snap_onto_layers: string
|
||||
icon: string
|
||||
text: string
|
||||
tags: string
|
||||
newTags: UIEventSource<any>
|
||||
targetLayer: string
|
||||
},
|
||||
state: SpecialVisualizationState,
|
||||
mergeConfigs: any[]
|
||||
): OsmCreateAction & { getPreview(): Promise<FeatureSource>; newElementId?: string } {
|
||||
const coors = feature.geometry.coordinates
|
||||
if (feature.geometry.type === "Polygon" && coors.length > 1) {
|
||||
const outer = coors[0]
|
||||
const inner = [...coors]
|
||||
inner.splice(0, 1)
|
||||
return new CreateMultiPolygonWithPointReuseAction(
|
||||
args.newTags.data,
|
||||
outer,
|
||||
inner,
|
||||
state,
|
||||
mergeConfigs,
|
||||
"import"
|
||||
)
|
||||
} else if (feature.geometry.type === "Polygon") {
|
||||
const outer = coors[0]
|
||||
return new CreateWayWithPointReuseAction(args.newTags.data, outer, state, mergeConfigs)
|
||||
} else if (feature.geometry.type === "LineString") {
|
||||
return new CreateWayWithPointReuseAction(args.newTags.data, coors, state, mergeConfigs)
|
||||
} else {
|
||||
throw "Unsupported type"
|
||||
}
|
||||
}
|
||||
|
||||
async applyActionOn(
|
||||
state: SpecialVisualizationState,
|
||||
originalFeatureTags: UIEventSource<Record<string, string>>,
|
||||
argument: string[]
|
||||
): Promise<void> {
|
||||
const id = originalFeatureTags.data.id
|
||||
if (AbstractImportButton.importedIds.has(originalFeatureTags.data.id)) {
|
||||
return
|
||||
}
|
||||
AbstractImportButton.importedIds.add(originalFeatureTags.data.id)
|
||||
const args = this.parseArgs(argument, originalFeatureTags)
|
||||
const feature = state.indexedFeatures.featuresById.data.get(id)
|
||||
const mergeConfigs = this.GetMergeConfig(args)
|
||||
const action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs)
|
||||
await state.changes.applyAction(action)
|
||||
}
|
||||
|
||||
canBeImported(feature: any) {
|
||||
const type = feature.geometry.type
|
||||
return type === "LineString" || type === "Polygon"
|
||||
}
|
||||
|
||||
constructElement(
|
||||
state: SpecialVisualizationState,
|
||||
args,
|
||||
originalFeatureTags: UIEventSource<Record<string, string>>,
|
||||
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 mergeConfigs = this.GetMergeConfig(args)
|
||||
let action: OsmCreateAction & { getPreview?: any } = ImportWayButton.CreateAction(
|
||||
feature,
|
||||
args,
|
||||
state,
|
||||
mergeConfigs
|
||||
)
|
||||
return this.createConfirmPanelForWay(
|
||||
state,
|
||||
args,
|
||||
feature,
|
||||
originalFeatureTags,
|
||||
action,
|
||||
onCancel
|
||||
)
|
||||
}
|
||||
|
||||
private GetMergeConfig(args: {
|
||||
max_snap_distance: string
|
||||
snap_onto_layers: string
|
||||
icon: string
|
||||
text: string
|
||||
tags: string
|
||||
newTags: UIEventSource<any>
|
||||
targetLayer: string
|
||||
}): 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
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
{
|
||||
name: "note_id",
|
||||
doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'",
|
||||
},
|
||||
{
|
||||
name: "location_picker",
|
||||
defaultValue: "photo",
|
||||
doc: "Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
},
|
||||
],
|
||||
{ showRemovedTags: false }
|
||||
)
|
||||
}
|
||||
|
||||
private static createConfirmPanelForPoint(
|
||||
args: {
|
||||
max_snap_distance: string
|
||||
snap_onto_layers: string
|
||||
icon: string
|
||||
text: string
|
||||
newTags: UIEventSource<any>
|
||||
targetLayer: string
|
||||
note_id: string
|
||||
maproulette_id: string
|
||||
},
|
||||
state: SpecialVisualizationState,
|
||||
originalFeatureTags: UIEventSource<any>,
|
||||
feature: Feature<Point>,
|
||||
onCancel: () => void,
|
||||
close: () => void
|
||||
): BaseUIElement {
|
||||
async function confirm(
|
||||
tags: any[],
|
||||
location: { lat: number; lon: number },
|
||||
snapOntoWayId: string
|
||||
) {
|
||||
originalFeatureTags.data["_imported"] = "yes"
|
||||
originalFeatureTags.ping() // will set isImported as per its definition
|
||||
let snapOnto: OsmObject | "deleted" = undefined
|
||||
if (snapOntoWayId !== undefined) {
|
||||
snapOnto = await state.osmObjectDownloader.DownloadObjectAsync(snapOntoWayId)
|
||||
}
|
||||
if(snapOnto === "deleted"){
|
||||
return new FixedUiElement("Error - way is deleted. Refresh the page").SetClass("alert")
|
||||
}
|
||||
let specialMotivation = undefined
|
||||
|
||||
let note_id = args.note_id
|
||||
if (args.note_id !== undefined && isNaN(Number(args.note_id))) {
|
||||
note_id = originalFeatureTags.data[args.note_id]
|
||||
specialMotivation = "source: https://osm.org/note/" + note_id
|
||||
}
|
||||
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
|
||||
theme: state.layout.id,
|
||||
changeType: "import",
|
||||
snapOnto: <OsmWay>snapOnto,
|
||||
specialMotivation: specialMotivation,
|
||||
})
|
||||
|
||||
await state.changes.applyAction(newElementAction)
|
||||
state.selectedElement.setData(
|
||||
state.indexedFeatures.featuresById.data.get(newElementAction.newElementId)
|
||||
)
|
||||
Hash.hash.setData(newElementAction.newElementId)
|
||||
|
||||
if (note_id !== undefined) {
|
||||
await state.osmConnection.closeNote(note_id, "imported")
|
||||
originalFeatureTags.data["closed_at"] = new Date().toISOString()
|
||||
originalFeatureTags.ping()
|
||||
}
|
||||
|
||||
let maproulette_id = originalFeatureTags.data[args.maproulette_id]
|
||||
console.log(
|
||||
"Checking if we need to mark a maproulette task as fixed (" + maproulette_id + ")"
|
||||
)
|
||||
if (maproulette_id !== undefined) {
|
||||
if (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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let preciseInputOption = args["location_picker"]
|
||||
let preciseInputSpec: PreciseInput = undefined
|
||||
console.log("Precise input location is ", preciseInputOption)
|
||||
if (preciseInputOption !== "none") {
|
||||
preciseInputSpec = {
|
||||
snapToLayers: args.snap_onto_layers?.split(";"),
|
||||
maxSnapDistance: Number(args.max_snap_distance),
|
||||
preferredBackground: args["location_picker"] ?? ["photo", "map"],
|
||||
}
|
||||
}
|
||||
|
||||
const presetInfo = <PresetInfo>{
|
||||
tags: args.newTags.data,
|
||||
icon: () => new Img(args.icon),
|
||||
layerToAddTo: state.layerState.filteredLayers.get(args.targetLayer),
|
||||
name: args.text,
|
||||
title: Translations.T(args.text),
|
||||
preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise
|
||||
boundsFactor: 3,
|
||||
}
|
||||
|
||||
const [lon, lat] = <[number, number]>feature.geometry.coordinates
|
||||
return new ConfirmLocationOfPoint(
|
||||
state,
|
||||
state.guistate.themeIsOpened ,
|
||||
presetInfo,
|
||||
Translations.W(args.text),
|
||||
{
|
||||
lon,
|
||||
lat,
|
||||
},
|
||||
confirm,
|
||||
onCancel,
|
||||
close
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
constructElement(
|
||||
state: SpecialVisualizationState,
|
||||
args,
|
||||
originalFeatureTags,
|
||||
feature,
|
||||
onCancel: () => void
|
||||
): BaseUIElement {
|
||||
const geometry = feature.geometry
|
||||
|
||||
if (geometry.type === "Point") {
|
||||
return new Lazy(() =>
|
||||
ImportPointButton.createConfirmPanelForPoint(
|
||||
args,
|
||||
state,
|
||||
originalFeatureTags,
|
||||
feature,
|
||||
onCancel,
|
||||
() => {
|
||||
// Close the current popup
|
||||
state.selectedElement.setData(undefined)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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