Refactoring: fix import buttons (WIP)
This commit is contained in:
parent
52b54d8b08
commit
8f942f0163
28 changed files with 970 additions and 779 deletions
|
@ -1,13 +1,15 @@
|
|||
import { OsmCreateAction } from "./OsmChangeAction"
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import {OsmCreateAction} from "./OsmChangeAction"
|
||||
import {Tag} from "../../Tags/Tag"
|
||||
import {Changes} from "../Changes"
|
||||
import {ChangeDescription} from "./ChangeDescription"
|
||||
import CreateNewWayAction from "./CreateNewWayAction"
|
||||
import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction"
|
||||
import { And } from "../../Tags/And"
|
||||
import { TagUtils } from "../../Tags/TagUtils"
|
||||
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
|
||||
import { FeatureSource } from "../../FeatureSource/FeatureSource"
|
||||
import CreateWayWithPointReuseAction, {MergePointConfig} from "./CreateWayWithPointReuseAction"
|
||||
import {And} from "../../Tags/And"
|
||||
import {TagUtils} from "../../Tags/TagUtils"
|
||||
import {IndexedFeatureSource} from "../../FeatureSource/FeatureSource"
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Position} from "geojson";
|
||||
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
|
||||
/**
|
||||
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
|
||||
|
@ -24,9 +26,14 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
|
|||
|
||||
constructor(
|
||||
tags: Tag[],
|
||||
outerRingCoordinates: [number, number][],
|
||||
innerRingsCoordinates: [number, number][][],
|
||||
state: SpecialVisualizationState,
|
||||
outerRingCoordinates: Position[],
|
||||
innerRingsCoordinates: Position[][],
|
||||
state: {
|
||||
layout: LayoutConfig;
|
||||
changes: Changes;
|
||||
indexedFeatures: IndexedFeatureSource,
|
||||
fullNodeDatabase?: FullNodeDatabaseSource
|
||||
},
|
||||
config: MergePointConfig[],
|
||||
changeType: "import" | "create" | string
|
||||
) {
|
||||
|
@ -36,7 +43,7 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
|
|||
this.theme = state?.layout?.id ?? ""
|
||||
this.createOuterWay = new CreateWayWithPointReuseAction(
|
||||
[],
|
||||
outerRingCoordinates,
|
||||
<[number,number][]> outerRingCoordinates,
|
||||
state,
|
||||
config
|
||||
)
|
||||
|
@ -59,10 +66,6 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
|
|||
}
|
||||
}
|
||||
|
||||
public async getPreview(): Promise<FeatureSource> {
|
||||
return undefined
|
||||
}
|
||||
|
||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
console.log("Running CMPWPRA")
|
||||
const descriptions: ChangeDescription[] = []
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import { OsmCreateAction } from "./OsmChangeAction"
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import { BBox } from "../../BBox"
|
||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import { FeatureSource } from "../../FeatureSource/FeatureSource"
|
||||
import {OsmCreateAction} from "./OsmChangeAction"
|
||||
import {Tag} from "../../Tags/Tag"
|
||||
import {Changes} from "../Changes"
|
||||
import {ChangeDescription} from "./ChangeDescription"
|
||||
import {BBox} from "../../BBox"
|
||||
import {TagsFilter} from "../../Tags/TagsFilter"
|
||||
import {GeoOperations} from "../../GeoOperations"
|
||||
import {FeatureSource, IndexedFeatureSource} from "../../FeatureSource/FeatureSource"
|
||||
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
|
||||
import CreateNewNodeAction from "./CreateNewNodeAction"
|
||||
import CreateNewWayAction from "./CreateNewWayAction"
|
||||
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
import {Position} from "geojson";
|
||||
|
||||
export interface MergePointConfig {
|
||||
withinRangeOfM: number
|
||||
|
@ -63,13 +65,23 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
* @private
|
||||
*/
|
||||
private readonly _coordinateInfo: CoordinateInfo[]
|
||||
private readonly _state: SpecialVisualizationState
|
||||
private readonly _state: {
|
||||
layout: LayoutConfig;
|
||||
changes: Changes;
|
||||
indexedFeatures: IndexedFeatureSource,
|
||||
fullNodeDatabase?: FullNodeDatabaseSource
|
||||
}
|
||||
private readonly _config: MergePointConfig[]
|
||||
|
||||
constructor(
|
||||
tags: Tag[],
|
||||
coordinates: [number, number][],
|
||||
state: SpecialVisualizationState,
|
||||
coordinates: Position[],
|
||||
state: {
|
||||
layout: LayoutConfig;
|
||||
changes: Changes;
|
||||
indexedFeatures: IndexedFeatureSource,
|
||||
fullNodeDatabase?: FullNodeDatabaseSource
|
||||
},
|
||||
config: MergePointConfig[]
|
||||
) {
|
||||
super(null, true)
|
||||
|
@ -78,7 +90,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
this._config = config
|
||||
|
||||
// The main logic of this class: the coordinateInfo contains all the changes
|
||||
this._coordinateInfo = this.CalculateClosebyNodes(coordinates)
|
||||
this._coordinateInfo = this.CalculateClosebyNodes(<[number,number][]> coordinates)
|
||||
}
|
||||
|
||||
public async getPreview(): Promise<FeatureSource> {
|
||||
|
@ -233,7 +245,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
},
|
||||
})
|
||||
}
|
||||
nodeIdsToUse.push({ lat, lon, nodeId: id })
|
||||
nodeIdsToUse.push({lat, lon, nodeId: id})
|
||||
}
|
||||
|
||||
const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, {
|
||||
|
@ -252,7 +264,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] {
|
||||
const bbox = new BBox(coordinates)
|
||||
const state = this._state
|
||||
const allNodes =state.fullNodeDatabase?.getNodesWithin(bbox.pad(1.2))
|
||||
const allNodes = state.fullNodeDatabase?.getNodesWithin(bbox.pad(1.2)) ?? []
|
||||
const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM))
|
||||
|
||||
// Init coordianteinfo with undefined but the same length as coordinates
|
||||
|
@ -309,7 +321,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
|||
if (!config.ifMatches.matchesProperties(node.properties)) {
|
||||
continue
|
||||
}
|
||||
closebyNodes.push({ node, d, config })
|
||||
closebyNodes.push({node, d, config})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Wrapper around 'subtleButton' with an arrow pointing to the right
|
||||
* See also: NextButton
|
||||
*/
|
||||
import SubtleButton from "./SubtleButton.svelte";
|
||||
import {ChevronLeftIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
|
@ -12,5 +13,5 @@
|
|||
|
||||
<SubtleButton on:click={() => dispatch("click")} options={{extraClasses:clss+ " flex items-center"}}>
|
||||
<ChevronLeftIcon class="w-12 h-12" slot="image"/>
|
||||
<slot name="message" slot="message"/>
|
||||
<slot slot="message"/>
|
||||
</SubtleButton>
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* A round button with an icon and possible a small text, which hovers above the map
|
||||
*/
|
||||
const dispatch = createEventDispatcher()
|
||||
export let cls = ""
|
||||
</script>
|
||||
|
||||
|
||||
<button on:click={e => dispatch("click", e)} class="rounded-full h-fit w-fit m-0.5 md:m-1 p-0.5 sm:p-1 pointer-events-auto">
|
||||
<button on:click={e => dispatch("click", e)} class={"rounded-full h-fit w-fit m-0.5 md:m-1 p-0.5 sm:p-1 pointer-events-auto "+cls} >
|
||||
<slot/>
|
||||
</button>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Wrapper around 'subtleButton' with an arrow pointing to the right
|
||||
* See also: BackButton
|
||||
*/
|
||||
import SubtleButton from "./SubtleButton.svelte";
|
||||
import {ChevronRightIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
|
|
|
@ -283,7 +283,7 @@ export class DownloadPanel extends Toggle {
|
|||
|
||||
for (const neededLayer of neededLayers) {
|
||||
const indexedFeatureSource = state.perLayer.get(neededLayer)
|
||||
let features = indexedFeatureSource.GetFeaturesWithin(bbox, true)
|
||||
let features = indexedFeatureSource.GetFeaturesWithin(bbox)
|
||||
// The 'indexedFeatureSources' contains _all_ features, they are not filtered yet
|
||||
const filter = state.layerState.filteredLayers.get(neededLayer)
|
||||
features = features.filter((f) =>
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
* - Show more layers
|
||||
* - Snap to layers
|
||||
*
|
||||
* This one is mostly used to insert new points
|
||||
* This one is mostly used to insert new points, including when importing
|
||||
*/
|
||||
export let state: SpecialVisualizationState;
|
||||
/**
|
||||
* The start coordinate
|
||||
*/
|
||||
export let coordinate: { lon: number, lat: number };
|
||||
export let coordinate: { lon: number, lat: number };
|
||||
export let snapToLayers: string[] | undefined;
|
||||
export let targetLayer: LayerConfig;
|
||||
export let maxSnapDistance: number = undefined;
|
||||
|
@ -105,4 +105,4 @@
|
|||
|
||||
|
||||
<LocationInput {map} mapProperties={initialMapProperties}
|
||||
value={preciseLocation} initialCoordinate={{...coordinate}} maxDistanceInMeters=50 />
|
||||
value={preciseLocation} initialCoordinate={coordinate} maxDistanceInMeters=50 />
|
||||
|
|
|
@ -14,11 +14,11 @@
|
|||
import {onDestroy} from "svelte";
|
||||
|
||||
/**
|
||||
* A visualisation to pick a direction on a map background
|
||||
* A visualisation to pick a location on a map background
|
||||
*/
|
||||
export let value: UIEventSource<{ lon: number, lat: number }>;
|
||||
export let initialCoordinate : {lon: number, lat :number}
|
||||
initialCoordinate = initialCoordinate ?? value .data
|
||||
initialCoordinate = initialCoordinate ?? value.data
|
||||
export let maxDistanceInMeters: number = undefined
|
||||
export let mapProperties: Partial<MapProperties> & {
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
|
|
|
@ -238,7 +238,7 @@
|
|||
<div class="flex w-full flex-wrap-reverse md:flex-nowrap">
|
||||
|
||||
<BackButton on:click={() => selectedPreset = undefined} clss="w-full">
|
||||
<Tr slot="message" t={t.backToSelect}/>
|
||||
<Tr t={t.backToSelect}/>
|
||||
</BackButton>
|
||||
|
||||
<NextButton on:click={() => confirmedCategory = true} clss="primary w-full">
|
||||
|
|
|
@ -25,11 +25,13 @@ import ShowDataLayer from "../Map/ShowDataLayer"
|
|||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
import SpecialVisualizations from "../SpecialVisualizations"
|
||||
import {Feature} from "geojson";
|
||||
|
||||
export interface AutoAction extends SpecialVisualization {
|
||||
supportsAutoAction: boolean
|
||||
|
||||
applyActionOn(
|
||||
feature: Feature,
|
||||
state: {
|
||||
layout: LayoutConfig
|
||||
changes: Changes
|
||||
|
|
|
@ -57,7 +57,7 @@ export class CloseNoteButton implements SpecialVisualization {
|
|||
comment: string
|
||||
minZoom: string
|
||||
zoomButton: string
|
||||
} = Utils.ParseVisArgs(this.args, args)
|
||||
} = <any> Utils.ParseVisArgs(this.args, args)
|
||||
|
||||
let icon = Svg.checkmark_svg()
|
||||
if (params.icon !== "checkmark.svg" && (args[2] ?? "") !== "") {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
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()
|
||||
}
|
||||
|
||||
}
|
|
@ -130,6 +130,7 @@ export default class MoveWizard extends Toggle {
|
|||
zoom: new UIEventSource(reason?.startZoom ?? 16),
|
||||
location: new UIEventSource({ lon, lat }),
|
||||
bounds: new UIEventSource(undefined),
|
||||
rasterLayer: state.mapProperties.rasterLayer
|
||||
}
|
||||
const value = new UIEventSource<{ lon: number; lat: number }>(undefined)
|
||||
const locationInput = new SvelteUIElement(LocationInput, {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
/**
|
||||
* If given, this function will be called to embed the given tags hint into this translation
|
||||
*/
|
||||
export let embedIn: (() => Translation) | undefined = undefined;
|
||||
export let embedIn: ((string: string) => Translation) | undefined = undefined;
|
||||
const userDetails = state.osmConnection.userDetails;
|
||||
let tagsExplanation = "";
|
||||
$: tagsExplanation = tags?.asHumanString(true, false, {});
|
||||
|
|
|
@ -80,7 +80,7 @@ export interface SpecialVisualization {
|
|||
readonly example?: string
|
||||
|
||||
/**
|
||||
* Indicates that this special visualsiation will make requests to the 'alLNodesDatabase' and that it thus should be included
|
||||
* Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included
|
||||
*/
|
||||
readonly needsNodeDatabase?: boolean
|
||||
readonly args: {
|
||||
|
|
|
@ -11,7 +11,6 @@ import {UploadToOsmViz} from "./Popup/UploadToOsmViz"
|
|||
import {MultiApplyViz} from "./Popup/MultiApplyViz"
|
||||
import {AddNoteCommentViz} from "./Popup/AddNoteCommentViz"
|
||||
import {PlantNetDetectionViz} from "./Popup/PlantNetDetectionViz"
|
||||
import {ConflateButton, ImportWayButton} from "./Popup/ImportButton"
|
||||
import TagApplyButton from "./Popup/TagApplyButton"
|
||||
import {CloseNoteButton} from "./Popup/CloseNoteButton"
|
||||
import {MapillaryLinkVis} from "./Popup/MapillaryLinkVis"
|
||||
|
@ -72,7 +71,8 @@ import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
|||
import {ExportAsGpxViz} from "./Popup/ExportAsGpxViz"
|
||||
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
|
||||
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import {ImportPointButton} from "./Popup/ImportButtons/ImportPointButton";
|
||||
import {ImportPointButtonViz} from "./Popup/ImportButtons/ImportPointButtonViz";
|
||||
import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz";
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
@ -619,9 +619,9 @@ export default class SpecialVisualizations {
|
|||
|
||||
new TagApplyButton(),
|
||||
|
||||
new ImportPointButton(),
|
||||
new ImportWayButton(),
|
||||
new ConflateButton(),
|
||||
new ImportPointButtonViz(),
|
||||
new WayImportButtonViz(),
|
||||
// TODO new ConflateButton(),
|
||||
|
||||
new NearbyImageVis(),
|
||||
|
||||
|
|
4
Utils.ts
4
Utils.ts
|
@ -153,8 +153,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
public static ParseVisArgs(
|
||||
specs: { name: string; defaultValue?: string }[],
|
||||
args: string[]
|
||||
): any {
|
||||
const parsed = {}
|
||||
): Record<string, string> {
|
||||
const parsed: Record<string, string> = {}
|
||||
if (args.length > specs.length) {
|
||||
throw (
|
||||
"To much arguments for special visualization: got " +
|
||||
|
|
|
@ -981,6 +981,14 @@ video {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.h-screen {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.h-32 {
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.h-8 {
|
||||
height: 2rem;
|
||||
}
|
||||
|
@ -989,10 +997,6 @@ video {
|
|||
height: 4rem;
|
||||
}
|
||||
|
||||
.h-32 {
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.h-12 {
|
||||
height: 3rem;
|
||||
}
|
||||
|
@ -1001,10 +1005,6 @@ video {
|
|||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.h-screen {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
|
@ -1075,8 +1075,8 @@ video {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
.w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.w-8 {
|
||||
|
@ -1087,10 +1087,6 @@ video {
|
|||
width: 4rem;
|
||||
}
|
||||
|
||||
.w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.w-12 {
|
||||
width: 3rem;
|
||||
}
|
||||
|
@ -1137,6 +1133,10 @@ video {
|
|||
width: 16rem;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.w-96 {
|
||||
width: 24rem;
|
||||
}
|
||||
|
@ -1257,6 +1257,10 @@ video {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-col-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
@ -1449,11 +1453,6 @@ video {
|
|||
border-style: dotted;
|
||||
}
|
||||
|
||||
.border-gray-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(107 114 128 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-black {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(0 0 0 / var(--tw-border-opacity));
|
||||
|
@ -1484,6 +1483,11 @@ video {
|
|||
border-color: rgb(75 85 99 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(107 114 128 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-opacity-50 {
|
||||
--tw-border-opacity: 0.5;
|
||||
}
|
||||
|
@ -1528,14 +1532,14 @@ video {
|
|||
background-color: rgb(254 202 202 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.p-8 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</head>
|
||||
<body>
|
||||
|
||||
<div id="maindiv" class="w-full">'maindiv' not attached</div>
|
||||
<div id="maindiv" class="w-full h-full">'maindiv' not attached</div>
|
||||
<div id="extradiv">'extradiv' not attached</div>
|
||||
|
||||
<script type="module" src="./test.ts"></script>
|
||||
|
|
66
test.ts
66
test.ts
|
@ -1,18 +1,15 @@
|
|||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"
|
||||
import * as theme from "./assets/generated/themes/shops.json"
|
||||
import * as theme from "./assets/generated/themes/bookcases.json"
|
||||
import ThemeViewState from "./Models/ThemeViewState"
|
||||
import Combine from "./UI/Base/Combine"
|
||||
import SpecialVisualizations from "./UI/SpecialVisualizations"
|
||||
import InputHelpers from "./UI/InputElement/InputHelpers"
|
||||
import BaseUIElement from "./UI/BaseUIElement"
|
||||
import { UIEventSource } from "./Logic/UIEventSource"
|
||||
import { VariableUiElement } from "./UI/Base/VariableUIElement"
|
||||
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
||||
import Title from "./UI/Base/Title"
|
||||
import {VariableUiElement} from "./UI/Base/VariableUIElement"
|
||||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||
import ValidatedInput from "./UI/InputElement/ValidatedInput.svelte"
|
||||
import { SvgToPdf } from "./Utils/svgToPdf"
|
||||
import { Utils } from "./Utils"
|
||||
import {SvgToPdf} from "./Utils/svgToPdf"
|
||||
import {Utils} from "./Utils"
|
||||
import {PointImportFlowState} from "./UI/Popup/ImportButtons/PointImportFlowState";
|
||||
import PointImportFlow from "./UI/Popup/ImportButtons/PointImportFlow.svelte";
|
||||
import {Feature, Point} from "geojson";
|
||||
|
||||
function testspecial() {
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
|
@ -24,30 +21,6 @@ function testspecial() {
|
|||
new Combine(all).AttachTo("maindiv")
|
||||
}
|
||||
|
||||
function testinput() {
|
||||
const els: BaseUIElement[] = []
|
||||
for (const key in InputHelpers.AvailableInputHelpers) {
|
||||
const value = new UIEventSource<string>(undefined)
|
||||
const helper = InputHelpers.AvailableInputHelpers[key](value, {
|
||||
mapProperties: {
|
||||
zoom: new UIEventSource(16),
|
||||
location: new UIEventSource({ lat: 51.1, lon: 3.2 }),
|
||||
},
|
||||
})
|
||||
|
||||
const feedback: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
els.push(
|
||||
new Combine([
|
||||
new Title(key),
|
||||
new SvelteUIElement(ValidatedInput, { value, type: key, feedback }),
|
||||
helper,
|
||||
new VariableUiElement(feedback),
|
||||
new VariableUiElement(value.map((v) => new FixedUiElement(v))),
|
||||
]).SetClass("flex flex-col p-1 border-3 border-gray-500")
|
||||
)
|
||||
}
|
||||
new Combine(els).SetClass("flex flex-col").AttachTo("maindiv")
|
||||
}
|
||||
|
||||
async function testPdf() {
|
||||
const svgs = await Promise.all(
|
||||
|
@ -59,8 +32,31 @@ async function testPdf() {
|
|||
await pdf.ConvertSvg("nl")
|
||||
}
|
||||
|
||||
function testImportButton() {
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
const state = new ThemeViewState(layout)
|
||||
const originalFeature: Feature<Point> = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: "note/-1"
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [3.2255, 51.2112]
|
||||
}
|
||||
}
|
||||
const importFlow = new PointImportFlowState(state, originalFeature, {
|
||||
text: "Import this point",
|
||||
newTags: undefined,
|
||||
targetLayer: "public_bookcase"
|
||||
}, tagsToApply)
|
||||
new SvelteUIElement(PointImportFlow, {
|
||||
importFlow
|
||||
}).SetClass("h-full").AttachTo("maindiv")
|
||||
}
|
||||
|
||||
testImportButton()
|
||||
// testPdf().then((_) => console.log("All done"))
|
||||
//testinput()
|
||||
/*/
|
||||
testspecial()
|
||||
//*/
|
||||
|
|
Loading…
Reference in a new issue