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
|
@ -1,6 +1,9 @@
|
||||||
import { Changes } from "../Osm/Changes"
|
import { Changes } from "../Osm/Changes"
|
||||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore";
|
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies tag changes onto the featureStore
|
||||||
|
*/
|
||||||
export default class ChangeToElementsActor {
|
export default class ChangeToElementsActor {
|
||||||
constructor(changes: Changes, allElements: FeaturePropertiesStore) {
|
constructor(changes: Changes, allElements: FeaturePropertiesStore) {
|
||||||
changes.pendingChanges.addCallbackAndRun((changes) => {
|
changes.pendingChanges.addCallbackAndRun((changes) => {
|
||||||
|
|
|
@ -55,12 +55,13 @@ class SingleTileSaver {
|
||||||
*/
|
*/
|
||||||
export default class SaveFeatureSourceToLocalStorage {
|
export default class SaveFeatureSourceToLocalStorage {
|
||||||
constructor(
|
constructor(
|
||||||
|
backend: string,
|
||||||
layername: string,
|
layername: string,
|
||||||
zoomlevel: number,
|
zoomlevel: number,
|
||||||
features: FeatureSource,
|
features: FeatureSource,
|
||||||
featureProperties: FeaturePropertiesStore
|
featureProperties: FeaturePropertiesStore
|
||||||
) {
|
) {
|
||||||
const storage = TileLocalStorage.construct<Feature[]>(layername)
|
const storage = TileLocalStorage.construct<Feature[]>(backend, layername)
|
||||||
const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>()
|
const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>()
|
||||||
features.features.addCallbackAndRunD((features) => {
|
features.features.addCallbackAndRunD((features) => {
|
||||||
const sliced = GeoOperations.slice(zoomlevel, features)
|
const sliced = GeoOperations.slice(zoomlevel, features)
|
||||||
|
|
|
@ -17,14 +17,15 @@ export default class TileLocalStorage<T> {
|
||||||
this._layername = layername
|
this._layername = layername
|
||||||
}
|
}
|
||||||
|
|
||||||
public static construct<T>(layername: string): TileLocalStorage<T> {
|
public static construct<T>(backend: string, layername: string): TileLocalStorage<T> {
|
||||||
const cached = TileLocalStorage.perLayer[layername]
|
const key = backend + "_" + layername
|
||||||
|
const cached = TileLocalStorage.perLayer[key]
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
|
|
||||||
const tls = new TileLocalStorage<T>(layername)
|
const tls = new TileLocalStorage<T>(key)
|
||||||
TileLocalStorage.perLayer[layername] = tls
|
TileLocalStorage.perLayer[key] = tls
|
||||||
return tls
|
return tls
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ export default class TileLocalStorage<T> {
|
||||||
return src
|
return src
|
||||||
}
|
}
|
||||||
|
|
||||||
private async SetIdb(tileIndex: number, data): Promise<void> {
|
private async SetIdb(tileIndex: number, data: any): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data)
|
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -3,22 +3,18 @@
|
||||||
*/
|
*/
|
||||||
import { Changes } from "../../Osm/Changes"
|
import { Changes } from "../../Osm/Changes"
|
||||||
import { UIEventSource } from "../../UIEventSource"
|
import { UIEventSource } from "../../UIEventSource"
|
||||||
import { FeatureSourceForLayer, IndexedFeatureSource } from "../FeatureSource"
|
import { FeatureSource, IndexedFeatureSource } from "../FeatureSource"
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
|
||||||
import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription"
|
import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
export default class ChangeGeometryApplicator implements FeatureSource {
|
||||||
public readonly features: UIEventSource<Feature[]> =
|
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
|
||||||
new UIEventSource<Feature[]>([])
|
|
||||||
public readonly layer: FilteredLayer
|
|
||||||
private readonly source: IndexedFeatureSource
|
private readonly source: IndexedFeatureSource
|
||||||
private readonly changes: Changes
|
private readonly changes: Changes
|
||||||
|
|
||||||
constructor(source: IndexedFeatureSource & FeatureSourceForLayer, changes: Changes) {
|
constructor(source: IndexedFeatureSource, changes: Changes) {
|
||||||
this.source = source
|
this.source = source
|
||||||
this.changes = changes
|
this.changes = changes
|
||||||
this.layer = source.layer
|
|
||||||
|
|
||||||
this.features = new UIEventSource<Feature[]>(undefined)
|
this.features = new UIEventSource<Feature[]>(undefined)
|
||||||
|
|
||||||
|
@ -30,10 +26,10 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
const upstreamFeatures = this.source.features.data
|
const upstreamFeatures = this.source.features.data
|
||||||
const upstreamIds = this.source.containedIds.data
|
const upstreamIds = this.source.featuresById.data
|
||||||
const changesToApply = this.changes.allChanges.data?.filter(
|
const changesToApply = this.changes.allChanges.data?.filter(
|
||||||
(ch) =>
|
(ch) =>
|
||||||
// Does upsteram have this element? If not, we skip
|
// Does upstream have this element? If not, we skip
|
||||||
upstreamIds.has(ch.type + "/" + ch.id) &&
|
upstreamIds.has(ch.type + "/" + ch.id) &&
|
||||||
// Are any (geometry) changes defined?
|
// Are any (geometry) changes defined?
|
||||||
ch.changes !== undefined &&
|
ch.changes !== undefined &&
|
||||||
|
@ -61,7 +57,7 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
||||||
for (const feature of upstreamFeatures) {
|
for (const feature of upstreamFeatures) {
|
||||||
const changesForFeature = changesPerId.get(feature.properties.id)
|
const changesForFeature = changesPerId.get(feature.properties.id)
|
||||||
if (changesForFeature === undefined) {
|
if (changesForFeature === undefined) {
|
||||||
// No changes for this element
|
// No changes for this element - simply pass it along to downstream
|
||||||
newFeatures.push(feature)
|
newFeatures.push(feature)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Store, UIEventSource } from "../../UIEventSource"
|
import { Store, UIEventSource } from "../../UIEventSource"
|
||||||
import { FeatureSource , IndexedFeatureSource } from "../FeatureSource"
|
import { FeatureSource, IndexedFeatureSource } from "../FeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { Utils } from "../../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
|
||||||
this._featuresById = new UIEventSource<Map<string, Feature>>(undefined)
|
this._featuresById = new UIEventSource<Map<string, Feature>>(undefined)
|
||||||
this.featuresById = this._featuresById
|
this.featuresById = this._featuresById
|
||||||
const self = this
|
const self = this
|
||||||
|
sources = Utils.NoNull(sources)
|
||||||
for (let source of sources) {
|
for (let source of sources) {
|
||||||
source.features.addCallback(() => {
|
source.features.addCallback(() => {
|
||||||
self.addData(sources.map((s) => s.features.data))
|
self.addData(sources.map((s) => s.features.data))
|
||||||
|
@ -28,7 +29,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
|
||||||
this._sources = sources
|
this._sources = sources
|
||||||
}
|
}
|
||||||
|
|
||||||
protected addSource(source: FeatureSource) {
|
public addSource(source: FeatureSource) {
|
||||||
this._sources.push(source)
|
this._sources.push(source)
|
||||||
source.features.addCallbackAndRun(() => {
|
source.features.addCallbackAndRun(() => {
|
||||||
this.addData(this._sources.map((s) => s.features.data))
|
this.addData(this._sources.map((s) => s.features.data))
|
||||||
|
|
|
@ -4,13 +4,14 @@ import { FeatureSource } from "../FeatureSource"
|
||||||
import { Or } from "../../Tags/Or"
|
import { Or } from "../../Tags/Or"
|
||||||
import FeatureSwitchState from "../../State/FeatureSwitchState"
|
import FeatureSwitchState from "../../State/FeatureSwitchState"
|
||||||
import OverpassFeatureSource from "./OverpassFeatureSource"
|
import OverpassFeatureSource from "./OverpassFeatureSource"
|
||||||
import { ImmutableStore, Store } from "../../UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
||||||
import OsmFeatureSource from "./OsmFeatureSource"
|
import OsmFeatureSource from "./OsmFeatureSource"
|
||||||
import FeatureSourceMerger from "./FeatureSourceMerger"
|
import FeatureSourceMerger from "./FeatureSourceMerger"
|
||||||
import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource"
|
import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource"
|
import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource"
|
||||||
import StaticFeatureSource from "./StaticFeatureSource"
|
import StaticFeatureSource from "./StaticFeatureSource"
|
||||||
|
import { OsmPreferences } from "../../Osm/OsmPreferences"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This source will fetch the needed data from various sources for the given layout.
|
* This source will fetch the needed data from various sources for the given layout.
|
||||||
|
@ -18,15 +19,14 @@ import StaticFeatureSource from "./StaticFeatureSource"
|
||||||
* Note that special layers (with `source=null` will be ignored)
|
* Note that special layers (with `source=null` will be ignored)
|
||||||
*/
|
*/
|
||||||
export default class LayoutSource extends FeatureSourceMerger {
|
export default class LayoutSource extends FeatureSourceMerger {
|
||||||
|
private readonly _isLoading: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||||
/**
|
/**
|
||||||
* Indicates if a data source is loading something
|
* Indicates if a data source is loading something
|
||||||
* TODO fixme
|
|
||||||
*/
|
*/
|
||||||
public readonly isLoading: Store<boolean> = new ImmutableStore(false)
|
public readonly isLoading: Store<boolean> = this._isLoading
|
||||||
constructor(
|
constructor(
|
||||||
layers: LayerConfig[],
|
layers: LayerConfig[],
|
||||||
featureSwitches: FeatureSwitchState,
|
featureSwitches: FeatureSwitchState,
|
||||||
newAndChangedElements: FeatureSource,
|
|
||||||
mapProperties: { bounds: Store<BBox>; zoom: Store<number> },
|
mapProperties: { bounds: Store<BBox>; zoom: Store<number> },
|
||||||
backend: string,
|
backend: string,
|
||||||
isDisplayed: (id: string) => Store<boolean>
|
isDisplayed: (id: string) => Store<boolean>
|
||||||
|
@ -39,7 +39,7 @@ export default class LayoutSource extends FeatureSourceMerger {
|
||||||
const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined)
|
const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined)
|
||||||
const fromCache = osmLayers.map(
|
const fromCache = osmLayers.map(
|
||||||
(l) =>
|
(l) =>
|
||||||
new LocalStorageFeatureSource(l.id, 15, mapProperties, {
|
new LocalStorageFeatureSource(backend, l.id, 15, mapProperties, {
|
||||||
isActive: isDisplayed(l.id),
|
isActive: isDisplayed(l.id),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -56,7 +56,17 @@ export default class LayoutSource extends FeatureSourceMerger {
|
||||||
)
|
)
|
||||||
|
|
||||||
const expiryInSeconds = Math.min(...(layers?.map((l) => l.maxAgeOfCache) ?? []))
|
const expiryInSeconds = Math.min(...(layers?.map((l) => l.maxAgeOfCache) ?? []))
|
||||||
super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources, ...fromCache)
|
|
||||||
|
super(overpassSource, osmApiSource, ...geojsonSources, ...fromCache)
|
||||||
|
|
||||||
|
const self = this
|
||||||
|
function setIsLoading() {
|
||||||
|
const loading = overpassSource?.runningQuery?.data && osmApiSource?.isRunning?.data
|
||||||
|
self._isLoading.setData(loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
overpassSource?.runningQuery?.addCallbackAndRun((_) => setIsLoading())
|
||||||
|
osmApiSource?.isRunning?.addCallbackAndRun((_) => setIsLoading())
|
||||||
}
|
}
|
||||||
|
|
||||||
private static setupGeojsonSource(
|
private static setupGeojsonSource(
|
||||||
|
@ -83,9 +93,9 @@ export default class LayoutSource extends FeatureSourceMerger {
|
||||||
zoom: Store<number>,
|
zoom: Store<number>,
|
||||||
backend: string,
|
backend: string,
|
||||||
featureSwitches: FeatureSwitchState
|
featureSwitches: FeatureSwitchState
|
||||||
): FeatureSource {
|
): OsmFeatureSource | undefined {
|
||||||
if (osmLayers.length == 0) {
|
if (osmLayers.length == 0) {
|
||||||
return new StaticFeatureSource(new ImmutableStore([]))
|
return undefined
|
||||||
}
|
}
|
||||||
const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom))
|
const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom))
|
||||||
const isActive = zoom.mapD((z) => {
|
const isActive = zoom.mapD((z) => {
|
||||||
|
@ -115,9 +125,9 @@ export default class LayoutSource extends FeatureSourceMerger {
|
||||||
bounds: Store<BBox>,
|
bounds: Store<BBox>,
|
||||||
zoom: Store<number>,
|
zoom: Store<number>,
|
||||||
featureSwitches: FeatureSwitchState
|
featureSwitches: FeatureSwitchState
|
||||||
): FeatureSource {
|
): OverpassFeatureSource | undefined {
|
||||||
if (osmLayers.length == 0) {
|
if (osmLayers.length == 0) {
|
||||||
return new StaticFeatureSource(new ImmutableStore([]))
|
return undefined
|
||||||
}
|
}
|
||||||
const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom))
|
const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom))
|
||||||
const isActive = zoom.mapD((z) => {
|
const isActive = zoom.mapD((z) => {
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { Changes } from "../../Osm/Changes"
|
import { Changes } from "../../Osm/Changes"
|
||||||
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject"
|
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject"
|
||||||
import { FeatureSource } from "../FeatureSource"
|
import { IndexedFeatureSource, WritableFeatureSource } from "../FeatureSource"
|
||||||
import { UIEventSource } from "../../UIEventSource"
|
import { UIEventSource } from "../../UIEventSource"
|
||||||
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
|
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
|
||||||
import { ElementStorage } from "../../ElementStorage"
|
|
||||||
import { OsmId, OsmTags } from "../../../Models/OsmFeature"
|
import { OsmId, OsmTags } from "../../../Models/OsmFeature"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource {
|
||||||
// This class name truly puts the 'Java' into 'Javascript'
|
// This class name truly puts the 'Java' into 'Javascript'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,7 +17,7 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
||||||
*/
|
*/
|
||||||
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
|
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
|
||||||
|
|
||||||
constructor(changes: Changes, allElementStorage: ElementStorage, backendUrl: string) {
|
constructor(changes: Changes, allElementStorage: IndexedFeatureSource, backendUrl: string) {
|
||||||
const seenChanges = new Set<ChangeDescription>()
|
const seenChanges = new Set<ChangeDescription>()
|
||||||
const features = this.features.data
|
const features = this.features.data
|
||||||
const self = this
|
const self = this
|
||||||
|
@ -53,7 +52,7 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
||||||
// In _most_ of the cases, this means that this _isn't_ a new object
|
// In _most_ of the cases, this means that this _isn't_ a new object
|
||||||
// However, when a point is snapped to an already existing point, we have to create a representation for this point!
|
// However, when a point is snapped to an already existing point, we have to create a representation for this point!
|
||||||
// For this, we introspect the change
|
// For this, we introspect the change
|
||||||
if (allElementStorage.has(change.type + "/" + change.id)) {
|
if (allElementStorage.featuresById.data.has(change.type + "/" + change.id)) {
|
||||||
// The current point already exists, we don't have to do anything here
|
// The current point already exists, we don't have to do anything here
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -65,7 +64,6 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
||||||
feat.tags[kv.k] = kv.v
|
feat.tags[kv.k] = kv.v
|
||||||
}
|
}
|
||||||
const geojson = feat.asGeoJson()
|
const geojson = feat.asGeoJson()
|
||||||
allElementStorage.addOrGetElement(geojson)
|
|
||||||
self.features.data.push(geojson)
|
self.features.data.push(geojson)
|
||||||
self.features.ping()
|
self.features.ping()
|
||||||
})
|
})
|
||||||
|
|
|
@ -41,7 +41,6 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
|
||||||
this.isActive = options.isActive ?? new ImmutableStore(true)
|
this.isActive = options.isActive ?? new ImmutableStore(true)
|
||||||
this._backend = options.backend ?? "https://www.openstreetmap.org"
|
this._backend = options.backend ?? "https://www.openstreetmap.org"
|
||||||
this._bounds.addCallbackAndRunD((bbox) => this.loadData(bbox))
|
this._bounds.addCallbackAndRunD((bbox) => this.loadData(bbox))
|
||||||
console.log("Allowed tags are:", this.allowedTags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadData(bbox: BBox) {
|
private async loadData(bbox: BBox) {
|
||||||
|
@ -108,7 +107,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async LoadTile(z, x, y): Promise<void> {
|
private async LoadTile(z, x, y): Promise<void> {
|
||||||
console.log("OsmFeatureSource: loading ", z, x, y)
|
console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend)
|
||||||
if (z >= 22) {
|
if (z >= 22) {
|
||||||
throw "This is an absurd high zoom level"
|
throw "This is an absurd high zoom level"
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,13 +22,18 @@ export interface SnappingOptions {
|
||||||
* The resulting snap coordinates will be written into this UIEventSource
|
* The resulting snap coordinates will be written into this UIEventSource
|
||||||
*/
|
*/
|
||||||
snapLocation?: UIEventSource<{ lon: number; lat: number }>
|
snapLocation?: UIEventSource<{ lon: number; lat: number }>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the projected point is within `reusePointWithin`-meter of an already existing point
|
||||||
|
*/
|
||||||
|
reusePointWithin?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SnappingFeatureSource implements FeatureSource {
|
export default class SnappingFeatureSource implements FeatureSource {
|
||||||
public readonly features: Store<Feature<Point>[]>
|
public readonly features: Store<Feature<Point>[]>
|
||||||
|
/*Contains the id of the way it snapped to*/
|
||||||
private readonly _snappedTo: UIEventSource<string>
|
|
||||||
public readonly snappedTo: Store<string>
|
public readonly snappedTo: Store<string>
|
||||||
|
private readonly _snappedTo: UIEventSource<string>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
snapTo: FeatureSource,
|
snapTo: FeatureSource,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import StaticFeatureSource from "../Sources/StaticFeatureSource"
|
||||||
|
|
||||||
export default class LocalStorageFeatureSource extends DynamicTileSource {
|
export default class LocalStorageFeatureSource extends DynamicTileSource {
|
||||||
constructor(
|
constructor(
|
||||||
|
backend: string,
|
||||||
layername: string,
|
layername: string,
|
||||||
zoomlevel: number,
|
zoomlevel: number,
|
||||||
mapProperties: {
|
mapProperties: {
|
||||||
|
@ -17,7 +18,7 @@ export default class LocalStorageFeatureSource extends DynamicTileSource {
|
||||||
isActive?: Store<boolean>
|
isActive?: Store<boolean>
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const storage = TileLocalStorage.construct<Feature[]>(layername)
|
const storage = TileLocalStorage.construct<Feature[]>(backend, layername)
|
||||||
super(
|
super(
|
||||||
zoomlevel,
|
zoomlevel,
|
||||||
(tileIndex) =>
|
(tileIndex) =>
|
||||||
|
|
|
@ -294,6 +294,10 @@ export class GeoOperations {
|
||||||
* Mostly used as helper for 'nearestPoint'
|
* Mostly used as helper for 'nearestPoint'
|
||||||
* @param way
|
* @param way
|
||||||
*/
|
*/
|
||||||
|
public static forceLineString(way: Feature<LineString | Polygon>): Feature<LineString>
|
||||||
|
public static forceLineString(
|
||||||
|
way: Feature<MultiLineString | MultiPolygon>
|
||||||
|
): Feature<MultiLineString>
|
||||||
public static forceLineString(
|
public static forceLineString(
|
||||||
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
|
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
|
||||||
): Feature<LineString | MultiLineString> {
|
): Feature<LineString | MultiLineString> {
|
||||||
|
@ -972,4 +976,9 @@ export class GeoOperations {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static centerpointCoordinatesObj(geojson: Feature) {
|
||||||
|
const [lon, lat] = GeoOperations.centerpointCoordinates(geojson)
|
||||||
|
return { lon, lat }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { GeoOperations } from "../../GeoOperations"
|
||||||
import OsmChangeAction from "./OsmChangeAction"
|
import OsmChangeAction from "./OsmChangeAction"
|
||||||
import { ChangeDescription } from "./ChangeDescription"
|
import { ChangeDescription } from "./ChangeDescription"
|
||||||
import RelationSplitHandler from "./RelationSplitHandler"
|
import RelationSplitHandler from "./RelationSplitHandler"
|
||||||
|
import { Feature, LineString } from "geojson"
|
||||||
|
|
||||||
interface SplitInfo {
|
interface SplitInfo {
|
||||||
originalIndex?: number // or negative for new elements
|
originalIndex?: number // or negative for new elements
|
||||||
|
@ -14,9 +15,9 @@ interface SplitInfo {
|
||||||
export default class SplitAction extends OsmChangeAction {
|
export default class SplitAction extends OsmChangeAction {
|
||||||
private readonly wayId: string
|
private readonly wayId: string
|
||||||
private readonly _splitPointsCoordinates: [number, number][] // lon, lat
|
private readonly _splitPointsCoordinates: [number, number][] // lon, lat
|
||||||
private _meta: { theme: string; changeType: "split" }
|
private readonly _meta: { theme: string; changeType: "split" }
|
||||||
private _toleranceInMeters: number
|
private readonly _toleranceInMeters: number
|
||||||
private _withNewCoordinates: (coordinates: [number, number][]) => void
|
private readonly _withNewCoordinates: (coordinates: [number, number][]) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a changedescription for splitting a point.
|
* Create a changedescription for splitting a point.
|
||||||
|
@ -197,7 +198,7 @@ export default class SplitAction extends OsmChangeAction {
|
||||||
* If another point is closer then ~5m, we reuse that point
|
* If another point is closer then ~5m, we reuse that point
|
||||||
*/
|
*/
|
||||||
private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] {
|
private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] {
|
||||||
const wayGeoJson = osmWay.asGeoJson()
|
const wayGeoJson = <Feature<LineString>>osmWay.asGeoJson()
|
||||||
// Should be [lon, lat][]
|
// Should be [lon, lat][]
|
||||||
const originalPoints: [number, number][] = osmWay.coordinates.map((c) => [c[1], c[0]])
|
const originalPoints: [number, number][] = osmWay.coordinates.map((c) => [c[1], c[0]])
|
||||||
const allPoints: {
|
const allPoints: {
|
||||||
|
|
|
@ -18,10 +18,6 @@ import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesSto
|
||||||
* Needs an authenticator via OsmConnection
|
* Needs an authenticator via OsmConnection
|
||||||
*/
|
*/
|
||||||
export class Changes {
|
export class Changes {
|
||||||
/**
|
|
||||||
* All the newly created features as featureSource + all the modified features
|
|
||||||
*/
|
|
||||||
public readonly features = new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
|
||||||
public readonly pendingChanges: UIEventSource<ChangeDescription[]> =
|
public readonly pendingChanges: UIEventSource<ChangeDescription[]> =
|
||||||
LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||||
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
|
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
|
||||||
|
|
|
@ -213,7 +213,7 @@ class RewriteMetaInfoTags extends SimpleMetaTagger {
|
||||||
move("changeset", "_last_edit:changeset")
|
move("changeset", "_last_edit:changeset")
|
||||||
move("timestamp", "_last_edit:timestamp")
|
move("timestamp", "_last_edit:timestamp")
|
||||||
move("version", "_version_number")
|
move("version", "_version_number")
|
||||||
feature.properties._backend = "https://openstreetmap.org"
|
feature.properties._backend = feature.properties._backend ?? "https://openstreetmap.org"
|
||||||
return movedSomething
|
return movedSomething
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,9 @@ export class IdbLocalStorage {
|
||||||
return src
|
return src
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SetDirectly(key: string, value): Promise<void> {
|
public static SetDirectly(key: string, value: any): Promise<void> {
|
||||||
return idb.set(key, value)
|
const copy = Utils.Clone(value)
|
||||||
|
return idb.set(key, copy)
|
||||||
}
|
}
|
||||||
|
|
||||||
static GetDirectly(key: string): Promise<void> {
|
static GetDirectly(key: string): Promise<void> {
|
||||||
|
|
|
@ -43,6 +43,7 @@ export default class Constants {
|
||||||
public static readonly no_include = [
|
public static readonly no_include = [
|
||||||
"conflation",
|
"conflation",
|
||||||
"split_point",
|
"split_point",
|
||||||
|
"split_road",
|
||||||
"current_view",
|
"current_view",
|
||||||
"matchpoint",
|
"matchpoint",
|
||||||
"import_candidate",
|
"import_candidate",
|
||||||
|
|
|
@ -596,6 +596,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
||||||
id: "split-button",
|
id: "split-button",
|
||||||
render: { "*": "{split_button()}" },
|
render: { "*": "{split_button()}" },
|
||||||
})
|
})
|
||||||
|
delete json.allowSplit
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.allowMove && !ValidationUtils.hasSpecialVisualisation(json, "move_button")) {
|
if (json.allowMove && !ValidationUtils.hasSpecialVisualisation(json, "move_button")) {
|
||||||
|
@ -611,7 +612,16 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.deletion && !ValidationUtils.hasSpecialVisualisation(json, "all_tags")) {
|
if (
|
||||||
|
json.source !== "special" &&
|
||||||
|
json.source !== "special:library" &&
|
||||||
|
json.tagRenderings &&
|
||||||
|
!json.tagRenderings.some((tr) => tr["id"] === "last_edit")
|
||||||
|
) {
|
||||||
|
json.tagRenderings.push(this._desugaring.tagRenderings.get("last_edit"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ValidationUtils.hasSpecialVisualisation(json, "all_tags")) {
|
||||||
const trc: TagRenderingConfigJson = {
|
const trc: TagRenderingConfigJson = {
|
||||||
id: "all-tags",
|
id: "all-tags",
|
||||||
render: { "*": "{all_tags()}" },
|
render: { "*": "{all_tags()}" },
|
||||||
|
@ -623,16 +633,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
json.tagRenderings.push(trc)
|
json.tagRenderings?.push(trc)
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
json.source !== "special" &&
|
|
||||||
json.source !== "special:library" &&
|
|
||||||
json.tagRenderings &&
|
|
||||||
!json.tagRenderings.some((tr) => tr["id"] === "last_edit")
|
|
||||||
) {
|
|
||||||
json.tagRenderings.push(this._desugaring.tagRenderings.get("last_edit"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { result: json }
|
return { result: json }
|
||||||
|
|
|
@ -26,7 +26,6 @@ import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
|
||||||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||||
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
||||||
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||||
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
|
|
||||||
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
|
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
|
||||||
import ShowDataLayer from "../UI/Map/ShowDataLayer"
|
import ShowDataLayer from "../UI/Map/ShowDataLayer"
|
||||||
import TitleHandler from "../Logic/Actors/TitleHandler"
|
import TitleHandler from "../Logic/Actors/TitleHandler"
|
||||||
|
@ -39,9 +38,10 @@ import Hotkeys from "../UI/Base/Hotkeys"
|
||||||
import Translations from "../UI/i18n/Translations"
|
import Translations from "../UI/i18n/Translations"
|
||||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||||
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
||||||
import SimpleFeatureSource from "../Logic/FeatureSource/Sources/SimpleFeatureSource"
|
|
||||||
import { MenuState } from "./MenuState"
|
import { MenuState } from "./MenuState"
|
||||||
import MetaTagging from "../Logic/MetaTagging"
|
import MetaTagging from "../Logic/MetaTagging"
|
||||||
|
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
|
||||||
|
import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -65,7 +65,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
readonly selectedElement: UIEventSource<Feature>
|
readonly selectedElement: UIEventSource<Feature>
|
||||||
readonly mapProperties: MapProperties & ExportableMap
|
readonly mapProperties: MapProperties & ExportableMap
|
||||||
|
|
||||||
readonly dataIsLoading: Store<boolean> // TODO
|
readonly dataIsLoading: Store<boolean>
|
||||||
readonly guistate: MenuState
|
readonly guistate: MenuState
|
||||||
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
|
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
|
||||||
|
|
||||||
|
@ -80,6 +80,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
readonly geolocation: GeoLocationHandler
|
readonly geolocation: GeoLocationHandler
|
||||||
|
|
||||||
readonly lastClickObject: WritableFeatureSource
|
readonly lastClickObject: WritableFeatureSource
|
||||||
|
|
||||||
constructor(layout: LayoutConfig) {
|
constructor(layout: LayoutConfig) {
|
||||||
this.layout = layout
|
this.layout = layout
|
||||||
this.guistate = new MenuState(layout.id)
|
this.guistate = new MenuState(layout.id)
|
||||||
|
@ -121,49 +122,69 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
|
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
|
||||||
this.newFeatures = new SimpleFeatureSource(undefined)
|
|
||||||
const layoutSource = new LayoutSource(
|
|
||||||
layout.layers,
|
|
||||||
this.featureSwitches,
|
|
||||||
this.newFeatures,
|
|
||||||
this.mapProperties,
|
|
||||||
this.osmConnection.Backend(),
|
|
||||||
(id) => self.layerState.filteredLayers.get(id).isDisplayed
|
|
||||||
)
|
|
||||||
this.indexedFeatures = layoutSource
|
|
||||||
this.dataIsLoading = layoutSource.isLoading
|
|
||||||
const lastClick = (this.lastClickObject = new LastClickFeatureSource(
|
|
||||||
this.mapProperties.lastClickLocation,
|
|
||||||
this.layout
|
|
||||||
))
|
|
||||||
const indexedElements = this.indexedFeatures
|
|
||||||
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
|
||||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
|
||||||
Array.from(this.layerState.filteredLayers.values()).filter(
|
|
||||||
(l) => l.layerDef?.source !== null
|
|
||||||
),
|
|
||||||
indexedElements,
|
|
||||||
{
|
|
||||||
constructStore: (features, layer) => new GeoIndexedStoreForLayer(features, layer),
|
|
||||||
handleLeftovers: (features) => {
|
|
||||||
console.warn(
|
|
||||||
"Got ",
|
|
||||||
features.length,
|
|
||||||
"leftover features, such as",
|
|
||||||
features[0].properties
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
this.perLayer = perLayer.perLayer
|
|
||||||
|
|
||||||
|
{
|
||||||
|
/* Setup the layout source
|
||||||
|
* A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too
|
||||||
|
*/
|
||||||
|
|
||||||
|
const layoutSource = new LayoutSource(
|
||||||
|
layout.layers,
|
||||||
|
this.featureSwitches,
|
||||||
|
this.mapProperties,
|
||||||
|
this.osmConnection.Backend(),
|
||||||
|
(id) => self.layerState.filteredLayers.get(id).isDisplayed
|
||||||
|
)
|
||||||
|
this.indexedFeatures = layoutSource
|
||||||
|
this.dataIsLoading = layoutSource.isLoading
|
||||||
|
|
||||||
|
const indexedElements = this.indexedFeatures
|
||||||
|
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
||||||
|
this.changes = new Changes(
|
||||||
|
{
|
||||||
|
dryRun: this.featureSwitches.featureSwitchIsTesting,
|
||||||
|
allElements: indexedElements,
|
||||||
|
featurePropertiesStore: this.featureProperties,
|
||||||
|
osmConnection: this.osmConnection,
|
||||||
|
historicalUserLocations: this.geolocation.historicalUserLocations,
|
||||||
|
},
|
||||||
|
layout?.isLeftRightSensitive() ?? false
|
||||||
|
)
|
||||||
|
this.newFeatures = new NewGeometryFromChangesFeatureSource(
|
||||||
|
this.changes,
|
||||||
|
indexedElements,
|
||||||
|
this.osmConnection.Backend()
|
||||||
|
)
|
||||||
|
layoutSource.addSource(this.newFeatures)
|
||||||
|
|
||||||
|
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||||
|
Array.from(this.layerState.filteredLayers.values()).filter(
|
||||||
|
(l) => l.layerDef?.source !== null
|
||||||
|
),
|
||||||
|
new ChangeGeometryApplicator(this.indexedFeatures, this.changes),
|
||||||
|
{
|
||||||
|
constructStore: (features, layer) =>
|
||||||
|
new GeoIndexedStoreForLayer(features, layer),
|
||||||
|
handleLeftovers: (features) => {
|
||||||
|
console.warn(
|
||||||
|
"Got ",
|
||||||
|
features.length,
|
||||||
|
"leftover features, such as",
|
||||||
|
features[0].properties
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.perLayer = perLayer.perLayer
|
||||||
|
}
|
||||||
this.perLayer.forEach((fs) => {
|
this.perLayer.forEach((fs) => {
|
||||||
new SaveFeatureSourceToLocalStorage(
|
/* TODO enable new SaveFeatureSourceToLocalStorage(
|
||||||
|
this.osmConnection.Backend(),
|
||||||
fs.layer.layerDef.id,
|
fs.layer.layerDef.id,
|
||||||
15,
|
15,
|
||||||
fs,
|
fs,
|
||||||
this.featureProperties
|
this.featureProperties
|
||||||
)
|
)//*/
|
||||||
|
|
||||||
const filtered = new FilteringFeatureSource(
|
const filtered = new FilteringFeatureSource(
|
||||||
fs.layer,
|
fs.layer,
|
||||||
|
@ -187,16 +208,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.changes = new Changes(
|
const lastClick = (this.lastClickObject = new LastClickFeatureSource(
|
||||||
{
|
this.mapProperties.lastClickLocation,
|
||||||
dryRun: this.featureSwitches.featureSwitchIsTesting,
|
this.layout
|
||||||
allElements: indexedElements,
|
))
|
||||||
featurePropertiesStore: this.featureProperties,
|
|
||||||
osmConnection: this.osmConnection,
|
|
||||||
historicalUserLocations: this.geolocation.historicalUserLocations,
|
|
||||||
},
|
|
||||||
layout?.isLeftRightSensitive() ?? false
|
|
||||||
)
|
|
||||||
|
|
||||||
this.initActors()
|
this.initActors()
|
||||||
this.drawSpecialLayers(lastClick)
|
this.drawSpecialLayers(lastClick)
|
||||||
|
@ -211,9 +226,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
private miscSetup() {
|
private miscSetup() {
|
||||||
this.userRelatedState.markLayoutAsVisited(this.layout)
|
this.userRelatedState.markLayoutAsVisited(this.layout)
|
||||||
|
|
||||||
this.selectedElement.addCallbackAndRunD(() => {
|
this.selectedElement.addCallbackAndRunD((feature) => {
|
||||||
// As soon as we have a selected element, we clear it
|
// As soon as we have a selected element, we clear the selected element
|
||||||
// This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
|
// This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
|
||||||
|
// The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear
|
||||||
|
if (feature.properties.id === "last_click") {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.lastClickObject.features.setData([])
|
this.lastClickObject.features.setData([])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { Utils } from "../../Utils"
|
||||||
import { MapillaryLink } from "./MapillaryLink"
|
import { MapillaryLink } from "./MapillaryLink"
|
||||||
import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
|
import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../Input/Toggle"
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
|
||||||
import { DefaultGuiState } from "../DefaultGuiState"
|
import { DefaultGuiState } from "../DefaultGuiState"
|
||||||
|
|
||||||
export class BackToThemeOverview extends Toggle {
|
export class BackToThemeOverview extends Toggle {
|
||||||
|
@ -78,14 +77,6 @@ export class ActionButtons extends Combine {
|
||||||
new OpenIdEditor(state, iconStyle),
|
new OpenIdEditor(state, iconStyle),
|
||||||
new MapillaryLink(state, iconStyle),
|
new MapillaryLink(state, iconStyle),
|
||||||
new OpenJosm(state, iconStyle).SetClass("hidden-on-mobile"),
|
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")
|
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.setAllowMoving(self.allowMoving.data)
|
||||||
self.setAllowZooming(self.allowZooming.data)
|
self.setAllowZooming(self.allowZooming.data)
|
||||||
self.setMinzoom(self.minzoom.data)
|
self.setMinzoom(self.minzoom.data)
|
||||||
|
self.setBounds(self.bounds.data)
|
||||||
})
|
})
|
||||||
self.MoveMapToCurrentLoc(self.location.data)
|
self.MoveMapToCurrentLoc(self.location.data)
|
||||||
self.SetZoom(self.zoom.data)
|
self.SetZoom(self.zoom.data)
|
||||||
|
@ -97,6 +98,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
self.setAllowMoving(self.allowMoving.data)
|
self.setAllowMoving(self.allowMoving.data)
|
||||||
self.setAllowZooming(self.allowZooming.data)
|
self.setAllowZooming(self.allowZooming.data)
|
||||||
self.setMinzoom(self.minzoom.data)
|
self.setMinzoom(self.minzoom.data)
|
||||||
|
self.setBounds(self.bounds.data)
|
||||||
this.updateStores()
|
this.updateStores()
|
||||||
map.on("moveend", () => this.updateStores())
|
map.on("moveend", () => this.updateStores())
|
||||||
map.on("click", (e) => {
|
map.on("click", (e) => {
|
||||||
|
@ -238,18 +240,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
container.style.height = document.documentElement.clientHeight + "px"
|
container.style.height = document.documentElement.clientHeight + "px"
|
||||||
}
|
}
|
||||||
|
|
||||||
const markerCanvas: HTMLCanvasElement = await html2canvas(
|
await html2canvas(
|
||||||
map.getCanvasContainer(),
|
map.getCanvasContainer(),
|
||||||
{
|
{
|
||||||
backgroundColor: "#00000000",
|
backgroundColor: "#00000000",
|
||||||
canvas: drawOn,
|
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) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -429,7 +426,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
private setBounds(bounds: BBox) {
|
private setBounds(bounds: BBox) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined) {
|
if (map === undefined || bounds === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const oldBounds = map.getBounds()
|
const oldBounds = map.getBounds()
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
$map.resize();
|
$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>
|
</script>
|
||||||
<main>
|
<main>
|
||||||
<Map bind:center={center}
|
<Map bind:center={center}
|
||||||
|
|
|
@ -197,7 +197,7 @@ class LineRenderingLayer {
|
||||||
this._fetchStore = fetchStore
|
this._fetchStore = fetchStore
|
||||||
this._onClick = onClick
|
this._onClick = onClick
|
||||||
const self = this
|
const self = this
|
||||||
features.features.addCallbackAndRunD((features) => self.update(features))
|
features.features.addCallbackAndRunD(() => self.update(features.features))
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculatePropsFor(
|
private calculatePropsFor(
|
||||||
|
@ -229,13 +229,23 @@ class LineRenderingLayer {
|
||||||
return calculatedProps
|
return calculatedProps
|
||||||
}
|
}
|
||||||
|
|
||||||
private async update(features: Feature[]) {
|
private currentSourceData
|
||||||
|
private async update(featureSource: Store<Feature[]>) {
|
||||||
const map = this._map
|
const map = this._map
|
||||||
while (!map.isStyleLoaded()) {
|
while (!map.isStyleLoaded()) {
|
||||||
await Utils.waitFor(100)
|
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)
|
const src = <GeoJSONSource>map.getSource(this._layername)
|
||||||
|
if (this.currentSourceData === features) {
|
||||||
|
// Already up to date
|
||||||
|
return
|
||||||
|
}
|
||||||
if (src === undefined) {
|
if (src === undefined) {
|
||||||
|
this.currentSourceData = features
|
||||||
map.addSource(this._layername, {
|
map.addSource(this._layername, {
|
||||||
type: "geojson",
|
type: "geojson",
|
||||||
data: {
|
data: {
|
||||||
|
@ -262,7 +272,6 @@ class LineRenderingLayer {
|
||||||
})
|
})
|
||||||
|
|
||||||
map.on("click", linelayer, (e) => {
|
map.on("click", linelayer, (e) => {
|
||||||
console.log("Click", e)
|
|
||||||
e.originalEvent["consumed"] = true
|
e.originalEvent["consumed"] = true
|
||||||
this._onClick(e.features[0])
|
this._onClick(e.features[0])
|
||||||
})
|
})
|
||||||
|
@ -297,9 +306,10 @@ class LineRenderingLayer {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
this.currentSourceData = features
|
||||||
src.setData({
|
src.setData({
|
||||||
type: "FeatureCollection",
|
type: "FeatureCollection",
|
||||||
features,
|
features: this.currentSourceData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,10 +355,21 @@ export default class ShowDataLayer {
|
||||||
"ShowDataLayer.ts:range.json"
|
"ShowDataLayer.ts:range.json"
|
||||||
)
|
)
|
||||||
private readonly _map: Store<MlMap>
|
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>
|
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._map = map
|
||||||
this._options = options
|
this._options = options
|
||||||
this._popupCache = new Map()
|
this._popupCache = new Map()
|
||||||
|
@ -405,28 +426,31 @@ export default class ShowDataLayer {
|
||||||
selectedElement?.setData(feature)
|
selectedElement?.setData(feature)
|
||||||
selectedLayer?.setData(this._options.layer)
|
selectedLayer?.setData(this._options.layer)
|
||||||
})
|
})
|
||||||
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
|
if (this._options.drawLines !== false) {
|
||||||
const lineRenderingConfig = this._options.layer.lineRendering[i]
|
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
|
||||||
new LineRenderingLayer(
|
const lineRenderingConfig = this._options.layer.lineRendering[i]
|
||||||
map,
|
new LineRenderingLayer(
|
||||||
features,
|
map,
|
||||||
this._options.layer.id + "_linerendering_" + i,
|
features,
|
||||||
lineRenderingConfig,
|
this._options.layer.id + "_linerendering_" + i,
|
||||||
doShowLayer,
|
lineRenderingConfig,
|
||||||
fetchStore,
|
doShowLayer,
|
||||||
onClick
|
fetchStore,
|
||||||
)
|
onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (this._options.drawMarkers !== false) {
|
||||||
for (const pointRenderingConfig of this._options.layer.mapRendering) {
|
for (const pointRenderingConfig of this._options.layer.mapRendering) {
|
||||||
new PointRenderingLayer(
|
new PointRenderingLayer(
|
||||||
map,
|
map,
|
||||||
features,
|
features,
|
||||||
pointRenderingConfig,
|
pointRenderingConfig,
|
||||||
doShowLayer,
|
doShowLayer,
|
||||||
fetchStore,
|
fetchStore,
|
||||||
onClick
|
onClick
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
features.features.addCallbackAndRunD((_) => this.zoomToCurrentFeatures(map))
|
features.features.addCallbackAndRunD((_) => this.zoomToCurrentFeatures(map))
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,19 +83,9 @@
|
||||||
snapOnto: snapToWay
|
snapOnto: snapToWay
|
||||||
});
|
});
|
||||||
await state.changes.applyAction(newElementAction);
|
await state.changes.applyAction(newElementAction);
|
||||||
|
// The 'changes' should have created a new point, which added this into the 'featureProperties'
|
||||||
const newId = newElementAction.newElementId;
|
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);
|
const tagsStore = state.featureProperties.getStore(newId);
|
||||||
{
|
{
|
||||||
// Set some metainfo
|
// 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.data.push(feature);
|
||||||
state.newFeatures.features.ping();
|
state.newFeatures.features.ping();
|
||||||
state.selectedElement?.setData(feature);
|
state.selectedElement?.setData(feature);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { OsmConnection, OsmServiceState } from "../../Logic/Osm/OsmConnection"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
import Loading from "../Base/Loading"
|
import Loading from "../Base/Loading"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import { Translation } from "../i18n/Translation"
|
import { Translation } from "../i18n/Translation"
|
||||||
|
|
||||||
|
@ -13,13 +13,13 @@ class LoginButton extends SubtleButton {
|
||||||
constructor(
|
constructor(
|
||||||
text: BaseUIElement | string,
|
text: BaseUIElement | string,
|
||||||
state: {
|
state: {
|
||||||
osmConnection: OsmConnection
|
osmConnection?: OsmConnection
|
||||||
},
|
},
|
||||||
icon?: BaseUIElement | string
|
icon?: BaseUIElement | string
|
||||||
) {
|
) {
|
||||||
super(icon ?? Svg.login_ui(), text)
|
super(icon ?? Svg.login_ui(), text)
|
||||||
this.onClick(() => {
|
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
|
* If logging in is not possible for some reason, an appropriate error message is shown
|
||||||
*
|
*
|
||||||
* State contains the 'osmConnection' to work with
|
* 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(
|
constructor(
|
||||||
el: BaseUIElement,
|
el: BaseUIElement,
|
||||||
text: BaseUIElement | string,
|
text: BaseUIElement | string,
|
||||||
state: {
|
state: {
|
||||||
readonly osmConnection: OsmConnection
|
readonly osmConnection?: OsmConnection
|
||||||
readonly featureSwitchUserbadge: Store<boolean>
|
readonly featureSwitchUserbadge?: Store<boolean>
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const loading = new Loading("Trying to log in...")
|
const loading = new Loading("Trying to log in...")
|
||||||
|
@ -51,14 +54,14 @@ export class LoginToggle extends VariableUiElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
super(
|
super(
|
||||||
state.osmConnection.loadingStatus.map(
|
state.osmConnection?.loadingStatus?.map(
|
||||||
(osmConnectionState) => {
|
(osmConnectionState) => {
|
||||||
if (state.featureSwitchUserbadge.data == false) {
|
if (state.featureSwitchUserbadge?.data == false) {
|
||||||
// All features to login with are disabled
|
// All features to login with are disabled
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiState = state.osmConnection.apiIsOnline.data
|
const apiState = state.osmConnection?.apiIsOnline?.data ?? "online"
|
||||||
const apiTranslation = offlineModes[apiState]
|
const apiTranslation = offlineModes[apiState]
|
||||||
if (apiTranslation !== undefined) {
|
if (apiTranslation !== undefined) {
|
||||||
return new Combine([
|
return new Combine([
|
||||||
|
@ -77,15 +80,15 @@ export class LoginToggle extends VariableUiElement {
|
||||||
return el
|
return el
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error!
|
// Fallback
|
||||||
return new LoginButton(
|
return new LoginButton(
|
||||||
Translations.t.general.loginFailed,
|
Translations.t.general.loginFailed,
|
||||||
state,
|
state,
|
||||||
Svg.invalid_svg()
|
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 Svg from "../../Svg"
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import { SubtleButton } from "../Base/SubtleButton"
|
import { SubtleButton } from "../Base/SubtleButton"
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import { Button } from "../Base/Button"
|
import { Button } from "../Base/Button"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
|
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
|
||||||
import Title from "../Base/Title"
|
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 BaseUIElement from "../BaseUIElement"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
|
||||||
import { LoginToggle } from "./LoginButton"
|
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 {
|
export default class SplitRoadWizard extends Combine {
|
||||||
private static splitLayerStyling = new LayerConfig(
|
|
||||||
split_point,
|
|
||||||
"(BUILTIN) SplitRoadWizard.ts",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
public dialogIsOpened: UIEventSource<boolean>
|
public dialogIsOpened: UIEventSource<boolean>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,20 +29,34 @@ export default class SplitRoadWizard extends Combine {
|
||||||
* @param id: The id of the road to remove
|
* @param id: The id of the road to remove
|
||||||
* @param state: the state of the application
|
* @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
|
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
|
// 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)
|
const hasBeenSplit = new UIEventSource(false)
|
||||||
|
|
||||||
// Toggle variable between show split button and map
|
// Toggle variable between show split button and map
|
||||||
const splitClicked = new UIEventSource<boolean>(false)
|
const splitClicked = new UIEventSource<boolean>(false)
|
||||||
|
|
||||||
const leafletMap = new UIEventSource<BaseUIElement>(
|
const leafletMap = new UIEventSource<BaseUIElement>(undefined)
|
||||||
SplitRoadWizard.setupMapComponent(id, splitPoints, state)
|
|
||||||
)
|
function initMap() {
|
||||||
|
SplitRoadWizard.setupMapComponent(id, splitPoints).then((mapComponent) =>
|
||||||
|
leafletMap.setData(mapComponent.SetClass("w-full h-80"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
initMap()
|
||||||
|
|
||||||
// Toggle between splitmap
|
// Toggle between splitmap
|
||||||
const splitButton = new SubtleButton(
|
const splitButton = new SubtleButton(
|
||||||
|
@ -70,23 +76,19 @@ export default class SplitRoadWizard extends Combine {
|
||||||
splitClicked.setData(false)
|
splitClicked.setData(false)
|
||||||
const splitAction = new SplitAction(
|
const splitAction = new SplitAction(
|
||||||
id,
|
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,
|
5
|
||||||
(coordinates) => {
|
|
||||||
state.allElements.ContainingFeatures.get(id).geometry["coordinates"] =
|
|
||||||
coordinates
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
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
|
// We throw away the old map and splitpoints, and create a new map from scratch
|
||||||
splitPoints.setData([])
|
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
|
// 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")
|
saveButton.SetClass("btn btn-primary mr-3")
|
||||||
|
@ -131,95 +133,14 @@ export default class SplitRoadWizard extends Combine {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private static setupMapComponent(
|
private static async setupMapComponent(
|
||||||
id: string,
|
id: WayId,
|
||||||
splitPoints: UIEventSource<{ feature: any; freshness: Date }[]>,
|
splitPoints: UIEventSource<Feature[]>
|
||||||
state: {
|
): Promise<BaseUIElement> {
|
||||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
const osmWay = await OsmObject.DownloadObjectAsync(id)
|
||||||
backgroundLayer: UIEventSource<BaseLayer>
|
return new SvelteUIElement(WaySplitMap, {
|
||||||
featureSwitchIsTesting: UIEventSource<boolean>
|
osmWay,
|
||||||
featureSwitchIsDebugging: UIEventSource<boolean>
|
splitPoints,
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
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 { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||||
import BaseUIElement from "./BaseUIElement"
|
import BaseUIElement from "./BaseUIElement";
|
||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||||
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource";
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||||
import { Changes } from "../Logic/Osm/Changes"
|
import { Changes } from "../Logic/Osm/Changes";
|
||||||
import { ExportableMap, MapProperties } from "../Models/MapProperties"
|
import { ExportableMap, MapProperties } from "../Models/MapProperties";
|
||||||
import LayerState from "../Logic/State/LayerState"
|
import LayerState from "../Logic/State/LayerState";
|
||||||
import { Feature, Geometry } from "geojson"
|
import { Feature, Geometry } from "geojson";
|
||||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
import { MangroveIdentity } from "../Logic/Web/MangroveReviews";
|
||||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||||
import { MenuState } from "../Models/MenuState"
|
import { MenuState } from "../Models/MenuState";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state needed to render a special Visualisation.
|
* The state needed to render a special Visualisation.
|
||||||
|
|
|
@ -77,8 +77,9 @@ import Lazy from "./Base/Lazy"
|
||||||
import { CheckBox } from "./Input/Checkboxes"
|
import { CheckBox } from "./Input/Checkboxes"
|
||||||
import Slider from "./Input/Slider"
|
import Slider from "./Input/Slider"
|
||||||
import DeleteWizard from "./Popup/DeleteWizard"
|
import DeleteWizard from "./Popup/DeleteWizard"
|
||||||
import { OsmId, OsmTags } from "../Models/OsmFeature"
|
import { OsmId, OsmTags, WayId } from "../Models/OsmFeature"
|
||||||
import MoveWizard from "./Popup/MoveWizard"
|
import MoveWizard from "./Popup/MoveWizard"
|
||||||
|
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
||||||
|
|
||||||
class NearbyImageVis implements SpecialVisualization {
|
class NearbyImageVis implements SpecialVisualization {
|
||||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||||
|
@ -532,16 +533,17 @@ export default class SpecialVisualizations {
|
||||||
args: [],
|
args: [],
|
||||||
constr(
|
constr(
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>
|
||||||
argument: string[],
|
|
||||||
feature: Feature,
|
|
||||||
layer: LayerConfig
|
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
// TODO
|
|
||||||
tagSource
|
tagSource
|
||||||
.map((tags) => tags.id)
|
.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
|
||||||
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
21
assets/layers/split_road/split_road.json
Normal file
21
assets/layers/split_road/split_road.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"id": "split_road",
|
||||||
|
"description": "Layer rendering the way to split in the 'splitRoadWizard'. This one is used instead of the variable rendering by the themes themselves, as they might not always be very visible",
|
||||||
|
"minzoom": 1,
|
||||||
|
"source": "special",
|
||||||
|
"name": null,
|
||||||
|
"title": null,
|
||||||
|
"mapRendering": [
|
||||||
|
{
|
||||||
|
"location": [
|
||||||
|
"point"
|
||||||
|
],
|
||||||
|
"icon": "bug",
|
||||||
|
"iconSize": "30,30,center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"width": "8",
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -379,7 +379,7 @@
|
||||||
],
|
],
|
||||||
"overrideAll": {
|
"overrideAll": {
|
||||||
"allowSplit": true,
|
"allowSplit": true,
|
||||||
"tagRenderings+": [
|
"+tagRenderings": [
|
||||||
{
|
{
|
||||||
"id": "is_cyclestreet",
|
"id": "is_cyclestreet",
|
||||||
"question": {
|
"question": {
|
||||||
|
|
|
@ -1061,6 +1061,10 @@ video {
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-80 {
|
||||||
|
height: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
.max-h-20vh {
|
.max-h-20vh {
|
||||||
max-height: 20vh;
|
max-height: 20vh;
|
||||||
}
|
}
|
||||||
|
@ -1081,6 +1085,10 @@ video {
|
||||||
min-height: 8rem;
|
min-height: 8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.w-8 {
|
.w-8 {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
}
|
}
|
||||||
|
@ -1105,10 +1113,6 @@ video {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-full {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-screen {
|
.w-screen {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id="maindiv">'maindiv' not attached</div>
|
<div id="maindiv" class="w-full h-full">'maindiv' not attached</div>
|
||||||
<div id="extradiv">'extradiv' not attached</div>
|
<div id="extradiv">'extradiv' not attached</div>
|
||||||
|
|
||||||
<script type="module" src="./test.ts"></script>
|
<script type="module" src="./test.ts"></script>
|
||||||
|
|
11
test.ts
11
test.ts
|
@ -9,6 +9,10 @@ import { UIEventSource } from "./Logic/UIEventSource"
|
||||||
import { VariableUiElement } from "./UI/Base/VariableUIElement"
|
import { VariableUiElement } from "./UI/Base/VariableUIElement"
|
||||||
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
||||||
import Title from "./UI/Base/Title"
|
import Title from "./UI/Base/Title"
|
||||||
|
import WaySplitMap from "./UI/BigComponents/WaySplitMap.svelte"
|
||||||
|
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||||
|
import { OsmObject } from "./Logic/Osm/OsmObject"
|
||||||
|
import SplitRoadWizard from "./UI/Popup/SplitRoadWizard"
|
||||||
|
|
||||||
function testspecial() {
|
function testspecial() {
|
||||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||||
|
@ -41,5 +45,10 @@ function testinput() {
|
||||||
}
|
}
|
||||||
new Combine(els).SetClass("flex flex-col").AttachTo("maindiv")
|
new Combine(els).SetClass("flex flex-col").AttachTo("maindiv")
|
||||||
}
|
}
|
||||||
testinput()
|
|
||||||
|
async function testWaySplit() {
|
||||||
|
new SplitRoadWizard("way/28717919", {}).SetClass("w-full h-full").AttachTo("maindiv")
|
||||||
|
}
|
||||||
|
testWaySplit().then((_) => console.log("inited"))
|
||||||
|
//testinput()
|
||||||
// testspecial()
|
// testspecial()
|
||||||
|
|
Loading…
Reference in a new issue