More work on splitting roads, WIP; refactoring tests
This commit is contained in:
parent
e374bb355c
commit
1f93923820
62 changed files with 1163 additions and 823 deletions
|
@ -2,12 +2,13 @@ import {UIEventSource} from "../UIEventSource";
|
|||
import {OsmObject} from "../Osm/OsmObject";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
|
||||
|
||||
/**
|
||||
* Makes sure the hash shows the selected element and vice-versa.
|
||||
*/
|
||||
export default class SelectedFeatureHandler {
|
||||
private static readonly _no_trigger_on = ["welcome", "copyright", "layers", "new"]
|
||||
private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "", undefined])
|
||||
hash: UIEventSource<string>;
|
||||
private readonly state: {
|
||||
selectedElement: UIEventSource<any>
|
||||
|
@ -17,30 +18,35 @@ export default class SelectedFeatureHandler {
|
|||
hash: UIEventSource<string>,
|
||||
state: {
|
||||
selectedElement: UIEventSource<any>,
|
||||
allElements: ElementStorage;
|
||||
allElements: ElementStorage,
|
||||
featurePipeline: FeaturePipeline
|
||||
}
|
||||
) {
|
||||
this.hash = hash;
|
||||
|
||||
this.state = state
|
||||
|
||||
// Getting a blank hash clears the selected element
|
||||
hash.addCallback(h => {
|
||||
|
||||
|
||||
// If the hash changes, set the selected element correctly
|
||||
function setSelectedElementFromHash(h){
|
||||
if (h === undefined || h === "") {
|
||||
// Hash has been cleared - we clear the selected element
|
||||
state.selectedElement.setData(undefined);
|
||||
}else{
|
||||
// we search the element to select
|
||||
const feature = state.allElements.ContainingFeatures.get(h)
|
||||
if(feature !== undefined){
|
||||
state.selectedElement.setData(feature)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
hash.addCallback(setSelectedElementFromHash)
|
||||
|
||||
|
||||
// IF the selected element changes, set the hash correctly
|
||||
state.selectedElement.addCallback(feature => {
|
||||
if (feature === undefined) {
|
||||
console.trace("Resetting hash")
|
||||
if (SelectedFeatureHandler._no_trigger_on.indexOf(hash.data) < 0) {
|
||||
if (SelectedFeatureHandler._no_trigger_on.has(hash.data)) {
|
||||
hash.setData("")
|
||||
}
|
||||
}
|
||||
|
@ -50,13 +56,26 @@ export default class SelectedFeatureHandler {
|
|||
hash.setData(h)
|
||||
}
|
||||
})
|
||||
|
||||
state.featurePipeline.newDataLoadedSignal.addCallbackAndRunD(_ => {
|
||||
// New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet
|
||||
if(hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)){
|
||||
// This is an invalid hash anyway
|
||||
return;
|
||||
}
|
||||
if(state.selectedElement.data !== undefined){
|
||||
// We already have something selected
|
||||
return;
|
||||
}
|
||||
setSelectedElementFromHash(hash.data)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// If a feature is selected via the hash, zoom there
|
||||
public zoomToSelectedFeature(location: UIEventSource<Loc>) {
|
||||
const hash = this.hash.data;
|
||||
if (hash === undefined || SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0) {
|
||||
if (hash === undefined || SelectedFeatureHandler._no_trigger_on.has(hash)) {
|
||||
return; // No valid feature selected
|
||||
}
|
||||
// We should have a valid osm-ID and zoom to it... But we wrap it in try-catch to be sure
|
||||
|
|
|
@ -2,65 +2,38 @@ import {UIEventSource} from "../UIEventSource";
|
|||
import Translations from "../../UI/i18n/Translations";
|
||||
import Locale from "../../UI/i18n/Locale";
|
||||
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import Combine from "../../UI/Base/Combine";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
|
||||
class TitleElement extends UIEventSource<string> {
|
||||
export default class TitleHandler {
|
||||
constructor(state) {
|
||||
const currentTitle: UIEventSource<string> = state.selectedElement.map(
|
||||
selected => {
|
||||
console.log("UPdating title")
|
||||
|
||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||
private readonly _selectedFeature: UIEventSource<any>;
|
||||
private readonly _allElementsStorage: ElementStorage;
|
||||
|
||||
constructor(layoutToUse: UIEventSource<LayoutConfig>,
|
||||
selectedFeature: UIEventSource<any>,
|
||||
allElementsStorage: ElementStorage) {
|
||||
super("MapComplete");
|
||||
|
||||
this._layoutToUse = layoutToUse;
|
||||
this._selectedFeature = selectedFeature;
|
||||
this._allElementsStorage = allElementsStorage;
|
||||
|
||||
this.syncWith(
|
||||
this._selectedFeature.map(
|
||||
selected => {
|
||||
const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ?? "MapComplete"
|
||||
|
||||
if (selected === undefined) {
|
||||
return defaultTitle
|
||||
}
|
||||
|
||||
const layout = layoutToUse.data;
|
||||
const tags = selected.properties;
|
||||
|
||||
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.title === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
const tagsSource = allElementsStorage.getEventSourceById(tags.id)
|
||||
const title = new TagRenderingAnswer(tagsSource, layer.title)
|
||||
return new Combine([defaultTitle, " | ", title]).ConstructElement().innerText;
|
||||
}
|
||||
}
|
||||
const layout = state.layoutToUse.data
|
||||
const defaultTitle = Translations.WT(layout?.title)?.txt ?? "MapComplete"
|
||||
|
||||
if (selected === undefined) {
|
||||
return defaultTitle
|
||||
}
|
||||
, [Locale.language, layoutToUse]
|
||||
)
|
||||
|
||||
const tags = selected.properties;
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.title === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
const tagsSource = state.allElements.getEventSourceById(tags.id)
|
||||
const title = new TagRenderingAnswer(tagsSource, layer.title)
|
||||
return new Combine([defaultTitle, " | ", title]).ConstructElement().innerText;
|
||||
}
|
||||
}
|
||||
return defaultTitle
|
||||
}, [Locale.language, state.layoutToUse]
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class TitleHandler {
|
||||
constructor(layoutToUse: UIEventSource<LayoutConfig>,
|
||||
selectedFeature: UIEventSource<any>,
|
||||
allElementsStorage: ElementStorage) {
|
||||
new TitleElement(layoutToUse, selectedFeature, allElementsStorage).addCallbackAndRunD(title => {
|
||||
currentTitle.addCallbackAndRunD(title => {
|
||||
document.title = title
|
||||
})
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ export class ExtraFunction {
|
|||
{
|
||||
name: "distanceTo",
|
||||
doc: "Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object",
|
||||
args: ["longitude", "latitude"]
|
||||
args: ["feature OR featureID OR longitude", "undefined OR latitude"]
|
||||
},
|
||||
(featuresPerLayer, feature) => {
|
||||
return (arg0, lat) => {
|
||||
|
@ -128,10 +128,10 @@ export class ExtraFunction {
|
|||
private static readonly ClosestNObjectFunc = new ExtraFunction(
|
||||
{
|
||||
name: "closestn",
|
||||
doc: "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. " +
|
||||
"Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet laoded)\n\n" +
|
||||
doc: "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. " +
|
||||
"Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet loaded)\n\n" +
|
||||
"If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)",
|
||||
args: ["list of features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"]
|
||||
args: ["list of features or layer name", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"]
|
||||
},
|
||||
(params, feature) => {
|
||||
return (features, amount, uniqueTag, maxDistanceInMeters) => ExtraFunction.GetClosestNFeatures(params, feature, features, {
|
||||
|
|
|
@ -4,15 +4,13 @@
|
|||
* Technically, more an Actor then a featuresource, but it fits more neatly this ay
|
||||
*/
|
||||
import {FeatureSourceForLayer} from "../FeatureSource";
|
||||
import {Utils} from "../../../Utils";
|
||||
|
||||
export default class LocalStorageSaverActor {
|
||||
export default class SaveTileToLocalStorageActor {
|
||||
public static readonly storageKey: string = "cached-features";
|
||||
|
||||
constructor(source: FeatureSourceForLayer, x: number, y: number, z: number) {
|
||||
constructor(source: FeatureSourceForLayer, tileIndex: number) {
|
||||
source.features.addCallbackAndRunD(features => {
|
||||
const index = Utils.tile_index(z, x, y)
|
||||
const key = `${LocalStorageSaverActor.storageKey}-${source.layer.layerDef.id}-${index}`
|
||||
const key = `${SaveTileToLocalStorageActor.storageKey}-${source.layer.layerDef.id}-${tileIndex}`
|
||||
const now = new Date().getTime()
|
||||
|
||||
if (features.length == 0) {
|
|
@ -1,202 +1,85 @@
|
|||
import FeatureSource, {IndexedFeatureSource} from "./FeatureSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource} from "./FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Changes} from "../Osm/Changes";
|
||||
import {ChangeDescription} from "../Osm/Actions/ChangeDescription";
|
||||
import {ChangeDescription, ChangeDescriptionTools} from "../Osm/Actions/ChangeDescription";
|
||||
import {Utils} from "../../Utils";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject";
|
||||
|
||||
/**
|
||||
* A feature source containing exclusively new elements
|
||||
*/
|
||||
export class NewGeometryChangeApplicatorFeatureSource implements FeatureSource{
|
||||
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
public readonly name: string = "newFeatures";
|
||||
constructor(changes: Changes) {
|
||||
const seenChanges = new Set<ChangeDescription>();
|
||||
changes.pendingChanges.addCallbackAndRunD(changes => {
|
||||
for (const change of changes) {
|
||||
if(seenChanges.has(change)){
|
||||
continue
|
||||
}
|
||||
seenChanges.add(change)
|
||||
|
||||
if(change.id < 0){
|
||||
// This is a new object!
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies changes from 'Changes' onto a featureSource
|
||||
* Applies geometry changes from 'Changes' onto every feature of a featureSource
|
||||
*/
|
||||
export default class ChangeApplicator implements FeatureSource {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string;
|
||||
private readonly source: IndexedFeatureSource;
|
||||
private readonly changes: Changes;
|
||||
private readonly mode?: {
|
||||
generateNewGeometries: boolean
|
||||
};
|
||||
public readonly layer: FilteredLayer
|
||||
|
||||
constructor(source: IndexedFeatureSource, changes: Changes, mode?: {
|
||||
generateNewGeometries: boolean
|
||||
}) {
|
||||
constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) {
|
||||
this.source = source;
|
||||
this.changes = changes;
|
||||
this.mode = mode;
|
||||
this.layer = source.layer
|
||||
|
||||
this.name = "ChangesApplied(" + source.name + ")"
|
||||
this.features = source.features
|
||||
const seenChanges = new Set<ChangeDescription>();
|
||||
this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined)
|
||||
|
||||
const self = this;
|
||||
let runningUpdate = false;
|
||||
source.features.addCallbackAndRunD(features => {
|
||||
if (runningUpdate) {
|
||||
return; // No need to ping again
|
||||
}
|
||||
self.ApplyChanges()
|
||||
seenChanges.clear()
|
||||
})
|
||||
source.features.addCallbackAndRunD(_ => self.update())
|
||||
|
||||
changes.allChanges.addCallbackAndRunD(_ => self.update())
|
||||
|
||||
changes.pendingChanges.addCallbackAndRunD(changes => {
|
||||
runningUpdate = true;
|
||||
changes = changes.filter(ch => !seenChanges.has(ch))
|
||||
changes.forEach(c => seenChanges.add(c))
|
||||
self.ApplyChanges()
|
||||
source.features.ping()
|
||||
runningUpdate = false;
|
||||
})
|
||||
}
|
||||
|
||||
private update() {
|
||||
const upstreamFeatures = this.source.features.data
|
||||
const upstreamIds = this.source.containedIds.data
|
||||
const changesToApply = this.changes.allChanges.data
|
||||
?.filter(ch =>
|
||||
// Does upsteram have this element? If not, we skip
|
||||
upstreamIds.has(ch.type + "/" + ch.id) &&
|
||||
// Are any (geometry) changes defined?
|
||||
ch.changes !== undefined &&
|
||||
// Ignore new elements, they are handled by the NewGeometryFromChangesFeatureSource
|
||||
ch.id > 0)
|
||||
|
||||
/**
|
||||
* Returns true if the geometry is changed and the source should be pinged
|
||||
*/
|
||||
private ApplyChanges(): boolean {
|
||||
const cs = this.changes.pendingChanges.data
|
||||
const features = this.source.features.data
|
||||
const loadedIds = this.source.containedIds
|
||||
if (cs.length === 0 || features === undefined) {
|
||||
if (changesToApply === undefined || changesToApply.length === 0) {
|
||||
// No changes to apply!
|
||||
// Pass the original feature and lets continue our day
|
||||
this.features.setData(upstreamFeatures);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Applying changes ", this.name, cs)
|
||||
let geometryChanged = false;
|
||||
const changesPerId: Map<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>()
|
||||
for (const c of cs) {
|
||||
const id = c.type + "/" + c.id
|
||||
if (!loadedIds.has(id)) {
|
||||
continue
|
||||
const changesPerId = new Map<string, ChangeDescription[]>()
|
||||
for (const ch of changesToApply) {
|
||||
const key = ch.type + "/" + ch.id
|
||||
if(changesPerId.has(key)){
|
||||
changesPerId.get(key).push(ch)
|
||||
}else{
|
||||
changesPerId.set(key, [ch])
|
||||
}
|
||||
if (!changesPerId.has(id)) {
|
||||
changesPerId.set(id, [])
|
||||
}
|
||||
changesPerId.get(id).push(c)
|
||||
}
|
||||
if (changesPerId.size === 0) {
|
||||
// The current feature source set doesn't contain any changed feature, so we can safely skip
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
|
||||
function add(feature) {
|
||||
feature.id = feature.properties.id
|
||||
features.push({
|
||||
feature: feature,
|
||||
freshness: now
|
||||
})
|
||||
console.log("Added a new feature: ", feature)
|
||||
geometryChanged = true;
|
||||
}
|
||||
|
||||
// First, create the new features - they have a negative ID
|
||||
// We don't set the properties yet though
|
||||
if (this.mode?.generateNewGeometries) {
|
||||
changesPerId.forEach(cs => {
|
||||
cs
|
||||
.forEach(change => {
|
||||
if (change.id >= 0) {
|
||||
return; // Nothing to do here, already created
|
||||
}
|
||||
|
||||
if (change.changes === undefined) {
|
||||
// An update to the object - not the actual created
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
n.lat = change.changes["lat"]
|
||||
n.lon = change.changes["lon"]
|
||||
const geojson = n.asGeoJson()
|
||||
add(geojson)
|
||||
break;
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.nodes = change.changes["nodes"]
|
||||
add(w.asGeoJson())
|
||||
break;
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.members = change.changes["members"]
|
||||
add(r.asGeoJson())
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
for (const feature of features) {
|
||||
const f = feature.feature;
|
||||
const id = f.properties.id;
|
||||
if (!changesPerId.has(id)) {
|
||||
const newFeatures: { feature: any, freshness: Date }[] = []
|
||||
for (const feature of upstreamFeatures) {
|
||||
const changesForFeature = changesPerId.get(feature.feature.properties.id)
|
||||
if (changesForFeature === undefined) {
|
||||
// No changes for this element
|
||||
newFeatures.push(feature)
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const changed = {}
|
||||
// Copy all the properties
|
||||
Utils.Merge(f, changed)
|
||||
// play the changes onto the copied object
|
||||
|
||||
for (const change of changesPerId.get(id)) {
|
||||
for (const kv of change.tags ?? []) {
|
||||
// Apply tag changes and ping the consumers
|
||||
f.properties[kv.k] = kv.v;
|
||||
}
|
||||
|
||||
// Apply other changes to the object
|
||||
if (change.changes !== undefined) {
|
||||
geometryChanged = true;
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
// @ts-ignore
|
||||
const coor: { lat, lon } = change.changes;
|
||||
f.geometry.coordinates = [coor.lon, coor.lat]
|
||||
break;
|
||||
case "way":
|
||||
f.geometry.coordinates = change.changes["locations"]
|
||||
break;
|
||||
case "relation":
|
||||
console.error("Changes to relations are not yet supported")
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Allright! We have a feature to rewrite!
|
||||
const copy = {
|
||||
...feature
|
||||
}
|
||||
// We only apply the last change as that one'll have the latest geometry
|
||||
const change = changesForFeature[changesForFeature.length - 1]
|
||||
copy.feature.geometry = ChangeDescriptionTools.getGeojsonGeometry(change)
|
||||
newFeatures.push(copy)
|
||||
}
|
||||
return geometryChanged
|
||||
this.features.setData(newFeatures)
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import FilteringFeatureSource from "./Sources/FilteringFeatureSource";
|
||||
import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter";
|
||||
import FeatureSource, {FeatureSourceForLayer, FeatureSourceState, Tiled} from "./FeatureSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, FeatureSourceState, IndexedFeatureSource, Tiled} from "./FeatureSource";
|
||||
import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy";
|
||||
|
@ -14,13 +14,14 @@ import GeoJsonSource from "./Sources/GeoJsonSource";
|
|||
import Loc from "../../Models/Loc";
|
||||
import WayHandlingApplyingFeatureSource from "./Sources/WayHandlingApplyingFeatureSource";
|
||||
import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor";
|
||||
import {Utils} from "../../Utils";
|
||||
import TiledFromLocalStorageSource from "./TiledFeatureSource/TiledFromLocalStorageSource";
|
||||
import LocalStorageSaverActor from "./Actors/LocalStorageSaverActor";
|
||||
import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor";
|
||||
import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource";
|
||||
import {BBox} from "../GeoOperations";
|
||||
import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger";
|
||||
import RelationsTracker from "../Osm/RelationsTracker";
|
||||
import {NewGeometryFromChangesFeatureSource} from "./Sources/NewGeometryFromChangesFeatureSource";
|
||||
import ChangeGeometryApplicator from "./ChangeApplicator";
|
||||
|
||||
|
||||
export default class FeaturePipeline implements FeatureSourceState {
|
||||
|
@ -29,10 +30,12 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
public readonly runningQuery: UIEventSource<boolean>;
|
||||
public readonly timeout: UIEventSource<number>;
|
||||
public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly newDataLoadedSignal: UIEventSource<FeatureSource> = new UIEventSource<FeatureSource>(undefined)
|
||||
|
||||
private readonly overpassUpdater: OverpassFeatureSource
|
||||
private readonly relationTracker: RelationsTracker
|
||||
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>;
|
||||
|
||||
constructor(
|
||||
handleFeatureSource: (source: FeatureSourceForLayer) => void,
|
||||
state: {
|
||||
|
@ -49,6 +52,7 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
|
||||
const self = this
|
||||
const updater = new OverpassFeatureSource(state);
|
||||
updater.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(updater))
|
||||
this.overpassUpdater = updater;
|
||||
this.sufficientlyZoomed = updater.sufficientlyZoomed
|
||||
this.runningQuery = updater.runningQuery
|
||||
|
@ -56,16 +60,17 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
this.relationTracker = updater.relationsTracker
|
||||
// Register everything in the state' 'AllElements'
|
||||
new RegisteringAllFromFeatureSourceActor(updater)
|
||||
|
||||
|
||||
const perLayerHierarchy = new Map<string, TileHierarchyMerger>()
|
||||
this.perLayerHierarchy = perLayerHierarchy
|
||||
|
||||
const patchedHandleFeatureSource = function (src: FeatureSourceForLayer) {
|
||||
const patchedHandleFeatureSource = function (src: FeatureSourceForLayer & IndexedFeatureSource) {
|
||||
// This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
|
||||
const srcFiltered =
|
||||
new FilteringFeatureSource(state,
|
||||
new WayHandlingApplyingFeatureSource(
|
||||
src,
|
||||
new ChangeGeometryApplicator(src, state.changes)
|
||||
)
|
||||
)
|
||||
handleFeatureSource(srcFiltered)
|
||||
|
@ -90,6 +95,7 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
(src) => {
|
||||
new RegisteringAllFromFeatureSourceActor(src)
|
||||
hierarchy.registerTile(src);
|
||||
src.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(src))
|
||||
}, state)
|
||||
continue
|
||||
}
|
||||
|
@ -103,6 +109,7 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
registerTile: (tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile)
|
||||
addToHierarchy(tile, id)
|
||||
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
@ -113,6 +120,7 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
registerTile: (tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile)
|
||||
addToHierarchy(tile, id)
|
||||
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
|
||||
}
|
||||
}),
|
||||
state
|
||||
|
@ -122,64 +130,90 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
}
|
||||
|
||||
// Actually load data from the overpass source
|
||||
|
||||
new PerLayerFeatureSourceSplitter(state.filteredLayers,
|
||||
(source) => TiledFeatureSource.createHierarchy(source, {
|
||||
layer: source.layer,
|
||||
registerTile: (tile) => {
|
||||
// We save the tile data for the given layer to local storage
|
||||
const [z, x, y] = Utils.tile_from_index(tile.tileIndex)
|
||||
new LocalStorageSaverActor(tile, x, y, z)
|
||||
new SaveTileToLocalStorageActor(tile, tile.tileIndex)
|
||||
addToHierarchy(tile, source.layer.layerDef.id);
|
||||
}
|
||||
}), new RememberingSource(updater))
|
||||
}),
|
||||
new RememberingSource(updater))
|
||||
|
||||
|
||||
// Also load points/lines that are newly added.
|
||||
const newGeometry = new NewGeometryFromChangesFeatureSource(state.changes)
|
||||
new RegisteringAllFromFeatureSourceActor(newGeometry)
|
||||
// A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
|
||||
new PerLayerFeatureSourceSplitter(state.filteredLayers,
|
||||
(perLayer) => {
|
||||
// We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
|
||||
addToHierarchy(perLayer, perLayer.layer.layerDef.id)
|
||||
// AT last, we always apply the metatags whenever possible
|
||||
perLayer.features.addCallbackAndRunD(_ => self.applyMetaTags(perLayer))
|
||||
},
|
||||
newGeometry
|
||||
)
|
||||
|
||||
|
||||
// Whenever fresh data comes in, we need to update the metatagging
|
||||
updater.features.addCallback(_ => {
|
||||
self.newDataLoadedSignal.stabilized(1000).addCallback(src => {
|
||||
console.log("Got an update from ", src.name)
|
||||
self.updateAllMetaTagging()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private applyMetaTags(src: FeatureSourceForLayer){
|
||||
const self = this
|
||||
MetaTagging.addMetatags(
|
||||
src.features.data,
|
||||
{
|
||||
memberships: this.relationTracker,
|
||||
getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox)
|
||||
},
|
||||
src.layer.layerDef,
|
||||
{
|
||||
includeDates: true,
|
||||
// We assume that the non-dated metatags are already set by the cache generator
|
||||
includeNonDates: !src.layer.layerDef.source.isOsmCacheLayer
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private updateAllMetaTagging() {
|
||||
console.log("Updating the meta tagging")
|
||||
const self = this;
|
||||
this.perLayerHierarchy.forEach(hierarchy => {
|
||||
hierarchy.loadedTiles.forEach(src => {
|
||||
MetaTagging.addMetatags(
|
||||
src.features.data,
|
||||
{
|
||||
memberships: this.relationTracker,
|
||||
getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox)
|
||||
},
|
||||
src.layer.layerDef
|
||||
)
|
||||
self.applyMetaTags(src)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public GetAllFeaturesWithin(bbox: BBox): any[][]{
|
||||
|
||||
public GetAllFeaturesWithin(bbox: BBox): any[][] {
|
||||
const self = this
|
||||
const tiles = []
|
||||
Array.from(this.perLayerHierarchy.keys())
|
||||
.forEach(key => tiles.push(...self.GetFeaturesWithin(key, bbox)))
|
||||
return tiles;
|
||||
}
|
||||
|
||||
public GetFeaturesWithin(layerId: string, bbox: BBox): any[][]{
|
||||
|
||||
public GetFeaturesWithin(layerId: string, bbox: BBox): any[][] {
|
||||
const requestedHierarchy = this.perLayerHierarchy.get(layerId)
|
||||
if (requestedHierarchy === undefined) {
|
||||
console.warn("Layer ", layerId, "is not defined. Try one of ", Array.from(this.perLayerHierarchy.keys()))
|
||||
return undefined;
|
||||
}
|
||||
return TileHierarchyTools.getTiles(requestedHierarchy, bbox)
|
||||
.filter(featureSource => featureSource.features?.data !== undefined)
|
||||
.map(featureSource => featureSource.features.data.map(fs => fs.feature))
|
||||
}
|
||||
|
||||
public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void){
|
||||
Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => {
|
||||
|
||||
public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void) {
|
||||
Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => {
|
||||
TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
|
||||
|
@ -12,10 +12,13 @@ import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
|
|||
export default class PerLayerFeatureSourceSplitter {
|
||||
|
||||
constructor(layers: UIEventSource<FilteredLayer[]>,
|
||||
handleLayerData: (source: FeatureSourceForLayer) => void,
|
||||
upstream: FeatureSource) {
|
||||
handleLayerData: (source: FeatureSourceForLayer & Tiled) => void,
|
||||
upstream: FeatureSource,
|
||||
options?:{
|
||||
handleLeftovers?: (featuresWithoutLayer: any[]) => void
|
||||
}) {
|
||||
|
||||
const knownLayers = new Map<string, FeatureSourceForLayer>()
|
||||
const knownLayers = new Map<string, FeatureSourceForLayer & Tiled>()
|
||||
|
||||
function update() {
|
||||
const features = upstream.features.data;
|
||||
|
@ -30,7 +33,7 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
// Note that this splitter is only run when it is invoked by the overpass feature source, so we can't be sure in which layer it should go
|
||||
|
||||
const featuresPerLayer = new Map<string, { feature, freshness } []>();
|
||||
|
||||
const noLayerFound = []
|
||||
function addTo(layer: FilteredLayer, feature: { feature, freshness }) {
|
||||
const id = layer.layerDef.id
|
||||
const list = featuresPerLayer.get(id)
|
||||
|
@ -51,6 +54,7 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
break;
|
||||
}
|
||||
}
|
||||
noLayerFound.push(f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,6 +79,11 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
featureSource.features.setData(features)
|
||||
}
|
||||
}
|
||||
|
||||
// AT last, the leftovers are handled
|
||||
if(options?.handleLeftovers !== undefined && noLayerFound.length > 0){
|
||||
options.handleLeftovers(noLayerFound)
|
||||
}
|
||||
}
|
||||
|
||||
layers.addCallback(_ => update())
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
* Uses the freshest feature available in the case multiple sources offer data with the same identifier
|
||||
*/
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {BBox} from "../../GeoOperations";
|
||||
import {Utils} from "../../../Utils";
|
||||
|
||||
export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled {
|
||||
export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource {
|
||||
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name;
|
||||
|
@ -16,6 +16,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
|
|||
private readonly _sources: UIEventSource<FeatureSource[]>;
|
||||
public readonly tileIndex: number;
|
||||
public readonly bbox: BBox;
|
||||
public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>(new Set())
|
||||
|
||||
constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
|
||||
this.tileIndex = tileIndex;
|
||||
|
@ -54,7 +55,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
|
|||
// We seed the dictionary with the previously loaded features
|
||||
const oldValues = this.features.data ?? [];
|
||||
for (const oldValue of oldValues) {
|
||||
all.set(oldValue.feature.id + oldValue.feature._matching_layer_id, oldValue)
|
||||
all.set(oldValue.feature.id, oldValue)
|
||||
}
|
||||
|
||||
for (const source of this._sources.data) {
|
||||
|
@ -62,7 +63,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
|
|||
continue;
|
||||
}
|
||||
for (const f of source.features.data) {
|
||||
const id = f.feature.properties.id + f.feature._matching_layer_id;
|
||||
const id = f.feature.properties.id;
|
||||
if (!all.has(id)) {
|
||||
// This is a new feature
|
||||
somethingChanged = true;
|
||||
|
@ -90,6 +91,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
|
|||
all.forEach((value, _) => {
|
||||
newList.push(value)
|
||||
})
|
||||
this.containedIds.setData(new Set(all.keys()))
|
||||
this.features.setData(newList);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import {Changes} from "../../Osm/Changes";
|
||||
import {OsmNode, OsmRelation, OsmWay} from "../../Osm/OsmObject";
|
||||
import FeatureSource from "../FeatureSource";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import {ChangeDescription} from "../../Osm/Actions/ChangeDescription";
|
||||
|
||||
export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
||||
// This class name truly puts the 'Java' into 'Javascript'
|
||||
|
||||
/**
|
||||
* A feature source containing exclusively new elements
|
||||
*/
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string = "newFeatures";
|
||||
|
||||
constructor(changes: Changes) {
|
||||
|
||||
const seenChanges = new Set<ChangeDescription>();
|
||||
const features = this.features.data;
|
||||
const self = this;
|
||||
|
||||
changes.pendingChanges
|
||||
.map(changes => changes.filter(ch =>
|
||||
// only new objects allowed
|
||||
ch.id < 0 &&
|
||||
// The change is an update to the object (e.g. tags or geometry) - not the actual create
|
||||
ch.changes !== undefined &&
|
||||
// If tags is undefined, this is probably a new point that is part of a split road
|
||||
ch.tags !== undefined &&
|
||||
// Already handled
|
||||
!seenChanges.has(ch)))
|
||||
.addCallbackAndRunD(changes => {
|
||||
|
||||
if (changes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
function add(feature) {
|
||||
feature.id = feature.properties.id
|
||||
features.push({
|
||||
feature: feature,
|
||||
freshness: now
|
||||
})
|
||||
console.warn("Added a new feature: ", JSON.stringify(feature))
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
seenChanges.add(change)
|
||||
try {
|
||||
const tags = {}
|
||||
for (const kv of change.tags) {
|
||||
tags[kv.k] = kv.v
|
||||
}
|
||||
tags["id"] = change.type+"/"+change.id
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
n.tags = tags
|
||||
n.lat = change.changes["lat"]
|
||||
n.lon = change.changes["lon"]
|
||||
const geojson = n.asGeoJson()
|
||||
add(geojson)
|
||||
break;
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.tags = tags
|
||||
w.nodes = change.changes["nodes"]
|
||||
w.coordinates = change.changes["coordinates"].map(coor => coor.reverse())
|
||||
add(w.asGeoJson())
|
||||
break;
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.tags = tags
|
||||
r.members = change.changes["members"]
|
||||
add(r.asGeoJson())
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not generate a new geometry to render on screen for:", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
self.features.ping()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +1,19 @@
|
|||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer} from "../FeatureSource";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {BBox} from "../../GeoOperations";
|
||||
import {Utils} from "../../../Utils";
|
||||
|
||||
export default class SimpleFeatureSource implements FeatureSourceForLayer {
|
||||
export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string = "SimpleFeatureSource";
|
||||
public readonly layer: FilteredLayer;
|
||||
public readonly bbox: BBox = BBox.global;
|
||||
public readonly tileIndex: number = Utils.tile_index(0, 0, 0);
|
||||
|
||||
constructor(layer: FilteredLayer) {
|
||||
this.name = "SimpleFeatureSource("+layer.layerDef.id+")"
|
||||
this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")"
|
||||
this.layer = layer
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import TileHierarchy from "./TileHierarchy";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {BBox} from "../../GeoOperations";
|
||||
|
@ -11,9 +11,9 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer
|
|||
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>();
|
||||
|
||||
public readonly layer: FilteredLayer;
|
||||
private _handleTile: (src: FeatureSourceForLayer, index: number) => void;
|
||||
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void;
|
||||
|
||||
constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer, index: number) => void) {
|
||||
constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void) {
|
||||
this.layer = layer;
|
||||
this._handleTile = handleTile;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import {UIEventSource} from "../../UIEventSource";
|
|||
import Loc from "../../../Models/Loc";
|
||||
import TileHierarchy from "./TileHierarchy";
|
||||
import {Utils} from "../../../Utils";
|
||||
import LocalStorageSaverActor from "../Actors/LocalStorageSaverActor";
|
||||
import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor";
|
||||
import {BBox} from "../../GeoOperations";
|
||||
|
||||
export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
|
||||
|
@ -17,7 +17,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
|
|||
leafletMap: any
|
||||
}) {
|
||||
|
||||
const prefix = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-"
|
||||
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-"
|
||||
// @ts-ignore
|
||||
const indexes: number[] = Object.keys(localStorage)
|
||||
.filter(key => {
|
||||
|
@ -76,7 +76,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
|
|||
for (const neededIndex of neededIndexes) {
|
||||
// We load the features from localStorage
|
||||
try {
|
||||
const key = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-" + neededIndex
|
||||
const key = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" + neededIndex
|
||||
const data = localStorage.getItem(key)
|
||||
const features = JSON.parse(data)
|
||||
const src = {
|
||||
|
|
|
@ -185,14 +185,14 @@ export class GeoOperations {
|
|||
static lengthInMeters(feature: any) {
|
||||
return turf.length(feature) * 1000
|
||||
}
|
||||
|
||||
static buffer(feature: any, bufferSizeInMeter: number){
|
||||
return turf.buffer(feature, bufferSizeInMeter/1000, {
|
||||
|
||||
static buffer(feature: any, bufferSizeInMeter: number) {
|
||||
return turf.buffer(feature, bufferSizeInMeter / 1000, {
|
||||
units: 'kilometers'
|
||||
})
|
||||
}
|
||||
|
||||
static bbox(feature: any){
|
||||
|
||||
static bbox(feature: any) {
|
||||
const [lon, lat, lon0, lat0] = turf.bbox(feature)
|
||||
return {
|
||||
"type": "Feature",
|
||||
|
@ -226,6 +226,11 @@ export class GeoOperations {
|
|||
|
||||
/**
|
||||
* Generates the closest point on a way from a given point
|
||||
*
|
||||
* The properties object will contain three values:
|
||||
// - `index`: closest point was found on nth line part,
|
||||
// - `dist`: distance between pt and the closest point (in kilometer),
|
||||
// `location`: distance along the line between start and the closest point.
|
||||
* @param way The road on which you want to find a point
|
||||
* @param point Point defined as [lon, lat]
|
||||
*/
|
||||
|
@ -379,7 +384,7 @@ export class BBox {
|
|||
readonly maxLon: number;
|
||||
readonly minLat: number;
|
||||
readonly minLon: number;
|
||||
static global: BBox = new BBox([[-180,-90],[180,90]]);
|
||||
static global: BBox = new BBox([[-180, -90], [180, 90]]);
|
||||
|
||||
constructor(coordinates) {
|
||||
this.maxLat = Number.MIN_VALUE;
|
||||
|
@ -447,7 +452,7 @@ export class BBox {
|
|||
}
|
||||
|
||||
static fromTile(z: number, x: number, y: number) {
|
||||
return new BBox( Utils.tile_bounds_lon_lat(z, x, y))
|
||||
return new BBox(Utils.tile_bounds_lon_lat(z, x, y))
|
||||
}
|
||||
|
||||
getEast() {
|
||||
|
@ -465,4 +470,20 @@ export class BBox {
|
|||
getSouth() {
|
||||
return this.minLat
|
||||
}
|
||||
|
||||
pad(factor: number) : BBox {
|
||||
const latDiff = this.maxLat - this.minLat
|
||||
const lat = (this.maxLat + this.minLat) / 2
|
||||
const lonDiff = this.maxLon - this.minLon
|
||||
const lon = (this.maxLon + this.minLon) / 2
|
||||
return new BBox([[
|
||||
lon - lonDiff * factor,
|
||||
lat - latDiff * factor
|
||||
], [lon + lonDiff * factor,
|
||||
lat + latDiff * factor]])
|
||||
}
|
||||
|
||||
toLeaflet() {
|
||||
return [[this.minLat, this.minLon], [this.maxLat, this.maxLon]]
|
||||
}
|
||||
}
|
|
@ -19,22 +19,31 @@ export default class MetaTagging {
|
|||
* This method (re)calculates all metatags and calculated tags on every given object.
|
||||
* The given features should be part of the given layer
|
||||
*/
|
||||
static addMetatags(features: { feature: any; freshness: Date }[],
|
||||
public static addMetatags(features: { feature: any; freshness: Date }[],
|
||||
params: ExtraFuncParams,
|
||||
layer: LayerConfig,
|
||||
includeDates = true) {
|
||||
options?: {
|
||||
includeDates?: true | boolean,
|
||||
includeNonDates?: true | boolean
|
||||
}) {
|
||||
|
||||
if (features === undefined || features.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const metatag of SimpleMetaTagger.metatags) {
|
||||
if (metatag.includesDates && !includeDates) {
|
||||
// We do not add dated entries
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
metatag.addMetaTags(features);
|
||||
if (metatag.includesDates) {
|
||||
if (options.includeDates ?? true) {
|
||||
metatag.addMetaTags(features);
|
||||
}
|
||||
} else {
|
||||
if (options.includeNonDates ?? true) {
|
||||
metatag.addMetaTags(features);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {OsmNode, OsmRelation, OsmWay} from "../OsmObject";
|
||||
|
||||
/**
|
||||
* Represents a single change to an object
|
||||
*/
|
||||
|
@ -29,8 +31,8 @@ export interface ChangeDescription {
|
|||
lat: number,
|
||||
lon: number
|
||||
} | {
|
||||
// Coordinates are only used for rendering. They should be lon, lat
|
||||
locations: [number, number][]
|
||||
// Coordinates are only used for rendering. They should be LAT, LON
|
||||
coordinates: [number, number][]
|
||||
nodes: number[],
|
||||
} | {
|
||||
members: { type: "node" | "way" | "relation", ref: number, role: string }[]
|
||||
|
@ -40,6 +42,26 @@ export interface ChangeDescription {
|
|||
Set to delete the object
|
||||
*/
|
||||
doDelete?: boolean
|
||||
}
|
||||
|
||||
|
||||
export class ChangeDescriptionTools{
|
||||
|
||||
public static getGeojsonGeometry(change: ChangeDescription): any{
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
n.lat = change.changes["lat"]
|
||||
n.lon = change.changes["lon"]
|
||||
return n.asGeoJson().geometry
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.nodes = change.changes["nodes"]
|
||||
w.coordinates = change.changes["coordinates"].map(coor => coor.reverse())
|
||||
return w.asGeoJson().geometry
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.members = change.changes["members"]
|
||||
return r.asGeoJson().geometry
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ export default class ChangeTagAction extends OsmChangeAction {
|
|||
return {k: key.trim(), v: value.trim()};
|
||||
}
|
||||
|
||||
CreateChangeDescriptions(changes: Changes): ChangeDescription [] {
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const changedTags: { k: string, v: string }[] = this._tagsFilter.asChange(this._currentTags).map(ChangeTagAction.checkChange)
|
||||
const typeId = this._elementId.split("/")
|
||||
const type = typeId[0]
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
|||
this._reusePointDistance = options?.reusePointWithinMeters ?? 1
|
||||
}
|
||||
|
||||
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const id = changes.getNewID()
|
||||
const properties = {
|
||||
id: "node/" + id
|
||||
|
@ -97,7 +97,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
|||
type: "way",
|
||||
id: this._snapOnto.id,
|
||||
changes: {
|
||||
locations: locations,
|
||||
coordinates: locations,
|
||||
nodes: ids
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ export default class DeleteAction {
|
|||
canBeDeleted: false,
|
||||
reason: t.notEnoughExperience
|
||||
})
|
||||
return;
|
||||
return true; // unregister this caller!
|
||||
}
|
||||
|
||||
if (!useTheInternet) {
|
||||
|
@ -167,13 +167,14 @@ export default class DeleteAction {
|
|||
}
|
||||
|
||||
// All right! We have arrived at a point that we should query OSM again to check that the point isn't a part of ways or relations
|
||||
OsmObject.DownloadReferencingRelations(id).addCallbackAndRunD(rels => {
|
||||
OsmObject.DownloadReferencingRelations(id).then(rels => {
|
||||
hasRelations.setData(rels.length > 0)
|
||||
})
|
||||
|
||||
OsmObject.DownloadReferencingWays(id).addCallbackAndRunD(ways => {
|
||||
OsmObject.DownloadReferencingWays(id).then(ways => {
|
||||
hasWays.setData(ways.length > 0)
|
||||
})
|
||||
return true; // unregister to only run once
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ export default abstract class OsmChangeAction {
|
|||
return this.CreateChangeDescriptions(changes)
|
||||
}
|
||||
|
||||
protected abstract CreateChangeDescriptions(changes: Changes): ChangeDescription[]
|
||||
protected abstract CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]>
|
||||
|
||||
|
||||
}
|
142
Logic/Osm/Actions/RelationSplitHandler.ts
Normal file
142
Logic/Osm/Actions/RelationSplitHandler.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {OsmObject, OsmRelation, OsmWay} from "../OsmObject";
|
||||
|
||||
export interface RelationSplitInput {
|
||||
relation: OsmRelation,
|
||||
originalWayId: number,
|
||||
allWayIdsInOrder: number[],
|
||||
originalNodes: number[],
|
||||
allWaysNodesInOrder: number[][]
|
||||
}
|
||||
|
||||
/**
|
||||
* When a way is split and this way is part of a relation, the relation should be updated too to have the new segment if relevant.
|
||||
*/
|
||||
export default class RelationSplitHandler extends OsmChangeAction {
|
||||
|
||||
constructor(input: RelationSplitInput) {
|
||||
super()
|
||||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A simple strategy to split relations:
|
||||
* -> Download the way members just before and just after the original way
|
||||
* -> Make sure they are still aligned
|
||||
*
|
||||
* Note that the feature might appear multiple times.
|
||||
*/
|
||||
export class InPlaceReplacedmentRTSH extends OsmChangeAction {
|
||||
private readonly _input: RelationSplitInput;
|
||||
|
||||
constructor(input: RelationSplitInput) {
|
||||
super();
|
||||
this._input = input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which node should border the member at the given index
|
||||
*/
|
||||
private async targetNodeAt(i: number, first: boolean) {
|
||||
const member = this._input.relation.members[i]
|
||||
if (member === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (member.type === "node") {
|
||||
return member.ref
|
||||
}
|
||||
if (member.type === "way") {
|
||||
const osmWay = <OsmWay>await OsmObject.DownloadObjectAsync("way/" + member.ref)
|
||||
const nodes = osmWay.nodes
|
||||
if (first) {
|
||||
return nodes[0]
|
||||
} else {
|
||||
return nodes[nodes.length - 1]
|
||||
}
|
||||
}
|
||||
if (member.type === "relation") {
|
||||
return undefined
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
|
||||
const wayId = this._input.originalWayId
|
||||
const relation = this._input.relation
|
||||
const members = relation.members
|
||||
const originalNodes = this._input.originalNodes;
|
||||
const firstNode = originalNodes[0]
|
||||
const lastNode = originalNodes[originalNodes.length - 1]
|
||||
const newMembers: { type: "node" | "way" | "relation", ref: number, role: string }[] = []
|
||||
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
const member = members[i];
|
||||
if (member.type !== "way" || member.ref !== wayId) {
|
||||
newMembers.push(member)
|
||||
continue;
|
||||
}
|
||||
|
||||
const nodeIdBefore = await this.targetNodeAt(i - 1, false)
|
||||
const nodeIdAfter = await this.targetNodeAt(i + 1, true)
|
||||
|
||||
const firstNodeMatches = nodeIdBefore === undefined || nodeIdBefore === firstNode
|
||||
const lastNodeMatches =nodeIdAfter === undefined || nodeIdAfter === lastNode
|
||||
|
||||
if (firstNodeMatches && lastNodeMatches) {
|
||||
// We have a classic situation, forward situation
|
||||
for (const wId of this._input.allWayIdsInOrder) {
|
||||
newMembers.push({
|
||||
ref: wId,
|
||||
type: "way",
|
||||
role: member.role
|
||||
})
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstNodeMatchesRev = nodeIdBefore === undefined || nodeIdBefore === lastNode
|
||||
const lastNodeMatchesRev =nodeIdAfter === undefined || nodeIdAfter === firstNode
|
||||
if (firstNodeMatchesRev || lastNodeMatchesRev) {
|
||||
// We (probably) have a reversed situation, backward situation
|
||||
for (let i1 = this._input.allWayIdsInOrder.length - 1; i1 >= 0; i1--){
|
||||
// Iterate BACKWARDS
|
||||
const wId = this._input.allWayIdsInOrder[i1];
|
||||
newMembers.push({
|
||||
ref: wId,
|
||||
type: "way",
|
||||
role: member.role
|
||||
})
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Euhm, allright... Something weird is going on, but let's not care too much
|
||||
// Lets pretend this is forward going
|
||||
for (const wId of this._input.allWayIdsInOrder) {
|
||||
newMembers.push({
|
||||
ref: wId,
|
||||
type: "way",
|
||||
role: member.role
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [{
|
||||
id: relation.id,
|
||||
type: "relation",
|
||||
changes: {members: newMembers}
|
||||
}];
|
||||
}
|
||||
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/**
|
||||
* The logic to handle relations after a way within
|
||||
*/
|
||||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {OsmRelation} from "../OsmObject";
|
||||
|
||||
export default class RelationSplitlHandler extends OsmChangeAction {
|
||||
|
||||
constructor(partOf: OsmRelation[], newWayIds: number[], originalNodes: number[]) {
|
||||
super()
|
||||
}
|
||||
|
||||
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import {OsmRelation, OsmWay} from "../OsmObject";
|
||||
import {OsmObject, OsmWay} from "../OsmObject";
|
||||
import {Changes} from "../Changes";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import RelationSplitlHandler from "./RelationSplitlHandler";
|
||||
import RelationSplitHandler from "./RelationSplitHandler";
|
||||
|
||||
interface SplitInfo {
|
||||
originalIndex?: number, // or negative for new elements
|
||||
|
@ -12,17 +12,13 @@ interface SplitInfo {
|
|||
}
|
||||
|
||||
export default class SplitAction extends OsmChangeAction {
|
||||
private readonly roadObject: any;
|
||||
private readonly osmWay: OsmWay;
|
||||
private _partOf: OsmRelation[];
|
||||
private readonly _splitPoints: any[];
|
||||
private readonly wayId: string;
|
||||
private readonly _splitPointsCoordinates: [number, number] []// lon, lat
|
||||
|
||||
constructor(osmWay: OsmWay, wayGeoJson: any, partOf: OsmRelation[], splitPoints: any[]) {
|
||||
constructor(wayId: string, splitPointCoordinates: [number, number][]) {
|
||||
super()
|
||||
this.osmWay = osmWay;
|
||||
this.roadObject = wayGeoJson;
|
||||
this._partOf = partOf;
|
||||
this._splitPoints = splitPoints;
|
||||
this.wayId = wayId;
|
||||
this._splitPointsCoordinates = splitPointCoordinates
|
||||
}
|
||||
|
||||
private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] {
|
||||
|
@ -42,26 +38,17 @@ export default class SplitAction extends OsmChangeAction {
|
|||
return wayParts.filter(wp => wp.length > 0)
|
||||
}
|
||||
|
||||
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||
const splitPoints = this._splitPoints
|
||||
// We mark the new split points with a new id
|
||||
console.log(splitPoints)
|
||||
for (const splitPoint of splitPoints) {
|
||||
splitPoint.properties["_is_split_point"] = true
|
||||
}
|
||||
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const self = this;
|
||||
const partOf = this._partOf
|
||||
const originalElement = this.osmWay
|
||||
const originalNodes = this.osmWay.nodes;
|
||||
const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId)
|
||||
const originalNodes = originalElement.nodes;
|
||||
|
||||
// First, calculate splitpoints and remove points close to one another
|
||||
const splitInfo = self.CalculateSplitCoordinates(splitPoints)
|
||||
const splitInfo = self.CalculateSplitCoordinates(originalElement)
|
||||
// Now we have a list with e.g.
|
||||
// [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
|
||||
|
||||
// Lets change 'originalIndex' to the actual node id first:
|
||||
// Lets change 'originalIndex' to the actual node id first (or assign a new id if needed):
|
||||
for (const element of splitInfo) {
|
||||
if (element.originalIndex >= 0) {
|
||||
element.originalIndex = originalElement.nodes[element.originalIndex]
|
||||
|
@ -102,25 +89,30 @@ export default class SplitAction extends OsmChangeAction {
|
|||
})
|
||||
}
|
||||
|
||||
const newWayIds: number[] = []
|
||||
// The ids of all the ways (including the original)
|
||||
const allWayIdsInOrder: number[] = []
|
||||
|
||||
const allWaysNodesInOrder: number[][] = []
|
||||
// Lets create OsmWays based on them
|
||||
for (const wayPart of wayParts) {
|
||||
|
||||
let isOriginal = wayPart === longest
|
||||
if (isOriginal) {
|
||||
// We change the actual element!
|
||||
const nodeIds = wayPart.map(p => p.originalIndex)
|
||||
changeDescription.push({
|
||||
type: "way",
|
||||
id: originalElement.id,
|
||||
changes: {
|
||||
locations: wayPart.map(p => p.lngLat),
|
||||
nodes: wayPart.map(p => p.originalIndex)
|
||||
coordinates: wayPart.map(p => p.lngLat),
|
||||
nodes: nodeIds
|
||||
}
|
||||
})
|
||||
allWayIdsInOrder.push(originalElement.id)
|
||||
allWaysNodesInOrder.push(nodeIds)
|
||||
} else {
|
||||
let id = changes.getNewID();
|
||||
newWayIds.push(id)
|
||||
|
||||
// Copy the tags from the original object onto the new
|
||||
const kv = []
|
||||
for (const k in originalElement.tags) {
|
||||
if (!originalElement.tags.hasOwnProperty(k)) {
|
||||
|
@ -131,22 +123,35 @@ export default class SplitAction extends OsmChangeAction {
|
|||
}
|
||||
kv.push({k: k, v: originalElement.tags[k]})
|
||||
}
|
||||
const nodeIds = wayPart.map(p => p.originalIndex)
|
||||
changeDescription.push({
|
||||
type: "way",
|
||||
id: id,
|
||||
tags: kv,
|
||||
changes: {
|
||||
locations: wayPart.map(p => p.lngLat),
|
||||
nodes: wayPart.map(p => p.originalIndex)
|
||||
coordinates: wayPart.map(p => p.lngLat),
|
||||
nodes: nodeIds
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
allWayIdsInOrder.push(id)
|
||||
allWaysNodesInOrder.push(nodeIds)
|
||||
}
|
||||
}
|
||||
|
||||
// At last, we still have to check that we aren't part of a relation...
|
||||
// At least, the order of the ways is identical, so we can keep the same roles
|
||||
changeDescription.push(...new RelationSplitlHandler(partOf, newWayIds, originalNodes).CreateChangeDescriptions(changes))
|
||||
const relations = await OsmObject.DownloadReferencingRelations(this.wayId)
|
||||
for (const relation of relations) {
|
||||
const changDescrs = await new RelationSplitHandler({
|
||||
relation: relation,
|
||||
allWayIdsInOrder: allWayIdsInOrder,
|
||||
originalNodes: originalNodes,
|
||||
allWaysNodesInOrder: allWaysNodesInOrder,
|
||||
originalWayId: originalElement.id
|
||||
}).CreateChangeDescriptions(changes)
|
||||
changeDescription.push(...changDescrs)
|
||||
}
|
||||
|
||||
// And we have our objects!
|
||||
// Time to upload
|
||||
|
@ -158,75 +163,96 @@ export default class SplitAction extends OsmChangeAction {
|
|||
* Calculates the actual points to split
|
||||
* If another point is closer then ~5m, we reuse that point
|
||||
*/
|
||||
private CalculateSplitCoordinates(
|
||||
splitPoints: any[],
|
||||
toleranceInM = 5): SplitInfo[] {
|
||||
private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] {
|
||||
const wayGeoJson = osmWay.asGeoJson()
|
||||
// Should be [lon, lat][]
|
||||
const originalPoints = osmWay.coordinates.map(c => <[number, number]>c.reverse())
|
||||
const allPoints: {
|
||||
coordinates: [number, number],
|
||||
isSplitPoint: boolean,
|
||||
originalIndex?: number, // Original index
|
||||
dist: number, // Distance from the nearest point on the original line
|
||||
location: number // Distance from the start of the way
|
||||
}[] = this._splitPointsCoordinates.map(c => {
|
||||
// From the turf.js docs:
|
||||
// The properties object will contain three values:
|
||||
// - `index`: closest point was found on nth line part,
|
||||
// - `dist`: distance between pt and the closest point,
|
||||
// `location`: distance along the line between start and the closest point.
|
||||
let projected = GeoOperations.nearestPoint(wayGeoJson, c)
|
||||
return ({
|
||||
coordinates: c,
|
||||
isSplitPoint: true,
|
||||
dist: projected.properties.dist,
|
||||
location: projected.properties.location
|
||||
});
|
||||
})
|
||||
|
||||
const allPoints = [...splitPoints];
|
||||
// We have a bunch of coordinates here: [ [lat, lon], [lat, lon], ...] ...
|
||||
const originalPoints: [number, number][] = this.roadObject.geometry.coordinates
|
||||
// We project them onto the line (which should yield pretty much the same point
|
||||
// We have a bunch of coordinates here: [ [lon, lon], [lat, lon], ...] ...
|
||||
// We project them onto the line (which should yield pretty much the same point and add them to allPoints
|
||||
for (let i = 0; i < originalPoints.length; i++) {
|
||||
let originalPoint = originalPoints[i];
|
||||
let projected = GeoOperations.nearestPoint(this.roadObject, originalPoint)
|
||||
projected.properties["_is_split_point"] = false
|
||||
projected.properties["_original_index"] = i
|
||||
allPoints.push(projected)
|
||||
let projected = GeoOperations.nearestPoint(wayGeoJson, originalPoint)
|
||||
allPoints.push({
|
||||
coordinates: originalPoint,
|
||||
isSplitPoint: false,
|
||||
location: projected.properties.location,
|
||||
originalIndex: i,
|
||||
dist: projected.properties.dist
|
||||
})
|
||||
}
|
||||
// At this point, we have a list of both the split point and the old points, with some properties to discriminate between them
|
||||
// We sort this list so that the new points are at the same location
|
||||
allPoints.sort((a, b) => a.properties.location - b.properties.location)
|
||||
allPoints.sort((a, b) => a.location - b.location)
|
||||
|
||||
// When this is done, we check that no now point is too close to an already existing point and no very small segments get created
|
||||
|
||||
/* for (let i = allPoints.length - 1; i > 0; i--) {
|
||||
|
||||
const point = allPoints[i];
|
||||
if (point.properties._original_index !== undefined) {
|
||||
// This point is already in OSM - we have to keep it!
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i != allPoints.length - 1) {
|
||||
const prevPoint = allPoints[i + 1]
|
||||
const diff = Math.abs(point.properties.location - prevPoint.properties.location) * 1000
|
||||
if (diff <= toleranceInM) {
|
||||
// To close to the previous point! We delete this point...
|
||||
allPoints.splice(i, 1)
|
||||
// ... and mark the previous point as a split point
|
||||
prevPoint.properties._is_split_point = true
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (i > 0) {
|
||||
const nextPoint = allPoints[i - 1]
|
||||
const diff = Math.abs(point.properties.location - nextPoint.properties.location) * 1000
|
||||
if (diff <= toleranceInM) {
|
||||
// To close to the next point! We delete this point...
|
||||
allPoints.splice(i, 1)
|
||||
// ... and mark the next point as a split point
|
||||
nextPoint.properties._is_split_point = true
|
||||
// noinspection UnnecessaryContinueJS
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// We don't have to remove this point...
|
||||
}*/
|
||||
for (let i = allPoints.length - 2; i >= 1; i--) {
|
||||
// We 'merge' points with already existing nodes if they are close enough to avoid closeby elements
|
||||
|
||||
// Note the loop bounds: we skip the first two and last two elements:
|
||||
// The first and last element are always part of the original way and should be kept
|
||||
// Furthermore, we run in reverse order as we'll delete elements on the go
|
||||
|
||||
const point = allPoints[i]
|
||||
if (point.originalIndex !== undefined) {
|
||||
// We keep the original points
|
||||
continue
|
||||
}
|
||||
if (point.dist * 1000 >= toleranceInM) {
|
||||
// No need to remove this one
|
||||
continue
|
||||
}
|
||||
|
||||
// At this point, 'dist' told us the point is pretty close to an already existing point.
|
||||
// Lets see which (already existing) point is closer and mark it as splitpoint
|
||||
const nextPoint = allPoints[i + 1]
|
||||
const prevPoint = allPoints[i - 1]
|
||||
const distToNext = nextPoint.location - point.location
|
||||
const distToPrev = prevPoint.location - point.location
|
||||
let closest = nextPoint
|
||||
if (distToNext > distToPrev) {
|
||||
closest = prevPoint
|
||||
}
|
||||
|
||||
// Ok, we have a closest point!
|
||||
closest.isSplitPoint = true;
|
||||
allPoints.splice(i, 1)
|
||||
|
||||
}
|
||||
|
||||
const splitInfo: SplitInfo[] = []
|
||||
let nextId = -1
|
||||
let nextId = -1 // Note: these IDs are overwritten later on, no need to use a global counter here
|
||||
|
||||
for (const p of allPoints) {
|
||||
let index = p.properties._original_index
|
||||
let index = p.originalIndex
|
||||
if (index === undefined) {
|
||||
index = nextId;
|
||||
nextId--;
|
||||
}
|
||||
const splitInfoElement = {
|
||||
originalIndex: index,
|
||||
lngLat: p.geometry.coordinates,
|
||||
doSplit: p.properties._is_split_point
|
||||
lngLat: p.coordinates,
|
||||
doSplit: p.isSplitPoint
|
||||
}
|
||||
splitInfo.push(splitInfoElement)
|
||||
}
|
||||
|
|
|
@ -21,12 +21,15 @@ export class Changes {
|
|||
*/
|
||||
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
|
||||
|
||||
public readonly pendingChanges = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||
public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
|
||||
private readonly isUploading = new UIEventSource(false);
|
||||
|
||||
private readonly previouslyCreated: OsmObject[] = []
|
||||
|
||||
constructor() {
|
||||
// We keep track of all changes just as well
|
||||
this.allChanges.setData([...this.pendingChanges.data])
|
||||
}
|
||||
|
||||
private static createChangesetFor(csId: string,
|
||||
|
@ -146,10 +149,13 @@ export class Changes {
|
|||
}
|
||||
|
||||
public applyAction(action: OsmChangeAction) {
|
||||
const changes = action.Perform(this)
|
||||
console.log("Received changes:", changes)
|
||||
this.pendingChanges.data.push(...changes);
|
||||
this.pendingChanges.ping();
|
||||
action.Perform(this).then(changes => {
|
||||
console.log("Received changes:", changes)
|
||||
this.pendingChanges.data.push(...changes);
|
||||
this.pendingChanges.ping();
|
||||
this.allChanges.data.push(...changes)
|
||||
this.allChanges.ping()
|
||||
})
|
||||
}
|
||||
|
||||
private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {Utils} from "../../Utils";
|
||||
import * as polygon_features from "../../assets/polygon-features.json";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {BBox} from "../GeoOperations";
|
||||
|
||||
|
||||
export abstract class OsmObject {
|
||||
|
@ -9,11 +10,12 @@ export abstract class OsmObject {
|
|||
protected static backendURL = OsmObject.defaultBackend;
|
||||
private static polygonFeatures = OsmObject.constructPolygonFeatures()
|
||||
private static objectCache = new Map<string, UIEventSource<OsmObject>>();
|
||||
private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>();
|
||||
private static referencingRelationsCache = new Map<string, UIEventSource<OsmRelation[]>>();
|
||||
private static historyCache = new Map<string, UIEventSource<OsmObject[]>>();
|
||||
type: string;
|
||||
id: number;
|
||||
/**
|
||||
* The OSM tags as simple object
|
||||
*/
|
||||
tags: {} = {};
|
||||
version: number;
|
||||
public changed: boolean = false;
|
||||
|
@ -37,7 +39,7 @@ export abstract class OsmObject {
|
|||
this.backendURL = url;
|
||||
}
|
||||
|
||||
static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
|
||||
public static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
|
||||
let src: UIEventSource<OsmObject>;
|
||||
if (OsmObject.objectCache.has(id)) {
|
||||
src = OsmObject.objectCache.get(id)
|
||||
|
@ -47,80 +49,62 @@ export abstract class OsmObject {
|
|||
return src;
|
||||
}
|
||||
} else {
|
||||
src = new UIEventSource<OsmObject>(undefined)
|
||||
src = UIEventSource.FromPromise(OsmObject.DownloadObjectAsync(id))
|
||||
}
|
||||
|
||||
OsmObject.objectCache.set(id, src);
|
||||
return src;
|
||||
}
|
||||
|
||||
static async DownloadObjectAsync(id: string): Promise<OsmObject> {
|
||||
const splitted = id.split("/");
|
||||
const type = splitted[0];
|
||||
const idN = Number(splitted[1]);
|
||||
if (idN < 0) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
OsmObject.objectCache.set(id, src);
|
||||
const newContinuation = (element: OsmObject) => {
|
||||
src.setData(element)
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case("node"):
|
||||
new OsmNode(idN).Download(newContinuation);
|
||||
break;
|
||||
return await new OsmNode(idN).Download();
|
||||
case("way"):
|
||||
new OsmWay(idN).Download(newContinuation);
|
||||
break;
|
||||
return await new OsmWay(idN).Download();
|
||||
case("relation"):
|
||||
new OsmRelation(idN).Download(newContinuation);
|
||||
break;
|
||||
return await new OsmRelation(idN).Download();
|
||||
default:
|
||||
throw "Invalid object type:" + type + id;
|
||||
throw ("Invalid object type:" + type + id);
|
||||
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Downloads the ways that are using this node.
|
||||
* Beware: their geometry will be incomplete!
|
||||
*/
|
||||
public static DownloadReferencingWays(id: string): UIEventSource<OsmWay[]> {
|
||||
if (OsmObject.referencingWaysCache.has(id)) {
|
||||
return OsmObject.referencingWaysCache.get(id);
|
||||
}
|
||||
const waysSrc = new UIEventSource<OsmWay[]>([])
|
||||
OsmObject.referencingWaysCache.set(id, waysSrc);
|
||||
Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/ways`)
|
||||
.then(data => {
|
||||
const ways = data.elements.map(wayInfo => {
|
||||
public static DownloadReferencingWays(id: string): Promise<OsmWay[]> {
|
||||
return Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/ways`).then(
|
||||
data => {
|
||||
return data.elements.map(wayInfo => {
|
||||
const way = new OsmWay(wayInfo.id)
|
||||
way.LoadData(wayInfo)
|
||||
return way
|
||||
})
|
||||
waysSrc.setData(ways)
|
||||
})
|
||||
return waysSrc;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the relations that are using this feature.
|
||||
* Beware: their geometry will be incomplete!
|
||||
*/
|
||||
public static DownloadReferencingRelations(id: string): UIEventSource<OsmRelation[]> {
|
||||
if (OsmObject.referencingRelationsCache.has(id)) {
|
||||
return OsmObject.referencingRelationsCache.get(id);
|
||||
}
|
||||
const relsSrc = new UIEventSource<OsmRelation[]>(undefined)
|
||||
OsmObject.referencingRelationsCache.set(id, relsSrc);
|
||||
Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`)
|
||||
.then(data => {
|
||||
const rels = data.elements.map(wayInfo => {
|
||||
const rel = new OsmRelation(wayInfo.id)
|
||||
rel.LoadData(wayInfo)
|
||||
rel.SaveExtraData(wayInfo)
|
||||
return rel
|
||||
})
|
||||
relsSrc.setData(rels)
|
||||
})
|
||||
return relsSrc;
|
||||
public static async DownloadReferencingRelations(id: string): Promise<OsmRelation[]> {
|
||||
const data = await Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`)
|
||||
return data.elements.map(wayInfo => {
|
||||
const rel = new OsmRelation(wayInfo.id)
|
||||
rel.LoadData(wayInfo)
|
||||
rel.SaveExtraData(wayInfo)
|
||||
return rel
|
||||
})
|
||||
}
|
||||
|
||||
public static DownloadHistory(id: string): UIEventSource<OsmObject []> {
|
||||
|
@ -158,18 +142,11 @@ export abstract class OsmObject {
|
|||
}
|
||||
|
||||
// bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
|
||||
public static LoadArea(bounds: [[number, number], [number, number]], callback: (objects: OsmObject[]) => void) {
|
||||
const minlon = bounds[0][1]
|
||||
const maxlon = bounds[1][1]
|
||||
const minlat = bounds[1][0]
|
||||
const maxlat = bounds[0][0];
|
||||
const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
|
||||
Utils.downloadJson(url).then(data => {
|
||||
const elements: any[] = data.elements;
|
||||
const objects = OsmObject.ParseObjects(elements)
|
||||
callback(objects);
|
||||
|
||||
})
|
||||
public static async LoadArea(bbox: BBox): Promise<OsmObject[]> {
|
||||
const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}`
|
||||
const data = await Utils.downloadJson(url)
|
||||
const elements: any[] = data.elements;
|
||||
return OsmObject.ParseObjects(elements);
|
||||
}
|
||||
|
||||
public static DownloadAll(neededIds, forceRefresh = true): UIEventSource<OsmObject[]> {
|
||||
|
@ -283,39 +260,34 @@ export abstract class OsmObject {
|
|||
return tags;
|
||||
}
|
||||
|
||||
Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
|
||||
/**
|
||||
* Downloads the object, a full download for ways and relations
|
||||
* @constructor
|
||||
*/
|
||||
async Download(): Promise<OsmObject> {
|
||||
const self = this;
|
||||
const full = this.type !== "way" ? "" : "/full";
|
||||
const url = `${OsmObject.backendURL}api/0.6/${this.type}/${this.id}${full}`;
|
||||
Utils.downloadJson(url).then(data => {
|
||||
|
||||
return await Utils.downloadJson(url).then(data => {
|
||||
const element = data.elements.pop();
|
||||
|
||||
let nodes = []
|
||||
if (self.type === "way" && data.elements.length >= 0) {
|
||||
nodes = OsmObject.ParseObjects(data.elements)
|
||||
}
|
||||
|
||||
if (self.type === "rellation") {
|
||||
throw "We should save some extra data"
|
||||
}
|
||||
|
||||
self.LoadData(element)
|
||||
self.SaveExtraData(element, nodes);
|
||||
|
||||
const meta = {
|
||||
"_last_edit:contributor": element.user,
|
||||
"_last_edit:contributor:uid": element.uid,
|
||||
"_last_edit:changeset": element.changeset,
|
||||
"_last_edit:timestamp": new Date(element.timestamp),
|
||||
"_version_number": element.version
|
||||
}
|
||||
|
||||
if (OsmObject.backendURL !== OsmObject.defaultBackend) {
|
||||
self.tags["_backend"] = OsmObject.backendURL
|
||||
meta["_backend"] = OsmObject.backendURL;
|
||||
}
|
||||
|
||||
continuation(self, meta);
|
||||
return this;
|
||||
}
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
@ -389,18 +361,10 @@ export class OsmNode extends OsmObject {
|
|||
}
|
||||
}
|
||||
|
||||
export interface OsmObjectMeta {
|
||||
"_last_edit:contributor": string,
|
||||
"_last_edit:contributor:uid": number,
|
||||
"_last_edit:changeset": number,
|
||||
"_last_edit:timestamp": Date,
|
||||
"_version_number": number
|
||||
|
||||
}
|
||||
|
||||
export class OsmWay extends OsmObject {
|
||||
|
||||
nodes: number[];
|
||||
// The coordinates of the way, [lat, lon][]
|
||||
coordinates: [number, number][] = []
|
||||
lat: number;
|
||||
lon: number;
|
||||
|
@ -455,12 +419,16 @@ export class OsmWay extends OsmObject {
|
|||
}
|
||||
|
||||
public asGeoJson() {
|
||||
let coordinates : ([number, number][] | [number, number][][]) = this.coordinates.map(c => <[number, number]>c.reverse());
|
||||
if(this.isPolygon()){
|
||||
coordinates = [coordinates]
|
||||
}
|
||||
return {
|
||||
"type": "Feature",
|
||||
"properties": this.tags,
|
||||
"geometry": {
|
||||
"type": this.isPolygon() ? "Polygon" : "LineString",
|
||||
"coordinates": this.coordinates.map(c => [c[1], c[0]])
|
||||
"coordinates": coordinates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -511,7 +479,7 @@ ${members}${tags} </relation>
|
|||
this.members = element.members;
|
||||
}
|
||||
|
||||
asGeoJson() {
|
||||
asGeoJson(): any {
|
||||
throw "Not Implemented"
|
||||
}
|
||||
}
|
|
@ -60,7 +60,12 @@ export class UIEventSource<T> {
|
|||
|
||||
run();
|
||||
return source;
|
||||
|
||||
}
|
||||
|
||||
public static FromPromise<T>(promise : Promise<T>): UIEventSource<T>{
|
||||
const src = new UIEventSource<T>(undefined)
|
||||
promise.then(d => src.setData(d))
|
||||
return src
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -191,6 +196,14 @@ export class UIEventSource<T> {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
addCallbackD(callback: (data: T) => void) {
|
||||
this.addCallback(data => {
|
||||
if (data !== undefined && data !== null) {
|
||||
return callback(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class UIEventSourceTools {
|
||||
|
|
|
@ -9,7 +9,7 @@ export default class Hash {
|
|||
public static hash: UIEventSource<string> = Hash.Get();
|
||||
|
||||
/**
|
||||
* Gets the current string, including the pound sign
|
||||
* Gets the current string, including the pound sign if there is any
|
||||
* @constructor
|
||||
*/
|
||||
public static Current(): string {
|
||||
|
|
|
@ -127,7 +127,6 @@ export class QueryParameters {
|
|||
parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data))
|
||||
}
|
||||
// Don't pollute the history every time a parameter changes
|
||||
|
||||
history.replaceState(null, "", "?" + parts.join("&") + Hash.Current());
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {Utils} from "../Utils";
|
|||
|
||||
export default class Constants {
|
||||
|
||||
public static vNumber = "0.10.0";
|
||||
public static vNumber = "0.10.0-alpha-0";
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
|
|
2
State.ts
2
State.ts
|
@ -433,7 +433,7 @@ export default class State {
|
|||
})
|
||||
.ping();
|
||||
|
||||
new TitleHandler(this.layoutToUse, this.selectedElement, this.allElements);
|
||||
new TitleHandler(this);
|
||||
}
|
||||
|
||||
private static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||
|
|
|
@ -16,6 +16,11 @@ export interface MinimapOptions {
|
|||
lastClickLocation?: UIEventSource<{ lat: number, lon: number }>
|
||||
}
|
||||
|
||||
export interface MinimapObj {
|
||||
readonly leafletMap: UIEventSource<any>,
|
||||
installBounds(factor: number | BBox, showRange?: boolean) : void
|
||||
}
|
||||
|
||||
export default class Minimap {
|
||||
/**
|
||||
* A stub implementation. The actual implementation is injected later on, but only in the browser.
|
||||
|
@ -25,6 +30,6 @@ export default class Minimap {
|
|||
/**
|
||||
* Construct a minimap
|
||||
*/
|
||||
public static createMiniMap: (options: MinimapOptions) => BaseUIElement & { readonly leafletMap: UIEventSource<any> }
|
||||
public static createMiniMap: (options: MinimapOptions) => (BaseUIElement & MinimapObj)
|
||||
|
||||
}
|
|
@ -7,9 +7,9 @@ import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
|||
import {BBox} from "../../Logic/GeoOperations";
|
||||
import * as L from "leaflet";
|
||||
import {Map} from "leaflet";
|
||||
import Minimap, {MinimapOptions} from "./Minimap";
|
||||
import Minimap, {MinimapObj, MinimapOptions} from "./Minimap";
|
||||
|
||||
export default class MinimapImplementation extends BaseUIElement {
|
||||
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
|
||||
private static _nextId = 0;
|
||||
public readonly leafletMap: UIEventSource<Map>
|
||||
private readonly _id: string;
|
||||
|
@ -44,6 +44,65 @@ export default class MinimapImplementation extends BaseUIElement {
|
|||
Minimap.createMiniMap = options => new MinimapImplementation(options)
|
||||
}
|
||||
|
||||
public installBounds(factor: number | BBox, showRange?: boolean) {
|
||||
this.leafletMap.addCallbackD(leaflet => {
|
||||
console.log("Installing max bounds")
|
||||
|
||||
let bounds;
|
||||
if (typeof factor === "number") {
|
||||
bounds = leaflet.getBounds()
|
||||
leaflet.setMaxBounds(bounds.pad(factor))
|
||||
}else{
|
||||
// @ts-ignore
|
||||
leaflet.setMaxBounds(factor.toLeaflet())
|
||||
bounds = leaflet.getBounds()
|
||||
}
|
||||
|
||||
if (showRange) {
|
||||
const data = {
|
||||
type: "FeatureCollection",
|
||||
features: [{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[
|
||||
bounds.getEast(),
|
||||
bounds.getNorth()
|
||||
],
|
||||
[
|
||||
bounds.getWest(),
|
||||
bounds.getNorth()
|
||||
],
|
||||
[
|
||||
bounds.getWest(),
|
||||
bounds.getSouth()
|
||||
],
|
||||
|
||||
[
|
||||
bounds.getEast(),
|
||||
bounds.getSouth()
|
||||
],
|
||||
[
|
||||
bounds.getEast(),
|
||||
bounds.getNorth()
|
||||
]
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
||||
// @ts-ignore
|
||||
L.geoJSON(data, {
|
||||
style: {
|
||||
color: "#f00",
|
||||
weight: 2,
|
||||
opacity: 0.4
|
||||
}
|
||||
}).addTo(leaflet)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const div = document.createElement("div")
|
||||
div.id = this._id;
|
||||
|
|
|
@ -65,7 +65,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
|||
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)]
|
||||
|
||||
const now = new Date()
|
||||
const date = now.getFullYear()+"-"+Utils.TwoDigits(now.getMonth()+1)+"-"+Utils.TwoDigits(now.getDate())
|
||||
const lastWeek = new Date(now.getDate() - 7 * 24 * 60 * 60 * 1000)
|
||||
const date = lastWeek.getFullYear()+"-"+Utils.TwoDigits(lastWeek.getMonth()+1)+"-"+Utils.TwoDigits(lastWeek.getDate())
|
||||
const osmcha_link = `https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%22${date}%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D`
|
||||
|
||||
tabsWithAboutMc.push({
|
||||
|
|
|
@ -20,6 +20,7 @@ import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
|
|||
import PresetConfig from "../../Models/ThemeConfig/PresetConfig";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {And} from "../../Logic/Tags/And";
|
||||
import {BBox} from "../../Logic/GeoOperations";
|
||||
|
||||
/*
|
||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||
|
@ -39,8 +40,6 @@ interface PresetInfo extends PresetConfig {
|
|||
export default class SimpleAddUI extends Toggle {
|
||||
|
||||
constructor(isShown: UIEventSource<boolean>) {
|
||||
|
||||
|
||||
const loginButton = new SubtleButton(Svg.osm_logo_ui(), Translations.t.general.add.pleaseLogin.Clone())
|
||||
.onClick(() => State.state.osmConnection.AttemptLogin());
|
||||
const readYourMessages = new Combine([
|
||||
|
@ -52,7 +51,8 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
|
||||
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
|
||||
|
||||
State.state.LastClickLocation.addCallback( _ => selectedPreset.setData(undefined))
|
||||
|
||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
|
||||
|
||||
|
||||
|
@ -82,11 +82,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
return true;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
|
||||
() => {
|
||||
selectedPreset.setData(undefined)
|
||||
})
|
||||
|
@ -98,9 +94,9 @@ export default class SimpleAddUI extends Toggle {
|
|||
new Toggle(
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
Translations.t.general.add.stillLoading.Clone().SetClass("alert"),
|
||||
addUi,
|
||||
State.state.featurePipeline.runningQuery
|
||||
Translations.t.general.add.stillLoading.Clone().SetClass("alert"),
|
||||
State.state.featurePipeline.somethingLoaded
|
||||
),
|
||||
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
|
||||
State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
|
||||
|
@ -126,6 +122,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
let location = State.state.LastClickLocation;
|
||||
let preciseInput: LocationInput = undefined
|
||||
if (preset.preciseInput !== undefined) {
|
||||
// We uncouple the event source
|
||||
const locationSrc = new UIEventSource({
|
||||
lat: location.data.lat,
|
||||
lon: location.data.lon,
|
||||
|
@ -137,24 +134,48 @@ export default class SimpleAddUI extends Toggle {
|
|||
backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
|
||||
}
|
||||
|
||||
let features: UIEventSource<{ feature: any }[]> = undefined
|
||||
let snapToFeatures: UIEventSource<{ feature: any }[]> = undefined
|
||||
let mapBounds: UIEventSource<BBox> = undefined
|
||||
if (preset.preciseInput.snapToLayers) {
|
||||
// We have to snap to certain layers.
|
||||
// Lets fetch tehm
|
||||
const asSet = new Set(preset.preciseInput.snapToLayers)
|
||||
features = State.state.featurePipeline.features.map(f => f.filter(feat => asSet.has(feat.feature._matching_layer_id)))
|
||||
snapToFeatures = new UIEventSource<{ feature: any }[]>([])
|
||||
mapBounds = new UIEventSource<BBox>(undefined)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
preciseInput = new LocationInput({
|
||||
mapBackground: backgroundLayer,
|
||||
centerLocation: locationSrc,
|
||||
snapTo: features,
|
||||
snapTo: snapToFeatures,
|
||||
snappedPointTags: tags,
|
||||
maxSnapDistance: preset.preciseInput.maxSnapDistance
|
||||
|
||||
maxSnapDistance: preset.preciseInput.maxSnapDistance,
|
||||
bounds: mapBounds
|
||||
})
|
||||
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
|
||||
|
||||
|
||||
if (preset.preciseInput.snapToLayers) {
|
||||
// We have to snap to certain layers.
|
||||
// Lets fetch them
|
||||
|
||||
let loadedBbox : BBox= undefined
|
||||
mapBounds?.addCallbackAndRunD(bbox => {
|
||||
if(loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)){
|
||||
// All is already there
|
||||
// return;
|
||||
}
|
||||
|
||||
bbox = bbox.pad(2);
|
||||
loadedBbox = bbox;
|
||||
const allFeatures: {feature: any}[] = []
|
||||
preset.preciseInput.snapToLayers.forEach(layerId => {
|
||||
State.state.featurePipeline.GetFeaturesWithin(layerId, bbox).forEach(feats => allFeatures.push(...feats.map(f => ({feature :f}))))
|
||||
})
|
||||
snapToFeatures.setData(allFeatures)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import Combine from "../Base/Combine";
|
|||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {BBox, GeoOperations} from "../../Logic/GeoOperations";
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||
import * as L from "leaflet";
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||
|
@ -38,6 +38,8 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
private readonly _snappedPoint: UIEventSource<any>
|
||||
private readonly _maxSnapDistance: number
|
||||
private readonly _snappedPointTags: any;
|
||||
private readonly _bounds: UIEventSource<BBox>;
|
||||
public readonly _matching_layer: UIEventSource<LayerConfig>;
|
||||
|
||||
constructor(options: {
|
||||
mapBackground?: UIEventSource<BaseLayer>,
|
||||
|
@ -46,32 +48,33 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
snappedPointTags?: any,
|
||||
requiresSnapping?: boolean,
|
||||
centerLocation: UIEventSource<Loc>,
|
||||
bounds?: UIEventSource<BBox>
|
||||
}) {
|
||||
super();
|
||||
this._snapTo = options.snapTo?.map(features => features?.filter(feat => feat.feature.geometry.type !== "Point"))
|
||||
this._maxSnapDistance = options.maxSnapDistance
|
||||
this._centerLocation = options.centerLocation;
|
||||
this._snappedPointTags = options.snappedPointTags
|
||||
this._bounds = options.bounds;
|
||||
if (this._snapTo === undefined) {
|
||||
this._value = this._centerLocation;
|
||||
} else {
|
||||
const self = this;
|
||||
|
||||
let matching_layer: UIEventSource<string>
|
||||
|
||||
if (self._snappedPointTags !== undefined) {
|
||||
matching_layer = State.state.layoutToUse.map(layout => {
|
||||
this._matching_layer = State.state.layoutToUse.map(layout => {
|
||||
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.source.osmTags.matchesProperties(self._snappedPointTags)) {
|
||||
return layer.id
|
||||
return layer
|
||||
}
|
||||
}
|
||||
console.error("No matching layer found for tags ", self._snappedPointTags)
|
||||
return "matchpoint"
|
||||
return LocationInput.matchLayer
|
||||
})
|
||||
} else {
|
||||
matching_layer = new UIEventSource<string>("matchpoint")
|
||||
this._matching_layer = new UIEventSource<LayerConfig>(LocationInput.matchLayer)
|
||||
}
|
||||
|
||||
this._snappedPoint = options.centerLocation.map(loc => {
|
||||
|
@ -83,7 +86,7 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
|
||||
let min = undefined;
|
||||
let matchedWay = undefined;
|
||||
for (const feature of self._snapTo.data) {
|
||||
for (const feature of self._snapTo.data ?? []) {
|
||||
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat])
|
||||
if (min === undefined) {
|
||||
min = nearestPointOnLine
|
||||
|
@ -98,19 +101,17 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
}
|
||||
}
|
||||
|
||||
if (min.properties.dist * 1000 > self._maxSnapDistance) {
|
||||
if (min === undefined || min.properties.dist * 1000 > self._maxSnapDistance) {
|
||||
if (options.requiresSnapping) {
|
||||
return undefined
|
||||
} else {
|
||||
return {
|
||||
"type": "Feature",
|
||||
"_matching_layer_id": matching_layer.data,
|
||||
"properties": options.snappedPointTags ?? min.properties,
|
||||
"geometry": {"type": "Point", "coordinates": [loc.lon, loc.lat]}
|
||||
}
|
||||
}
|
||||
}
|
||||
min._matching_layer_id = matching_layer?.data ?? "matchpoint"
|
||||
min.properties = options.snappedPointTags ?? min.properties
|
||||
self.snappedOnto.setData(matchedWay)
|
||||
return min
|
||||
|
@ -144,84 +145,40 @@ export default class LocationInput extends InputElement<Loc> {
|
|||
location: this._centerLocation,
|
||||
background: this.mapBackground,
|
||||
attribution: this.mapBackground !== State.state.backgroundLayer,
|
||||
lastClickLocation: clickLocation
|
||||
lastClickLocation: clickLocation,
|
||||
bounds: this._bounds
|
||||
}
|
||||
)
|
||||
clickLocation.addCallbackAndRunD(location => this._centerLocation.setData(location))
|
||||
map.leafletMap.addCallbackAndRunD(leaflet => {
|
||||
const bounds = leaflet.getBounds()
|
||||
leaflet.setMaxBounds(bounds.pad(0.15))
|
||||
const data = {
|
||||
type: "FeatureCollection",
|
||||
features: [{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[
|
||||
bounds.getEast(),
|
||||
bounds.getNorth()
|
||||
],
|
||||
[
|
||||
bounds.getWest(),
|
||||
bounds.getNorth()
|
||||
],
|
||||
[
|
||||
bounds.getWest(),
|
||||
bounds.getSouth()
|
||||
],
|
||||
|
||||
[
|
||||
bounds.getEast(),
|
||||
bounds.getSouth()
|
||||
],
|
||||
[
|
||||
bounds.getEast(),
|
||||
bounds.getNorth()
|
||||
]
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
||||
// @ts-ignore
|
||||
L.geoJSON(data, {
|
||||
style: {
|
||||
color: "#f00",
|
||||
weight: 2,
|
||||
opacity: 0.4
|
||||
}
|
||||
}).addTo(leaflet)
|
||||
})
|
||||
map.installBounds(0.15, true);
|
||||
|
||||
if (this._snapTo !== undefined) {
|
||||
|
||||
|
||||
// Show the lines to snap to
|
||||
new ShowDataMultiLayer({
|
||||
features: new StaticFeatureSource(this._snapTo, true),
|
||||
enablePopups: false,
|
||||
zoomToFeatures: false,
|
||||
leafletMap: map.leafletMap,
|
||||
layers: State.state.filteredLayers
|
||||
}
|
||||
)
|
||||
// Show the central point
|
||||
const matchPoint = this._snappedPoint.map(loc => {
|
||||
if (loc === undefined) {
|
||||
return []
|
||||
}
|
||||
return [{feature: loc}];
|
||||
})
|
||||
if (this._snapTo) {
|
||||
if (this._snappedPointTags === undefined) {
|
||||
// No special tags - we show a default crosshair
|
||||
new ShowDataLayer({
|
||||
features: new StaticFeatureSource(matchPoint),
|
||||
enablePopups: false,
|
||||
zoomToFeatures: false,
|
||||
leafletMap: map.leafletMap,
|
||||
layerToShow: LocationInput.matchLayer
|
||||
})
|
||||
}else{
|
||||
new ShowDataMultiLayer({
|
||||
features: new StaticFeatureSource(matchPoint),
|
||||
enablePopups: false,
|
||||
zoomToFeatures: false,
|
||||
leafletMap: map.leafletMap,
|
||||
layers: State.state.filteredLayers
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
new ShowDataLayer({
|
||||
features: new StaticFeatureSource(matchPoint, true),
|
||||
enablePopups: false,
|
||||
zoomToFeatures: false,
|
||||
leafletMap: map.leafletMap,
|
||||
layerToShow: this._matching_layer.data
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
this.mapBackground.map(layer => {
|
||||
|
|
|
@ -130,7 +130,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
if (!userbadge) {
|
||||
return undefined
|
||||
}
|
||||
return new Combine(editElements)
|
||||
return new Combine(editElements).SetClass("flex flex-col")
|
||||
}
|
||||
))
|
||||
renderings.push(editors)
|
||||
|
|
|
@ -5,13 +5,12 @@ import {SubtleButton} from "../Base/SubtleButton";
|
|||
import Minimap from "../Base/Minimap";
|
||||
import State from "../../State";
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {BBox, GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {LeafletMouseEvent} from "leaflet";
|
||||
import Combine from "../Base/Combine";
|
||||
import {Button} from "../Base/Button";
|
||||
import Translations from "../i18n/Translations";
|
||||
import SplitAction from "../../Logic/Osm/Actions/SplitAction";
|
||||
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
|
||||
import Title from "../Base/Title";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||
|
@ -21,9 +20,12 @@ export default class SplitRoadWizard extends Toggle {
|
|||
private static splitLayerStyling = new LayerConfig({
|
||||
id: "splitpositions",
|
||||
source: {osmTags: "_cutposition=yes"},
|
||||
icon: "./assets/svg/plus.svg"
|
||||
icon: {render: "circle:white;./assets/svg/scissors.svg"},
|
||||
iconSize: {render: "30,30,center"},
|
||||
}, "(BUILTIN) SplitRoadWizard.ts", true)
|
||||
|
||||
public dialogIsOpened: UIEventSource<boolean>
|
||||
|
||||
/**
|
||||
* A UI Element used for splitting roads
|
||||
*
|
||||
|
@ -40,30 +42,40 @@ export default class SplitRoadWizard extends Toggle {
|
|||
|
||||
// Toggle variable between show split button and map
|
||||
const splitClicked = new UIEventSource<boolean>(false);
|
||||
// Load the road with given id on the minimap
|
||||
const roadElement = State.state.allElements.ContainingFeatures.get(id)
|
||||
|
||||
// Minimap on which you can select the points to be splitted
|
||||
const miniMap = Minimap.createMiniMap({background: State.state.backgroundLayer, allowMoving: false});
|
||||
miniMap.SetStyle("width: 100%; height: 24rem;");
|
||||
const miniMap = Minimap.createMiniMap(
|
||||
{
|
||||
background: State.state.backgroundLayer,
|
||||
allowMoving: true,
|
||||
leafletOptions: {
|
||||
minZoom: 14
|
||||
}
|
||||
});
|
||||
miniMap.SetStyle("width: 100%; height: 24rem")
|
||||
.SetClass("rounded-xl overflow-hidden");
|
||||
|
||||
miniMap.installBounds(BBox.get(roadElement))
|
||||
|
||||
// Define how a cut is displayed on the map
|
||||
|
||||
// Load the road with given id on the minimap
|
||||
const roadElement = State.state.allElements.ContainingFeatures.get(id)
|
||||
const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]);
|
||||
// Datalayer displaying the road and the cut points (if any)
|
||||
new ShowDataMultiLayer({
|
||||
features: new StaticFeatureSource(roadEventSource, true),
|
||||
layers: State.state.filteredLayers,
|
||||
leafletMap: miniMap.leafletMap,
|
||||
enablePopups: false,
|
||||
zoomToFeatures: true
|
||||
})
|
||||
new ShowDataLayer({
|
||||
features: new StaticFeatureSource(splitPoints, true),
|
||||
leafletMap: miniMap.leafletMap,
|
||||
zoomToFeatures: false,
|
||||
enablePopups: false,
|
||||
layerToShow: SplitRoadWizard.splitLayerStyling
|
||||
layerToShow: SplitRoadWizard.splitLayerStyling
|
||||
})
|
||||
|
||||
new ShowDataMultiLayer({
|
||||
features: new StaticFeatureSource([roadElement]),
|
||||
layers: State.state.filteredLayers,
|
||||
leafletMap: miniMap.leafletMap,
|
||||
enablePopups: false,
|
||||
zoomToFeatures: true
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -72,12 +84,25 @@ export default class SplitRoadWizard extends Toggle {
|
|||
* @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) * 1000 < 5)
|
||||
.map(p => p[1])
|
||||
.sort()
|
||||
.reverse()
|
||||
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(roadElement, coordinates); // pointOnRoad is a geojson
|
||||
|
||||
// Update point properties to let it match the layer
|
||||
pointOnRoad.properties._cutposition = "yes";
|
||||
pointOnRoad["_matching_layer_id"] = "splitpositions";
|
||||
|
||||
// let the state remember the point, to be able to retrieve it later by id
|
||||
State.state.allElements.addOrGetElement(pointOnRoad);
|
||||
|
@ -94,7 +119,7 @@ export default class SplitRoadWizard extends Toggle {
|
|||
}))
|
||||
|
||||
// Toggle between splitmap
|
||||
const splitButton = new SubtleButton(Svg.scissors_ui(), t.inviteToSplit.Clone());
|
||||
const splitButton = new SubtleButton(Svg.scissors_ui(), t.inviteToSplit.Clone().SetClass("text-lg font-bold"));
|
||||
splitButton.onClick(
|
||||
() => {
|
||||
splitClicked.setData(true)
|
||||
|
@ -110,27 +135,9 @@ export default class SplitRoadWizard extends Toggle {
|
|||
// Save button
|
||||
const saveButton = new Button(t.split.Clone(), () => {
|
||||
hasBeenSplit.setData(true)
|
||||
const way = OsmObject.DownloadObject(id)
|
||||
const partOfSrc = OsmObject.DownloadReferencingRelations(id);
|
||||
let hasRun = false
|
||||
way.map(way => {
|
||||
const partOf = partOfSrc.data
|
||||
if (way === undefined || partOf === undefined) {
|
||||
return;
|
||||
}
|
||||
if (hasRun) {
|
||||
return
|
||||
}
|
||||
hasRun = true
|
||||
const splitAction = new SplitAction(
|
||||
<OsmWay>way, way.asGeoJson(), partOf, splitPoints.data.map(ff => ff.feature)
|
||||
)
|
||||
State.state.changes.applyAction(splitAction)
|
||||
State.state.changes.applyAction(new SplitAction(id, splitPoints.data.map(ff => ff.feature.geometry.coordinates)))
|
||||
})
|
||||
|
||||
}, [partOfSrc])
|
||||
|
||||
|
||||
});
|
||||
saveButton.SetClass("btn btn-primary mr-3");
|
||||
const disabledSaveButton = new Button("Split", undefined);
|
||||
disabledSaveButton.SetClass("btn btn-disabled mr-3");
|
||||
|
@ -152,5 +159,6 @@ export default class SplitRoadWizard extends Toggle {
|
|||
mapView.SetClass("question")
|
||||
const confirm = new Toggle(mapView, splitToggle, splitClicked);
|
||||
super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit)
|
||||
this.dialogIsOpened = splitClicked
|
||||
}
|
||||
}
|
|
@ -37,8 +37,8 @@ export default class ShowDataLayer {
|
|||
this._layerToShow = options.layerToShow;
|
||||
const self = this;
|
||||
|
||||
features.addCallback(() => self.update(options));
|
||||
options.leafletMap.addCallback(() => self.update(options));
|
||||
features.addCallback(_ => self.update(options));
|
||||
options.leafletMap.addCallback(_ => self.update(options));
|
||||
this.update(options);
|
||||
|
||||
|
||||
|
@ -83,13 +83,17 @@ export default class ShowDataLayer {
|
|||
mp.removeLayer(this.geoLayer);
|
||||
}
|
||||
|
||||
this.geoLayer= this.CreateGeojsonLayer()
|
||||
const allFeats = this._features.data;
|
||||
this.geoLayer = this.CreateGeojsonLayer();
|
||||
for (const feat of allFeats) {
|
||||
if (feat === undefined) {
|
||||
continue
|
||||
}
|
||||
this.geoLayer.addData(feat);
|
||||
try{
|
||||
this.geoLayer.addData(feat);
|
||||
}catch(e){
|
||||
console.error("Could not add ", feat, "to the geojson layer in leaflet")
|
||||
}
|
||||
}
|
||||
|
||||
mp.addLayer(this.geoLayer)
|
||||
|
@ -122,7 +126,8 @@ export default class ShowDataLayer {
|
|||
}
|
||||
|
||||
const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) : State.state.allElements.getEventSourceById(feature.properties.id)
|
||||
const style = layer.GenerateLeafletStyle(tagSource, !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0));
|
||||
const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)
|
||||
const style = layer.GenerateLeafletStyle(tagSource, clickable);
|
||||
const baseElement = style.icon.html;
|
||||
if (!this._enablePopups) {
|
||||
baseElement.SetStyle("cursor: initial !important")
|
||||
|
@ -132,7 +137,7 @@ export default class ShowDataLayer {
|
|||
html: baseElement.ConstructElement(),
|
||||
className: style.icon.className,
|
||||
iconAnchor: style.icon.iconAnchor,
|
||||
iconUrl: style.icon.iconUrl,
|
||||
iconUrl: style.icon.iconUrl ?? "./assets/svg/bug.svg",
|
||||
popupAnchor: style.icon.popupAnchor,
|
||||
iconSize: style.icon.iconSize
|
||||
})
|
||||
|
|
2
Utils.ts
2
Utils.ts
|
@ -8,7 +8,7 @@ export class Utils {
|
|||
* However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed.
|
||||
* This is a workaround and yet another hack
|
||||
*/
|
||||
public static runningFromConsole = false;
|
||||
public static runningFromConsole = typeof window === "undefined";
|
||||
public static readonly assets_path = "./assets/svg/";
|
||||
public static externalDownloadFunction: (url: string) => Promise<any>;
|
||||
private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"]
|
||||
|
|
|
@ -606,7 +606,7 @@
|
|||
"fi": "Lisää uusi penkki",
|
||||
"pl": "Dodaj nową ławkę"
|
||||
},
|
||||
"presiceInput": {
|
||||
"preciseInput": {
|
||||
"preferredBackground": "photo"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
}
|
||||
],
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"question": {
|
||||
"en": "What kind of crossing is this?",
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
},
|
||||
"calculatedTags": [
|
||||
"_closest_other_drinking_water_id=feat.closest('drinking_water')?.id",
|
||||
"_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')) * 1000)"
|
||||
"_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')).distance * 1000)"
|
||||
],
|
||||
"minzoom": 13,
|
||||
"wayHandling": 1,
|
||||
|
|
|
@ -1153,10 +1153,10 @@
|
|||
"path": "scissors.svg",
|
||||
"license": "CC-BY 3.0",
|
||||
"authors": [
|
||||
"The noun project - Basith Ibrahi"
|
||||
"The noun project - Icons8"
|
||||
],
|
||||
"sources": [
|
||||
"https://commons.wikimedia.org/wiki/File:Media-floppy.svg"
|
||||
"https://commons.wikimedia.org/wiki/File:Scissors_-_The_Noun_Project.svg"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -1377,4 +1377,4 @@
|
|||
"https://www.wikipedia.org/"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -1,20 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="97.287025"
|
||||
height="97.287033"
|
||||
viewBox="0 0 97.287025 97.287033"
|
||||
version="1.1"
|
||||
id="svg132"
|
||||
style="fill:none">
|
||||
<defs
|
||||
id="defs8"/>
|
||||
<path
|
||||
id="path815"
|
||||
d="m 12.078125,36.966797 a 11.909361,11.909361 0 1 0 0,23.816406 h 73.130859 a 11.909361,11.909361 0 1 0 0,-23.816406 z"
|
||||
style="color:#000000;"/>
|
||||
<path
|
||||
id="path815-3"
|
||||
d="M 48.34375,0.00195313 A 11.909361,11.909361 0 0 0 36.613281,12.078125 v 73.130859 a 11.909361,11.909361 0 1 0 23.816407,0 V 12.078125 A 11.909361,11.909361 0 0 0 48.34375,0.00195313 Z"
|
||||
style="color:#000000;"/>
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="97.287025"
|
||||
height="97.287033"
|
||||
viewBox="0 0 97.287025 97.287033"
|
||||
version="1.1"
|
||||
id="svg132"
|
||||
style="fill:none"
|
||||
sodipodi:docname="plus.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="999"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.4258115"
|
||||
inkscape:cx="6.5957272"
|
||||
inkscape:cy="-32.536483"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg132" />
|
||||
<defs
|
||||
id="defs8" />
|
||||
<path
|
||||
id="path815"
|
||||
d="m 12.078125,36.966797 a 11.909361,11.909361 0 1 0 0,23.816406 h 73.130859 a 11.909361,11.909361 0 1 0 0,-23.816406 z"
|
||||
style="color:#000000;fill:#000000;fill-opacity:1" />
|
||||
<path
|
||||
id="path815-3"
|
||||
d="M 48.34375,0.00195313 A 11.909361,11.909361 0 0 0 36.613281,12.078125 v 73.130859 a 11.909361,11.909361 0 1 0 23.816407,0 V 12.078125 A 11.909361,11.909361 0 0 0 48.34375,0.00195313 Z"
|
||||
style="color:#000000;fill:#000000;fill-opacity:1" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 788 B After Width: | Height: | Size: 1.9 KiB |
|
@ -1,69 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 64 62"
|
||||
xml:space="preserve"
|
||||
id="svg18"
|
||||
sodipodi:docname="scissors.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
width="64"
|
||||
height="62"><metadata
|
||||
id="metadata24"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
|
||||
<defs
|
||||
id="defs22"/>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1053"
|
||||
id="namedview20"
|
||||
showgrid="false"
|
||||
units="in"
|
||||
height="0.8"
|
||||
inkscape:zoom="2.95"
|
||||
inkscape:cx="-35.288136"
|
||||
inkscape:cy="40"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg18"/>
|
||||
<path
|
||||
d="m 31.850939,35.56215 c -1.123789,0 -2.038585,-0.918488 -2.038585,-2.045617 0,-1.129524 0.914796,-2.048011 2.038585,-2.048011 1.123791,0 2.037396,0.918487 2.037396,2.048011 0,1.127129 -0.913605,2.045617 -2.037396,2.045617 z m 0,-2.86578 c -0.450231,0 -0.815672,0.366916 -0.815672,0.820163 0,0.450852 0.365441,0.817768 0.815672,0.817768 0.44904,0 0.81448,-0.366916 0.81448,-0.817768 0,-0.453247 -0.36544,-0.820163 -0.81448,-0.820163 z"
|
||||
id="path2"
|
||||
inkscape:connector-curvature="0"/>
|
||||
<path
|
||||
d="m 16.746047,56.399613 c -2.171149,0 -4.172715,-0.872925 -5.862581,-2.604385 -1.7651031,-2.472485 -3.0035427,-6.391049 0.585183,-11.654973 2.216531,-3.244686 8.030148,-6.249561 11.901911,-5.46297 L 44.668845,5.1920716 c 0.259151,-0.384902 0.763126,-0.515601 1.177528,-0.300967 2.548535,1.312983 3.566036,4.3634207 2.315655,6.9450214 -2.161594,4.455748 -5.032574,10.411536 -7.553639,15.641885 -2.484043,5.153608 -4.628921,9.602163 -5.419513,11.216112 -0.298565,0.611526 -0.929131,0.628315 -1.345925,0.640304 -0.73088,0.02158 -1.640899,0.04557 -2.412387,1.654721 l -3.560063,7.266372 c -1.507147,3.069622 -3.686655,5.429394 -6.304455,6.822716 -1.658815,0.882515 -3.281803,1.321377 -4.819999,1.321377 z m 5.288147,-18.010052 c -3.069228,0 -7.427051,2.410134 -9.051234,4.791489 -2.402834,3.522869 -2.636907,6.698013 -0.697443,9.434292 2.242805,2.280635 5.106621,2.59719 8.421862,0.836952 2.273855,-1.213459 4.18227,-3.290253 5.517443,-6.012142 l 3.556482,-7.256783 c 1.221718,-2.549225 3.094305,-2.661938 3.950582,-2.688316 0.907631,-1.863358 2.919945,-6.036125 5.226044,-10.820419 2.521069,-5.230349 5.393242,-11.187336 7.553642,-15.644284 0.690277,-1.4232984 0.314091,-3.0684235 -0.833587,-4.0516624 L 24.520625,38.255265 c -0.235268,0.342934 -0.668782,0.484425 -1.060496,0.352528 -0.426347,-0.151086 -0.907631,-0.218232 -1.425935,-0.218232 z"
|
||||
id="path4"
|
||||
inkscape:connector-curvature="0"/>
|
||||
<path
|
||||
d="m 16.740077,52.672898 c -1.177533,0 -2.234446,-0.390896 -2.989212,-1.20147 -1.846314,-1.980865 -1.089158,-5.604458 1.724498,-8.247211 2.816046,-2.645153 6.468067,-3.160751 8.310797,-1.179886 1.846314,1.980865 1.089157,5.604458 -1.723305,8.247211 0,0 0,0 -0.0012,0.0024 -1.662399,1.561188 -3.618582,2.378956 -5.321584,2.378956 z m 4.067619,-9.988262 c -1.234856,0 -2.787382,0.671478 -4.078367,1.882541 -1.994401,1.875347 -2.744392,4.460545 -1.638515,5.647625 1.108266,1.187081 3.724872,0.601933 5.718078,-1.26622 v 0 c 1.993206,-1.875347 2.743195,-4.460544 1.637318,-5.647625 -0.389327,-0.417278 -0.967343,-0.616321 -1.638514,-0.616321 z m 0.628178,6.935424 h 0.02269 z"
|
||||
id="path6"
|
||||
inkscape:connector-curvature="0"/>
|
||||
<path
|
||||
d="m 46.54143,57.788136 c -1.620602,0 -3.312855,-0.501213 -5.026604,-1.496441 -2.56406,-1.489246 -4.656387,-3.928158 -6.048885,-7.052937 l -3.286579,-7.38388 c -0.206608,-0.465241 0.0012,-1.009621 0.463367,-1.218258 0.459786,-0.201443 1.004368,0.0024 1.210972,0.46524 l 3.286579,7.383881 c 1.234856,2.769856 3.06445,4.918591 5.292923,6.213589 3.245976,1.882541 6.121735,1.671505 8.542481,-0.630715 1.941856,-2.556419 1.824818,-5.738754 -0.445454,-9.345562 -1.793766,-2.85139 -7.324344,-5.889835 -10.301613,-4.961753 -0.396492,0.119903 -0.826423,-0.03837 -1.047359,-0.3909 l -1.437882,-2.304615 c -0.268703,-0.431664 -0.13853,-1.000027 0.290203,-1.268616 0.431126,-0.270992 0.996006,-0.139091 1.264714,0.290173 l 1.091545,1.750644 c 3.890873,-0.673877 9.597008,2.573208 11.690529,5.901829 3.392872,5.391026 2.01112,9.261627 0.253181,11.559048 -1.740024,1.661915 -3.706955,2.489273 -5.792118,2.489273 z"
|
||||
id="path8"
|
||||
inkscape:connector-curvature="0"/>
|
||||
<path
|
||||
d="m 27.243519,33.454182 c -0.352304,0 -0.689083,-0.206238 -0.839559,-0.551571 -1.329203,-3.038448 -3.294941,-7.067327 -5.194994,-10.965507 -1.81168,-3.714724 -3.523043,-7.224406 -4.677883,-9.846774 -1.154843,-2.6247684 -0.02627,-5.6356364 2.570028,-6.8538924 0.4156,-0.196648 0.917185,-0.04796 1.16559,0.345332 L 32.383578,25.003128 c 0.268707,0.431665 0.138531,1.000025 -0.290203,1.268618 -0.43232,0.27099 -0.996006,0.139091 -1.264714,-0.290177 L 19.191874,7.3288165 c -1.183502,0.9400721 -1.619404,2.5696101 -0.982868,4.0180885 1.139315,2.588795 2.843511,6.08169 4.646831,9.780825 1.909609,3.913769 3.882513,7.959434 5.22724,11.036253 0.204218,0.46524 -0.0072,1.007217 -0.470535,1.213459 -0.120619,0.05276 -0.246016,0.07674 -0.369023,0.07674 z"
|
||||
id="path10"
|
||||
inkscape:connector-curvature="0"/>
|
||||
<path
|
||||
d="m 46.636969,54.068615 c -0.385742,0 -0.789398,-0.04556 -1.204998,-0.134296 -1.488037,-0.316551 -2.992794,-1.184681 -4.237205,-2.446102 -1.245606,-1.259026 -2.098301,-2.774655 -2.401642,-4.271095 -0.333195,-1.637928 0.01672,-3.069622 0.982872,-4.033673 0.968536,-0.961658 2.400444,-1.290203 4.023433,-0.956859 1.488038,0.31895 2.993988,1.184681 4.238396,2.443706 2.712147,2.748272 3.335545,6.398243 1.418774,8.307167 -0.720135,0.719441 -1.695839,1.091152 -2.81963,1.091152 z M 42.601592,43.938862 c -0.518303,0 -1.101098,0.122307 -1.533417,0.551575 -0.692665,0.690662 -0.597126,1.772224 -0.477699,2.357372 0.232875,1.148713 0.910018,2.335793 1.906025,3.34301 0,0.0024 0,0.0024 0,0.0024 0.996006,1.007221 2.173538,1.697883 3.316436,1.940097 0.578019,0.119908 1.65762,0.225426 2.351479,-0.462841 1.147678,-1.143917 0.493229,-3.755493 -1.428322,-5.702785 -0.996006,-1.007217 -2.174732,-1.695484 -3.316437,-1.937698 -0.218548,-0.04796 -0.506363,-0.09113 -0.818065,-0.09113 z"
|
||||
id="path12"
|
||||
inkscape:connector-curvature="0"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" x="0px" y="0px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve"><path d="M6.5,3C4.6,3,3,4.6,3,6.5S4.6,10,6.5,10S10,8.4,10,6.5S8.4,3,6.5,3z M6.5,8C5.7,8,5,7.3,5,6.5S5.7,5,6.5,5S8,5.7,8,6.5 S7.3,8,6.5,8z"></path><path d="M6.5,14C4.6,14,3,15.6,3,17.5S4.6,21,6.5,21s3.5-1.6,3.5-3.5S8.4,14,6.5,14z M6.5,19C5.7,19,5,18.3,5,17.5S5.7,16,6.5,16 S8,16.7,8,17.5S7.3,19,6.5,19z"></path><polygon points="13.5,9.5 15.5,11.5 22,5 18,5 "></polygon><polygon points="10.8,12.2 12.8,14.2 9.7,17.3 8,15 "></polygon><path d="M9.1,7.1L7,9l11,11h4L9.1,7.1z M12.5,13c-0.3,0-0.5-0.2-0.5-0.5c0-0.3,0.2-0.5,0.5-0.5s0.5,0.2,0.5,0.5 C13,12.8,12.8,13,12.5,13z"></path></svg>
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 771 B |
|
@ -24,7 +24,7 @@
|
|||
"startLat": 51,
|
||||
"startLon": 3.75,
|
||||
"startZoom": 11,
|
||||
"widenFactor": 0.05,
|
||||
"widenFactor": 1,
|
||||
"socialImage": "./assets/themes/cycle_infra/cycle-infra.svg",
|
||||
"enableDownload": true,
|
||||
"layers": [
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Deze straat i een fietsstraat",
|
||||
"nl": "Deze straat is een fietsstraat",
|
||||
"en": "This street is a cyclestreet",
|
||||
"ja": "この通りはcyclestreetだ",
|
||||
"nb_NO": "Denne gaten er en sykkelvei"
|
||||
|
@ -276,8 +276,10 @@
|
|||
},
|
||||
"tagRenderings": [
|
||||
"images"
|
||||
],
|
||||
"allowSplit": false
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"overrideAll": {
|
||||
"allowSplit": true
|
||||
}
|
||||
}
|
|
@ -39,7 +39,7 @@ Contains tweaks for small screens
|
|||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.leaflet-control-attribution {
|
||||
#leafletDiv .leaflet-control-attribution {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ writeFileSync("cycleHighwayFix.osc", "<osmChange version=\"0.6\" generator=\"Han
|
|||
" <modify>", "utf8")
|
||||
const ids = JSON.parse(readFileSync("export.geojson", "utf-8")).features.map(f => f.properties["@id"])
|
||||
console.log(ids)
|
||||
ids.map(id => OsmObject.DownloadReferencingRelations(id).addCallbackAndRunD(relations => {
|
||||
ids.map(id => OsmObject.DownloadReferencingRelations(id).then(relations => {
|
||||
console.log(relations)
|
||||
const changeparts = relations.filter(relation => relation.tags["cycle_highway"] == "yes" && relation.tags["note:state"] == undefined)
|
||||
.map(relation => {
|
||||
|
@ -18,5 +18,4 @@ ids.map(id => OsmObject.DownloadReferencingRelations(id).addCallbackAndRunD(rela
|
|||
return relation.ChangesetXML(undefined)
|
||||
})
|
||||
appendFileSync("cycleHighwayFix.osc", changeparts.join("\n"), "utf8")
|
||||
return true;
|
||||
}))
|
|
@ -56,7 +56,7 @@ export default class ScriptUtils {
|
|||
|
||||
const headers = options?.headers ?? {}
|
||||
headers.accept = "application/json"
|
||||
|
||||
console.log("Fetching", url)
|
||||
const urlObj = new URL(url)
|
||||
https.get({
|
||||
host: urlObj.host,
|
||||
|
@ -75,6 +75,7 @@ export default class ScriptUtils {
|
|||
res.addListener('end', function () {
|
||||
const result = parts.join("")
|
||||
try {
|
||||
console.log("Fetched", result)
|
||||
resolve(JSON.parse(result))
|
||||
} catch (e) {
|
||||
console.error("Could not parse the following as JSON:", result)
|
||||
|
|
|
@ -201,7 +201,10 @@ function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsT
|
|||
}
|
||||
},
|
||||
layer,
|
||||
false);
|
||||
{
|
||||
includeDates: false,
|
||||
includeNonDates: true
|
||||
});
|
||||
|
||||
const createdTiles = []
|
||||
// At this point, we have all the features of the entire area.
|
||||
|
|
62
test.ts
62
test.ts
|
@ -1,16 +1,50 @@
|
|||
const client_token = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
||||
import SplitRoadWizard from "./UI/Popup/SplitRoadWizard";
|
||||
import State from "./State";
|
||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
||||
import MinimapImplementation from "./UI/Base/MinimapImplementation";
|
||||
import {UIEventSource} from "./Logic/UIEventSource";
|
||||
import FilteredLayer from "./Models/FilteredLayer";
|
||||
import {And} from "./Logic/Tags/And";
|
||||
|
||||
const image_id = '196804715753265';
|
||||
const api_url = 'https://graph.mapillary.com/' + image_id + '?fields=thumb_1024_url&&access_token=' + client_token;
|
||||
fetch(api_url,
|
||||
{
|
||||
headers: {'Authorization': 'OAuth ' + client_token}
|
||||
const layout = AllKnownLayouts.allKnownLayouts.get("cyclestreets")
|
||||
State.state = new State(layout)
|
||||
MinimapImplementation.initialize()
|
||||
const feature = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
id: "way/1234",
|
||||
"highway":"residential",
|
||||
"cyclestreet":"yes"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[
|
||||
3.2207107543945312,
|
||||
51.21978729870313
|
||||
],
|
||||
[
|
||||
3.2198524475097656,
|
||||
51.21899435057332
|
||||
],
|
||||
[
|
||||
3.2155394554138184,
|
||||
51.21617188199714
|
||||
]
|
||||
]
|
||||
}
|
||||
).then(response => {
|
||||
return response.json()
|
||||
}).then(
|
||||
json => {
|
||||
const thumbnail_url = json["thumb_1024"]
|
||||
console.log(thumbnail_url)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
State.state.allElements.addOrGetElement(feature)
|
||||
State.state.filteredLayers = new UIEventSource<FilteredLayer[]>(
|
||||
layout.layers.map( l => ({
|
||||
layerDef :l,
|
||||
appliedFilters: new UIEventSource<And>(undefined),
|
||||
isDisplayed: new UIEventSource<boolean>(undefined)
|
||||
}))
|
||||
)
|
||||
|
||||
const splitroad = new SplitRoadWizard("way/1234")
|
||||
splitroad.AttachTo("maindiv")
|
||||
|
||||
splitroad.dialogIsOpened.setData(true)
|
||||
|
|
|
@ -10,7 +10,7 @@ Utils.runningFromConsole = true;
|
|||
export default class ImageAttributionSpec extends T {
|
||||
constructor() {
|
||||
super(
|
||||
"ImageAttribution Tests", [
|
||||
"imageattribution", [
|
||||
[
|
||||
"Should find all the images",
|
||||
() => {
|
||||
|
|
|
@ -8,7 +8,7 @@ Utils.runningFromConsole = true;
|
|||
export default class ImageSearcherSpec extends T {
|
||||
|
||||
constructor() {
|
||||
super("ImageSearcher", [
|
||||
super("imagesearcher", [
|
||||
[
|
||||
"Should find images",
|
||||
() => {
|
||||
|
|
|
@ -12,7 +12,7 @@ export default class OsmConnectionSpec extends T {
|
|||
private static _osm_token = "LJFmv2nUicSNmBNsFeyCHx5KKx6Aiesx8pXPbX4n"
|
||||
|
||||
constructor() {
|
||||
super("OsmConnectionSpec-test", [
|
||||
super("osmconnection", [
|
||||
["login on dev",
|
||||
() => {
|
||||
const osmConn = new OsmConnection(false, false,
|
||||
|
|
|
@ -1,27 +1,26 @@
|
|||
import T from "./TestHelper";
|
||||
import {OsmObject} from "../Logic/Osm/OsmObject";
|
||||
import ScriptUtils from "../scripts/ScriptUtils";
|
||||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
|
||||
export default class OsmObjectSpec extends T {
|
||||
private static async runTest(){
|
||||
const ways = await OsmObject.DownloadReferencingWays("node/1124134958")
|
||||
if(ways === undefined){
|
||||
throw "Did not get the ways"
|
||||
}
|
||||
if (ways.length !== 4) {
|
||||
throw "Expected 4 ways but got "+ways.length
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
constructor() {
|
||||
super("OsmObject", [
|
||||
super("osmobject", [
|
||||
[
|
||||
"Download referencing ways",
|
||||
() => {
|
||||
let downloaded = false;
|
||||
OsmObject.DownloadReferencingWays("node/1124134958").addCallbackAndRunD(ways => {
|
||||
downloaded = true;
|
||||
console.log(ways)
|
||||
})
|
||||
let timeout = 10
|
||||
while (!downloaded && timeout >= 0) {
|
||||
ScriptUtils.sleep(1000)
|
||||
|
||||
timeout--;
|
||||
}
|
||||
if (!downloaded) {
|
||||
throw "Timeout: referencing ways not found"
|
||||
}
|
||||
OsmObjectSpec.runTest().then(_ => console.log("Referencing ways test is done (async)"))
|
||||
}
|
||||
|
||||
]
|
||||
|
|
66
test/RelationSplitHandler.spec.ts
Normal file
66
test/RelationSplitHandler.spec.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import T from "./TestHelper";
|
||||
import {InPlaceReplacedmentRTSH} from "../Logic/Osm/Actions/RelationSplitHandler";
|
||||
import {OsmObject, OsmRelation} from "../Logic/Osm/OsmObject";
|
||||
import {Changes} from "../Logic/Osm/Changes";
|
||||
import {equal} from "assert";
|
||||
|
||||
export default class RelationSplitHandlerSpec extends T {
|
||||
|
||||
private static async split(): Promise<void> {
|
||||
// Lets mimick a split action of https://www.openstreetmap.org/way/295132739
|
||||
|
||||
const relation: OsmRelation = <OsmRelation>await OsmObject.DownloadObjectAsync("relation/9572808")
|
||||
const originalNodeIds = [5273988967,
|
||||
170497153,
|
||||
1507524582,
|
||||
4524321710,
|
||||
170497155,
|
||||
170497157,
|
||||
170497158,
|
||||
3208166179,
|
||||
1507524610,
|
||||
170497160,
|
||||
3208166178,
|
||||
1507524573,
|
||||
1575932830,
|
||||
6448669326]
|
||||
|
||||
const withSplit = [[5273988967,
|
||||
170497153,
|
||||
1507524582,
|
||||
4524321710,
|
||||
170497155,
|
||||
170497157,
|
||||
170497158],
|
||||
[
|
||||
3208166179,
|
||||
1507524610,
|
||||
170497160,
|
||||
3208166178,
|
||||
1507524573,
|
||||
1575932830,
|
||||
6448669326]]
|
||||
|
||||
const splitter = new InPlaceReplacedmentRTSH(
|
||||
{
|
||||
relation: relation,
|
||||
originalWayId: 295132739,
|
||||
allWayIdsInOrder: [295132739, -1],
|
||||
originalNodes: originalNodeIds,
|
||||
allWaysNodesInOrder: withSplit
|
||||
})
|
||||
const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
|
||||
const allIds = changeDescription[0].changes["members"].map(m => m.ref).join(",")
|
||||
const expected = "687866206,295132739,-1,690497698"
|
||||
if (allIds.indexOf(expected) < 0) {
|
||||
throw "Invalid order or the split ways. If this suddenly breaks, the parent relation at https://osm.org/relation/9572808 has probably changed and the test must be updated"
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super("relationsplithandler", [
|
||||
["split 295132739",
|
||||
() => RelationSplitHandlerSpec.split().then(_ => console.log("OK"))]
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ Utils.runningFromConsole = true;
|
|||
export default class TagSpec extends T {
|
||||
|
||||
constructor() {
|
||||
super("Tags", [
|
||||
super("tag", [
|
||||
["Tag replacement works in translation", () => {
|
||||
const tr = new Translation({
|
||||
"en": "Test {key} abc"
|
||||
|
|
|
@ -1,36 +1,16 @@
|
|||
import {Utils} from "../Utils";
|
||||
Utils.runningFromConsole = true;
|
||||
import TagSpec from "./Tag.spec";
|
||||
import ImageAttributionSpec from "./ImageAttribution.spec";
|
||||
import GeoOperationsSpec from "./GeoOperations.spec";
|
||||
import ImageSearcherSpec from "./ImageSearcher.spec";
|
||||
import ThemeSpec from "./Theme.spec";
|
||||
import UtilsSpec from "./Utils.spec";
|
||||
import OsmConnectionSpec from "./OsmConnection.spec";
|
||||
import T from "./TestHelper";
|
||||
import {FixedUiElement} from "../UI/Base/FixedUiElement";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import OsmObjectSpec from "./OsmObject.spec";
|
||||
import ScriptUtils from "../scripts/ScriptUtils";
|
||||
import UnitsSpec from "./Units.spec";
|
||||
import RelationSplitHandlerSpec from "./RelationSplitHandler.spec";
|
||||
|
||||
|
||||
|
||||
export default class TestAll {
|
||||
private needsBrowserTests: T[] = [new OsmConnectionSpec()]
|
||||
|
||||
public testAll(): void {
|
||||
Utils.runningFromConsole = false
|
||||
for (const test of this.needsBrowserTests.concat(allTests)) {
|
||||
if (test.failures.length > 0) {
|
||||
new Combine([new FixedUiElement("TEST FAILED: " + test.name).SetStyle("background: red"),
|
||||
...test.failures])
|
||||
.AttachTo("maindiv")
|
||||
throw "Some test failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ScriptUtils.fixUtils()
|
||||
const allTests = [
|
||||
new OsmObjectSpec(),
|
||||
|
@ -40,12 +20,34 @@ const allTests = [
|
|||
new ImageSearcherSpec(),
|
||||
new ThemeSpec(),
|
||||
new UtilsSpec(),
|
||||
new UnitsSpec()
|
||||
new UnitsSpec(),
|
||||
new RelationSplitHandlerSpec()
|
||||
]
|
||||
|
||||
let args = [...process.argv]
|
||||
args.splice(0, 2)
|
||||
args = args.map(a => a.toLowerCase())
|
||||
|
||||
for (const test of allTests) {
|
||||
if (test.failures.length > 0) {
|
||||
throw "Some test failed: " + test.failures.join(", ")
|
||||
const allFailures: { testsuite: string, name: string, msg: string } [] = []
|
||||
let testsToRun = allTests
|
||||
if (args.length > 0) {
|
||||
testsToRun = allTests.filter(t => args.indexOf(t.name) >= 0)
|
||||
}
|
||||
|
||||
if(testsToRun.length == 0){
|
||||
throw "No tests found"
|
||||
}
|
||||
|
||||
for (let i = 0; i < testsToRun.length; i++){
|
||||
const test = testsToRun[i];
|
||||
ScriptUtils.erasableLog(" Running test", i, "/", allTests.length)
|
||||
allFailures.push(...(test.Run() ?? []))
|
||||
|
||||
}
|
||||
if (allFailures.length > 0) {
|
||||
for (const failure of allFailures) {
|
||||
console.error(" !! " + failure.testsuite + "." + failure.name + " failed due to: " + failure.msg)
|
||||
}
|
||||
}
|
||||
throw "Some test failed"
|
||||
}
|
||||
console.log("All tests successful: ", allTests.map(t => t.name).join(", "))
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
export default class T {
|
||||
|
||||
public readonly failures: string[] = []
|
||||
public readonly name: string;
|
||||
private readonly _tests: [string, (() => void)][];
|
||||
|
||||
constructor(testsuite: string, tests: [string, () => void][]) {
|
||||
this.name = testsuite
|
||||
for (const [name, test] of tests) {
|
||||
this._tests = tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* RUns the test, returns the error messages.
|
||||
* Returns an empty list if successful
|
||||
* @constructor
|
||||
*/
|
||||
public Run() : ({testsuite: string, name: string, msg: string} []) {
|
||||
const failures: {testsuite: string, name: string, msg: string} [] = []
|
||||
for (const [name, test] of this._tests) {
|
||||
try {
|
||||
test();
|
||||
} catch (e) {
|
||||
this.failures.push(name);
|
||||
console.warn(`>>> Failed test in ${this.name}: ${name}because${e}`);
|
||||
failures.push({testsuite: this.name, name: name, msg: ""+e});
|
||||
}
|
||||
}
|
||||
if (this.failures.length == 0) {
|
||||
console.log(`All tests of ${testsuite} done!`)
|
||||
if (failures.length == 0) {
|
||||
return undefined
|
||||
} else {
|
||||
console.warn(this.failures.length, `tests of ${testsuite} failed :(`)
|
||||
console.log("Failed tests: ", this.failures.join(","))
|
||||
return failures
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ Utils.runningFromConsole = true;
|
|||
|
||||
export default class ThemeSpec extends T {
|
||||
constructor() {
|
||||
super("Theme tests",
|
||||
super("theme",
|
||||
[
|
||||
["Nested overrides work", () => {
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import {Denomination} from "../Models/Denomination";
|
|||
export default class UnitsSpec extends T {
|
||||
|
||||
constructor() {
|
||||
super("Units", [
|
||||
super("units", [
|
||||
["Simple canonicalize", () => {
|
||||
|
||||
const unit = new Denomination({
|
||||
|
|
|
@ -39,7 +39,7 @@ export default class UtilsSpec extends T {
|
|||
}
|
||||
|
||||
constructor() {
|
||||
super("Utils", [
|
||||
super("utils", [
|
||||
["Sort object keys", () => {
|
||||
const o = {
|
||||
x: 'x',
|
||||
|
|
Loading…
Reference in a new issue