forked from MapComplete/MapComplete
Refactoring: fix rendering of new roads, generated by a split
This commit is contained in:
parent
840990c08b
commit
8eb2c68f79
34 changed files with 443 additions and 333 deletions
|
@ -11,7 +11,6 @@ import { Utils } from "../../Utils"
|
|||
import { MapillaryLink } from "./MapillaryLink"
|
||||
import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
|
||||
export class BackToThemeOverview extends Toggle {
|
||||
|
@ -78,14 +77,6 @@ export class ActionButtons extends Combine {
|
|||
new OpenIdEditor(state, iconStyle),
|
||||
new MapillaryLink(state, iconStyle),
|
||||
new OpenJosm(state, iconStyle).SetClass("hidden-on-mobile"),
|
||||
new SubtleButton(
|
||||
Svg.translate_ui().SetStyle(iconStyle),
|
||||
Translations.t.translations.activateButton
|
||||
).onClick(() => {
|
||||
ScrollableFullScreen.collapse()
|
||||
state.defaultGuiState.userInfoIsOpened.setData(true)
|
||||
state.defaultGuiState.userInfoFocusedQuestion.setData("translation-mode")
|
||||
}),
|
||||
])
|
||||
this.SetClass("block w-full link-no-underline")
|
||||
}
|
||||
|
|
104
UI/BigComponents/WaySplitMap.svelte
Normal file
104
UI/BigComponents/WaySplitMap.svelte
Normal file
|
@ -0,0 +1,104 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* This component shows a map which focuses on a single OSM-Way (linestring) feature.
|
||||
* Clicking the map will add a new 'scissor' point, projected on the linestring (and possible snapped to an already existing node within the linestring;
|
||||
* clicking this point again will remove it.
|
||||
* The bound 'value' will contain the location of these projected points.
|
||||
* Points are not coalesced with already existing nodes within the way; it is up to the code actually splitting the way to decide to reuse an existing point or not
|
||||
*
|
||||
* This component is _not_ responsible for the rest of the flow, e.g. the confirm button
|
||||
*/
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import split_point from "../../assets/layers/split_point/split_point.json";
|
||||
import split_road from "../../assets/layers/split_road/split_road.json";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import type { MapProperties } from "../../Models/MapProperties";
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor";
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte";
|
||||
import { OsmWay } from "../../Logic/Osm/OsmObject";
|
||||
import ShowDataLayer from "../Map/ShowDataLayer";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||
import { BBox } from "../../Logic/BBox";
|
||||
import type { Feature, LineString, Point } from "geojson";
|
||||
|
||||
const splitpoint_style = new LayerConfig(
|
||||
<LayerConfigJson>split_point,
|
||||
"(BUILTIN) SplitRoadWizard.ts",
|
||||
true
|
||||
) as const;
|
||||
|
||||
const splitroad_style = new LayerConfig(
|
||||
<LayerConfigJson>split_road,
|
||||
"(BUILTIN) SplitRoadWizard.ts",
|
||||
true
|
||||
) as const;
|
||||
|
||||
/**
|
||||
* The way to focus on
|
||||
*/
|
||||
export let osmWay: OsmWay
|
||||
/**
|
||||
* How to render this layer.
|
||||
* A default is given
|
||||
*/
|
||||
export let layer: LayerConfig = splitroad_style
|
||||
/**
|
||||
* Optional: use these properties to set e.g. background layer
|
||||
*/
|
||||
export let mapProperties: undefined | Partial<MapProperties> = undefined;
|
||||
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
let adaptor = new MapLibreAdaptor(map, mapProperties);
|
||||
|
||||
const wayGeojson: Feature<LineString> = GeoOperations.forceLineString( osmWay.asGeoJson())
|
||||
adaptor.location.setData(GeoOperations.centerpointCoordinatesObj(wayGeojson))
|
||||
adaptor.bounds.setData(BBox.get(wayGeojson).pad(2))
|
||||
adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2))
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
features: new StaticFeatureSource([wayGeojson]),
|
||||
drawMarkers: false,
|
||||
layer: layer
|
||||
})
|
||||
|
||||
export let splitPoints: UIEventSource< Feature<
|
||||
Point,
|
||||
{
|
||||
id: number
|
||||
index: number
|
||||
dist: number
|
||||
location: number
|
||||
}
|
||||
>[]> = new UIEventSource([])
|
||||
const splitPointsFS = new StaticFeatureSource(splitPoints)
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: splitpoint_style,
|
||||
features: splitPointsFS,
|
||||
onClick: (clickedFeature: Feature) => {
|
||||
console.log("Clicked feature is", clickedFeature, splitPoints.data)
|
||||
const i = splitPoints.data.findIndex(f => f === clickedFeature)
|
||||
if(i < 0){
|
||||
return
|
||||
}
|
||||
splitPoints.data.splice(i, 1)
|
||||
splitPoints.ping()
|
||||
}
|
||||
})
|
||||
let id = 0
|
||||
adaptor.lastClickLocation.addCallbackD(({lon, lat}) => {
|
||||
const projected = GeoOperations.nearestPoint(wayGeojson, [lon, lat])
|
||||
|
||||
projected.properties["id"] = id
|
||||
id++
|
||||
splitPoints.data.push(<any> projected)
|
||||
splitPoints.ping()
|
||||
})
|
||||
|
||||
</script>
|
||||
<div class="w-full h-full">
|
||||
<MaplibreMap {map}></MaplibreMap>
|
||||
</div>
|
|
@ -90,6 +90,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
self.setAllowMoving(self.allowMoving.data)
|
||||
self.setAllowZooming(self.allowZooming.data)
|
||||
self.setMinzoom(self.minzoom.data)
|
||||
self.setBounds(self.bounds.data)
|
||||
})
|
||||
self.MoveMapToCurrentLoc(self.location.data)
|
||||
self.SetZoom(self.zoom.data)
|
||||
|
@ -97,6 +98,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
self.setAllowMoving(self.allowMoving.data)
|
||||
self.setAllowZooming(self.allowZooming.data)
|
||||
self.setMinzoom(self.minzoom.data)
|
||||
self.setBounds(self.bounds.data)
|
||||
this.updateStores()
|
||||
map.on("moveend", () => this.updateStores())
|
||||
map.on("click", (e) => {
|
||||
|
@ -238,18 +240,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
container.style.height = document.documentElement.clientHeight + "px"
|
||||
}
|
||||
|
||||
const markerCanvas: HTMLCanvasElement = await html2canvas(
|
||||
await html2canvas(
|
||||
map.getCanvasContainer(),
|
||||
{
|
||||
backgroundColor: "#00000000",
|
||||
canvas: drawOn,
|
||||
}
|
||||
)
|
||||
const markers = await new Promise<Blob>((resolve) =>
|
||||
markerCanvas.toBlob((data) => resolve(data))
|
||||
)
|
||||
console.log("Markers:", markers, markerCanvas)
|
||||
// destinationCtx.drawImage(markerCanvas, 0, 0)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
|
@ -429,7 +426,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
private setBounds(bounds: BBox) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
if (map === undefined || bounds === undefined) {
|
||||
return
|
||||
}
|
||||
const oldBounds = map.getBounds()
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
$map.resize();
|
||||
});
|
||||
});
|
||||
const styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=GvoVAJgu46I5rZapJuAy";
|
||||
const styleUrl = "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy";
|
||||
</script>
|
||||
<main>
|
||||
<Map bind:center={center}
|
||||
|
|
|
@ -197,7 +197,7 @@ class LineRenderingLayer {
|
|||
this._fetchStore = fetchStore
|
||||
this._onClick = onClick
|
||||
const self = this
|
||||
features.features.addCallbackAndRunD((features) => self.update(features))
|
||||
features.features.addCallbackAndRunD(() => self.update(features.features))
|
||||
}
|
||||
|
||||
private calculatePropsFor(
|
||||
|
@ -229,13 +229,23 @@ class LineRenderingLayer {
|
|||
return calculatedProps
|
||||
}
|
||||
|
||||
private async update(features: Feature[]) {
|
||||
private currentSourceData
|
||||
private async update(featureSource: Store<Feature[]>) {
|
||||
const map = this._map
|
||||
while (!map.isStyleLoaded()) {
|
||||
await Utils.waitFor(100)
|
||||
}
|
||||
|
||||
// After waiting 'till the map has loaded, the data might have changed already
|
||||
// As such, we only now read the features from the featureSource and compare with the previously set data
|
||||
const features = featureSource.data
|
||||
const src = <GeoJSONSource>map.getSource(this._layername)
|
||||
if (this.currentSourceData === features) {
|
||||
// Already up to date
|
||||
return
|
||||
}
|
||||
if (src === undefined) {
|
||||
this.currentSourceData = features
|
||||
map.addSource(this._layername, {
|
||||
type: "geojson",
|
||||
data: {
|
||||
|
@ -262,7 +272,6 @@ class LineRenderingLayer {
|
|||
})
|
||||
|
||||
map.on("click", linelayer, (e) => {
|
||||
console.log("Click", e)
|
||||
e.originalEvent["consumed"] = true
|
||||
this._onClick(e.features[0])
|
||||
})
|
||||
|
@ -297,9 +306,10 @@ class LineRenderingLayer {
|
|||
}
|
||||
})
|
||||
} else {
|
||||
this.currentSourceData = features
|
||||
src.setData({
|
||||
type: "FeatureCollection",
|
||||
features,
|
||||
features: this.currentSourceData,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -345,10 +355,21 @@ export default class ShowDataLayer {
|
|||
"ShowDataLayer.ts:range.json"
|
||||
)
|
||||
private readonly _map: Store<MlMap>
|
||||
private readonly _options: ShowDataLayerOptions & { layer: LayerConfig }
|
||||
private readonly _options: ShowDataLayerOptions & {
|
||||
layer: LayerConfig
|
||||
drawMarkers?: true | boolean
|
||||
drawLines?: true | boolean
|
||||
}
|
||||
private readonly _popupCache: Map<string, ScrollableFullScreen>
|
||||
|
||||
constructor(map: Store<MlMap>, options: ShowDataLayerOptions & { layer: LayerConfig }) {
|
||||
constructor(
|
||||
map: Store<MlMap>,
|
||||
options: ShowDataLayerOptions & {
|
||||
layer: LayerConfig
|
||||
drawMarkers?: true | boolean
|
||||
drawLines?: true | boolean
|
||||
}
|
||||
) {
|
||||
this._map = map
|
||||
this._options = options
|
||||
this._popupCache = new Map()
|
||||
|
@ -405,28 +426,31 @@ export default class ShowDataLayer {
|
|||
selectedElement?.setData(feature)
|
||||
selectedLayer?.setData(this._options.layer)
|
||||
})
|
||||
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
|
||||
const lineRenderingConfig = this._options.layer.lineRendering[i]
|
||||
new LineRenderingLayer(
|
||||
map,
|
||||
features,
|
||||
this._options.layer.id + "_linerendering_" + i,
|
||||
lineRenderingConfig,
|
||||
doShowLayer,
|
||||
fetchStore,
|
||||
onClick
|
||||
)
|
||||
if (this._options.drawLines !== false) {
|
||||
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
|
||||
const lineRenderingConfig = this._options.layer.lineRendering[i]
|
||||
new LineRenderingLayer(
|
||||
map,
|
||||
features,
|
||||
this._options.layer.id + "_linerendering_" + i,
|
||||
lineRenderingConfig,
|
||||
doShowLayer,
|
||||
fetchStore,
|
||||
onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const pointRenderingConfig of this._options.layer.mapRendering) {
|
||||
new PointRenderingLayer(
|
||||
map,
|
||||
features,
|
||||
pointRenderingConfig,
|
||||
doShowLayer,
|
||||
fetchStore,
|
||||
onClick
|
||||
)
|
||||
if (this._options.drawMarkers !== false) {
|
||||
for (const pointRenderingConfig of this._options.layer.mapRendering) {
|
||||
new PointRenderingLayer(
|
||||
map,
|
||||
features,
|
||||
pointRenderingConfig,
|
||||
doShowLayer,
|
||||
fetchStore,
|
||||
onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
features.features.addCallbackAndRunD((_) => this.zoomToCurrentFeatures(map))
|
||||
}
|
||||
|
|
|
@ -83,19 +83,9 @@
|
|||
snapOnto: snapToWay
|
||||
});
|
||||
await state.changes.applyAction(newElementAction);
|
||||
// The 'changes' should have created a new point, which added this into the 'featureProperties'
|
||||
const newId = newElementAction.newElementId;
|
||||
state.newFeatures.features.data.push({
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: newId,
|
||||
...TagUtils.KVtoProperties(tags)
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [location.lon, location.lat]
|
||||
}
|
||||
});
|
||||
state.newFeatures.features.ping();
|
||||
|
||||
const tagsStore = state.featureProperties.getStore(newId);
|
||||
{
|
||||
// Set some metainfo
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
])
|
||||
}
|
||||
};
|
||||
// Normally, the 'Changes' will generate the new element. The 'notes' are an exception to this
|
||||
state.newFeatures.features.data.push(feature);
|
||||
state.newFeatures.features.ping();
|
||||
state.selectedElement?.setData(feature);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { OsmConnection, OsmServiceState } from "../../Logic/Osm/OsmConnection"
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Loading from "../Base/Loading"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
|
||||
|
@ -13,13 +13,13 @@ class LoginButton extends SubtleButton {
|
|||
constructor(
|
||||
text: BaseUIElement | string,
|
||||
state: {
|
||||
osmConnection: OsmConnection
|
||||
osmConnection?: OsmConnection
|
||||
},
|
||||
icon?: BaseUIElement | string
|
||||
) {
|
||||
super(icon ?? Svg.login_ui(), text)
|
||||
this.onClick(() => {
|
||||
state.osmConnection.AttemptLogin()
|
||||
state.osmConnection?.AttemptLogin()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -32,13 +32,16 @@ export class LoginToggle extends VariableUiElement {
|
|||
* If logging in is not possible for some reason, an appropriate error message is shown
|
||||
*
|
||||
* State contains the 'osmConnection' to work with
|
||||
* @param el: Element to show when logged in
|
||||
* @param text: To show on the login button. Default: nothing
|
||||
* @param state: if no osmConnection is given, assumes test situation and will show 'el' as if logged in
|
||||
*/
|
||||
constructor(
|
||||
el: BaseUIElement,
|
||||
text: BaseUIElement | string,
|
||||
state: {
|
||||
readonly osmConnection: OsmConnection
|
||||
readonly featureSwitchUserbadge: Store<boolean>
|
||||
readonly osmConnection?: OsmConnection
|
||||
readonly featureSwitchUserbadge?: Store<boolean>
|
||||
}
|
||||
) {
|
||||
const loading = new Loading("Trying to log in...")
|
||||
|
@ -51,14 +54,14 @@ export class LoginToggle extends VariableUiElement {
|
|||
}
|
||||
|
||||
super(
|
||||
state.osmConnection.loadingStatus.map(
|
||||
state.osmConnection?.loadingStatus?.map(
|
||||
(osmConnectionState) => {
|
||||
if (state.featureSwitchUserbadge.data == false) {
|
||||
if (state.featureSwitchUserbadge?.data == false) {
|
||||
// All features to login with are disabled
|
||||
return undefined
|
||||
}
|
||||
|
||||
const apiState = state.osmConnection.apiIsOnline.data
|
||||
const apiState = state.osmConnection?.apiIsOnline?.data ?? "online"
|
||||
const apiTranslation = offlineModes[apiState]
|
||||
if (apiTranslation !== undefined) {
|
||||
return new Combine([
|
||||
|
@ -77,15 +80,15 @@ export class LoginToggle extends VariableUiElement {
|
|||
return el
|
||||
}
|
||||
|
||||
// Error!
|
||||
// Fallback
|
||||
return new LoginButton(
|
||||
Translations.t.general.loginFailed,
|
||||
state,
|
||||
Svg.invalid_svg()
|
||||
)
|
||||
},
|
||||
[state.featureSwitchUserbadge, state.osmConnection.apiIsOnline]
|
||||
)
|
||||
[state.featureSwitchUserbadge, state.osmConnection?.apiIsOnline]
|
||||
) ?? new ImmutableStore(el) //
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,33 +2,25 @@ import Toggle from "../Input/Toggle"
|
|||
import Svg from "../../Svg"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import Combine from "../Base/Combine"
|
||||
import { Button } from "../Base/Button"
|
||||
import Translations from "../i18n/Translations"
|
||||
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
|
||||
import Title from "../Base/Title"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import split_point from "../../assets/layers/split_point/split_point.json"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import { LoginToggle } from "./LoginButton"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import WaySplitMap from "../BigComponents/WaySplitMap.svelte"
|
||||
import { OsmObject } from "../../Logic/Osm/OsmObject"
|
||||
import { Feature, Point } from "geojson"
|
||||
import { WayId } from "../../Models/OsmFeature"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
|
||||
export default class SplitRoadWizard extends Combine {
|
||||
private static splitLayerStyling = new LayerConfig(
|
||||
split_point,
|
||||
"(BUILTIN) SplitRoadWizard.ts",
|
||||
true
|
||||
)
|
||||
|
||||
public dialogIsOpened: UIEventSource<boolean>
|
||||
|
||||
/**
|
||||
|
@ -37,20 +29,34 @@ export default class SplitRoadWizard extends Combine {
|
|||
* @param id: The id of the road to remove
|
||||
* @param state: the state of the application
|
||||
*/
|
||||
constructor(id: string, state: SpecialVisualizationState) {
|
||||
constructor(
|
||||
id: WayId,
|
||||
state: {
|
||||
layout?: LayoutConfig
|
||||
osmConnection?: OsmConnection
|
||||
changes?: Changes
|
||||
indexedFeatures?: IndexedFeatureSource
|
||||
selectedElement?: UIEventSource<Feature>
|
||||
}
|
||||
) {
|
||||
const t = Translations.t.split
|
||||
|
||||
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
|
||||
const splitPoints = new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
const splitPoints = new UIEventSource<Feature<Point>[]>([])
|
||||
|
||||
const hasBeenSplit = new UIEventSource(false)
|
||||
|
||||
// Toggle variable between show split button and map
|
||||
const splitClicked = new UIEventSource<boolean>(false)
|
||||
|
||||
const leafletMap = new UIEventSource<BaseUIElement>(
|
||||
SplitRoadWizard.setupMapComponent(id, splitPoints, state)
|
||||
)
|
||||
const leafletMap = new UIEventSource<BaseUIElement>(undefined)
|
||||
|
||||
function initMap() {
|
||||
SplitRoadWizard.setupMapComponent(id, splitPoints).then((mapComponent) =>
|
||||
leafletMap.setData(mapComponent.SetClass("w-full h-80"))
|
||||
)
|
||||
}
|
||||
initMap()
|
||||
|
||||
// Toggle between splitmap
|
||||
const splitButton = new SubtleButton(
|
||||
|
@ -70,23 +76,19 @@ export default class SplitRoadWizard extends Combine {
|
|||
splitClicked.setData(false)
|
||||
const splitAction = new SplitAction(
|
||||
id,
|
||||
splitPoints.data.map((ff) => ff.feature.geometry.coordinates),
|
||||
splitPoints.data.map((ff) => <[number, number]>(<Point>ff.geometry).coordinates),
|
||||
{
|
||||
theme: state?.layoutToUse?.id,
|
||||
theme: state?.layout?.id,
|
||||
},
|
||||
5,
|
||||
(coordinates) => {
|
||||
state.allElements.ContainingFeatures.get(id).geometry["coordinates"] =
|
||||
coordinates
|
||||
}
|
||||
5
|
||||
)
|
||||
await state.changes.applyAction(splitAction)
|
||||
await state.changes?.applyAction(splitAction)
|
||||
// We throw away the old map and splitpoints, and create a new map from scratch
|
||||
splitPoints.setData([])
|
||||
leafletMap.setData(SplitRoadWizard.setupMapComponent(id, splitPoints, state))
|
||||
initMap()
|
||||
|
||||
// Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219
|
||||
ScrollableFullScreen.collapse()
|
||||
state.selectedElement?.setData(undefined)
|
||||
})
|
||||
|
||||
saveButton.SetClass("btn btn-primary mr-3")
|
||||
|
@ -131,95 +133,14 @@ export default class SplitRoadWizard extends Combine {
|
|||
})
|
||||
}
|
||||
|
||||
private static setupMapComponent(
|
||||
id: string,
|
||||
splitPoints: UIEventSource<{ feature: any; freshness: Date }[]>,
|
||||
state: {
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
backgroundLayer: UIEventSource<BaseLayer>
|
||||
featureSwitchIsTesting: UIEventSource<boolean>
|
||||
featureSwitchIsDebugging: UIEventSource<boolean>
|
||||
featureSwitchShowAllQuestions: UIEventSource<boolean>
|
||||
osmConnection: OsmConnection
|
||||
featureSwitchUserbadge: UIEventSource<boolean>
|
||||
changes: Changes
|
||||
layoutToUse: LayoutConfig
|
||||
allElements: ElementStorage
|
||||
}
|
||||
): BaseUIElement {
|
||||
// Load the road with given id on the minimap
|
||||
const roadElement = state.allElements.ContainingFeatures.get(id)
|
||||
|
||||
// Minimap on which you can select the points to be splitted
|
||||
const miniMap = Minimap.createMiniMap({
|
||||
background: state.backgroundLayer,
|
||||
allowMoving: true,
|
||||
leafletOptions: {
|
||||
minZoom: 14,
|
||||
},
|
||||
private static async setupMapComponent(
|
||||
id: WayId,
|
||||
splitPoints: UIEventSource<Feature[]>
|
||||
): Promise<BaseUIElement> {
|
||||
const osmWay = await OsmObject.DownloadObjectAsync(id)
|
||||
return new SvelteUIElement(WaySplitMap, {
|
||||
osmWay,
|
||||
splitPoints,
|
||||
})
|
||||
miniMap.SetStyle("width: 100%; height: 24rem").SetClass("rounded-xl overflow-hidden")
|
||||
|
||||
miniMap.installBounds(BBox.get(roadElement).pad(0.25), false)
|
||||
|
||||
// Define how a cut is displayed on the map
|
||||
|
||||
// Datalayer displaying the road and the cut points (if any)
|
||||
new ShowDataMultiLayer({
|
||||
features: StaticFeatureSource.fromGeojson([roadElement]),
|
||||
layers: state.filteredLayers,
|
||||
leafletMap: miniMap.leafletMap,
|
||||
zoomToFeatures: true,
|
||||
state,
|
||||
})
|
||||
|
||||
new ShowDataLayer({
|
||||
features: new StaticFeatureSource(splitPoints),
|
||||
leafletMap: miniMap.leafletMap,
|
||||
zoomToFeatures: false,
|
||||
layerToShow: SplitRoadWizard.splitLayerStyling,
|
||||
state,
|
||||
})
|
||||
/**
|
||||
* Handles a click on the overleaf map.
|
||||
* Finds the closest intersection with the road and adds a point there, ready to confirm the cut.
|
||||
* @param coordinates Clicked location, [lon, lat]
|
||||
*/
|
||||
function onMapClick(coordinates) {
|
||||
// First, we check if there is another, already existing point nearby
|
||||
const points = splitPoints.data
|
||||
.map((f, i) => [f.feature, i])
|
||||
.filter(
|
||||
(p) => GeoOperations.distanceBetween(p[0].geometry.coordinates, coordinates) < 5
|
||||
)
|
||||
.map((p) => p[1])
|
||||
.sort((a, b) => a - b)
|
||||
.reverse(/*Copy/derived list, inplace reverse is fine*/)
|
||||
if (points.length > 0) {
|
||||
for (const point of points) {
|
||||
splitPoints.data.splice(point, 1)
|
||||
}
|
||||
splitPoints.ping()
|
||||
return
|
||||
}
|
||||
|
||||
// Get nearest point on the road
|
||||
const pointOnRoad = GeoOperations.nearestPoint(<any>roadElement, coordinates) // pointOnRoad is a geojson
|
||||
|
||||
// Update point properties to let it match the layer
|
||||
pointOnRoad.properties["_split_point"] = "yes"
|
||||
|
||||
// Add it to the list of all points and notify observers
|
||||
splitPoints.data.push({ feature: pointOnRoad, freshness: new Date() }) // show the point on the data layer
|
||||
splitPoints.ping() // not updated using .setData, so manually ping observers
|
||||
}
|
||||
|
||||
// When clicked, pass clicked location coordinates to onMapClick function
|
||||
miniMap.leafletMap.addCallbackAndRunD((leafletMap) =>
|
||||
leafletMap.on("click", (mouseEvent: LeafletMouseEvent) => {
|
||||
onMapClick([mouseEvent.latlng.lng, mouseEvent.latlng.lat])
|
||||
})
|
||||
)
|
||||
return miniMap
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import BaseUIElement from "./BaseUIElement"
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../Logic/Osm/Changes"
|
||||
import { ExportableMap, MapProperties } from "../Models/MapProperties"
|
||||
import LayerState from "../Logic/State/LayerState"
|
||||
import { Feature, Geometry } from "geojson"
|
||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import { MenuState } from "../Models/MenuState"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import BaseUIElement from "./BaseUIElement";
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource";
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||
import { Changes } from "../Logic/Osm/Changes";
|
||||
import { ExportableMap, MapProperties } from "../Models/MapProperties";
|
||||
import LayerState from "../Logic/State/LayerState";
|
||||
import { Feature, Geometry } from "geojson";
|
||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews";
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||
import { MenuState } from "../Models/MenuState";
|
||||
|
||||
/**
|
||||
* The state needed to render a special Visualisation.
|
||||
|
|
|
@ -77,8 +77,9 @@ import Lazy from "./Base/Lazy"
|
|||
import { CheckBox } from "./Input/Checkboxes"
|
||||
import Slider from "./Input/Slider"
|
||||
import DeleteWizard from "./Popup/DeleteWizard"
|
||||
import { OsmId, OsmTags } from "../Models/OsmFeature"
|
||||
import { OsmId, OsmTags, WayId } from "../Models/OsmFeature"
|
||||
import MoveWizard from "./Popup/MoveWizard"
|
||||
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
@ -532,16 +533,17 @@ export default class SpecialVisualizations {
|
|||
args: [],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
tagSource: UIEventSource<Record<string, string>>
|
||||
): BaseUIElement {
|
||||
return new VariableUiElement(
|
||||
// TODO
|
||||
tagSource
|
||||
.map((tags) => tags.id)
|
||||
.map((id) => new FixedUiElement("TODO: enable splitting")) // new SplitRoadWizard(id, state))
|
||||
.map((id) => {
|
||||
if (id.startsWith("way/")) {
|
||||
return new SplitRoadWizard(<WayId>id, state)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue