Add initial clustering per tile, very broken

This commit is contained in:
Pieter Vander Vennet 2021-09-26 17:36:39 +02:00
parent 2b78c4b53f
commit c5e9448720
88 changed files with 1080 additions and 651 deletions

View file

@ -36,6 +36,8 @@ export default class ScrollableFullScreen extends UIElement {
this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown)
.SetClass("hidden md:block");
this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown);
const self = this;
isShown.addCallback(isShown => {
if (isShown) {

View file

@ -2,22 +2,23 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
export class VariableUiElement extends BaseUIElement {
private _element: HTMLElement;
private readonly _contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>;
constructor(
contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>
) {
constructor(contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>) {
super();
this._contents = contents;
this._element = document.createElement("span");
const el = this._element;
contents.addCallbackAndRun((contents) => {
}
protected InnerConstructElement(): HTMLElement {
const el = document.createElement("span");
this._contents.addCallbackAndRun((contents) => {
while (el.firstChild) {
el.removeChild(el.lastChild);
}
if (contents === undefined) {
return el;
return;
}
if (typeof contents === "string") {
el.innerHTML = contents;
@ -35,9 +36,6 @@ export class VariableUiElement extends BaseUIElement {
}
}
});
}
protected InnerConstructElement(): HTMLElement {
return this._element;
return el;
}
}

View file

@ -100,8 +100,6 @@ export default abstract class BaseUIElement {
throw "ERROR! This is not a correct baseUIElement: " + this.constructor.name
}
try {
const el = this.InnerConstructElement();
if (el === undefined) {

View file

@ -13,17 +13,16 @@ export default class Attribution extends VariableUiElement {
}
super(
license.map((license: LicenseInfo) => {
if (license?.artist === undefined) {
return undefined;
if(license === undefined){
return undefined
}
return new Combine([
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),
new Combine([
Translations.W(license.artist).SetClass("block font-bold"),
Translations.W((license.license ?? "") === "" ? "CC0" : (license.license ?? ""))
Translations.W(license?.artist ?? ".").SetClass("block font-bold"),
Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? ""))
]).SetClass("flex flex-col")
]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg")

View file

@ -48,7 +48,7 @@ export default class DeleteImage extends Toggle {
tags.map(tags => (tags[key] ?? "") !== "")
),
undefined /*Login (and thus editing) is disabled*/,
State.state?.featureSwitchUserbadge ?? new UIEventSource<boolean>(true)
State.state.osmConnection.isLoggedIn
)
this.SetClass("cursor-pointer")
}

View file

@ -9,7 +9,6 @@ import State from "../../State";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {BBox, GeoOperations} from "../../Logic/GeoOperations";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import * as L from "leaflet";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";

View file

@ -31,8 +31,10 @@ export default class EditableTagRendering extends Toggle {
const answerWithEditButton = new Combine([answer,
new Toggle(editButton, undefined, State.state.osmConnection.isLoggedIn)])
.SetClass("flex justify-between w-full")
new Toggle(editButton,
undefined,
State.state.osmConnection.isLoggedIn)
]).SetClass("flex justify-between w-full")
const cancelbutton =

View file

@ -71,7 +71,7 @@ export default class SplitRoadWizard extends Toggle {
})
new ShowDataMultiLayer({
features: new StaticFeatureSource([roadElement]),
features: new StaticFeatureSource([roadElement], false),
layers: State.state.filteredLayers,
leafletMap: miniMap.leafletMap,
enablePopups: false,

View file

@ -0,0 +1,156 @@
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../../Logic/FeatureSource/FeatureSource";
import {BBox} from "../../Logic/GeoOperations";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {UIEventSource} from "../../Logic/UIEventSource";
import {Tiles} from "../../Models/TileRange";
/**
* A feature source containing meta features.
* It will contain exactly one point for every tile of the specified (dynamic) zoom level
*/
export default class PerTileCountAggregator implements FeatureSource {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name: string = "PerTileCountAggregator"
private readonly perTile: Map<number, SingleTileCounter> = new Map<number, SingleTileCounter>()
private readonly _requestedZoomLevel: UIEventSource<number>;
constructor(requestedZoomLevel: UIEventSource<number>) {
this._requestedZoomLevel = requestedZoomLevel;
const self = this;
this._requestedZoomLevel.addCallbackAndRun(_ => self.update())
}
private update() {
const now = new Date()
const allCountsAsFeatures : {feature: any, freshness: Date}[] = []
const aggregate = this.calculatePerTileCount()
aggregate.forEach((totalsPerLayer, tileIndex) => {
const totals = {}
let totalCount = 0
totalsPerLayer.forEach((total, layerId) => {
totals[layerId] = total
totalCount += total
})
totals["tileId"] = tileIndex
totals["count"] = totalCount
const feature = {
"type": "Feature",
"properties": totals,
"geometry": {
"type": "Point",
"coordinates": Tiles.centerPointOf(...Tiles.tile_from_index(tileIndex))
}
}
allCountsAsFeatures.push({feature: feature, freshness: now})
const bbox= BBox.fromTileIndex(tileIndex)
const box = {
"type": "Feature",
"properties":totals,
"geometry": {
"type": "Polygon",
"coordinates": [
[
[bbox.minLon, bbox.minLat],
[bbox.minLon, bbox.maxLat],
[bbox.maxLon, bbox.maxLat],
[bbox.maxLon, bbox.minLat],
[bbox.minLon, bbox.minLat]
]
]
}
}
allCountsAsFeatures.push({feature:box, freshness: now})
})
this.features.setData(allCountsAsFeatures)
}
/**
* Calculates an aggregate count per tile and per subtile
* @private
*/
private calculatePerTileCount() {
const perTileCount = new Map<number, Map<string, number>>()
const targetZoom = this._requestedZoomLevel.data;
// We only search for tiles of the same zoomlevel or a higher zoomlevel, which is embedded
for (const singleTileCounter of Array.from(this.perTile.values())) {
let tileZ = singleTileCounter.z
let tileX = singleTileCounter.x
let tileY = singleTileCounter.y
if (tileZ < targetZoom) {
continue;
}
while (tileZ > targetZoom) {
tileX = Math.floor(tileX / 2)
tileY = Math.floor(tileY / 2)
tileZ--
}
const tileI = Tiles.tile_index(tileZ, tileX, tileY)
let counts = perTileCount.get(tileI)
if (counts === undefined) {
counts = new Map<string, number>()
perTileCount.set(tileI, counts)
}
singleTileCounter.countsPerLayer.data.forEach((count, layerId) => {
if (counts.has(layerId)) {
counts.set(layerId, count + counts.get(layerId))
} else {
counts.set(layerId, count)
}
})
}
return perTileCount;
}
public addTile(tile: FeatureSourceForLayer & Tiled, shouldBeCounted: UIEventSource<boolean>) {
let counter = this.perTile.get(tile.tileIndex)
if (counter === undefined) {
counter = new SingleTileCounter(tile.tileIndex)
this.perTile.set(tile.tileIndex, counter)
// We do **NOT** add a callback on the perTile index, even though we could! It'll update just fine without it
}
counter.addTileCount(tile, shouldBeCounted)
}
}
/**
* Keeps track of a single tile
*/
class SingleTileCounter implements Tiled {
public readonly bbox: BBox;
public readonly tileIndex: number;
public readonly countsPerLayer: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>())
private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>();
public readonly z: number
public readonly x: number
public readonly y: number
constructor(tileIndex: number) {
this.tileIndex = tileIndex
this.bbox = BBox.fromTileIndex(tileIndex)
const [z, x, y] = Tiles.tile_from_index(tileIndex)
this.z = z;
this.x = x;
this.y = y
}
public addTileCount(source: FeatureSourceForLayer, shouldBeCounted: UIEventSource<boolean>) {
const layer = source.layer.layerDef
this.registeredLayers.set(layer.id, layer)
const self = this
source.features.map(f => {
/*if (!shouldBeCounted.data) {
return;
}*/
self.countsPerLayer.data.set(layer.id, f.length)
self.countsPerLayer.ping()
}, [shouldBeCounted])
}
}

View file

@ -41,13 +41,14 @@ export default class ShowDataLayer {
options.leafletMap.addCallback(_ => self.update(options));
this.update(options);
State.state.selectedElement.addCallbackAndRunD(selected => {
if (self._leafletMap.data === undefined) {
return;
}
const v = self.leafletLayersPerId.get(selected.properties.id)
if(v === undefined){return;}
if (v === undefined) {
return;
}
const leafletLayer = v.leafletlayer
const feature = v.feature
if (leafletLayer.getPopup().isOpen()) {
@ -66,6 +67,21 @@ export default class ShowDataLayer {
}
})
options.doShowLayer?.addCallbackAndRun(doShow => {
const mp = options.leafletMap.data;
if (this.geoLayer == undefined || mp == undefined) {
return;
}
if (doShow) {
mp.addLayer(this.geoLayer)
} else {
mp.removeLayer(this.geoLayer)
}
})
}
private update(options) {
@ -83,21 +99,19 @@ export default class ShowDataLayer {
mp.removeLayer(this.geoLayer);
}
this.geoLayer= this.CreateGeojsonLayer()
this.geoLayer = this.CreateGeojsonLayer()
const allFeats = this._features.data;
for (const feat of allFeats) {
if (feat === undefined) {
continue
}
try{
try {
this.geoLayer.addData(feat);
}catch(e){
} catch (e) {
console.error("Could not add ", feat, "to the geojson layer in leaflet")
}
}
mp.addLayer(this.geoLayer)
if (options.zoomToFeatures ?? false) {
try {
mp.fitBounds(this.geoLayer.getBounds(), {animate: false})
@ -105,6 +119,10 @@ export default class ShowDataLayer {
console.error(e)
}
}
if (options.doShowLayer?.data ?? true) {
mp.addLayer(this.geoLayer)
}
}
@ -125,7 +143,8 @@ export default class ShowDataLayer {
return;
}
const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) : State.state.allElements.getEventSourceById(feature.properties.id)
const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) :
State.state.allElements.getEventSourceById(feature.properties.id)
const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)
const style = layer.GenerateLeafletStyle(tagSource, clickable);
const baseElement = style.icon.html;
@ -193,8 +212,10 @@ export default class ShowDataLayer {
infobox.Activate();
});
// Add the feature to the index to open the popup when needed
this.leafletLayersPerId.set(feature.properties.id, {feature: feature, leafletlayer: leafletLayer})
}
private CreateGeojsonLayer(): L.Layer {

View file

@ -6,4 +6,5 @@ export interface ShowDataLayerOptions {
leafletMap: UIEventSource<L.Map>,
enablePopups?: true | boolean,
zoomToFeatures?: false | boolean,
doShowLayer?: UIEventSource<boolean>
}

View file

@ -0,0 +1,79 @@
import FeatureSource, {Tiled} from "../../Logic/FeatureSource/FeatureSource";
import {UIEventSource} from "../../Logic/UIEventSource";
import {Utils} from "../../Utils";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ShowDataLayer from "./ShowDataLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import {GeoOperations} from "../../Logic/GeoOperations";
import {Tiles} from "../../Models/TileRange";
export default class ShowTileInfo {
public static readonly styling = new LayerConfig({
id: "tileinfo_styling",
title: {
render: "Tile {z}/{x}/{y}"
},
tagRenderings: [
"all_tags"
],
source: {
osmTags: "tileId~*"
},
color: {"render": "#3c3"},
width: {
"render": "1"
},
label: {
render: "<div class='rounded-full text-xl font-bold' style='width: 2rem; height: 2rem; background: white'>{count}</div>"
}
}, "tileinfo", true)
constructor(options: {
source: FeatureSource & Tiled, leafletMap: UIEventSource<any>, layer?: LayerConfig,
doShowLayer?: UIEventSource<boolean>
}) {
const source = options.source
const metaFeature: UIEventSource<any[]> =
source.features.map(features => {
const bbox = source.bbox
const [z, x, y] = Tiles.tile_from_index(source.tileIndex)
const box = {
"type": "Feature",
"properties": {
"z": z,
"x": x,
"y": y,
"tileIndex": source.tileIndex,
"source": source.name,
"count": features.length,
tileId: source.name + "/" + source.tileIndex
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[bbox.minLon, bbox.minLat],
[bbox.minLon, bbox.maxLat],
[bbox.maxLon, bbox.maxLat],
[bbox.maxLon, bbox.minLat],
[bbox.minLon, bbox.minLat]
]
]
}
}
const center = GeoOperations.centerpoint(box)
return [box, center]
})
new ShowDataLayer({
layerToShow: ShowTileInfo.styling,
features: new StaticFeatureSource(metaFeature, false),
leafletMap: options.leafletMap,
doShowLayer: options.doShowLayer
})
}
}