Full code cleanup
This commit is contained in:
parent
8e6ee8c87f
commit
bd21212eba
246 changed files with 19418 additions and 11729 deletions
|
@ -5,8 +5,10 @@ import Loc from "../../Models/Loc";
|
|||
export interface AvailableBaseLayersObj {
|
||||
readonly osmCarto: BaseLayer;
|
||||
layerOverview: BaseLayer[];
|
||||
AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]>
|
||||
SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> ;
|
||||
|
||||
AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]>
|
||||
|
||||
SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer>;
|
||||
|
||||
}
|
||||
|
||||
|
@ -15,13 +17,13 @@ export interface AvailableBaseLayersObj {
|
|||
* Changes the basemap
|
||||
*/
|
||||
export default class AvailableBaseLayers {
|
||||
|
||||
|
||||
|
||||
|
||||
public static layerOverview: BaseLayer[];
|
||||
public static osmCarto: BaseLayer;
|
||||
|
||||
private static implementation: AvailableBaseLayersObj
|
||||
|
||||
|
||||
static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
|
||||
return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new UIEventSource<BaseLayer[]>([]);
|
||||
}
|
||||
|
@ -31,7 +33,7 @@ export default class AvailableBaseLayers {
|
|||
|
||||
}
|
||||
|
||||
public static implement(backend: AvailableBaseLayersObj){
|
||||
public static implement(backend: AvailableBaseLayersObj) {
|
||||
AvailableBaseLayers.layerOverview = backend.layerOverview
|
||||
AvailableBaseLayers.osmCarto = backend.osmCarto
|
||||
AvailableBaseLayers.implementation = backend
|
||||
|
|
|
@ -3,13 +3,13 @@ import {UIEventSource} from "../UIEventSource";
|
|||
import Loc from "../../Models/Loc";
|
||||
import {GeoOperations} from "../GeoOperations";
|
||||
import * as editorlayerindex from "../../assets/editor-layer-index.json";
|
||||
import * as L from "leaflet";
|
||||
import {TileLayer} from "leaflet";
|
||||
import * as X from "leaflet-providers";
|
||||
import * as L from "leaflet";
|
||||
import {Utils} from "../../Utils";
|
||||
import {AvailableBaseLayersObj} from "./AvailableBaseLayers";
|
||||
|
||||
export default class AvailableBaseLayersImplementation implements AvailableBaseLayersObj{
|
||||
export default class AvailableBaseLayersImplementation implements AvailableBaseLayersObj {
|
||||
|
||||
public readonly osmCarto: BaseLayer =
|
||||
{
|
||||
|
@ -28,102 +28,6 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
|
|||
|
||||
public layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex());
|
||||
|
||||
public AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
|
||||
const source = location.map(
|
||||
(currentLocation) => {
|
||||
|
||||
if (currentLocation === undefined) {
|
||||
return this.layerOverview;
|
||||
}
|
||||
|
||||
const currentLayers = source?.data; // A bit unorthodox - I know
|
||||
const newLayers = this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
|
||||
|
||||
if (currentLayers === undefined) {
|
||||
return newLayers;
|
||||
}
|
||||
if (newLayers.length !== currentLayers.length) {
|
||||
return newLayers;
|
||||
}
|
||||
for (let i = 0; i < newLayers.length; i++) {
|
||||
if (newLayers[i].name !== currentLayers[i].name) {
|
||||
return newLayers;
|
||||
}
|
||||
}
|
||||
|
||||
return currentLayers;
|
||||
});
|
||||
return source;
|
||||
}
|
||||
|
||||
public SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
|
||||
return this.AvailableLayersAt(location).map(available => {
|
||||
// First float all 'best layers' to the top
|
||||
available.sort((a, b) => {
|
||||
if (a.isBest && b.isBest) {
|
||||
return 0;
|
||||
}
|
||||
if (!a.isBest) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
)
|
||||
|
||||
if (preferedCategory.data === undefined) {
|
||||
return available[0]
|
||||
}
|
||||
|
||||
let prefered: string []
|
||||
if (typeof preferedCategory.data === "string") {
|
||||
prefered = [preferedCategory.data]
|
||||
} else {
|
||||
prefered = preferedCategory.data;
|
||||
}
|
||||
|
||||
prefered.reverse();
|
||||
for (const category of prefered) {
|
||||
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
|
||||
available.sort((a, b) => {
|
||||
if (a.category === category && b.category === category) {
|
||||
return 0;
|
||||
}
|
||||
if (a.category !== category) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
)
|
||||
}
|
||||
return available[0]
|
||||
})
|
||||
}
|
||||
|
||||
private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
|
||||
const availableLayers = [this.osmCarto]
|
||||
const globalLayers = [];
|
||||
for (const layerOverviewItem of this.layerOverview) {
|
||||
const layer = layerOverviewItem;
|
||||
|
||||
if (layer.feature?.geometry === undefined || layer.feature?.geometry === null) {
|
||||
globalLayers.push(layer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lon === undefined || lat === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GeoOperations.inside([lon, lat], layer.feature)) {
|
||||
availableLayers.push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
return availableLayers.concat(globalLayers);
|
||||
}
|
||||
|
||||
private static LoadRasterIndex(): BaseLayer[] {
|
||||
const layers: BaseLayer[] = []
|
||||
// @ts-ignore
|
||||
|
@ -289,4 +193,100 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
|
|||
subdomains: domains
|
||||
});
|
||||
}
|
||||
|
||||
public AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
|
||||
const source = location.map(
|
||||
(currentLocation) => {
|
||||
|
||||
if (currentLocation === undefined) {
|
||||
return this.layerOverview;
|
||||
}
|
||||
|
||||
const currentLayers = source?.data; // A bit unorthodox - I know
|
||||
const newLayers = this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
|
||||
|
||||
if (currentLayers === undefined) {
|
||||
return newLayers;
|
||||
}
|
||||
if (newLayers.length !== currentLayers.length) {
|
||||
return newLayers;
|
||||
}
|
||||
for (let i = 0; i < newLayers.length; i++) {
|
||||
if (newLayers[i].name !== currentLayers[i].name) {
|
||||
return newLayers;
|
||||
}
|
||||
}
|
||||
|
||||
return currentLayers;
|
||||
});
|
||||
return source;
|
||||
}
|
||||
|
||||
public SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
|
||||
return this.AvailableLayersAt(location).map(available => {
|
||||
// First float all 'best layers' to the top
|
||||
available.sort((a, b) => {
|
||||
if (a.isBest && b.isBest) {
|
||||
return 0;
|
||||
}
|
||||
if (!a.isBest) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
)
|
||||
|
||||
if (preferedCategory.data === undefined) {
|
||||
return available[0]
|
||||
}
|
||||
|
||||
let prefered: string []
|
||||
if (typeof preferedCategory.data === "string") {
|
||||
prefered = [preferedCategory.data]
|
||||
} else {
|
||||
prefered = preferedCategory.data;
|
||||
}
|
||||
|
||||
prefered.reverse();
|
||||
for (const category of prefered) {
|
||||
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
|
||||
available.sort((a, b) => {
|
||||
if (a.category === category && b.category === category) {
|
||||
return 0;
|
||||
}
|
||||
if (a.category !== category) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
)
|
||||
}
|
||||
return available[0]
|
||||
})
|
||||
}
|
||||
|
||||
private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
|
||||
const availableLayers = [this.osmCarto]
|
||||
const globalLayers = [];
|
||||
for (const layerOverviewItem of this.layerOverview) {
|
||||
const layer = layerOverviewItem;
|
||||
|
||||
if (layer.feature?.geometry === undefined || layer.feature?.geometry === null) {
|
||||
globalLayers.push(layer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lon === undefined || lat === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GeoOperations.inside([lon, lat], layer.feature)) {
|
||||
availableLayers.push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
return availableLayers.concat(globalLayers);
|
||||
}
|
||||
}
|
|
@ -13,11 +13,11 @@ export default class BackgroundLayerResetter {
|
|||
location: UIEventSource<Loc>,
|
||||
availableLayers: UIEventSource<BaseLayer[]>,
|
||||
defaultLayerId: string = undefined) {
|
||||
|
||||
if(Utils.runningFromConsole){
|
||||
|
||||
if (Utils.runningFromConsole) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
defaultLayerId = defaultLayerId ?? AvailableBaseLayers.osmCarto.id;
|
||||
|
||||
// Change the baselayer back to OSM if we go out of the current range of the layer
|
||||
|
|
|
@ -8,9 +8,9 @@ import FeatureSource from "../FeatureSource/FeatureSource";
|
|||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource";
|
||||
|
||||
export default class GeoLocationHandler extends VariableUiElement {
|
||||
|
||||
public readonly currentLocation : FeatureSource
|
||||
|
||||
|
||||
public readonly currentLocation: FeatureSource
|
||||
|
||||
/**
|
||||
* Wether or not the geolocation is active, aka the user requested the current location
|
||||
* @private
|
||||
|
@ -182,25 +182,25 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
}
|
||||
})
|
||||
|
||||
this.currentLocation = new StaticFeatureSource([], false)
|
||||
this.currentLocation = new StaticFeatureSource([], false)
|
||||
this._currentGPSLocation.addCallback((location) => {
|
||||
self._previousLocationGrant.setData("granted");
|
||||
|
||||
const feature = {
|
||||
"type": "Feature",
|
||||
properties: {
|
||||
"user:location":"yes",
|
||||
"accuracy":location.accuracy,
|
||||
"speed":location.speed,
|
||||
"user:location": "yes",
|
||||
"accuracy": location.accuracy,
|
||||
"speed": location.speed,
|
||||
},
|
||||
geometry:{
|
||||
type:"Point",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [location.longitude, location.latitude],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.currentLocation.features.setData([{feature, freshness: new Date()}])
|
||||
|
||||
|
||||
const timeSinceRequest =
|
||||
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
|
||||
if (timeSinceRequest < 30) {
|
||||
|
@ -210,7 +210,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
private init(askPermission: boolean, zoomToLocation: boolean) {
|
||||
|
@ -279,7 +279,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
);
|
||||
} else {
|
||||
const currentZoom = this._leafletMap.data.getZoom()
|
||||
|
||||
|
||||
this._leafletMap.data.setView([location.latitude, location.longitude], Math.max(targetZoom ?? 0, currentZoom));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,8 +113,7 @@ export default class OverpassFeatureSource implements FeatureSource {
|
|||
let data: any = undefined
|
||||
let date: Date = undefined
|
||||
let lastUsed = 0;
|
||||
|
||||
|
||||
|
||||
|
||||
const layersToDownload = []
|
||||
for (const layer of this.state.layoutToUse.layers) {
|
||||
|
@ -137,7 +136,7 @@ export default class OverpassFeatureSource implements FeatureSource {
|
|||
|
||||
const self = this;
|
||||
const overpassUrls = self.state.overpassUrl.data
|
||||
let bounds : BBox
|
||||
let bounds: BBox
|
||||
do {
|
||||
try {
|
||||
|
||||
|
@ -180,9 +179,9 @@ export default class OverpassFeatureSource implements FeatureSource {
|
|||
}
|
||||
} while (data === undefined && this._isActive.data);
|
||||
|
||||
|
||||
|
||||
try {
|
||||
if(data === undefined){
|
||||
if (data === undefined) {
|
||||
return undefined
|
||||
}
|
||||
data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined));
|
||||
|
|
|
@ -31,10 +31,10 @@ export default class PendingChangesUploader {
|
|||
}
|
||||
});
|
||||
|
||||
if(Utils.runningFromConsole){
|
||||
if (Utils.runningFromConsole) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('mouseout', e => {
|
||||
// @ts-ignore
|
||||
if (!e.toElement && !e.relatedTarget) {
|
||||
|
|
|
@ -10,7 +10,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
|||
* Makes sure the hash shows the selected element and vice-versa.
|
||||
*/
|
||||
export default class SelectedFeatureHandler {
|
||||
private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "filters","", undefined])
|
||||
private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "filters", "", undefined])
|
||||
private readonly hash: UIEventSource<string>;
|
||||
private readonly state: {
|
||||
selectedElement: UIEventSource<any>,
|
||||
|
@ -88,7 +88,7 @@ export default class SelectedFeatureHandler {
|
|||
if (!(hash.startsWith("node") || hash.startsWith("way") || hash.startsWith("relation"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
OsmObject.DownloadObjectAsync(hash).then(obj => {
|
||||
|
||||
|
@ -114,7 +114,7 @@ export default class SelectedFeatureHandler {
|
|||
// 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) {
|
||||
|
|
|
@ -80,6 +80,5 @@ export default class StrayClickHandler {
|
|||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -8,7 +8,7 @@ import {ElementStorage} from "../ElementStorage";
|
|||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class TitleHandler {
|
||||
constructor(state : {
|
||||
constructor(state: {
|
||||
selectedElement: UIEventSource<any>,
|
||||
layoutToUse: LayoutConfig,
|
||||
allElements: ElementStorage
|
||||
|
@ -39,7 +39,7 @@ export default class TitleHandler {
|
|||
|
||||
|
||||
currentTitle.addCallbackAndRunD(title => {
|
||||
if(Utils.runningFromConsole){
|
||||
if (Utils.runningFromConsole) {
|
||||
return
|
||||
}
|
||||
document.title = title
|
||||
|
|
|
@ -4,11 +4,11 @@ import {GeoOperations} from "./GeoOperations";
|
|||
|
||||
export class BBox {
|
||||
|
||||
static global: BBox = new BBox([[-180, -90], [180, 90]]);
|
||||
readonly maxLat: number;
|
||||
readonly maxLon: number;
|
||||
readonly minLat: number;
|
||||
readonly minLon: number;
|
||||
static global: BBox = new BBox([[-180, -90], [180, 90]]);
|
||||
|
||||
constructor(coordinates) {
|
||||
this.maxLat = -90;
|
||||
|
@ -45,6 +45,17 @@ export class BBox {
|
|||
return feature.bbox;
|
||||
}
|
||||
|
||||
static fromTile(z: number, x: number, y: number): BBox {
|
||||
return new BBox(Tiles.tile_bounds_lon_lat(z, x, y))
|
||||
}
|
||||
|
||||
static fromTileIndex(i: number): BBox {
|
||||
if (i === 0) {
|
||||
return BBox.global
|
||||
}
|
||||
return BBox.fromTile(...Tiles.tile_from_index(i))
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a tilerange which fully contains this bbox (thus might be a bit larger)
|
||||
* @param zoomlevel
|
||||
|
@ -83,24 +94,6 @@ export class BBox {
|
|||
return true;
|
||||
}
|
||||
|
||||
private check() {
|
||||
if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) {
|
||||
console.log(this);
|
||||
throw "BBOX has NAN";
|
||||
}
|
||||
}
|
||||
|
||||
static fromTile(z: number, x: number, y: number): BBox {
|
||||
return new BBox(Tiles.tile_bounds_lon_lat(z, x, y))
|
||||
}
|
||||
|
||||
static fromTileIndex(i: number): BBox {
|
||||
if (i === 0) {
|
||||
return BBox.global
|
||||
}
|
||||
return BBox.fromTile(...Tiles.tile_from_index(i))
|
||||
}
|
||||
|
||||
getEast() {
|
||||
return this.maxLon
|
||||
}
|
||||
|
@ -116,10 +109,10 @@ export class BBox {
|
|||
getSouth() {
|
||||
return this.minLat
|
||||
}
|
||||
|
||||
contains(lonLat: [number, number]){
|
||||
|
||||
contains(lonLat: [number, number]) {
|
||||
return this.minLat <= lonLat[1] && lonLat[1] <= this.maxLat
|
||||
&& this.minLon<= lonLat[0] && lonLat[0] <= this.maxLon
|
||||
&& this.minLon <= lonLat[0] && lonLat[0] <= this.maxLon
|
||||
}
|
||||
|
||||
pad(factor: number, maxIncrease = 2): BBox {
|
||||
|
@ -179,4 +172,11 @@ export class BBox {
|
|||
|
||||
|
||||
}
|
||||
|
||||
private check() {
|
||||
if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) {
|
||||
console.log(this);
|
||||
throw "BBOX has NAN";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ export default class ContributorCount {
|
|||
|
||||
public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>());
|
||||
private readonly state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> };
|
||||
private lastUpdate: Date = undefined;
|
||||
|
||||
constructor(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> }) {
|
||||
this.state = state;
|
||||
|
@ -16,15 +17,13 @@ export default class ContributorCount {
|
|||
self.update(bbox)
|
||||
})
|
||||
state.featurePipeline.runningQuery.addCallbackAndRun(
|
||||
_ => self.update(state.currentBounds.data)
|
||||
_ => self.update(state.currentBounds.data)
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
private lastUpdate: Date = undefined;
|
||||
|
||||
private update(bbox: BBox) {
|
||||
if(bbox === undefined){
|
||||
if (bbox === undefined) {
|
||||
return;
|
||||
}
|
||||
const now = new Date();
|
||||
|
|
|
@ -61,43 +61,10 @@ export default class DetermineLayout {
|
|||
layer.minzoom = Math.max(16, layer.minzoom)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return [layoutToUse, undefined]
|
||||
}
|
||||
|
||||
private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> {
|
||||
console.log("Downloading map theme from ", link);
|
||||
|
||||
new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`)
|
||||
.AttachTo("centermessage");
|
||||
|
||||
try {
|
||||
|
||||
const parsed = await Utils.downloadJson(link)
|
||||
console.log("Got ", parsed)
|
||||
LegacyJsonConvert.fixThemeConfig(parsed)
|
||||
try {
|
||||
parsed.id = link;
|
||||
return new LayoutConfig(parsed, false).patchImages(link, JSON.stringify(parsed));
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static LoadLayoutFromHash(
|
||||
userLayoutParam: UIEventSource<string>
|
||||
): [LayoutConfig, string] | null {
|
||||
|
@ -166,4 +133,37 @@ export default class DetermineLayout {
|
|||
.AttachTo("centermessage");
|
||||
}
|
||||
|
||||
private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> {
|
||||
console.log("Downloading map theme from ", link);
|
||||
|
||||
new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`)
|
||||
.AttachTo("centermessage");
|
||||
|
||||
try {
|
||||
|
||||
const parsed = await Utils.downloadJson(link)
|
||||
console.log("Got ", parsed)
|
||||
LegacyJsonConvert.fixThemeConfig(parsed)
|
||||
try {
|
||||
parsed.id = link;
|
||||
return new LayoutConfig(parsed, false).patchImages(link, JSON.stringify(parsed));
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -39,10 +39,10 @@ export class ElementStorage {
|
|||
}
|
||||
|
||||
getEventSourceById(elementId): UIEventSource<any> {
|
||||
if(elementId === undefined){
|
||||
if (elementId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return this._elements.get(elementId);
|
||||
return this._elements.get(elementId);
|
||||
}
|
||||
|
||||
has(id) {
|
||||
|
|
|
@ -64,7 +64,7 @@ export class ExtraFunction {
|
|||
},
|
||||
(params, feat) => {
|
||||
return (...layerIds: string[]) => {
|
||||
const result : {feat:any, overlap: number}[]= []
|
||||
const result: { feat: any, overlap: number }[] = []
|
||||
|
||||
const bbox = BBox.get(feat)
|
||||
|
||||
|
@ -80,9 +80,9 @@ export class ExtraFunction {
|
|||
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result.sort((a, b) => b.overlap - a.overlap)
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -181,7 +181,7 @@ export class ExtraFunction {
|
|||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
if(parsed === null){
|
||||
if (parsed === null) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed;
|
||||
|
|
|
@ -39,7 +39,7 @@ export default class SaveTileToLocalStorageActor {
|
|||
}
|
||||
}
|
||||
|
||||
public static poison(layers: string[], lon: number, lat: number) {
|
||||
public static poison(layers: string[], lon: number, lat: number) {
|
||||
for (let z = 0; z < 25; z++) {
|
||||
|
||||
const {x, y} = Tiles.embedded_tile(lat, lon, z)
|
||||
|
|
|
@ -129,7 +129,7 @@ export default class FeaturePipeline {
|
|||
// 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, src.tileIndex,
|
||||
new ChangeGeometryApplicator(src, state.changes)
|
||||
new ChangeGeometryApplicator(src, state.changes)
|
||||
)
|
||||
|
||||
handleFeatureSource(srcFiltered)
|
||||
|
@ -147,7 +147,7 @@ export default class FeaturePipeline {
|
|||
|
||||
this.freshnesses.set(id, new TileFreshnessCalculator())
|
||||
|
||||
if(id === "type_node"){
|
||||
if (id === "type_node") {
|
||||
// Handles by the 'FullNodeDatabaseSource'
|
||||
continue;
|
||||
}
|
||||
|
@ -226,15 +226,15 @@ export default class FeaturePipeline {
|
|||
self.freshnesses.get(flayer.layerDef.id).addTileLoad(tileId, new Date())
|
||||
})
|
||||
})
|
||||
|
||||
if(state.layoutToUse.trackAllNodes){
|
||||
const fullNodeDb = new FullNodeDatabaseSource(
|
||||
state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0],
|
||||
tile => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile)
|
||||
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
|
||||
})
|
||||
|
||||
if (state.layoutToUse.trackAllNodes) {
|
||||
const fullNodeDb = new FullNodeDatabaseSource(
|
||||
state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0],
|
||||
tile => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile)
|
||||
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
|
||||
})
|
||||
|
||||
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => fullNodeDb.handleOsmJson(osmJson, tileId))
|
||||
}
|
||||
|
@ -299,6 +299,34 @@ export default class FeaturePipeline {
|
|||
|
||||
}
|
||||
|
||||
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[][] {
|
||||
if (layerId === "*") {
|
||||
return this.GetAllFeaturesWithin(bbox)
|
||||
}
|
||||
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 => {
|
||||
TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
|
||||
})
|
||||
}
|
||||
|
||||
private freshnessForVisibleLayers(z: number, x: number, y: number): Date {
|
||||
let oldestDate = undefined;
|
||||
for (const flayer of this.state.filteredLayers.data) {
|
||||
|
@ -438,32 +466,4 @@ export default class FeaturePipeline {
|
|||
|
||||
}
|
||||
|
||||
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[][] {
|
||||
if (layerId === "*") {
|
||||
return this.GetAllFeaturesWithin(bbox)
|
||||
}
|
||||
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 => {
|
||||
TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {BBox} from "../BBox";
|
||||
|
||||
|
@ -19,7 +18,7 @@ export interface Tiled {
|
|||
/**
|
||||
* A feature source which only contains features for the defined layer
|
||||
*/
|
||||
export interface FeatureSourceForLayer extends FeatureSource{
|
||||
export interface FeatureSourceForLayer extends FeatureSource {
|
||||
readonly layer: FilteredLayer
|
||||
}
|
||||
|
||||
|
|
|
@ -14,9 +14,9 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
constructor(layers: UIEventSource<FilteredLayer[]>,
|
||||
handleLayerData: (source: FeatureSourceForLayer & Tiled) => void,
|
||||
upstream: FeatureSource,
|
||||
options?:{
|
||||
tileIndex?: number,
|
||||
handleLeftovers?: (featuresWithoutLayer: any[]) => void
|
||||
options?: {
|
||||
tileIndex?: number,
|
||||
handleLeftovers?: (featuresWithoutLayer: any[]) => void
|
||||
}) {
|
||||
|
||||
const knownLayers = new Map<string, FeatureSourceForLayer & Tiled>()
|
||||
|
@ -35,6 +35,7 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
|
||||
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)
|
||||
|
@ -80,9 +81,9 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
featureSource.features.setData(features)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// AT last, the leftovers are handled
|
||||
if(options?.handleLeftovers !== undefined && noLayerFound.length > 0){
|
||||
if (options?.handleLeftovers !== undefined && noLayerFound.length > 0) {
|
||||
options.handleLeftovers(noLayerFound)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@ import {ChangeDescription, ChangeDescriptionTools} from "../../Osm/Actions/Chang
|
|||
export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string;
|
||||
public readonly layer: FilteredLayer
|
||||
private readonly source: IndexedFeatureSource;
|
||||
private readonly changes: Changes;
|
||||
public readonly layer: FilteredLayer
|
||||
|
||||
constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) {
|
||||
this.source = source;
|
||||
|
@ -22,10 +22,10 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
|||
|
||||
this.name = "ChangesApplied(" + source.name + ")"
|
||||
this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined)
|
||||
|
||||
|
||||
const self = this;
|
||||
source.features.addCallbackAndRunD(_ => self.update())
|
||||
|
||||
|
||||
changes.allChanges.addCallbackAndRunD(_ => self.update())
|
||||
|
||||
}
|
||||
|
@ -52,9 +52,9 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
|||
const changesPerId = new Map<string, ChangeDescription[]>()
|
||||
for (const ch of changesToApply) {
|
||||
const key = ch.type + "/" + ch.id
|
||||
if(changesPerId.has(key)){
|
||||
if (changesPerId.has(key)) {
|
||||
changesPerId.get(key).push(ch)
|
||||
}else{
|
||||
} else {
|
||||
changesPerId.set(key, [ch])
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
|||
newFeatures.push(feature)
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Allright! We have a feature to rewrite!
|
||||
const copy = {
|
||||
...feature
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
|
||||
|
@ -14,17 +13,17 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
|
|||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name;
|
||||
public readonly layer: FilteredLayer
|
||||
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())
|
||||
private readonly _sources: UIEventSource<FeatureSource[]>;
|
||||
|
||||
constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
|
||||
this.tileIndex = tileIndex;
|
||||
this.bbox = bbox;
|
||||
this._sources = sources;
|
||||
this.layer = layer;
|
||||
this.name = "FeatureSourceMerger("+layer.layerDef.id+", "+Tiles.tile_from_index(tileIndex).join(",")+")"
|
||||
this.name = "FeatureSourceMerger(" + layer.layerDef.id + ", " + Tiles.tile_from_index(tileIndex).join(",") + ")"
|
||||
const self = this;
|
||||
|
||||
const handledSources = new Set<FeatureSource>();
|
||||
|
|
|
@ -18,6 +18,8 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
locationControl: UIEventSource<{ zoom: number }>; selectedElement: UIEventSource<any>,
|
||||
allElements: ElementStorage
|
||||
};
|
||||
private readonly _alreadyRegistered = new Set<UIEventSource<any>>();
|
||||
private readonly _is_dirty = new UIEventSource(false)
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
|
@ -55,24 +57,6 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
this.update();
|
||||
}
|
||||
|
||||
private readonly _alreadyRegistered = new Set<UIEventSource<any>>();
|
||||
private readonly _is_dirty = new UIEventSource(false)
|
||||
|
||||
private registerCallback(feature: any, layer: LayerConfig) {
|
||||
const src = this.state.allElements.addOrGetElement(feature)
|
||||
if (this._alreadyRegistered.has(src)) {
|
||||
return
|
||||
}
|
||||
this._alreadyRegistered.add(src)
|
||||
if (layer.isShown !== undefined) {
|
||||
|
||||
const self = this;
|
||||
src.map(tags => layer.isShown?.GetRenderValue(tags, "yes").txt).addCallbackAndRunD(isShown => {
|
||||
self._is_dirty.setData(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public update() {
|
||||
const self = this;
|
||||
const layer = this.upstream.layer;
|
||||
|
@ -116,4 +100,19 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
this._is_dirty.setData(false)
|
||||
}
|
||||
|
||||
private registerCallback(feature: any, layer: LayerConfig) {
|
||||
const src = this.state.allElements.addOrGetElement(feature)
|
||||
if (this._alreadyRegistered.has(src)) {
|
||||
return
|
||||
}
|
||||
this._alreadyRegistered.add(src)
|
||||
if (layer.isShown !== undefined) {
|
||||
|
||||
const self = this;
|
||||
src.map(tags => layer.isShown?.GetRenderValue(tags, "yes").txt).addCallbackAndRunD(isShown => {
|
||||
self._is_dirty.setData(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,12 +15,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
|||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
public readonly name;
|
||||
public readonly isOsmCache: boolean
|
||||
private readonly seenids: Set<string> = new Set<string>()
|
||||
public readonly layer: FilteredLayer;
|
||||
|
||||
public readonly tileIndex
|
||||
public readonly bbox;
|
||||
|
||||
private readonly seenids: Set<string> = new Set<string>()
|
||||
/**
|
||||
* Only used if the actual source is a tiled geojson.
|
||||
* A big feature might be contained in multiple tiles.
|
||||
|
@ -32,7 +30,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
|||
public constructor(flayer: FilteredLayer,
|
||||
zxy?: [number, number, number],
|
||||
options?: {
|
||||
featureIdBlacklist?: UIEventSource<Set<string>>
|
||||
featureIdBlacklist?: UIEventSource<Set<string>>
|
||||
}) {
|
||||
|
||||
if (flayer.layerDef.source.geojsonZoomLevel !== undefined && zxy === undefined) {
|
||||
|
@ -45,18 +43,18 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
|||
if (zxy !== undefined) {
|
||||
const [z, x, y] = zxy;
|
||||
let tile_bbox = BBox.fromTile(z, x, y)
|
||||
let bounds : { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox
|
||||
if(this.layer.layerDef.source.mercatorCrs){
|
||||
let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox
|
||||
if (this.layer.layerDef.source.mercatorCrs) {
|
||||
bounds = tile_bbox.toMercator()
|
||||
}
|
||||
url = url
|
||||
.replace('{z}', "" + z)
|
||||
.replace('{x}', "" + x)
|
||||
.replace('{y}', "" + y)
|
||||
.replace('{y_min}',""+bounds.minLat)
|
||||
.replace('{y_max}',""+bounds.maxLat)
|
||||
.replace('{x_min}',""+bounds.minLon)
|
||||
.replace('{x_max}',""+bounds.maxLon)
|
||||
.replace('{y_min}', "" + bounds.minLat)
|
||||
.replace('{y_max}', "" + bounds.maxLat)
|
||||
.replace('{x_min}', "" + bounds.minLon)
|
||||
.replace('{x_max}', "" + bounds.maxLon)
|
||||
|
||||
this.tileIndex = Tiles.tile_index(z, x, y)
|
||||
this.bbox = BBox.fromTile(z, x, y)
|
||||
|
@ -78,11 +76,11 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
|||
const self = this;
|
||||
Utils.downloadJson(url)
|
||||
.then(json => {
|
||||
if(json.features === undefined || json.features === null){
|
||||
if (json.features === undefined || json.features === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(self.layer.layerDef.source.mercatorCrs){
|
||||
|
||||
if (self.layer.layerDef.source.mercatorCrs) {
|
||||
json = GeoOperations.GeoJsonToWGS84(json)
|
||||
}
|
||||
|
||||
|
@ -109,8 +107,8 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
|||
continue;
|
||||
}
|
||||
self.seenids.add(props.id)
|
||||
|
||||
if(self.featureIdBlacklist?.data?.has(props.id)){
|
||||
|
||||
if (self.featureIdBlacklist?.data?.has(props.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -122,7 +120,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
|||
newFeatures.push({feature: feature, freshness: freshness})
|
||||
}
|
||||
|
||||
if ( newFeatures.length == 0) {
|
||||
if (newFeatures.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import State from "../../../State";
|
|||
|
||||
export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
||||
// This class name truly puts the 'Java' into 'Javascript'
|
||||
|
||||
|
||||
/**
|
||||
* A feature source containing exclusively new elements
|
||||
*/
|
||||
|
@ -53,10 +53,10 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
|||
for (const kv of change.tags) {
|
||||
tags[kv.k] = kv.v
|
||||
}
|
||||
tags["id"] = change.type+"/"+change.id
|
||||
|
||||
tags["id"] = change.type + "/" + change.id
|
||||
|
||||
tags["_backend"] = State.state.osmConnection._oauth_config.url
|
||||
|
||||
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
|
@ -85,7 +85,7 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
self.features.ping()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,19 +6,19 @@ import FeatureSource, {Tiled} from "../FeatureSource";
|
|||
import {UIEventSource} from "../../UIEventSource";
|
||||
import {BBox} from "../../BBox";
|
||||
|
||||
export default class RememberingSource implements FeatureSource , Tiled{
|
||||
export default class RememberingSource implements FeatureSource, Tiled {
|
||||
|
||||
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>;
|
||||
public readonly name;
|
||||
public readonly tileIndex : number
|
||||
public readonly bbox : BBox
|
||||
|
||||
public readonly tileIndex: number
|
||||
public readonly bbox: BBox
|
||||
|
||||
constructor(source: FeatureSource & Tiled) {
|
||||
const self = this;
|
||||
this.name = "RememberingSource of " + source.name;
|
||||
this.tileIndex= source.tileIndex
|
||||
this.tileIndex = source.tileIndex
|
||||
this.bbox = source.bbox;
|
||||
|
||||
|
||||
const empty = [];
|
||||
this.features = source.features.map(features => {
|
||||
const oldFeatures = self.features?.data ?? empty;
|
||||
|
|
|
@ -32,7 +32,7 @@ export default class RenderingMultiPlexerFeatureSource {
|
|||
const withIndex: (any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[] = [];
|
||||
|
||||
|
||||
function addAsPoint(feat, rendering, coordinate) {
|
||||
function addAsPoint(feat, rendering, coordinate) {
|
||||
const patched = {
|
||||
...feat,
|
||||
pointRenderingIndex: rendering.index
|
||||
|
|
|
@ -13,35 +13,35 @@ export default class TileFreshnessCalculator {
|
|||
* @param tileId
|
||||
* @param freshness
|
||||
*/
|
||||
public addTileLoad(tileId: number, freshness: Date){
|
||||
public addTileLoad(tileId: number, freshness: Date) {
|
||||
const existingFreshness = this.freshnessFor(...Tiles.tile_from_index(tileId))
|
||||
if(existingFreshness >= freshness){
|
||||
if (existingFreshness >= freshness) {
|
||||
return;
|
||||
}
|
||||
this.freshnesses.set(tileId, freshness)
|
||||
|
||||
|
||||
// Do we have freshness for the neighbouring tiles? If so, we can mark the tile above as loaded too!
|
||||
let [z, x, y] = Tiles.tile_from_index(tileId)
|
||||
if(z === 0){
|
||||
if (z === 0) {
|
||||
return;
|
||||
}
|
||||
x = x - (x % 2) // Make the tiles always even
|
||||
y = y - (y % 2)
|
||||
|
||||
|
||||
const ul = this.freshnessFor(z, x, y)?.getTime()
|
||||
if(ul === undefined){
|
||||
if (ul === undefined) {
|
||||
return
|
||||
}
|
||||
const ur = this.freshnessFor(z, x + 1, y)?.getTime()
|
||||
if(ur === undefined){
|
||||
if (ur === undefined) {
|
||||
return
|
||||
}
|
||||
const ll = this.freshnessFor(z, x, y + 1)?.getTime()
|
||||
if(ll === undefined){
|
||||
if (ll === undefined) {
|
||||
return
|
||||
}
|
||||
const lr = this.freshnessFor(z, x + 1, y + 1)?.getTime()
|
||||
if(lr === undefined){
|
||||
if (lr === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -50,22 +50,22 @@ export default class TileFreshnessCalculator {
|
|||
date.setTime(leastFresh)
|
||||
this.addTileLoad(
|
||||
Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)),
|
||||
date
|
||||
date
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
public freshnessFor(z: number, x: number, y:number): Date {
|
||||
if(z < 0){
|
||||
|
||||
public freshnessFor(z: number, x: number, y: number): Date {
|
||||
if (z < 0) {
|
||||
return undefined
|
||||
}
|
||||
const tileId = Tiles.tile_index(z, x, y)
|
||||
if(this.freshnesses.has(tileId)) {
|
||||
if (this.freshnesses.has(tileId)) {
|
||||
return this.freshnesses.get(tileId)
|
||||
}
|
||||
// recurse up
|
||||
return this.freshnessFor(z - 1, Math.floor(x /2), Math.floor(y / 2))
|
||||
|
||||
return this.freshnessFor(z - 1, Math.floor(x / 2), Math.floor(y / 2))
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -9,9 +9,8 @@ import {Tiles} from "../../../Models/TileRange";
|
|||
* A tiled source which dynamically loads the required tiles at a fixed zoom level
|
||||
*/
|
||||
export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
|
||||
private readonly _loadedTiles = new Set<number>();
|
||||
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>;
|
||||
private readonly _loadedTiles = new Set<number>();
|
||||
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
|
@ -24,7 +23,7 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
|
|||
) {
|
||||
const self = this;
|
||||
|
||||
this.loadedTiles = new Map<number,FeatureSourceForLayer & Tiled>()
|
||||
this.loadedTiles = new Map<number, FeatureSourceForLayer & Tiled>()
|
||||
const neededTiles = state.locationControl.map(
|
||||
location => {
|
||||
if (!layer.isDisplayed.data) {
|
||||
|
@ -54,14 +53,14 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
|
|||
, [layer.isDisplayed, state.leafletMap]).stabilized(250);
|
||||
|
||||
neededTiles.addCallbackAndRunD(neededIndexes => {
|
||||
console.log("Tiled geojson source ",layer.layerDef.id," needs", neededIndexes)
|
||||
console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes)
|
||||
if (neededIndexes === undefined) {
|
||||
return;
|
||||
}
|
||||
for (const neededIndex of neededIndexes) {
|
||||
self._loadedTiles.add(neededIndex)
|
||||
const src = constructTile(Tiles.tile_from_index(neededIndex))
|
||||
if(src !== undefined){
|
||||
if (src !== undefined) {
|
||||
self.loadedTiles.set(neededIndex, src)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
throw "Layer is undefined"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public handleOsmJson(osmJson: any, tileId: number) {
|
||||
|
||||
const allObjects = OsmObject.ParseObjects(osmJson.elements)
|
||||
|
|
|
@ -13,9 +13,10 @@ import {Or} from "../../Tags/Or";
|
|||
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||
|
||||
export default class OsmFeatureSource {
|
||||
private readonly _backend: string;
|
||||
|
||||
public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly downloadedTiles = new Set<number>()
|
||||
public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = []
|
||||
private readonly _backend: string;
|
||||
private readonly filteredLayers: UIEventSource<FilteredLayer[]>;
|
||||
private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void;
|
||||
private isActive: UIEventSource<boolean>;
|
||||
|
@ -28,10 +29,7 @@ export default class OsmFeatureSource {
|
|||
},
|
||||
markTileVisited?: (tileId: number) => void
|
||||
};
|
||||
public readonly downloadedTiles = new Set<number>()
|
||||
private readonly allowedTags: TagsFilter;
|
||||
|
||||
public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = []
|
||||
|
||||
constructor(options: {
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
|
||||
|
@ -54,13 +52,13 @@ export default class OsmFeatureSource {
|
|||
if (options.isActive?.data === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
neededTiles = neededTiles.filter(tile => !self.downloadedTiles.has(tile))
|
||||
|
||||
if(neededTiles.length == 0){
|
||||
if (neededTiles.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
self.isRunning.setData(true)
|
||||
try {
|
||||
|
||||
|
@ -73,7 +71,7 @@ export default class OsmFeatureSource {
|
|||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}finally {
|
||||
} finally {
|
||||
console.log("Done")
|
||||
self.isRunning.setData(false)
|
||||
}
|
||||
|
@ -111,7 +109,7 @@ export default class OsmFeatureSource {
|
|||
|
||||
geojson.features = geojson.features.filter(feature => this.allowedTags.matchesProperties(feature.properties))
|
||||
geojson.features.forEach(f => f.properties["_backend"] = this._backend)
|
||||
|
||||
|
||||
const index = Tiles.tile_index(z, x, y);
|
||||
new PerLayerFeatureSourceSplitter(this.filteredLayers,
|
||||
this.handleTile,
|
||||
|
|
|
@ -11,17 +11,14 @@ Currently, they are:
|
|||
When the data enters from Overpass or from the OSM-API, they are first distributed per layer:
|
||||
|
||||
OVERPASS | ---PerLayerFeatureSource---> FeatureSourceForLayer[]
|
||||
OSM |
|
||||
OSM |
|
||||
|
||||
The GeoJSon files (not tiled) are then added to this list
|
||||
|
||||
A single FeatureSourcePerLayer is then further handled by splitting it into a tile hierarchy.
|
||||
|
||||
|
||||
|
||||
In order to keep thins snappy, they are distributed over a tiled database per layer.
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
`cached-featuresbookcases` is the old key used `cahced-features{themeid}` and should be cleaned up
|
|
@ -8,9 +8,8 @@ import {BBox} from "../../BBox";
|
|||
|
||||
export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
|
||||
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>();
|
||||
|
||||
public readonly layer: FilteredLayer;
|
||||
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>();
|
||||
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void;
|
||||
|
||||
constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, index: number) => void) {
|
||||
|
@ -24,7 +23,7 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer
|
|||
* @param src
|
||||
* @param index
|
||||
*/
|
||||
public registerTile(src: FeatureSource & Tiled) {
|
||||
public registerTile(src: FeatureSource & Tiled) {
|
||||
|
||||
const index = src.tileIndex
|
||||
if (this.sources.has(index)) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import {Utils} from "../../../Utils";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import TileHierarchy from "./TileHierarchy";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
|
@ -28,13 +27,13 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
public readonly containedIds: UIEventSource<Set<string>>
|
||||
|
||||
public readonly bbox: BBox;
|
||||
public readonly tileIndex: number;
|
||||
private upper_left: TiledFeatureSource
|
||||
private upper_right: TiledFeatureSource
|
||||
private lower_left: TiledFeatureSource
|
||||
private lower_right: TiledFeatureSource
|
||||
private readonly maxzoom: number;
|
||||
private readonly options: TiledFeatureSourceOptions
|
||||
public readonly tileIndex: number;
|
||||
|
||||
private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) {
|
||||
this.z = z;
|
||||
|
@ -92,25 +91,25 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
return root;
|
||||
}
|
||||
|
||||
private isSplitNeeded(featureCount: number){
|
||||
if(this.upper_left !== undefined){
|
||||
private isSplitNeeded(featureCount: number) {
|
||||
if (this.upper_left !== undefined) {
|
||||
// This tile has been split previously, so we keep on splitting
|
||||
return true;
|
||||
}
|
||||
if(this.z >= this.maxzoom){
|
||||
if (this.z >= this.maxzoom) {
|
||||
// We are not allowed to split any further
|
||||
return false
|
||||
}
|
||||
if(this.options.minZoomLevel !== undefined && this.z < this.options.minZoomLevel){
|
||||
if (this.options.minZoomLevel !== undefined && this.z < this.options.minZoomLevel) {
|
||||
// We must have at least this zoom level before we are allowed to start splitting
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// To much features - we split
|
||||
return featureCount > this.maxFeatureCount
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/***
|
||||
* Adds the list of features to this hierarchy.
|
||||
* If there are too much features, the list will be broken down and distributed over the subtiles (only retaining features that don't fit a subtile on this level)
|
||||
|
@ -121,7 +120,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
if (features === undefined || features.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!this.isSplitNeeded(features.length)) {
|
||||
this.features.setData(features)
|
||||
return;
|
||||
|
@ -155,7 +154,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
} else {
|
||||
overlapsboundary.push(feature)
|
||||
}
|
||||
}else if (this.options.minZoomLevel === undefined) {
|
||||
} else if (this.options.minZoomLevel === undefined) {
|
||||
if (bbox.isContainedIn(this.upper_left.bbox)) {
|
||||
ulf.push(feature)
|
||||
} else if (bbox.isContainedIn(this.upper_right.bbox)) {
|
||||
|
|
|
@ -13,44 +13,6 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
|
|||
private readonly handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void;
|
||||
private readonly undefinedTiles: Set<number>;
|
||||
|
||||
public static GetFreshnesses(layerId: string): Map<number, Date> {
|
||||
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layerId + "-"
|
||||
const freshnesses = new Map<number, Date>()
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (!(key.startsWith(prefix) && key.endsWith("-time"))) {
|
||||
continue
|
||||
}
|
||||
const index = Number(key.substring(prefix.length, key.length - "-time".length))
|
||||
const time = Number(localStorage.getItem(key))
|
||||
const freshness = new Date()
|
||||
freshness.setTime(time)
|
||||
freshnesses.set(index, freshness)
|
||||
}
|
||||
return freshnesses
|
||||
}
|
||||
|
||||
|
||||
static cleanCacheForLayer(layer: LayerConfig) {
|
||||
const now = new Date()
|
||||
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.id + "-"
|
||||
console.log("Cleaning tiles of ", prefix, "with max age",layer.maxAgeOfCache)
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (!(key.startsWith(prefix) && key.endsWith("-time"))) {
|
||||
continue
|
||||
}
|
||||
const index = Number(key.substring(prefix.length, key.length - "-time".length))
|
||||
const time = Number(localStorage.getItem(key))
|
||||
const timeDiff = (now.getTime() - time) / 1000
|
||||
|
||||
if(timeDiff >= layer.maxAgeOfCache){
|
||||
const k = prefix+index;
|
||||
localStorage.removeItem(k)
|
||||
localStorage.removeItem(k+"-format")
|
||||
localStorage.removeItem(k+"-time")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(layer: FilteredLayer,
|
||||
handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void,
|
||||
state: {
|
||||
|
@ -110,6 +72,43 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
|
|||
|
||||
}
|
||||
|
||||
public static GetFreshnesses(layerId: string): Map<number, Date> {
|
||||
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layerId + "-"
|
||||
const freshnesses = new Map<number, Date>()
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (!(key.startsWith(prefix) && key.endsWith("-time"))) {
|
||||
continue
|
||||
}
|
||||
const index = Number(key.substring(prefix.length, key.length - "-time".length))
|
||||
const time = Number(localStorage.getItem(key))
|
||||
const freshness = new Date()
|
||||
freshness.setTime(time)
|
||||
freshnesses.set(index, freshness)
|
||||
}
|
||||
return freshnesses
|
||||
}
|
||||
|
||||
static cleanCacheForLayer(layer: LayerConfig) {
|
||||
const now = new Date()
|
||||
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.id + "-"
|
||||
console.log("Cleaning tiles of ", prefix, "with max age", layer.maxAgeOfCache)
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (!(key.startsWith(prefix) && key.endsWith("-time"))) {
|
||||
continue
|
||||
}
|
||||
const index = Number(key.substring(prefix.length, key.length - "-time".length))
|
||||
const time = Number(localStorage.getItem(key))
|
||||
const timeDiff = (now.getTime() - time) / 1000
|
||||
|
||||
if (timeDiff >= layer.maxAgeOfCache) {
|
||||
const k = prefix + index;
|
||||
localStorage.removeItem(k)
|
||||
localStorage.removeItem(k + "-format")
|
||||
localStorage.removeItem(k + "-time")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadTile(neededIndex: number) {
|
||||
try {
|
||||
const key = SaveTileToLocalStorageActor.storageKey + "-" + this.layer.layerDef.id + "-" + neededIndex
|
||||
|
|
|
@ -3,6 +3,9 @@ import {BBox} from "./BBox";
|
|||
|
||||
export class GeoOperations {
|
||||
|
||||
private static readonly _earthRadius = 6378137;
|
||||
private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2;
|
||||
|
||||
static surfaceAreaInSqMeters(feature: any) {
|
||||
return turf.area(feature);
|
||||
}
|
||||
|
@ -40,7 +43,7 @@ export class GeoOperations {
|
|||
* If 'feature' is a Polygon, overlapping points and points within the polygon will be returned
|
||||
*
|
||||
* If 'feature' is a point, it will return every feature the point is embedded in. Overlap will be undefined
|
||||
*
|
||||
*
|
||||
*/
|
||||
static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any, overlap: number }[] {
|
||||
|
||||
|
@ -237,13 +240,13 @@ export class GeoOperations {
|
|||
* @param point Point defined as [lon, lat]
|
||||
*/
|
||||
public static nearestPoint(way, point: [number, number]) {
|
||||
if(way.geometry.type === "Polygon"){
|
||||
if (way.geometry.type === "Polygon") {
|
||||
way = {...way}
|
||||
way.geometry = {...way.geometry}
|
||||
way.geometry.type = "LineString"
|
||||
way.geometry.coordinates = way.geometry.coordinates[0]
|
||||
}
|
||||
|
||||
|
||||
return turf.nearestPointOnLine(way, point, {units: "kilometers"});
|
||||
}
|
||||
|
||||
|
@ -292,10 +295,6 @@ export class GeoOperations {
|
|||
return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n")
|
||||
}
|
||||
|
||||
|
||||
private static readonly _earthRadius = 6378137;
|
||||
private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2;
|
||||
|
||||
//Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913
|
||||
public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] {
|
||||
const lon = lonLat[0];
|
||||
|
@ -315,11 +314,36 @@ export class GeoOperations {
|
|||
y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
public static GeoJsonToWGS84(geojson){
|
||||
|
||||
public static GeoJsonToWGS84(geojson) {
|
||||
return turf.toWgs84(geojson)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to remove points which do not contribute much to the general outline.
|
||||
* Points for which the angle is ~ 180° are removed
|
||||
* @param coordinates
|
||||
* @constructor
|
||||
*/
|
||||
public static SimplifyCoordinates(coordinates: [number, number][]) {
|
||||
const newCoordinates = []
|
||||
for (let i = 1; i < coordinates.length - 1; i++) {
|
||||
const coordinate = coordinates[i];
|
||||
const prev = coordinates[i - 1]
|
||||
const next = coordinates[i + 1]
|
||||
const b0 = turf.bearing(prev, coordinate, {final: true})
|
||||
const b1 = turf.bearing(coordinate, next)
|
||||
|
||||
const diff = Math.abs(b1 - b0)
|
||||
if (diff < 2) {
|
||||
continue
|
||||
}
|
||||
newCoordinates.push(coordinate)
|
||||
}
|
||||
return newCoordinates
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the intersection between two features.
|
||||
* Returns the length if intersecting a linestring and a (multi)polygon (in meters), returns a surface area (in m²) if intersecting two (multi)polygons
|
||||
|
@ -412,31 +436,6 @@ export class GeoOperations {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to remove points which do not contribute much to the general outline.
|
||||
* Points for which the angle is ~ 180° are removed
|
||||
* @param coordinates
|
||||
* @constructor
|
||||
*/
|
||||
public static SimplifyCoordinates(coordinates: [number, number][]){
|
||||
const newCoordinates = []
|
||||
for (let i = 1; i < coordinates.length - 1; i++){
|
||||
const coordinate = coordinates[i];
|
||||
const prev = coordinates[i - 1]
|
||||
const next = coordinates[i + 1]
|
||||
const b0 = turf.bearing(prev, coordinate, {final: true})
|
||||
const b1 = turf.bearing(coordinate, next)
|
||||
|
||||
const diff = Math.abs(b1 - b0)
|
||||
if(diff < 2){
|
||||
continue
|
||||
}
|
||||
newCoordinates.push(coordinate)
|
||||
}
|
||||
return newCoordinates
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -19,9 +19,9 @@ export default class AllImageProviders {
|
|||
new GenericImageProvider(
|
||||
[].concat(...Imgur.defaultValuePrefix, ...WikimediaImageProvider.commonsPrefixes, ...Mapillary.valuePrefixes)
|
||||
)
|
||||
|
||||
|
||||
]
|
||||
|
||||
|
||||
public static defaultKeys = [].concat(AllImageProviders.ImageAttributionSource.map(provider => provider.defaultKeyPrefixes))
|
||||
|
||||
|
||||
|
@ -32,7 +32,7 @@ export default class AllImageProviders {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const cacheKey = tags.data.id+tagKey
|
||||
const cacheKey = tags.data.id + tagKey
|
||||
const cached = this._cache.get(cacheKey)
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
|
@ -43,22 +43,22 @@ export default class AllImageProviders {
|
|||
this._cache.set(cacheKey, source)
|
||||
const allSources = []
|
||||
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
|
||||
|
||||
|
||||
let prefixes = imageProvider.defaultKeyPrefixes
|
||||
if(tagKey !== undefined){
|
||||
if (tagKey !== undefined) {
|
||||
prefixes = tagKey
|
||||
}
|
||||
|
||||
|
||||
const singleSource = imageProvider.GetRelevantUrls(tags, {
|
||||
prefixes: prefixes
|
||||
})
|
||||
allSources.push(singleSource)
|
||||
singleSource.addCallbackAndRunD(_ => {
|
||||
const all : ProvidedImage[] = [].concat(...allSources.map(source => source.data))
|
||||
const all: ProvidedImage[] = [].concat(...allSources.map(source => source.data))
|
||||
const uniq = []
|
||||
const seen = new Set<string>()
|
||||
for (const img of all) {
|
||||
if(seen.has(img.url)){
|
||||
if (seen.has(img.url)) {
|
||||
continue
|
||||
}
|
||||
seen.add(img.url)
|
||||
|
|
|
@ -10,24 +10,19 @@ export default class GenericImageProvider extends ImageProvider {
|
|||
this._valuePrefixBlacklist = valuePrefixBlacklist;
|
||||
}
|
||||
|
||||
|
||||
protected DownloadAttribution(url: string) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
|
||||
if (this._valuePrefixBlacklist.some(prefix => value.startsWith(prefix))) {
|
||||
return []
|
||||
}
|
||||
|
||||
try{
|
||||
|
||||
try {
|
||||
new URL(value)
|
||||
}catch (_){
|
||||
} catch (_) {
|
||||
// Not a valid URL
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
return [Promise.resolve({
|
||||
key: key,
|
||||
url: value,
|
||||
|
@ -39,5 +34,9 @@ export default class GenericImageProvider extends ImageProvider {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
protected DownloadAttribution(url: string) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@ export default abstract class ImageProvider {
|
|||
public abstract readonly defaultKeyPrefixes: string[]
|
||||
|
||||
private _cache = new Map<string, UIEventSource<LicenseInfo>>()
|
||||
|
||||
|
||||
GetAttributionFor(url: string): UIEventSource<LicenseInfo> {
|
||||
const cached = this._cache.get(url);
|
||||
if (cached !== undefined) {
|
||||
|
@ -27,8 +27,6 @@ export default abstract class ImageProvider {
|
|||
|
||||
public abstract SourceIcon(backlinkSource?: string): BaseUIElement;
|
||||
|
||||
protected abstract DownloadAttribution(url: string): Promise<LicenseInfo>;
|
||||
|
||||
/**
|
||||
* Given a properies object, maps it onto _all_ the available pictures for this imageProvider
|
||||
*/
|
||||
|
@ -77,4 +75,6 @@ export default abstract class ImageProvider {
|
|||
|
||||
public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>;
|
||||
|
||||
protected abstract DownloadAttribution(url: string): Promise<LicenseInfo>;
|
||||
|
||||
}
|
|
@ -7,10 +7,9 @@ import {LicenseInfo} from "./LicenseInfo";
|
|||
|
||||
export class Imgur extends ImageProvider {
|
||||
|
||||
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
|
||||
public readonly defaultKeyPrefixes: string[] = ["image"];
|
||||
|
||||
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
|
||||
public static readonly singleton = new Imgur();
|
||||
public readonly defaultKeyPrefixes: string[] = ["image"];
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
|
@ -89,6 +88,17 @@ export class Imgur extends ImageProvider {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
if (Imgur.defaultValuePrefix.some(prefix => value.startsWith(prefix))) {
|
||||
return [Promise.resolve({
|
||||
url: value,
|
||||
key: key,
|
||||
provider: this
|
||||
})]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
protected DownloadAttribution: (url: string) => Promise<LicenseInfo> = async (url: string) => {
|
||||
const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0];
|
||||
|
||||
|
@ -112,16 +122,5 @@ export class Imgur extends ImageProvider {
|
|||
return licenseInfo
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
if (Imgur.defaultValuePrefix.some(prefix => value.startsWith(prefix))) {
|
||||
return [Promise.resolve({
|
||||
url: value,
|
||||
key: key,
|
||||
provider: this
|
||||
})]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -6,9 +6,8 @@ export default class ImgurUploader {
|
|||
public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([]);
|
||||
private readonly _handleSuccessUrl: (string) => void;
|
||||
|
||||
public maxFileSizeInMegabytes = 10;
|
||||
private readonly _handleSuccessUrl: (string) => void;
|
||||
|
||||
constructor(handleSuccessUrl: (string) => void) {
|
||||
this._handleSuccessUrl = handleSuccessUrl;
|
||||
|
|
|
@ -7,17 +7,16 @@ import Constants from "../../Models/Constants";
|
|||
|
||||
export class Mapillary extends ImageProvider {
|
||||
|
||||
defaultKeyPrefixes = ["mapillary","image"]
|
||||
|
||||
public static readonly singleton = new Mapillary();
|
||||
private static readonly valuePrefix = "https://a.mapillary.com"
|
||||
public static readonly valuePrefixes = [Mapillary.valuePrefix, "http://mapillary.com","https://mapillary.com","http://www.mapillary.com","https://www.mapillary.com"]
|
||||
public static readonly valuePrefixes = [Mapillary.valuePrefix, "http://mapillary.com", "https://mapillary.com", "http://www.mapillary.com", "https://www.mapillary.com"]
|
||||
defaultKeyPrefixes = ["mapillary", "image"]
|
||||
|
||||
private static ExtractKeyFromURL(value: string, failIfNoMath = false): {
|
||||
key: string,
|
||||
isApiv4?: boolean
|
||||
} {
|
||||
|
||||
|
||||
if (value.startsWith(Mapillary.valuePrefix)) {
|
||||
const key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1)
|
||||
return {key: key, isApiv4: !isNaN(Number(key))};
|
||||
|
@ -43,11 +42,11 @@ export class Mapillary extends ImageProvider {
|
|||
if (matchApi !== null) {
|
||||
return {key: matchApi[1]};
|
||||
}
|
||||
|
||||
if(failIfNoMath){
|
||||
|
||||
if (failIfNoMath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
return {key: value, isApiv4: !isNaN(Number(value))};
|
||||
}
|
||||
|
||||
|
@ -59,33 +58,6 @@ export class Mapillary extends ImageProvider {
|
|||
return [this.PrepareUrlAsync(key, value)]
|
||||
}
|
||||
|
||||
private async PrepareUrlAsync(key: string, value: string): Promise<ProvidedImage> {
|
||||
const failIfNoMatch = key.indexOf("mapillary") < 0
|
||||
const keyV = Mapillary.ExtractKeyFromURL(value, failIfNoMatch)
|
||||
if(keyV === undefined){
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!keyV.isApiv4) {
|
||||
const url = `https://images.mapillary.com/${keyV.key}/thumb-640.jpg?client_id=${Constants.mapillary_client_token_v3}`
|
||||
return {
|
||||
url: url,
|
||||
provider: this,
|
||||
key: key
|
||||
}
|
||||
} else {
|
||||
const mapillaryId = keyV.key;
|
||||
const metadataUrl = 'https://graph.mapillary.com/' + mapillaryId + '?fields=thumb_1024_url&&access_token=' + Constants.mapillary_client_token_v4;
|
||||
const response = await Utils.downloadJson(metadataUrl)
|
||||
const url = <string> response["thumb_1024_url"];
|
||||
return {
|
||||
url: url,
|
||||
provider: this,
|
||||
key: key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async DownloadAttribution(url: string): Promise<LicenseInfo> {
|
||||
|
||||
const keyV = Mapillary.ExtractKeyFromURL(url)
|
||||
|
@ -110,4 +82,31 @@ export class Mapillary extends ImageProvider {
|
|||
|
||||
return license
|
||||
}
|
||||
|
||||
private async PrepareUrlAsync(key: string, value: string): Promise<ProvidedImage> {
|
||||
const failIfNoMatch = key.indexOf("mapillary") < 0
|
||||
const keyV = Mapillary.ExtractKeyFromURL(value, failIfNoMatch)
|
||||
if (keyV === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!keyV.isApiv4) {
|
||||
const url = `https://images.mapillary.com/${keyV.key}/thumb-640.jpg?client_id=${Constants.mapillary_client_token_v3}`
|
||||
return {
|
||||
url: url,
|
||||
provider: this,
|
||||
key: key
|
||||
}
|
||||
} else {
|
||||
const mapillaryId = keyV.key;
|
||||
const metadataUrl = 'https://graph.mapillary.com/' + mapillaryId + '?fields=thumb_1024_url&&access_token=' + Constants.mapillary_client_token_v4;
|
||||
const response = await Utils.downloadJson(metadataUrl)
|
||||
const url = <string>response["thumb_1024_url"];
|
||||
return {
|
||||
url: url,
|
||||
provider: this,
|
||||
key: key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import {Utils} from "../../Utils";
|
||||
import ImageProvider, {ProvidedImage} from "./ImageProvider";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import Svg from "../../Svg";
|
||||
|
@ -7,10 +6,6 @@ import Wikidata from "../Web/Wikidata";
|
|||
|
||||
export class WikidataImageProvider extends ImageProvider {
|
||||
|
||||
public SourceIcon(backlinkSource?: string): BaseUIElement {
|
||||
throw Svg.wikidata_svg();
|
||||
}
|
||||
|
||||
public static readonly singleton = new WikidataImageProvider()
|
||||
public readonly defaultKeyPrefixes = ["wikidata"]
|
||||
|
||||
|
@ -18,17 +13,17 @@ export class WikidataImageProvider extends ImageProvider {
|
|||
super()
|
||||
}
|
||||
|
||||
protected DownloadAttribution(url: string): Promise<any> {
|
||||
throw new Error("Method not implemented; shouldn't be needed!");
|
||||
public SourceIcon(backlinkSource?: string): BaseUIElement {
|
||||
throw Svg.wikidata_svg();
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
const entity = await Wikidata.LoadWikidataEntryAsync(value)
|
||||
if(entity === undefined){
|
||||
if (entity === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const allImages : Promise<ProvidedImage>[] = []
|
||||
|
||||
const allImages: Promise<ProvidedImage>[] = []
|
||||
// P18 is the claim 'depicted in this image'
|
||||
for (const img of Array.from(entity.claims.get("P18") ?? [])) {
|
||||
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, img)
|
||||
|
@ -36,19 +31,23 @@ export class WikidataImageProvider extends ImageProvider {
|
|||
}
|
||||
// P373 is 'commons category'
|
||||
for (let cat of Array.from(entity.claims.get("P373") ?? [])) {
|
||||
if(!cat.startsWith("Category:")){
|
||||
cat = "Category:"+cat
|
||||
if (!cat.startsWith("Category:")) {
|
||||
cat = "Category:" + cat
|
||||
}
|
||||
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, cat)
|
||||
allImages.push(...promises)
|
||||
}
|
||||
|
||||
|
||||
const commons = entity.commons
|
||||
if (commons !== undefined && (commons.startsWith("Category:") || commons.startsWith("File:"))) {
|
||||
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined , commons)
|
||||
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, commons)
|
||||
allImages.push(...promises)
|
||||
}
|
||||
return allImages
|
||||
}
|
||||
|
||||
protected DownloadAttribution(url: string): Promise<any> {
|
||||
throw new Error("Method not implemented; shouldn't be needed!");
|
||||
}
|
||||
|
||||
}
|
|
@ -12,10 +12,10 @@ import Wikimedia from "../Web/Wikimedia";
|
|||
export class WikimediaImageProvider extends ImageProvider {
|
||||
|
||||
|
||||
private readonly commons_key = "wikimedia_commons"
|
||||
public readonly defaultKeyPrefixes = [this.commons_key,"image"]
|
||||
public static readonly singleton = new WikimediaImageProvider();
|
||||
public static readonly commonsPrefixes = ["https://commons.wikimedia.org/wiki/", "https://upload.wikimedia.org", "File:"]
|
||||
private readonly commons_key = "wikimedia_commons"
|
||||
public readonly defaultKeyPrefixes = [this.commons_key, "image"]
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
|
@ -30,6 +30,40 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
|
||||
}
|
||||
|
||||
private static PrepareUrl(value: string): string {
|
||||
|
||||
if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||
return value;
|
||||
}
|
||||
return (`https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(value)}?width=500&height=400`)
|
||||
}
|
||||
|
||||
private static startsWithCommonsPrefix(value: string): boolean {
|
||||
return WikimediaImageProvider.commonsPrefixes.some(prefix => value.startsWith(prefix))
|
||||
}
|
||||
|
||||
private static removeCommonsPrefix(value: string): string {
|
||||
if (value.startsWith("https://upload.wikimedia.org/")) {
|
||||
value = value.substring(value.lastIndexOf("/") + 1)
|
||||
value = decodeURIComponent(value)
|
||||
if (!value.startsWith("File:")) {
|
||||
value = "File:" + value
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
for (const prefix of WikimediaImageProvider.commonsPrefixes) {
|
||||
if (value.startsWith(prefix)) {
|
||||
let part = value.substr(prefix.length)
|
||||
if (prefix.startsWith("http")) {
|
||||
part = decodeURIComponent(part)
|
||||
}
|
||||
return part
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
SourceIcon(backlink: string): BaseUIElement {
|
||||
const img = Svg.wikimedia_commons_white_svg()
|
||||
.SetStyle("width:2em;height: 2em");
|
||||
|
@ -44,12 +78,38 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
|
||||
}
|
||||
|
||||
private static PrepareUrl(value: string): string {
|
||||
public PrepUrl(value: string): ProvidedImage {
|
||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
|
||||
if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||
return value;
|
||||
if (value.startsWith("File:")) {
|
||||
return this.UrlForImage(value)
|
||||
}
|
||||
return (`https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(value)}?width=500&height=400`)
|
||||
|
||||
// We do a last effort and assume this is a file
|
||||
return this.UrlForImage("File:" + value)
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||
if (key !== undefined && key !== this.commons_key && !hasCommonsPrefix) {
|
||||
return []
|
||||
}
|
||||
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
if (value.startsWith("Category:")) {
|
||||
const urls = await Wikimedia.GetCategoryContents(value)
|
||||
return urls.filter(url => url.startsWith("File:")).map(image => Promise.resolve(this.UrlForImage(image)))
|
||||
}
|
||||
if (value.startsWith("File:")) {
|
||||
return [Promise.resolve(this.UrlForImage(value))]
|
||||
}
|
||||
if (value.startsWith("http")) {
|
||||
// PRobably an error
|
||||
return []
|
||||
}
|
||||
// We do a last effort and assume this is a file
|
||||
return [Promise.resolve(this.UrlForImage("File:" + value))]
|
||||
}
|
||||
|
||||
protected async DownloadAttribution(filename: string): Promise<LicenseInfo> {
|
||||
|
@ -66,24 +126,24 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
const data = await Utils.downloadJson(url)
|
||||
const licenseInfo = new LicenseInfo();
|
||||
const pageInfo = data.query.pages[-1]
|
||||
if(pageInfo === undefined){
|
||||
if (pageInfo === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata;
|
||||
if (license === undefined) {
|
||||
console.warn("The file", filename ,"has no usable metedata or license attached... Please fix the license info file yourself!")
|
||||
console.warn("The file", filename, "has no usable metedata or license attached... Please fix the license info file yourself!")
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let title = pageInfo.title
|
||||
if(title.startsWith("File:")){
|
||||
title= title.substr("File:".length)
|
||||
if (title.startsWith("File:")) {
|
||||
title = title.substr("File:".length)
|
||||
}
|
||||
if(title.endsWith(".jpg") || title.endsWith(".png")){
|
||||
if (title.endsWith(".jpg") || title.endsWith(".png")) {
|
||||
title = title.substring(0, title.length - 4)
|
||||
}
|
||||
|
||||
|
||||
licenseInfo.title = title
|
||||
licenseInfo.artist = license.Artist?.value;
|
||||
licenseInfo.license = license.License?.value;
|
||||
|
@ -103,66 +163,6 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
}
|
||||
return {url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this}
|
||||
}
|
||||
|
||||
private static startsWithCommonsPrefix(value: string): boolean{
|
||||
return WikimediaImageProvider.commonsPrefixes.some(prefix => value.startsWith(prefix))
|
||||
}
|
||||
|
||||
private static removeCommonsPrefix(value: string): string{
|
||||
if(value.startsWith("https://upload.wikimedia.org/")){
|
||||
value = value.substring(value.lastIndexOf("/") + 1)
|
||||
value = decodeURIComponent(value)
|
||||
if(!value.startsWith("File:")){
|
||||
value = "File:"+value
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
for (const prefix of WikimediaImageProvider.commonsPrefixes) {
|
||||
if(value.startsWith(prefix)){
|
||||
let part = value.substr(prefix.length)
|
||||
if(prefix.startsWith("http")){
|
||||
part = decodeURIComponent(part)
|
||||
}
|
||||
return part
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public PrepUrl(value: string): ProvidedImage {
|
||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
|
||||
if (value.startsWith("File:")) {
|
||||
return this.UrlForImage(value)
|
||||
}
|
||||
|
||||
// We do a last effort and assume this is a file
|
||||
return this.UrlForImage("File:" + value)
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||
if(key !== undefined && key !== this.commons_key && !hasCommonsPrefix){
|
||||
return []
|
||||
}
|
||||
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
if (value.startsWith("Category:")) {
|
||||
const urls = await Wikimedia.GetCategoryContents(value)
|
||||
return urls.filter(url => url.startsWith("File:")).map(image => Promise.resolve(this.UrlForImage(image)))
|
||||
}
|
||||
if (value.startsWith("File:")) {
|
||||
return [Promise.resolve(this.UrlForImage(value))]
|
||||
}
|
||||
if (value.startsWith("http")) {
|
||||
// PRobably an error
|
||||
return []
|
||||
}
|
||||
// We do a last effort and assume this is a file
|
||||
return [Promise.resolve(this.UrlForImage("File:" + value))]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ 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
|
||||
*
|
||||
*
|
||||
* Returns true if at least one feature has changed properties
|
||||
*/
|
||||
public static addMetatags(features: { feature: any; freshness: Date }[],
|
||||
|
@ -63,15 +63,15 @@ export default class MetaTagging {
|
|||
// All keys are already defined, we probably already ran this one
|
||||
continue
|
||||
}
|
||||
|
||||
if(metatag.isLazy){
|
||||
|
||||
if (metatag.isLazy) {
|
||||
somethingChanged = true;
|
||||
|
||||
|
||||
metatag.applyMetaTagsOnFeature(feature, freshness, layer)
|
||||
|
||||
}else{
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
|
||||
const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness, layer)
|
||||
/* Note that the expression:
|
||||
* `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)`
|
||||
|
@ -146,12 +146,13 @@ export default class MetaTagging {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}} )
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
functions.push(f)
|
||||
}
|
||||
return functions;
|
||||
|
|
|
@ -16,13 +16,13 @@ export interface ChangeDescription {
|
|||
/**
|
||||
* The type of the change
|
||||
*/
|
||||
changeType: "answer" | "create" | "split" | "delete" | "move" | "import" | string | null
|
||||
changeType: "answer" | "create" | "split" | "delete" | "move" | "import" | string | null
|
||||
/**
|
||||
* THe motivation for the change, e.g. 'deleted because does not exist anymore'
|
||||
*/
|
||||
specialMotivation?: string
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Identifier of the object
|
||||
*/
|
||||
|
@ -32,11 +32,11 @@ export interface ChangeDescription {
|
|||
* Negative for new objects
|
||||
*/
|
||||
id: number,
|
||||
|
||||
|
||||
/**
|
||||
* All changes to tags
|
||||
* v = "" or v = undefined to erase this tag
|
||||
*
|
||||
*
|
||||
* Note that this list will only contain the _changes_ to the tags, not the full set of tags
|
||||
*/
|
||||
tags?: { k: string, v: string }[],
|
||||
|
@ -65,9 +65,9 @@ export interface ChangeDescription {
|
|||
doDelete?: boolean
|
||||
}
|
||||
|
||||
export class ChangeDescriptionTools{
|
||||
|
||||
public static getGeojsonGeometry(change: ChangeDescription): any{
|
||||
export class ChangeDescriptionTools {
|
||||
|
||||
public static getGeojsonGeometry(change: ChangeDescription): any {
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
|
|
|
@ -7,7 +7,7 @@ export default class ChangeTagAction extends OsmChangeAction {
|
|||
private readonly _elementId: string;
|
||||
private readonly _tagsFilter: TagsFilter;
|
||||
private readonly _currentTags: any;
|
||||
private readonly _meta: {theme: string, changeType: string};
|
||||
private readonly _meta: { theme: string, changeType: string };
|
||||
|
||||
constructor(elementId: string, tagsFilter: TagsFilter, currentTags: any, meta: {
|
||||
theme: string,
|
||||
|
@ -31,11 +31,11 @@ export default class ChangeTagAction extends OsmChangeAction {
|
|||
return undefined;
|
||||
}
|
||||
if (value === undefined || value === null) {
|
||||
console.error("Invalid value for ", key,":", value);
|
||||
console.error("Invalid value for ", key, ":", value);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(typeof value !== "string"){
|
||||
|
||||
if (typeof value !== "string") {
|
||||
console.error("Invalid value for ", key, "as it is not a string:", value)
|
||||
return undefined;
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ export default class ChangeTagAction extends OsmChangeAction {
|
|||
const type = typeId[0]
|
||||
const id = Number(typeId [1])
|
||||
return [{
|
||||
type: <"node"|"way"|"relation"> type,
|
||||
type: <"node" | "way" | "relation">type,
|
||||
id: id,
|
||||
tags: changedTags,
|
||||
meta: this._meta
|
||||
|
|
|
@ -156,7 +156,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
|||
|
||||
private setElementId(id: number) {
|
||||
this.newElementIdNumber = id;
|
||||
this.newElementId = "node/"+id
|
||||
this.newElementId = "node/" + id
|
||||
if (!this._reusePreviouslyCreatedPoint) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -163,17 +163,18 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
|
|||
})
|
||||
|
||||
allChanges.push(...(await newNodeAction.CreateChangeDescriptions(changes)))
|
||||
|
||||
|
||||
nodeIdsToUse.push({
|
||||
lat, lon,
|
||||
nodeId : newNodeAction.newElementIdNumber})
|
||||
nodeId: newNodeAction.newElementIdNumber
|
||||
})
|
||||
continue
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
const closestPoint = info.closebyNodes[0]
|
||||
const id = Number(closestPoint.node.properties.id.split("/")[1])
|
||||
if(closestPoint.config.mode === "move_osm_point"){
|
||||
if (closestPoint.config.mode === "move_osm_point") {
|
||||
allChanges.push({
|
||||
type: "node",
|
||||
id,
|
||||
|
@ -193,9 +194,9 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
|
|||
const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, {
|
||||
theme
|
||||
})
|
||||
|
||||
|
||||
allChanges.push(...(await newWay.Perform(changes)))
|
||||
|
||||
|
||||
return allChanges
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ export interface RelationSplitInput {
|
|||
originalNodes: number[],
|
||||
allWaysNodesInOrder: number[][]
|
||||
}
|
||||
|
||||
abstract class AbstractRelationSplitHandler extends OsmChangeAction {
|
||||
protected readonly _input: RelationSplitInput;
|
||||
protected readonly _theme: string;
|
||||
|
@ -57,11 +58,11 @@ export default class RelationSplitHandler extends AbstractRelationSplitHandler {
|
|||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
if(this._input.relation.tags["type"] === "restriction"){
|
||||
if (this._input.relation.tags["type"] === "restriction") {
|
||||
// This is a turn restriction
|
||||
return new TurnRestrictionRSH(this._input, this._theme).CreateChangeDescriptions(changes)
|
||||
}
|
||||
return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes)
|
||||
return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes)
|
||||
}
|
||||
|
||||
|
||||
|
@ -72,68 +73,71 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler {
|
|||
constructor(input: RelationSplitInput, theme: string) {
|
||||
super(input, theme);
|
||||
}
|
||||
|
||||
|
||||
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
|
||||
const relation = this._input.relation
|
||||
const members = relation.members
|
||||
|
||||
|
||||
const selfMembers = members.filter(m => m.type === "way" && m.ref === this._input.originalWayId)
|
||||
|
||||
if(selfMembers.length > 1){
|
||||
|
||||
if (selfMembers.length > 1) {
|
||||
console.warn("Detected a turn restriction where this way has multiple occurances. This is an error")
|
||||
}
|
||||
const selfMember = selfMembers[0]
|
||||
|
||||
if(selfMember.role === "via"){
|
||||
|
||||
if (selfMember.role === "via") {
|
||||
// A via way can be replaced in place
|
||||
return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// We have to keep only the way with a common point with the rest of the relation
|
||||
// Let's figure out which member is neighbouring our way
|
||||
|
||||
let commonStartPoint : number = await this.targetNodeAt(members.indexOf(selfMember), true)
|
||||
let commonEndPoint : number = await this.targetNodeAt(members.indexOf(selfMember), false)
|
||||
|
||||
|
||||
let commonStartPoint: number = await this.targetNodeAt(members.indexOf(selfMember), true)
|
||||
let commonEndPoint: number = await this.targetNodeAt(members.indexOf(selfMember), false)
|
||||
|
||||
// In normal circumstances, only one of those should be defined
|
||||
let commonPoint = commonStartPoint ?? commonEndPoint
|
||||
|
||||
|
||||
// Let's select the way to keep
|
||||
const idToKeep : {id: number} = this._input.allWaysNodesInOrder.map((nodes, i) => ({nodes: nodes, id: this._input.allWayIdsInOrder[i]}))
|
||||
const idToKeep: { id: number } = this._input.allWaysNodesInOrder.map((nodes, i) => ({
|
||||
nodes: nodes,
|
||||
id: this._input.allWayIdsInOrder[i]
|
||||
}))
|
||||
.filter(nodesId => {
|
||||
const nds = nodesId.nodes
|
||||
return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint
|
||||
return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint
|
||||
})[0]
|
||||
|
||||
if(idToKeep === undefined){
|
||||
|
||||
if (idToKeep === undefined) {
|
||||
console.error("No common point found, this was a broken turn restriction!", relation.id)
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
const originalWayId = this._input.originalWayId
|
||||
if(idToKeep.id === originalWayId){
|
||||
if (idToKeep.id === originalWayId) {
|
||||
console.log("Turn_restriction fixer: the original ID can be kept, nothing to do")
|
||||
return []
|
||||
}
|
||||
|
||||
const newMembers : {
|
||||
ref:number,
|
||||
type:"way" | "node" | "relation",
|
||||
role:string
|
||||
|
||||
const newMembers: {
|
||||
ref: number,
|
||||
type: "way" | "node" | "relation",
|
||||
role: string
|
||||
} [] = relation.members.map(m => {
|
||||
if(m.type === "way" && m.ref === originalWayId){
|
||||
if (m.type === "way" && m.ref === originalWayId) {
|
||||
return {
|
||||
ref: idToKeep.id,
|
||||
type:"way",
|
||||
type: "way",
|
||||
role: m.role
|
||||
}
|
||||
}
|
||||
return m
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
return [
|
||||
{
|
||||
type: "relation",
|
||||
|
@ -148,7 +152,7 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler {
|
|||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -184,8 +188,8 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler {
|
|||
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
|
||||
const firstNodeMatches = nodeIdBefore === undefined || nodeIdBefore === firstNode
|
||||
const lastNodeMatches = nodeIdAfter === undefined || nodeIdAfter === lastNode
|
||||
|
||||
if (firstNodeMatches && lastNodeMatches) {
|
||||
// We have a classic situation, forward situation
|
||||
|
@ -200,10 +204,10 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler {
|
|||
}
|
||||
|
||||
const firstNodeMatchesRev = nodeIdBefore === undefined || nodeIdBefore === lastNode
|
||||
const lastNodeMatchesRev =nodeIdAfter === undefined || nodeIdAfter === firstNode
|
||||
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--){
|
||||
for (let i1 = this._input.allWayIdsInOrder.length - 1; i1 >= 0; i1--) {
|
||||
// Iterate BACKWARDS
|
||||
const wId = this._input.allWayIdsInOrder[i1];
|
||||
newMembers.push({
|
||||
|
@ -214,7 +218,7 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler {
|
|||
}
|
||||
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) {
|
||||
|
@ -231,7 +235,7 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler {
|
|||
id: relation.id,
|
||||
type: "relation",
|
||||
changes: {members: newMembers},
|
||||
meta:{
|
||||
meta: {
|
||||
changeType: "relation-fix",
|
||||
theme: this._theme
|
||||
}
|
||||
|
|
|
@ -77,13 +77,13 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
|
||||
public async getPreview(): Promise<FeatureSource> {
|
||||
const {closestIds, allNodesById} = await this.GetClosestIds();
|
||||
console.log("Generating preview, identicals are ", )
|
||||
console.log("Generating preview, identicals are ",)
|
||||
const preview = closestIds.map((newId, i) => {
|
||||
if(this.identicalTo[i] !== undefined){
|
||||
if (this.identicalTo[i] !== undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if (newId === undefined) {
|
||||
return {
|
||||
type: "Feature",
|
||||
|
@ -123,7 +123,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
const {closestIds, osmWay} = await this.GetClosestIds()
|
||||
|
||||
for (let i = 0; i < closestIds.length; i++) {
|
||||
if(this.identicalTo[i] !== undefined){
|
||||
if (this.identicalTo[i] !== undefined) {
|
||||
const j = this.identicalTo[i]
|
||||
actualIdsToUse.push(actualIdsToUse[j])
|
||||
continue
|
||||
|
@ -221,7 +221,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
|
||||
const closestIds = []
|
||||
const distances = []
|
||||
for (let i = 0; i < this.targetCoordinates.length; i++){
|
||||
for (let i = 0; i < this.targetCoordinates.length; i++) {
|
||||
const target = this.targetCoordinates[i];
|
||||
let closestDistance = undefined
|
||||
let closestId = undefined;
|
||||
|
@ -240,15 +240,15 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
|
||||
// Next step: every closestId can only occur once in the list
|
||||
// We skip the ones which are identical
|
||||
console.log("Erasing double ids")
|
||||
console.log("Erasing double ids")
|
||||
for (let i = 0; i < closestIds.length; i++) {
|
||||
if(this.identicalTo[i] !== undefined){
|
||||
if (this.identicalTo[i] !== undefined) {
|
||||
closestIds[i] = closestIds[this.identicalTo[i]]
|
||||
continue
|
||||
}
|
||||
const closestId = closestIds[i]
|
||||
for (let j = i + 1; j < closestIds.length; j++) {
|
||||
if(this.identicalTo[j] !== undefined){
|
||||
if (this.identicalTo[j] !== undefined) {
|
||||
continue
|
||||
}
|
||||
const otherClosestId = closestIds[j]
|
||||
|
|
|
@ -25,7 +25,7 @@ export default class SplitAction extends OsmChangeAction {
|
|||
* @param meta
|
||||
* @param toleranceInMeters: if a splitpoint closer then this amount of meters to an existing point, the existing point will be used to split the line instead of a new point
|
||||
*/
|
||||
constructor(wayId: string, splitPointCoordinates: [number, number][], meta: {theme: string}, toleranceInMeters = 5) {
|
||||
constructor(wayId: string, splitPointCoordinates: [number, number][], meta: { theme: string }, toleranceInMeters = 5) {
|
||||
super()
|
||||
this.wayId = wayId;
|
||||
this._splitPointsCoordinates = splitPointCoordinates
|
||||
|
@ -51,7 +51,7 @@ export default class SplitAction extends OsmChangeAction {
|
|||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const originalElement = <OsmWay> await OsmObject.DownloadObjectAsync(this.wayId)
|
||||
const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId)
|
||||
const originalNodes = originalElement.nodes;
|
||||
|
||||
// First, calculate splitpoints and remove points close to one another
|
||||
|
@ -180,7 +180,7 @@ export default class SplitAction extends OsmChangeAction {
|
|||
private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] {
|
||||
const wayGeoJson = osmWay.asGeoJson()
|
||||
// Should be [lon, lat][]
|
||||
const originalPoints : [number, number][] = osmWay.coordinates.map(c => [c[1], c[0]])
|
||||
const originalPoints: [number, number][] = osmWay.coordinates.map(c => [c[1], c[0]])
|
||||
const allPoints: {
|
||||
// lon, lat
|
||||
coordinates: [number, number],
|
||||
|
@ -234,25 +234,25 @@ export default class SplitAction extends OsmChangeAction {
|
|||
// We keep the original points
|
||||
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 = point.location - prevPoint.location
|
||||
|
||||
if(distToNext * 1000 > toleranceInM && distToPrev * 1000 > toleranceInM){
|
||||
|
||||
if (distToNext * 1000 > toleranceInM && distToPrev * 1000 > toleranceInM) {
|
||||
// Both are too far away to mark them as the split point
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
let closest = nextPoint
|
||||
if (distToNext > distToPrev) {
|
||||
closest = prevPoint
|
||||
}
|
||||
// Ok, we have a closest point!
|
||||
if(closest.originalIndex === 0 || closest.originalIndex === originalPoints.length){
|
||||
if (closest.originalIndex === 0 || closest.originalIndex === originalPoints.length) {
|
||||
// We can not split on the first or last points...
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -53,61 +53,6 @@ export class ChangesetHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private handleIdRewrite(node: any, type: string): [string, string] {
|
||||
const oldId = parseInt(node.attributes.old_id.value);
|
||||
if (node.attributes.new_id === undefined) {
|
||||
// We just removed this point!
|
||||
const element = this.allElements.getEventSourceById("node/" + oldId);
|
||||
element.data._deleted = "yes"
|
||||
element.ping();
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = parseInt(node.attributes.new_id.value);
|
||||
const result: [string, string] = [type + "/" + oldId, type + "/" + newId]
|
||||
if (!(oldId !== undefined && newId !== undefined &&
|
||||
!isNaN(oldId) && !isNaN(newId))) {
|
||||
return undefined;
|
||||
}
|
||||
if (oldId == newId) {
|
||||
return undefined;
|
||||
}
|
||||
console.log("Rewriting id: ", type + "/" + oldId, "-->", type + "/" + newId);
|
||||
const element = this.allElements.getEventSourceById("node/" + oldId);
|
||||
if(element === undefined){
|
||||
// Element to rewrite not found, probably a node or relation that is not rendered
|
||||
return undefined
|
||||
}
|
||||
element.data.id = type + "/" + newId;
|
||||
this.allElements.addElementById(type + "/" + newId, element);
|
||||
this.allElements.ContainingFeatures.set(type + "/" + newId, this.allElements.ContainingFeatures.get(type + "/" + oldId))
|
||||
element.ping();
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseUploadChangesetResponse(response: XMLDocument): void {
|
||||
const nodes = response.getElementsByTagName("node");
|
||||
const mappings = new Map<string, string>()
|
||||
// @ts-ignore
|
||||
for (const node of nodes) {
|
||||
const mapping = this.handleIdRewrite(node, "node")
|
||||
if (mapping !== undefined) {
|
||||
mappings.set(mapping[0], mapping[1])
|
||||
}
|
||||
}
|
||||
|
||||
const ways = response.getElementsByTagName("way");
|
||||
// @ts-ignore
|
||||
for (const way of ways) {
|
||||
const mapping = this.handleIdRewrite(way, "way")
|
||||
if (mapping !== undefined) {
|
||||
mappings.set(mapping[0], mapping[1])
|
||||
}
|
||||
}
|
||||
this.changes.registerIdRewrites(mappings)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The full logic to upload a change to one or more elements.
|
||||
*
|
||||
|
@ -191,7 +136,7 @@ export class ChangesetHandler {
|
|||
// The old value is overwritten, thus we drop
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await this.UpdateTags(csId, extraMetaTags.map(csTag => <[string, string]>[csTag.key, csTag.value]))
|
||||
|
||||
|
||||
|
@ -207,6 +152,60 @@ export class ChangesetHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private handleIdRewrite(node: any, type: string): [string, string] {
|
||||
const oldId = parseInt(node.attributes.old_id.value);
|
||||
if (node.attributes.new_id === undefined) {
|
||||
// We just removed this point!
|
||||
const element = this.allElements.getEventSourceById("node/" + oldId);
|
||||
element.data._deleted = "yes"
|
||||
element.ping();
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = parseInt(node.attributes.new_id.value);
|
||||
const result: [string, string] = [type + "/" + oldId, type + "/" + newId]
|
||||
if (!(oldId !== undefined && newId !== undefined &&
|
||||
!isNaN(oldId) && !isNaN(newId))) {
|
||||
return undefined;
|
||||
}
|
||||
if (oldId == newId) {
|
||||
return undefined;
|
||||
}
|
||||
console.log("Rewriting id: ", type + "/" + oldId, "-->", type + "/" + newId);
|
||||
const element = this.allElements.getEventSourceById("node/" + oldId);
|
||||
if (element === undefined) {
|
||||
// Element to rewrite not found, probably a node or relation that is not rendered
|
||||
return undefined
|
||||
}
|
||||
element.data.id = type + "/" + newId;
|
||||
this.allElements.addElementById(type + "/" + newId, element);
|
||||
this.allElements.ContainingFeatures.set(type + "/" + newId, this.allElements.ContainingFeatures.get(type + "/" + oldId))
|
||||
element.ping();
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseUploadChangesetResponse(response: XMLDocument): void {
|
||||
const nodes = response.getElementsByTagName("node");
|
||||
const mappings = new Map<string, string>()
|
||||
// @ts-ignore
|
||||
for (const node of nodes) {
|
||||
const mapping = this.handleIdRewrite(node, "node")
|
||||
if (mapping !== undefined) {
|
||||
mappings.set(mapping[0], mapping[1])
|
||||
}
|
||||
}
|
||||
|
||||
const ways = response.getElementsByTagName("way");
|
||||
// @ts-ignore
|
||||
for (const way of ways) {
|
||||
const mapping = this.handleIdRewrite(way, "way")
|
||||
if (mapping !== undefined) {
|
||||
mappings.set(mapping[0], mapping[1])
|
||||
}
|
||||
}
|
||||
this.changes.registerIdRewrites(mappings)
|
||||
|
||||
}
|
||||
|
||||
private async CloseChangeset(changesetId: number = undefined): Promise<void> {
|
||||
const self = this
|
||||
|
|
|
@ -50,26 +50,28 @@ export class OsmConnection {
|
|||
_dryRun: boolean;
|
||||
public preferencesHandler: OsmPreferences;
|
||||
public changesetHandler: ChangesetHandler;
|
||||
private fakeUser: boolean;
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
||||
private readonly _iframeMode: Boolean | boolean;
|
||||
private readonly _singlePage: boolean;
|
||||
public readonly _oauth_config: {
|
||||
oauth_consumer_key: string,
|
||||
oauth_secret: string,
|
||||
url: string
|
||||
};
|
||||
private fakeUser: boolean;
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
||||
private readonly _iframeMode: Boolean | boolean;
|
||||
private readonly _singlePage: boolean;
|
||||
private isChecking = false;
|
||||
|
||||
constructor(options:{dryRun?: false | boolean,
|
||||
fakeUser?: false | boolean,
|
||||
allElements: ElementStorage,
|
||||
changes: Changes,
|
||||
oauth_token?: UIEventSource<string>,
|
||||
// Used to keep multiple changesets open and to write to the correct changeset
|
||||
layoutName: string,
|
||||
singlePage?: boolean,
|
||||
osmConfiguration?: "osm" | "osm-test" }
|
||||
constructor(options: {
|
||||
dryRun?: false | boolean,
|
||||
fakeUser?: false | boolean,
|
||||
allElements: ElementStorage,
|
||||
changes: Changes,
|
||||
oauth_token?: UIEventSource<string>,
|
||||
// Used to keep multiple changesets open and to write to the correct changeset
|
||||
layoutName: string,
|
||||
singlePage?: boolean,
|
||||
osmConfiguration?: "osm" | "osm-test"
|
||||
}
|
||||
) {
|
||||
this.fakeUser = options.fakeUser ?? false;
|
||||
this._singlePage = options.singlePage ?? true;
|
||||
|
@ -79,7 +81,7 @@ export class OsmConnection {
|
|||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
|
||||
|
||||
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails");
|
||||
this.userDetails.data.dryRun = (options.dryRun ?? false) || (options.fakeUser ?? false) ;
|
||||
this.userDetails.data.dryRun = (options.dryRun ?? false) || (options.fakeUser ?? false);
|
||||
if (options.fakeUser) {
|
||||
const ud = this.userDetails.data;
|
||||
ud.csCount = 5678
|
||||
|
@ -112,7 +114,7 @@ export class OsmConnection {
|
|||
self.AttemptLogin();
|
||||
}, this.auth);
|
||||
|
||||
options. oauth_token.setData(undefined);
|
||||
options.oauth_token.setData(undefined);
|
||||
|
||||
}
|
||||
if (this.auth.authenticated()) {
|
||||
|
|
|
@ -56,7 +56,7 @@ export abstract class OsmObject {
|
|||
OsmObject.objectCache.set(id, src);
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
static async DownloadPropertiesOf(id: string): Promise<any> {
|
||||
const splitted = id.split("/");
|
||||
const idN = Number(splitted[1]);
|
||||
|
@ -84,18 +84,18 @@ export abstract class OsmObject {
|
|||
const parsed = OsmObject.ParseObjects(rawData.elements);
|
||||
// Lets fetch the object we need
|
||||
for (const osmObject of parsed) {
|
||||
if(osmObject.type !== type){
|
||||
if (osmObject.type !== type) {
|
||||
continue;
|
||||
}
|
||||
if(osmObject.id !== idN){
|
||||
if (osmObject.id !== idN) {
|
||||
continue
|
||||
}
|
||||
// Found the one!
|
||||
return osmObject
|
||||
}
|
||||
throw "PANIC: requested object is not part of the response"
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -170,6 +170,43 @@ export abstract class OsmObject {
|
|||
const elements: any[] = data.elements;
|
||||
return OsmObject.ParseObjects(elements);
|
||||
}
|
||||
|
||||
public static ParseObjects(elements: any[]): OsmObject[] {
|
||||
const objects: OsmObject[] = [];
|
||||
const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>()
|
||||
|
||||
for (const element of elements) {
|
||||
const type = element.type;
|
||||
const idN = element.id;
|
||||
let osmObject: OsmObject = null
|
||||
switch (type) {
|
||||
case("node"):
|
||||
const node = new OsmNode(idN);
|
||||
allNodes.set(idN, node);
|
||||
osmObject = node
|
||||
node.SaveExtraData(element);
|
||||
break;
|
||||
case("way"):
|
||||
osmObject = new OsmWay(idN);
|
||||
const nodes = element.nodes.map(i => allNodes.get(i));
|
||||
osmObject.SaveExtraData(element, nodes)
|
||||
break;
|
||||
case("relation"):
|
||||
osmObject = new OsmRelation(idN);
|
||||
osmObject.SaveExtraData(element, [])
|
||||
break;
|
||||
}
|
||||
|
||||
if (osmObject !== undefined && OsmObject.backendURL !== OsmObject.defaultBackend) {
|
||||
osmObject.tags["_backend"] = OsmObject.backendURL
|
||||
}
|
||||
|
||||
osmObject?.LoadData(element)
|
||||
objects.push(osmObject)
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
protected static isPolygon(tags: any): boolean {
|
||||
for (const tagsKey in tags) {
|
||||
if (!tags.hasOwnProperty(tagsKey)) {
|
||||
|
@ -206,42 +243,6 @@ export abstract class OsmObject {
|
|||
return result;
|
||||
}
|
||||
|
||||
public static ParseObjects(elements: any[]): OsmObject[] {
|
||||
const objects: OsmObject[] = [];
|
||||
const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>()
|
||||
|
||||
for (const element of elements) {
|
||||
const type = element.type;
|
||||
const idN = element.id;
|
||||
let osmObject: OsmObject = null
|
||||
switch (type) {
|
||||
case("node"):
|
||||
const node = new OsmNode(idN);
|
||||
allNodes.set(idN, node);
|
||||
osmObject = node
|
||||
node.SaveExtraData(element);
|
||||
break;
|
||||
case("way"):
|
||||
osmObject = new OsmWay(idN);
|
||||
const nodes = element.nodes.map(i => allNodes.get(i));
|
||||
osmObject.SaveExtraData(element, nodes)
|
||||
break;
|
||||
case("relation"):
|
||||
osmObject = new OsmRelation(idN);
|
||||
osmObject.SaveExtraData(element, [])
|
||||
break;
|
||||
}
|
||||
|
||||
if (osmObject !== undefined && OsmObject.backendURL !== OsmObject.defaultBackend) {
|
||||
osmObject.tags["_backend"] = OsmObject.backendURL
|
||||
}
|
||||
|
||||
osmObject?.LoadData(element)
|
||||
objects.push(osmObject)
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
// The centerpoint of the feature, as [lat, lon]
|
||||
public abstract centerpoint(): [number, number];
|
||||
|
||||
|
|
|
@ -42,13 +42,13 @@ export class Overpass {
|
|||
}
|
||||
const self = this;
|
||||
const json = await Utils.downloadJson(query)
|
||||
|
||||
|
||||
if (json.elements.length === 0 && json.remark !== undefined) {
|
||||
console.warn("Timeout or other runtime error while querying overpass", json.remark);
|
||||
throw `Runtime error (timeout or similar)${json.remark}`
|
||||
}
|
||||
if(json.elements.length === 0){
|
||||
console.warn("No features for" ,json)
|
||||
if (json.elements.length === 0) {
|
||||
console.warn("No features for", json)
|
||||
}
|
||||
|
||||
self._relationTracker.RegisterRelations(json)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import State from "../../State";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
|
||||
export interface Relation {
|
||||
|
@ -21,10 +20,6 @@ export default class RelationsTracker {
|
|||
constructor() {
|
||||
}
|
||||
|
||||
public RegisterRelations(overpassJson: any): void {
|
||||
this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an overview of the relations - except for multipolygons. We don't care about those
|
||||
* @param overpassJson
|
||||
|
@ -39,6 +34,10 @@ export default class RelationsTracker {
|
|||
return relations
|
||||
}
|
||||
|
||||
public RegisterRelations(overpassJson: any): void {
|
||||
this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson))
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a mapping of {memberId --> {role in relation, id of relation} }
|
||||
* @param relations
|
||||
|
|
|
@ -49,6 +49,8 @@ export default class SimpleMetaTagger {
|
|||
return true;
|
||||
}
|
||||
)
|
||||
public static readonly lazyTags: string[] = [].concat(...SimpleMetaTagger.metatags.filter(tagger => tagger.isLazy)
|
||||
.map(tagger => tagger.keys));
|
||||
private static latlon = new SimpleMetaTagger({
|
||||
keys: ["_lat", "_lon"],
|
||||
doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)"
|
||||
|
@ -78,83 +80,6 @@ export default class SimpleMetaTagger {
|
|||
return true;
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme.
|
||||
* These changes are performed in-place.
|
||||
*
|
||||
* Returns 'true' is at least one change has been made
|
||||
* @param tags
|
||||
*/
|
||||
public static removeBothTagging(tags: any): boolean{
|
||||
let somethingChanged = false
|
||||
/**
|
||||
* Sets the key onto the properties (but doesn't overwrite if already existing)
|
||||
*/
|
||||
function set(k, value) {
|
||||
if (tags[k] === undefined || tags[k] === "") {
|
||||
tags[k] = value
|
||||
somethingChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
if (tags["sidewalk"]) {
|
||||
|
||||
const v = tags["sidewalk"]
|
||||
switch (v) {
|
||||
case "none":
|
||||
case "no":
|
||||
set("sidewalk:left", "no");
|
||||
set("sidewalk:right", "no");
|
||||
break
|
||||
case "both":
|
||||
set("sidewalk:left", "yes");
|
||||
set("sidewalk:right", "yes");
|
||||
break;
|
||||
case "left":
|
||||
set("sidewalk:left", "yes");
|
||||
set("sidewalk:right", "no");
|
||||
break;
|
||||
case "right":
|
||||
set("sidewalk:left", "no");
|
||||
set("sidewalk:right", "yes");
|
||||
break;
|
||||
default:
|
||||
set("sidewalk:left", v);
|
||||
set("sidewalk:right", v);
|
||||
break;
|
||||
}
|
||||
delete tags["sidewalk"]
|
||||
somethingChanged = true
|
||||
}
|
||||
|
||||
|
||||
const regex = /\([^:]*\):both:\(.*\)/
|
||||
for (const key in tags) {
|
||||
const v = tags[key]
|
||||
if (key.endsWith(":both")) {
|
||||
const strippedKey = key.substring(0, key.length - ":both".length)
|
||||
set(strippedKey + ":left", v)
|
||||
set(strippedKey + ":right", v)
|
||||
delete tags[key]
|
||||
continue
|
||||
}
|
||||
|
||||
const match = key.match(regex)
|
||||
if (match !== null) {
|
||||
const strippedKey = match[1]
|
||||
const property = match[1]
|
||||
set(strippedKey + ":left:" + property, v)
|
||||
set(strippedKey + ":right:" + property, v)
|
||||
console.log("Left-right rewritten " + key)
|
||||
delete tags[key]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return somethingChanged
|
||||
}
|
||||
|
||||
private static noBothButLeftRight = new SimpleMetaTagger(
|
||||
{
|
||||
keys: ["sidewalk:left", "sidewalk:right", "generic_key:left:property", "generic_key:right:property"],
|
||||
|
@ -163,11 +88,11 @@ export default class SimpleMetaTagger {
|
|||
cleanupRetagger: true
|
||||
},
|
||||
((feature, state, layer) => {
|
||||
|
||||
if(!layer.lineRendering.some(lr => lr.leftRightSensitive)){
|
||||
|
||||
if (!layer.lineRendering.some(lr => lr.leftRightSensitive)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
return SimpleMetaTagger.removeBothTagging(feature.properties)
|
||||
})
|
||||
)
|
||||
|
@ -451,9 +376,6 @@ export default class SimpleMetaTagger {
|
|||
SimpleMetaTagger.noBothButLeftRight
|
||||
|
||||
];
|
||||
public static readonly lazyTags: string[] = [].concat(...SimpleMetaTagger.metatags.filter(tagger => tagger.isLazy)
|
||||
.map(tagger => tagger.keys));
|
||||
|
||||
public readonly keys: string[];
|
||||
public readonly doc: string;
|
||||
public readonly isLazy: boolean;
|
||||
|
@ -481,6 +403,83 @@ export default class SimpleMetaTagger {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme.
|
||||
* These changes are performed in-place.
|
||||
*
|
||||
* Returns 'true' is at least one change has been made
|
||||
* @param tags
|
||||
*/
|
||||
public static removeBothTagging(tags: any): boolean {
|
||||
let somethingChanged = false
|
||||
|
||||
/**
|
||||
* Sets the key onto the properties (but doesn't overwrite if already existing)
|
||||
*/
|
||||
function set(k, value) {
|
||||
if (tags[k] === undefined || tags[k] === "") {
|
||||
tags[k] = value
|
||||
somethingChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
if (tags["sidewalk"]) {
|
||||
|
||||
const v = tags["sidewalk"]
|
||||
switch (v) {
|
||||
case "none":
|
||||
case "no":
|
||||
set("sidewalk:left", "no");
|
||||
set("sidewalk:right", "no");
|
||||
break
|
||||
case "both":
|
||||
set("sidewalk:left", "yes");
|
||||
set("sidewalk:right", "yes");
|
||||
break;
|
||||
case "left":
|
||||
set("sidewalk:left", "yes");
|
||||
set("sidewalk:right", "no");
|
||||
break;
|
||||
case "right":
|
||||
set("sidewalk:left", "no");
|
||||
set("sidewalk:right", "yes");
|
||||
break;
|
||||
default:
|
||||
set("sidewalk:left", v);
|
||||
set("sidewalk:right", v);
|
||||
break;
|
||||
}
|
||||
delete tags["sidewalk"]
|
||||
somethingChanged = true
|
||||
}
|
||||
|
||||
|
||||
const regex = /\([^:]*\):both:\(.*\)/
|
||||
for (const key in tags) {
|
||||
const v = tags[key]
|
||||
if (key.endsWith(":both")) {
|
||||
const strippedKey = key.substring(0, key.length - ":both".length)
|
||||
set(strippedKey + ":left", v)
|
||||
set(strippedKey + ":right", v)
|
||||
delete tags[key]
|
||||
continue
|
||||
}
|
||||
|
||||
const match = key.match(regex)
|
||||
if (match !== null) {
|
||||
const strippedKey = match[1]
|
||||
const property = match[1]
|
||||
set(strippedKey + ":left:" + property, v)
|
||||
set(strippedKey + ":right:" + property, v)
|
||||
console.log("Left-right rewritten " + key)
|
||||
delete tags[key]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return somethingChanged
|
||||
}
|
||||
|
||||
public static HelpText(): BaseUIElement {
|
||||
const subElements: (string | BaseUIElement)[] = [
|
||||
new Combine([
|
||||
|
|
|
@ -97,7 +97,7 @@ export default class FeaturePipelineState extends MapState {
|
|||
}, this
|
||||
);
|
||||
new SelectedFeatureHandler(Hash.hash, this)
|
||||
|
||||
|
||||
this.AddClusteringToMap(this.leafletMap)
|
||||
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ export default class FeatureSwitchState {
|
|||
|
||||
this.featureSwitchIsTesting = QueryParameters.GetBooleanQueryParameter(
|
||||
"test",
|
||||
""+testingDefaultValue,
|
||||
"" + testingDefaultValue,
|
||||
"If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org"
|
||||
)
|
||||
|
||||
|
@ -158,7 +158,7 @@ export default class FeatureSwitchState {
|
|||
|
||||
this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter("fake-user", "false",
|
||||
"If true, 'dryrun' mode is activated and a fake user account is loaded")
|
||||
|
||||
|
||||
|
||||
this.overpassUrl = QueryParameters.GetQueryParameter("overpassUrl",
|
||||
(layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","),
|
||||
|
|
|
@ -14,7 +14,6 @@ import {QueryParameters} from "../Web/QueryParameters";
|
|||
import * as personal from "../../assets/themes/personal/personal.json";
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
|
||||
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer";
|
||||
import {Coord} from "@turf/turf";
|
||||
|
||||
/**
|
||||
* Contains all the leaflet-map related state
|
||||
|
@ -123,7 +122,21 @@ export default class MapState extends UserRelatedState {
|
|||
this.AddAllOverlaysToMap(this.leafletMap)
|
||||
}
|
||||
|
||||
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) {
|
||||
const initialized = new Set()
|
||||
for (const overlayToggle of this.overlayToggles) {
|
||||
new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed)
|
||||
initialized.add(overlayToggle.config)
|
||||
}
|
||||
|
||||
for (const tileLayerSource of this.layoutToUse.tileLayerSources) {
|
||||
if (initialized.has(tileLayerSource)) {
|
||||
continue
|
||||
}
|
||||
new ShowOverlayLayer(tileLayerSource, leafletMap)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private lockBounds() {
|
||||
const layout = this.layoutToUse;
|
||||
|
@ -201,21 +214,5 @@ export default class MapState extends UserRelatedState {
|
|||
return new UIEventSource<FilteredLayer[]>(flayers);
|
||||
}
|
||||
|
||||
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) {
|
||||
const initialized = new Set()
|
||||
for (const overlayToggle of this.overlayToggles) {
|
||||
new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed)
|
||||
initialized.add(overlayToggle.config)
|
||||
}
|
||||
|
||||
for (const tileLayerSource of this.layoutToUse.tileLayerSources) {
|
||||
if (initialized.has(tileLayerSource)) {
|
||||
continue
|
||||
}
|
||||
new ShowOverlayLayer(tileLayerSource, leafletMap)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -64,7 +64,7 @@ export default class UserRelatedState extends ElementsState {
|
|||
|
||||
if (layoutToUse?.hideFromOverview) {
|
||||
this.osmConnection.isLoggedIn.addCallbackAndRunD(loggedIn => {
|
||||
if(loggedIn){
|
||||
if (loggedIn) {
|
||||
this.osmConnection
|
||||
.GetPreference("hidden-theme-" + layoutToUse?.id + "-enabled")
|
||||
.setData("true");
|
||||
|
@ -129,7 +129,7 @@ export default class UserRelatedState extends ElementsState {
|
|||
}
|
||||
return [home.lon, home.lat]
|
||||
})).map(homeLonLat => {
|
||||
if(homeLonLat === undefined){
|
||||
if (homeLonLat === undefined) {
|
||||
return empty
|
||||
}
|
||||
return [{
|
||||
|
@ -148,5 +148,5 @@ export default class UserRelatedState extends ElementsState {
|
|||
|
||||
this.homeLocation = new StaticFeatureSource(feature, false)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -19,7 +19,7 @@ export class RegexTag extends TagsFilter {
|
|||
if (fromTag === undefined) {
|
||||
return;
|
||||
}
|
||||
if(typeof fromTag === "number"){
|
||||
if (typeof fromTag === "number") {
|
||||
fromTag = "" + fromTag;
|
||||
}
|
||||
if (typeof possibleRegex === "string") {
|
||||
|
@ -47,11 +47,11 @@ export class RegexTag extends TagsFilter {
|
|||
}
|
||||
|
||||
matchesProperties(tags: any): boolean {
|
||||
if(typeof this.key === "string"){
|
||||
if (typeof this.key === "string") {
|
||||
const value = tags[this.key] ?? ""
|
||||
return RegexTag.doesMatch(value, this.value) != this.invert;
|
||||
}
|
||||
|
||||
|
||||
for (const key in tags) {
|
||||
if (key === undefined) {
|
||||
continue;
|
||||
|
|
|
@ -27,14 +27,14 @@ export class TagUtils {
|
|||
return properties;
|
||||
}
|
||||
|
||||
static changeAsProperties(kvs : {k: string, v: string}[]): any {
|
||||
static changeAsProperties(kvs: { k: string, v: string }[]): any {
|
||||
const tags = {}
|
||||
for (const kv of kvs) {
|
||||
tags[kv.k] = kv.v
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given two hashes of {key --> values[]}, makes sure that every neededTag is present in availableTags
|
||||
*/
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {Utils} from "../Utils";
|
||||
import * as Events from "events";
|
||||
|
||||
export class UIEventSource<T> {
|
||||
|
||||
|
@ -75,27 +74,6 @@ export class UIEventSource<T> {
|
|||
promise?.catch(err => console.warn("Promise failed:", err))
|
||||
return src
|
||||
}
|
||||
|
||||
public AsPromise(): Promise<T>{
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
if(self.data !== undefined){
|
||||
resolve(self.data)
|
||||
}else{
|
||||
self.addCallbackD(data => {
|
||||
resolve(data)
|
||||
return true; // return true to unregister as we only need to be called once
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public WaitForPromise(promise: Promise<T>, onFail: ((any) => void)): UIEventSource<T> {
|
||||
const self = this;
|
||||
promise?.then(d => self.setData(d))
|
||||
promise?.catch(err =>onFail(err))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated.
|
||||
|
@ -109,20 +87,6 @@ export class UIEventSource<T> {
|
|||
promise?.catch(err => src.setData({error: err}))
|
||||
return src
|
||||
}
|
||||
|
||||
public withEqualityStabilized(comparator: (t:T | undefined, t1:T | undefined) => boolean): UIEventSource<T>{
|
||||
let oldValue = undefined;
|
||||
return this.map(v => {
|
||||
if(v == oldValue){
|
||||
return oldValue
|
||||
}
|
||||
if(comparator(oldValue, v)){
|
||||
return oldValue
|
||||
}
|
||||
oldValue = v;
|
||||
return v;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a UIEVentSource with a list, returns a new UIEventSource which is only updated if the _contents_ of the list are different.
|
||||
|
@ -168,6 +132,57 @@ export class UIEventSource<T> {
|
|||
return stable
|
||||
}
|
||||
|
||||
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||
return source.map(
|
||||
(str) => {
|
||||
let parsed = parseFloat(str);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
},
|
||||
[],
|
||||
(fl) => {
|
||||
if (fl === undefined || isNaN(fl)) {
|
||||
return undefined;
|
||||
}
|
||||
return ("" + fl).substr(0, 8);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public AsPromise(): Promise<T> {
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (self.data !== undefined) {
|
||||
resolve(self.data)
|
||||
} else {
|
||||
self.addCallbackD(data => {
|
||||
resolve(data)
|
||||
return true; // return true to unregister as we only need to be called once
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public WaitForPromise(promise: Promise<T>, onFail: ((any) => void)): UIEventSource<T> {
|
||||
const self = this;
|
||||
promise?.then(d => self.setData(d))
|
||||
promise?.catch(err => onFail(err))
|
||||
return this
|
||||
}
|
||||
|
||||
public withEqualityStabilized(comparator: (t: T | undefined, t1: T | undefined) => boolean): UIEventSource<T> {
|
||||
let oldValue = undefined;
|
||||
return this.map(v => {
|
||||
if (v == oldValue) {
|
||||
return oldValue
|
||||
}
|
||||
if (comparator(oldValue, v)) {
|
||||
return oldValue
|
||||
}
|
||||
oldValue = v;
|
||||
return v;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a callback
|
||||
*
|
||||
|
@ -234,14 +249,14 @@ export class UIEventSource<T> {
|
|||
sink.setData(null)
|
||||
} else if (newEventSource === undefined) {
|
||||
sink.setData(undefined)
|
||||
}else if (!seenEventSources.has(newEventSource)) {
|
||||
} else if (!seenEventSources.has(newEventSource)) {
|
||||
seenEventSources.add(newEventSource)
|
||||
newEventSource.addCallbackAndRun(resultData => {
|
||||
if (mapped.data === newEventSource) {
|
||||
sink.setData(resultData);
|
||||
}
|
||||
})
|
||||
}else{
|
||||
} else {
|
||||
// Already seen, so we don't have to add a callback, just update the value
|
||||
sink.setData(newEventSource.data)
|
||||
}
|
||||
|
@ -300,7 +315,7 @@ export class UIEventSource<T> {
|
|||
}
|
||||
|
||||
public stabilized(millisToStabilize): UIEventSource<T> {
|
||||
if(Utils.runningFromConsole){
|
||||
if (Utils.runningFromConsole) {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -335,20 +350,4 @@ export class UIEventSource<T> {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||
return source.map(
|
||||
(str) => {
|
||||
let parsed = parseFloat(str);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
},
|
||||
[],
|
||||
(fl) => {
|
||||
if (fl === undefined || isNaN(fl)) {
|
||||
return undefined;
|
||||
}
|
||||
return ("" + fl).substr(0, 8);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -55,8 +55,8 @@ export class QueryParameters {
|
|||
return source;
|
||||
}
|
||||
|
||||
public static GetBooleanQueryParameter(key: string, deflt: string, documentation?: string): UIEventSource<boolean>{
|
||||
return QueryParameters.GetQueryParameter(key, deflt, documentation).map(str => str === "true", [], b => ""+b)
|
||||
public static GetBooleanQueryParameter(key: string, deflt: string, documentation?: string): UIEventSource<boolean> {
|
||||
return QueryParameters.GetQueryParameter(key, deflt, documentation).map(str => str === "true", [], b => "" + b)
|
||||
}
|
||||
|
||||
public static GenerateQueryParameterDocs(): string {
|
||||
|
|
|
@ -64,11 +64,11 @@ export class WikidataResponse {
|
|||
}
|
||||
|
||||
static extractClaims(claimsJson: any): Map<string, Set<string>> {
|
||||
|
||||
const simplified = wds.simplify.claims(claimsJson, {
|
||||
|
||||
const simplified = wds.simplify.claims(claimsJson, {
|
||||
timeConverter: 'simple-day'
|
||||
})
|
||||
|
||||
|
||||
const claims = new Map<string, Set<string>>();
|
||||
for (const claimId in simplified) {
|
||||
const claimsList: any[] = simplified[claimId]
|
||||
|
@ -98,11 +98,11 @@ export class WikidataLexeme {
|
|||
for (const sense of json.senses) {
|
||||
const glosses = sense.glosses
|
||||
for (const language in glosses) {
|
||||
let previousSenses = this.senses.get(language)
|
||||
if(previousSenses === undefined){
|
||||
let previousSenses = this.senses.get(language)
|
||||
if (previousSenses === undefined) {
|
||||
previousSenses = ""
|
||||
}else{
|
||||
previousSenses = previousSenses+"; "
|
||||
} else {
|
||||
previousSenses = previousSenses + "; "
|
||||
}
|
||||
this.senses.set(language, previousSenses + glosses[language].value ?? "")
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ export default class Wikidata {
|
|||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static async searchAndFetch(
|
||||
search: string,
|
||||
options?: WikidataSearchoptions
|
||||
|
@ -248,7 +248,7 @@ export default class Wikidata {
|
|||
for (const identifierPrefix of Wikidata._identifierPrefixes) {
|
||||
if (value.startsWith(identifierPrefix)) {
|
||||
const trimmed = value.substring(identifierPrefix.length);
|
||||
if(trimmed === ""){
|
||||
if (trimmed === "") {
|
||||
return undefined
|
||||
}
|
||||
const n = Number(trimmed)
|
||||
|
@ -266,14 +266,14 @@ export default class Wikidata {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
public static IdToArticle(id: string){
|
||||
if(id.startsWith("Q")){
|
||||
return "https://wikidata.org/wiki/"+id
|
||||
public static IdToArticle(id: string) {
|
||||
if (id.startsWith("Q")) {
|
||||
return "https://wikidata.org/wiki/" + id
|
||||
}
|
||||
if(id.startsWith("L")){
|
||||
return "https://wikidata.org/wiki/Lexeme:"+id
|
||||
if (id.startsWith("L")) {
|
||||
return "https://wikidata.org/wiki/Lexeme:" + id
|
||||
}
|
||||
throw "Unknown id type: "+id
|
||||
throw "Unknown id type: " + id
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -289,7 +289,7 @@ export default class Wikidata {
|
|||
|
||||
const url = "https://www.wikidata.org/wiki/Special:EntityData/" + id + ".json";
|
||||
const entities = (await Utils.downloadJsonCached(url, 10000)).entities
|
||||
const firstKey = <string> Array.from(Object.keys(entities))[0] // Roundabout way to fetch the entity; it might have been a redirect
|
||||
const firstKey = <string>Array.from(Object.keys(entities))[0] // Roundabout way to fetch the entity; it might have been a redirect
|
||||
const response = entities[firstKey]
|
||||
|
||||
if (id.startsWith("L")) {
|
||||
|
|
|
@ -9,8 +9,8 @@ export default class Wikimedia {
|
|||
* @param continueParameter: if the page indicates that more pages should be loaded, this uses a token to continue. Provided by wikimedia
|
||||
*/
|
||||
public static async GetCategoryContents(categoryName: string,
|
||||
maxLoad = 10,
|
||||
continueParameter: string = undefined): Promise<string[]> {
|
||||
maxLoad = 10,
|
||||
continueParameter: string = undefined): Promise<string[]> {
|
||||
if (categoryName === undefined || categoryName === null || categoryName === "") {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export default class Wikipedia {
|
|||
private static readonly classesToRemove = [
|
||||
"shortdescription",
|
||||
"sidebar",
|
||||
"infobox","infobox_v2",
|
||||
"infobox", "infobox_v2",
|
||||
"noprint",
|
||||
"ambox",
|
||||
"mw-editsection",
|
||||
|
@ -22,26 +22,27 @@ export default class Wikipedia {
|
|||
"mw-empty-elt",
|
||||
"hatnote" // Often redirects
|
||||
]
|
||||
|
||||
|
||||
private static readonly idsToRemove = [
|
||||
"sjabloon_zie"
|
||||
]
|
||||
|
||||
private static readonly _cache = new Map<string, UIEventSource<{ success: string } | { error: any }>>()
|
||||
|
||||
|
||||
public static GetArticle(options: {
|
||||
pageName: string,
|
||||
language?: "en" | string}): UIEventSource<{ success: string } | { error: any }>{
|
||||
const key = (options.language ?? "en")+":"+options.pageName
|
||||
language?: "en" | string
|
||||
}): UIEventSource<{ success: string } | { error: any }> {
|
||||
const key = (options.language ?? "en") + ":" + options.pageName
|
||||
const cached = Wikipedia._cache.get(key)
|
||||
if(cached !== undefined){
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
}
|
||||
const v = UIEventSource.FromPromiseWithErr(Wikipedia.GetArticleAsync(options))
|
||||
Wikipedia._cache.set(key, v)
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
public static async GetArticleAsync(options: {
|
||||
pageName: string,
|
||||
language?: "en" | string
|
||||
|
@ -57,24 +58,22 @@ export default class Wikipedia {
|
|||
const content = Array.from(div.children)[0]
|
||||
|
||||
for (const forbiddenClass of Wikipedia.classesToRemove) {
|
||||
const toRemove = content.getElementsByClassName(forbiddenClass)
|
||||
const toRemove = content.getElementsByClassName(forbiddenClass)
|
||||
for (const toRemoveElement of Array.from(toRemove)) {
|
||||
toRemoveElement.parentElement?.removeChild(toRemoveElement)
|
||||
}
|
||||
}
|
||||
|
||||
for (const forbiddenId of Wikipedia.idsToRemove) {
|
||||
const toRemove = content.querySelector("#"+forbiddenId)
|
||||
const toRemove = content.querySelector("#" + forbiddenId)
|
||||
toRemove?.parentElement?.removeChild(toRemove)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const links = Array.from(content.getElementsByTagName("a"))
|
||||
|
||||
// Rewrite relative links to absolute links + open them in a new tab
|
||||
links.filter(link => link.getAttribute("href")?.startsWith("/") ?? false).
|
||||
forEach(link => {
|
||||
links.filter(link => link.getAttribute("href")?.startsWith("/") ?? false).forEach(link => {
|
||||
link.target = '_blank'
|
||||
// note: link.getAttribute("href") gets the textual value, link.href is the rewritten version which'll contain the host for relative paths
|
||||
link.href = `https://${language}.wikipedia.org${link.getAttribute("href")}`;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue