forked from MapComplete/MapComplete
Add initial clustering per tile, very broken
This commit is contained in:
parent
2b78c4b53f
commit
c5e9448720
88 changed files with 1080 additions and 651 deletions
156
UI/ShowDataLayer/PerTileCountAggregator.ts
Normal file
156
UI/ShowDataLayer/PerTileCountAggregator.ts
Normal 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])
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -6,4 +6,5 @@ export interface ShowDataLayerOptions {
|
|||
leafletMap: UIEventSource<L.Map>,
|
||||
enablePopups?: true | boolean,
|
||||
zoomToFeatures?: false | boolean,
|
||||
doShowLayer?: UIEventSource<boolean>
|
||||
}
|
79
UI/ShowDataLayer/ShowTileInfo.ts
Normal file
79
UI/ShowDataLayer/ShowTileInfo.ts
Normal 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
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue