mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
perf(map): optimize DeckGLMap pan/zoom by deferring work off hot path (#620)
- Guard filterByTime() with layer enablement checks (skip 9/11 calls when layers disabled) - Defer getLeaves() from viewport change to click time (lazy-load cluster items on demand) - Pre-aggregate riotTimeMs in supercluster map/reduce to avoid getLeaves for riot pulse - Hoist iconMapping objects to module-level constants (stable references prevent atlas rebuild) - Precompute conflict zones GeoJSON at module level (constant data, no per-render allocation) - Remove 10 of 11 redundant ghost pick layers (pickingRadius:10 provides sufficient tolerance) - Cache datacenterSCSource to avoid redundant filter on every click/viewport change
This commit is contained in:
@@ -250,6 +250,19 @@ const MARKER_ICONS = {
|
||||
star: 'data:image/svg+xml;base64,' + btoa(`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><polygon points="16,2 20,12 30,12 22,19 25,30 16,23 7,30 10,19 2,12 12,12" fill="white"/></svg>`),
|
||||
};
|
||||
|
||||
const BASES_ICON_MAPPING = { triangleUp: { x: 0, y: 0, width: 32, height: 32, mask: true } };
|
||||
const NUCLEAR_ICON_MAPPING = { hexagon: { x: 0, y: 0, width: 32, height: 32, mask: true } };
|
||||
const DATACENTER_ICON_MAPPING = { square: { x: 0, y: 0, width: 32, height: 32, mask: true } };
|
||||
|
||||
const CONFLICT_ZONES_GEOJSON: GeoJSON.FeatureCollection = {
|
||||
type: 'FeatureCollection',
|
||||
features: CONFLICT_ZONES.map(zone => ({
|
||||
type: 'Feature' as const,
|
||||
properties: { id: zone.id, name: zone.name, intensity: zone.intensity },
|
||||
geometry: { type: 'Polygon' as const, coordinates: [zone.coords] },
|
||||
})),
|
||||
};
|
||||
|
||||
export class DeckGLMap {
|
||||
private static readonly MAX_CLUSTER_LEAVES = 200;
|
||||
|
||||
@@ -335,6 +348,7 @@ export class DeckGLMap {
|
||||
private techHQSC: Supercluster | null = null;
|
||||
private techEventSC: Supercluster | null = null;
|
||||
private datacenterSC: Supercluster | null = null;
|
||||
private datacenterSCSource: AIDataCenter[] = [];
|
||||
private protestClusters: MapProtestCluster[] = [];
|
||||
private techHQClusters: MapTechHQCluster[] = [];
|
||||
private techEventClusters: MapTechEventCluster[] = [];
|
||||
@@ -630,8 +644,10 @@ export class DeckGLMap {
|
||||
country: p.country,
|
||||
severity: p.severity,
|
||||
eventType: p.eventType,
|
||||
sourceType: p.sourceType,
|
||||
validated: Boolean(p.validated),
|
||||
fatalities: Number.isFinite(p.fatalities) ? Number(p.fatalities) : 0,
|
||||
timeMs: p.time.getTime(),
|
||||
},
|
||||
}));
|
||||
this.protestSC = new Supercluster({
|
||||
@@ -645,6 +661,7 @@ export class DeckGLMap {
|
||||
highSeverityCount: props.severity === 'high' ? 1 : 0,
|
||||
verifiedCount: props.validated ? 1 : 0,
|
||||
totalFatalities: Number(props.fatalities ?? 0) || 0,
|
||||
riotTimeMs: props.eventType === 'riot' && props.sourceType !== 'gdelt' && Number.isFinite(Number(props.timeMs)) ? Number(props.timeMs) : 0,
|
||||
}),
|
||||
reduce: (acc: Record<string, unknown>, props: Record<string, unknown>) => {
|
||||
acc.maxSeverityRank = Math.max(Number(acc.maxSeverityRank ?? 0), Number(props.maxSeverityRank ?? 0));
|
||||
@@ -652,6 +669,9 @@ export class DeckGLMap {
|
||||
acc.highSeverityCount = Number(acc.highSeverityCount ?? 0) + Number(props.highSeverityCount ?? 0);
|
||||
acc.verifiedCount = Number(acc.verifiedCount ?? 0) + Number(props.verifiedCount ?? 0);
|
||||
acc.totalFatalities = Number(acc.totalFatalities ?? 0) + Number(props.totalFatalities ?? 0);
|
||||
const accRiot = Number(acc.riotTimeMs ?? 0);
|
||||
const propRiot = Number(props.riotTimeMs ?? 0);
|
||||
acc.riotTimeMs = Number.isFinite(propRiot) ? Math.max(accRiot, propRiot) : accRiot;
|
||||
if (!acc.country && props.country) acc.country = props.country;
|
||||
},
|
||||
});
|
||||
@@ -733,6 +753,7 @@ export class DeckGLMap {
|
||||
|
||||
private rebuildDatacenterSupercluster(): void {
|
||||
const activeDCs = AI_DATA_CENTERS.filter(dc => dc.status !== 'decommissioned');
|
||||
this.datacenterSCSource = activeDCs;
|
||||
const points = activeDCs.map((dc, i) => ({
|
||||
type: 'Feature' as const,
|
||||
geometry: { type: 'Point' as const, coordinates: [dc.lon, dc.lat] as [number, number] },
|
||||
@@ -791,34 +812,29 @@ export class DeckGLMap {
|
||||
const coords = f.geometry.coordinates as [number, number];
|
||||
if (f.properties.cluster) {
|
||||
const props = f.properties as Record<string, unknown>;
|
||||
const leaves = this.protestSC!.getLeaves(f.properties.cluster_id!, DeckGLMap.MAX_CLUSTER_LEAVES);
|
||||
const items = leaves.map(l => this.protestSuperclusterSource[l.properties.index]).filter((x): x is SocialUnrestEvent => !!x);
|
||||
const maxSeverityRank = Number(props.maxSeverityRank ?? 0);
|
||||
const maxSev = maxSeverityRank >= 2 ? 'high' : maxSeverityRank === 1 ? 'medium' : 'low';
|
||||
const riotCount = Number(props.riotCount ?? 0);
|
||||
const highSeverityCount = Number(props.highSeverityCount ?? 0);
|
||||
const verifiedCount = Number(props.verifiedCount ?? 0);
|
||||
const totalFatalities = Number(props.totalFatalities ?? 0);
|
||||
const clusterCount = Number(f.properties.point_count ?? items.length);
|
||||
const latestRiotEventTimeMs = items.reduce((max, it) => {
|
||||
if (it.eventType !== 'riot' || it.sourceType === 'gdelt') return max;
|
||||
const ts = it.time.getTime();
|
||||
return Number.isFinite(ts) ? Math.max(max, ts) : max;
|
||||
}, 0);
|
||||
const clusterCount = Number(f.properties.point_count ?? 0);
|
||||
const riotTimeMs = Number(props.riotTimeMs ?? 0);
|
||||
return {
|
||||
id: `pc-${f.properties.cluster_id}`,
|
||||
_clusterId: f.properties.cluster_id!,
|
||||
lat: coords[1], lon: coords[0],
|
||||
count: clusterCount,
|
||||
items,
|
||||
country: String(props.country ?? items[0]?.country ?? ''),
|
||||
items: [] as SocialUnrestEvent[],
|
||||
country: String(props.country ?? ''),
|
||||
maxSeverity: maxSev as 'low' | 'medium' | 'high',
|
||||
hasRiot: riotCount > 0,
|
||||
latestRiotEventTimeMs: latestRiotEventTimeMs || undefined,
|
||||
latestRiotEventTimeMs: riotTimeMs || undefined,
|
||||
totalFatalities,
|
||||
riotCount,
|
||||
highSeverityCount,
|
||||
verifiedCount,
|
||||
sampled: items.length < clusterCount,
|
||||
sampled: clusterCount > DeckGLMap.MAX_CLUSTER_LEAVES,
|
||||
};
|
||||
}
|
||||
const item = this.protestSuperclusterSource[f.properties.index]!;
|
||||
@@ -846,12 +862,10 @@ export class DeckGLMap {
|
||||
const coords = f.geometry.coordinates as [number, number];
|
||||
if (f.properties.cluster) {
|
||||
const props = f.properties as Record<string, unknown>;
|
||||
const leaves = this.techHQSC!.getLeaves(f.properties.cluster_id!, DeckGLMap.MAX_CLUSTER_LEAVES);
|
||||
const items = leaves.map(l => TECH_HQS[l.properties.index]).filter(Boolean) as typeof TECH_HQS;
|
||||
const faangCount = Number(props.faangCount ?? 0);
|
||||
const unicornCount = Number(props.unicornCount ?? 0);
|
||||
const publicCount = Number(props.publicCount ?? 0);
|
||||
const clusterCount = Number(f.properties.point_count ?? items.length);
|
||||
const clusterCount = Number(f.properties.point_count ?? 0);
|
||||
const primaryType = faangCount >= unicornCount && faangCount >= publicCount
|
||||
? 'faang'
|
||||
: unicornCount >= publicCount
|
||||
@@ -859,16 +873,17 @@ export class DeckGLMap {
|
||||
: 'public';
|
||||
return {
|
||||
id: `hc-${f.properties.cluster_id}`,
|
||||
_clusterId: f.properties.cluster_id!,
|
||||
lat: coords[1], lon: coords[0],
|
||||
count: clusterCount,
|
||||
items,
|
||||
city: String(props.city ?? items[0]?.city ?? ''),
|
||||
country: String(props.country ?? items[0]?.country ?? ''),
|
||||
items: [] as import('@/config/tech-geo').TechHQ[],
|
||||
city: String(props.city ?? ''),
|
||||
country: String(props.country ?? ''),
|
||||
primaryType,
|
||||
faangCount,
|
||||
unicornCount,
|
||||
publicCount,
|
||||
sampled: items.length < clusterCount,
|
||||
sampled: clusterCount > DeckGLMap.MAX_CLUSTER_LEAVES,
|
||||
};
|
||||
}
|
||||
const item = TECH_HQS[f.properties.index]!;
|
||||
@@ -891,21 +906,20 @@ export class DeckGLMap {
|
||||
const coords = f.geometry.coordinates as [number, number];
|
||||
if (f.properties.cluster) {
|
||||
const props = f.properties as Record<string, unknown>;
|
||||
const leaves = this.techEventSC!.getLeaves(f.properties.cluster_id!, DeckGLMap.MAX_CLUSTER_LEAVES);
|
||||
const items = leaves.map(l => this.techEvents[l.properties.index]).filter((x): x is TechEventMarker => !!x);
|
||||
const clusterCount = Number(f.properties.point_count ?? items.length);
|
||||
const clusterCount = Number(f.properties.point_count ?? 0);
|
||||
const soonestDaysUntil = Number(props.soonestDaysUntil ?? Number.MAX_SAFE_INTEGER);
|
||||
const soonCount = Number(props.soonCount ?? 0);
|
||||
return {
|
||||
id: `ec-${f.properties.cluster_id}`,
|
||||
_clusterId: f.properties.cluster_id!,
|
||||
lat: coords[1], lon: coords[0],
|
||||
count: clusterCount,
|
||||
items,
|
||||
location: String(props.location ?? items[0]?.location ?? ''),
|
||||
country: String(props.country ?? items[0]?.country ?? ''),
|
||||
items: [] as TechEventMarker[],
|
||||
location: String(props.location ?? ''),
|
||||
country: String(props.country ?? ''),
|
||||
soonestDaysUntil: Number.isFinite(soonestDaysUntil) ? soonestDaysUntil : Number.MAX_SAFE_INTEGER,
|
||||
soonCount,
|
||||
sampled: items.length < clusterCount,
|
||||
sampled: clusterCount > DeckGLMap.MAX_CLUSTER_LEAVES,
|
||||
};
|
||||
}
|
||||
const item = this.techEvents[f.properties.index]!;
|
||||
@@ -922,31 +936,30 @@ export class DeckGLMap {
|
||||
}
|
||||
|
||||
if (useDatacenterClusters && this.datacenterSC) {
|
||||
const activeDCs = AI_DATA_CENTERS.filter(dc => dc.status !== 'decommissioned');
|
||||
const activeDCs = this.datacenterSCSource;
|
||||
this.datacenterClusters = this.datacenterSC.getClusters(bbox, zoom).map(f => {
|
||||
const coords = f.geometry.coordinates as [number, number];
|
||||
if (f.properties.cluster) {
|
||||
const props = f.properties as Record<string, unknown>;
|
||||
const leaves = this.datacenterSC!.getLeaves(f.properties.cluster_id!, DeckGLMap.MAX_CLUSTER_LEAVES);
|
||||
const items = leaves.map(l => activeDCs[l.properties.index]).filter((x): x is AIDataCenter => !!x);
|
||||
const clusterCount = Number(f.properties.point_count ?? items.length);
|
||||
const clusterCount = Number(f.properties.point_count ?? 0);
|
||||
const existingCount = Number(props.existingCount ?? 0);
|
||||
const plannedCount = Number(props.plannedCount ?? 0);
|
||||
const totalChips = Number(props.totalChips ?? 0);
|
||||
const totalPowerMW = Number(props.totalPowerMW ?? 0);
|
||||
return {
|
||||
id: `dc-${f.properties.cluster_id}`,
|
||||
_clusterId: f.properties.cluster_id!,
|
||||
lat: coords[1], lon: coords[0],
|
||||
count: clusterCount,
|
||||
items,
|
||||
region: String(props.country ?? items[0]?.country ?? ''),
|
||||
country: String(props.country ?? items[0]?.country ?? ''),
|
||||
items: [] as AIDataCenter[],
|
||||
region: String(props.country ?? ''),
|
||||
country: String(props.country ?? ''),
|
||||
totalChips,
|
||||
totalPowerMW,
|
||||
majorityExisting: existingCount >= Math.max(1, clusterCount / 2),
|
||||
existingCount,
|
||||
plannedCount,
|
||||
sampled: items.length < clusterCount,
|
||||
sampled: clusterCount > DeckGLMap.MAX_CLUSTER_LEAVES,
|
||||
};
|
||||
}
|
||||
const item = activeDCs[f.properties.index]!;
|
||||
@@ -981,17 +994,17 @@ export class DeckGLMap {
|
||||
COLORS = getOverlayColors();
|
||||
const layers: (Layer | null | false)[] = [];
|
||||
const { layers: mapLayers } = this.state;
|
||||
const filteredEarthquakes = this.filterByTime(this.earthquakes, (eq) => eq.occurredAt);
|
||||
const filteredNaturalEvents = this.filterByTime(this.naturalEvents, (event) => event.date);
|
||||
const filteredWeatherAlerts = this.filterByTime(this.weatherAlerts, (alert) => alert.onset);
|
||||
const filteredOutages = this.filterByTime(this.outages, (outage) => outage.pubDate);
|
||||
const filteredCableAdvisories = this.filterByTime(this.cableAdvisories, (advisory) => advisory.reported);
|
||||
const filteredFlightDelays = this.filterByTime(this.flightDelays, (delay) => delay.updatedAt);
|
||||
const filteredMilitaryFlights = this.filterByTime(this.militaryFlights, (flight) => flight.lastSeen);
|
||||
const filteredMilitaryVessels = this.filterByTime(this.militaryVessels, (vessel) => vessel.lastAisUpdate);
|
||||
const filteredMilitaryFlightClusters = this.filterMilitaryFlightClustersByTime(this.militaryFlightClusters);
|
||||
const filteredMilitaryVesselClusters = this.filterMilitaryVesselClustersByTime(this.militaryVesselClusters);
|
||||
const filteredUcdpEvents = this.filterByTime(this.ucdpEvents, (event) => event.date_start);
|
||||
const filteredEarthquakes = mapLayers.natural ? this.filterByTime(this.earthquakes, (eq) => eq.occurredAt) : [];
|
||||
const filteredNaturalEvents = mapLayers.natural ? this.filterByTime(this.naturalEvents, (event) => event.date) : [];
|
||||
const filteredWeatherAlerts = mapLayers.weather ? this.filterByTime(this.weatherAlerts, (alert) => alert.onset) : [];
|
||||
const filteredOutages = mapLayers.outages ? this.filterByTime(this.outages, (outage) => outage.pubDate) : [];
|
||||
const filteredCableAdvisories = mapLayers.cables ? this.filterByTime(this.cableAdvisories, (advisory) => advisory.reported) : [];
|
||||
const filteredFlightDelays = mapLayers.flights ? this.filterByTime(this.flightDelays, (delay) => delay.updatedAt) : [];
|
||||
const filteredMilitaryFlights = mapLayers.military ? this.filterByTime(this.militaryFlights, (flight) => flight.lastSeen) : [];
|
||||
const filteredMilitaryVessels = mapLayers.military ? this.filterByTime(this.militaryVessels, (vessel) => vessel.lastAisUpdate) : [];
|
||||
const filteredMilitaryFlightClusters = mapLayers.military ? this.filterMilitaryFlightClustersByTime(this.militaryFlightClusters) : [];
|
||||
const filteredMilitaryVesselClusters = mapLayers.military ? this.filterMilitaryVesselClustersByTime(this.militaryVesselClusters) : [];
|
||||
const filteredUcdpEvents = mapLayers.ucdpEvents ? this.filterByTime(this.ucdpEvents, (event) => event.date_start) : [];
|
||||
|
||||
// Day/night overlay (rendered first as background)
|
||||
if (mapLayers.dayNight) {
|
||||
@@ -1025,14 +1038,11 @@ export class DeckGLMap {
|
||||
if (mapLayers.bases && this.isLayerVisible('bases')) {
|
||||
layers.push(this.createBasesLayer());
|
||||
layers.push(...this.createBasesClusterLayer());
|
||||
const basesData = this.getBasesData();
|
||||
layers.push(this.createGhostLayer('bases-layer', basesData, d => [d.lon, d.lat], { radiusMinPixels: 12 }));
|
||||
}
|
||||
|
||||
// Nuclear facilities layer — hidden at low zoom + ghost
|
||||
if (mapLayers.nuclear && this.isLayerVisible('nuclear')) {
|
||||
layers.push(this.createNuclearLayer());
|
||||
layers.push(this.createGhostLayer('nuclear-layer', NUCLEAR_FACILITIES.filter(f => f.status !== 'decommissioned'), d => [d.lon, d.lat], { radiusMinPixels: 12 }));
|
||||
}
|
||||
|
||||
// Gamma irradiators layer — hidden at low zoom
|
||||
@@ -1063,7 +1073,6 @@ export class DeckGLMap {
|
||||
// Earthquakes layer + ghost for easier picking
|
||||
if (mapLayers.natural && filteredEarthquakes.length > 0) {
|
||||
layers.push(this.createEarthquakesLayer(filteredEarthquakes));
|
||||
layers.push(this.createGhostLayer('earthquakes-layer', filteredEarthquakes, d => [d.location?.longitude ?? 0, d.location?.latitude ?? 0], { radiusMinPixels: 12 }));
|
||||
}
|
||||
|
||||
// Natural events layer
|
||||
@@ -1090,13 +1099,11 @@ export class DeckGLMap {
|
||||
// Internet outages layer + ghost for easier picking
|
||||
if (mapLayers.outages && filteredOutages.length > 0) {
|
||||
layers.push(this.createOutagesLayer(filteredOutages));
|
||||
layers.push(this.createGhostLayer('outages-layer', filteredOutages, d => [d.lon, d.lat], { radiusMinPixels: 12 }));
|
||||
}
|
||||
|
||||
// Cyber threat IOC layer
|
||||
if (mapLayers.cyberThreats && this.cyberThreats.length > 0) {
|
||||
layers.push(this.createCyberThreatsLayer());
|
||||
layers.push(this.createGhostLayer('cyber-threats-layer', this.cyberThreats, d => [d.lon, d.lat], { radiusMinPixels: 12 }));
|
||||
}
|
||||
|
||||
// AIS density layer
|
||||
@@ -1352,21 +1359,9 @@ export class DeckGLMap {
|
||||
private createConflictZonesLayer(): GeoJsonLayer {
|
||||
const cacheKey = 'conflict-zones-layer';
|
||||
|
||||
const geojsonData = {
|
||||
type: 'FeatureCollection' as const,
|
||||
features: CONFLICT_ZONES.map(zone => ({
|
||||
type: 'Feature' as const,
|
||||
properties: { id: zone.id, name: zone.name, intensity: zone.intensity },
|
||||
geometry: {
|
||||
type: 'Polygon' as const,
|
||||
coordinates: [zone.coords],
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
const layer = new GeoJsonLayer({
|
||||
id: cacheKey,
|
||||
data: geojsonData,
|
||||
data: CONFLICT_ZONES_GEOJSON,
|
||||
filled: true,
|
||||
stroked: true,
|
||||
getFillColor: () => COLORS.conflict,
|
||||
@@ -1410,7 +1405,7 @@ export class DeckGLMap {
|
||||
getPosition: (d) => [d.lon, d.lat],
|
||||
getIcon: () => 'triangleUp',
|
||||
iconAtlas: MARKER_ICONS.triangleUp,
|
||||
iconMapping: { triangleUp: { x: 0, y: 0, width: 32, height: 32, mask: true } },
|
||||
iconMapping: BASES_ICON_MAPPING,
|
||||
getSize: (d) => highlightedBases.has(d.id) ? 16 : 11,
|
||||
getColor: (d) => {
|
||||
if (highlightedBases.has(d.id)) {
|
||||
@@ -1468,7 +1463,7 @@ export class DeckGLMap {
|
||||
getPosition: (d) => [d.lon, d.lat],
|
||||
getIcon: () => 'hexagon',
|
||||
iconAtlas: MARKER_ICONS.hexagon,
|
||||
iconMapping: { hexagon: { x: 0, y: 0, width: 32, height: 32, mask: true } },
|
||||
iconMapping: NUCLEAR_ICON_MAPPING,
|
||||
getSize: (d) => highlightedNuclear.has(d.id) ? 15 : 11,
|
||||
getColor: (d) => {
|
||||
if (highlightedNuclear.has(d.id)) {
|
||||
@@ -1583,7 +1578,7 @@ export class DeckGLMap {
|
||||
getPosition: (d) => [d.lon, d.lat],
|
||||
getIcon: () => 'square',
|
||||
iconAtlas: MARKER_ICONS.square,
|
||||
iconMapping: { square: { x: 0, y: 0, width: 32, height: 32, mask: true } },
|
||||
iconMapping: DATACENTER_ICON_MAPPING,
|
||||
getSize: (d) => highlightedDC.has(d.id) ? 14 : 10,
|
||||
getColor: (d) => {
|
||||
if (highlightedDC.has(d.id)) {
|
||||
@@ -2103,8 +2098,6 @@ export class DeckGLMap {
|
||||
updateTriggers: { getRadius: this.lastSCZoom, getFillColor: this.lastSCZoom },
|
||||
}));
|
||||
|
||||
layers.push(this.createGhostLayer('protest-clusters-layer', this.protestClusters, d => [d.lon, d.lat], { radiusMinPixels: 14 }));
|
||||
|
||||
const multiClusters = this.protestClusters.filter(c => c.count > 1);
|
||||
if (multiClusters.length > 0) {
|
||||
layers.push(new TextLayer<MapProtestCluster>({
|
||||
@@ -2168,8 +2161,6 @@ export class DeckGLMap {
|
||||
updateTriggers: { getRadius: this.lastSCZoom },
|
||||
}));
|
||||
|
||||
layers.push(this.createGhostLayer('tech-hq-clusters-layer', this.techHQClusters, d => [d.lon, d.lat], { radiusMinPixels: 14 }));
|
||||
|
||||
const multiClusters = this.techHQClusters.filter(c => c.count > 1);
|
||||
if (multiClusters.length > 0) {
|
||||
layers.push(new TextLayer<MapTechHQCluster>({
|
||||
@@ -2228,8 +2219,6 @@ export class DeckGLMap {
|
||||
updateTriggers: { getRadius: this.lastSCZoom },
|
||||
}));
|
||||
|
||||
layers.push(this.createGhostLayer('tech-event-clusters-layer', this.techEventClusters, d => [d.lon, d.lat], { radiusMinPixels: 14 }));
|
||||
|
||||
const multiClusters = this.techEventClusters.filter(c => c.count > 1);
|
||||
if (multiClusters.length > 0) {
|
||||
layers.push(new TextLayer<MapTechEventCluster>({
|
||||
@@ -2271,8 +2260,6 @@ export class DeckGLMap {
|
||||
updateTriggers: { getRadius: this.lastSCZoom },
|
||||
}));
|
||||
|
||||
layers.push(this.createGhostLayer('datacenter-clusters-layer', this.datacenterClusters, d => [d.lon, d.lat], { radiusMinPixels: 14 }));
|
||||
|
||||
const multiClusters = this.datacenterClusters.filter(c => c.count > 1);
|
||||
if (multiClusters.length > 0) {
|
||||
layers.push(new TextLayer<MapDatacenterCluster>({
|
||||
@@ -2326,8 +2313,6 @@ export class DeckGLMap {
|
||||
lineWidthMinPixels: 2,
|
||||
}));
|
||||
|
||||
layers.push(this.createGhostLayer('hotspots-layer', this.hotspots, d => [d.lon, d.lat], { radiusMinPixels: 14 }));
|
||||
|
||||
const highHotspots = this.hotspots.filter(h => h.level === 'high' || h.hasBreaking);
|
||||
if (highHotspots.length > 0) {
|
||||
const pulse = 1.0 + 0.8 * (0.5 + 0.5 * Math.sin((this.pulseTime || Date.now()) / 400));
|
||||
@@ -2899,6 +2884,16 @@ export class DeckGLMap {
|
||||
// Handle cluster layers with single/multi logic
|
||||
if (layerId === 'protest-clusters-layer') {
|
||||
const cluster = info.object as MapProtestCluster;
|
||||
if (cluster.items.length === 0 && cluster._clusterId != null && this.protestSC) {
|
||||
try {
|
||||
const leaves = this.protestSC.getLeaves(cluster._clusterId, DeckGLMap.MAX_CLUSTER_LEAVES);
|
||||
cluster.items = leaves.map(l => this.protestSuperclusterSource[l.properties.index]).filter((x): x is SocialUnrestEvent => !!x);
|
||||
cluster.sampled = cluster.items.length < cluster.count;
|
||||
} catch (e) {
|
||||
console.warn('[DeckGLMap] stale protest cluster', cluster._clusterId, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cluster.count === 1 && cluster.items[0]) {
|
||||
this.popup.show({ type: 'protest', data: cluster.items[0], x: info.x, y: info.y });
|
||||
} else {
|
||||
@@ -2922,6 +2917,16 @@ export class DeckGLMap {
|
||||
}
|
||||
if (layerId === 'tech-hq-clusters-layer') {
|
||||
const cluster = info.object as MapTechHQCluster;
|
||||
if (cluster.items.length === 0 && cluster._clusterId != null && this.techHQSC) {
|
||||
try {
|
||||
const leaves = this.techHQSC.getLeaves(cluster._clusterId, DeckGLMap.MAX_CLUSTER_LEAVES);
|
||||
cluster.items = leaves.map(l => TECH_HQS[l.properties.index]).filter(Boolean) as typeof TECH_HQS;
|
||||
cluster.sampled = cluster.items.length < cluster.count;
|
||||
} catch (e) {
|
||||
console.warn('[DeckGLMap] stale techHQ cluster', cluster._clusterId, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cluster.count === 1 && cluster.items[0]) {
|
||||
this.popup.show({ type: 'techHQ', data: cluster.items[0], x: info.x, y: info.y });
|
||||
} else {
|
||||
@@ -2945,6 +2950,16 @@ export class DeckGLMap {
|
||||
}
|
||||
if (layerId === 'tech-event-clusters-layer') {
|
||||
const cluster = info.object as MapTechEventCluster;
|
||||
if (cluster.items.length === 0 && cluster._clusterId != null && this.techEventSC) {
|
||||
try {
|
||||
const leaves = this.techEventSC.getLeaves(cluster._clusterId, DeckGLMap.MAX_CLUSTER_LEAVES);
|
||||
cluster.items = leaves.map(l => this.techEvents[l.properties.index]).filter((x): x is TechEventMarker => !!x);
|
||||
cluster.sampled = cluster.items.length < cluster.count;
|
||||
} catch (e) {
|
||||
console.warn('[DeckGLMap] stale techEvent cluster', cluster._clusterId, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cluster.count === 1 && cluster.items[0]) {
|
||||
this.popup.show({ type: 'techEvent', data: cluster.items[0], x: info.x, y: info.y });
|
||||
} else {
|
||||
@@ -2966,6 +2981,16 @@ export class DeckGLMap {
|
||||
}
|
||||
if (layerId === 'datacenter-clusters-layer') {
|
||||
const cluster = info.object as MapDatacenterCluster;
|
||||
if (cluster.items.length === 0 && cluster._clusterId != null && this.datacenterSC) {
|
||||
try {
|
||||
const leaves = this.datacenterSC.getLeaves(cluster._clusterId, DeckGLMap.MAX_CLUSTER_LEAVES);
|
||||
cluster.items = leaves.map(l => this.datacenterSCSource[l.properties.index]).filter((x): x is AIDataCenter => !!x);
|
||||
cluster.sampled = cluster.items.length < cluster.count;
|
||||
} catch (e) {
|
||||
console.warn('[DeckGLMap] stale datacenter cluster', cluster._clusterId, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cluster.count === 1 && cluster.items[0]) {
|
||||
this.popup.show({ type: 'datacenter', data: cluster.items[0], x: info.x, y: info.y });
|
||||
} else {
|
||||
|
||||
@@ -1243,6 +1243,7 @@ export interface GulfInvestment {
|
||||
|
||||
export interface MapProtestCluster {
|
||||
id: string;
|
||||
_clusterId?: number;
|
||||
lat: number;
|
||||
lon: number;
|
||||
count: number;
|
||||
@@ -1260,6 +1261,7 @@ export interface MapProtestCluster {
|
||||
|
||||
export interface MapTechHQCluster {
|
||||
id: string;
|
||||
_clusterId?: number;
|
||||
lat: number;
|
||||
lon: number;
|
||||
count: number;
|
||||
@@ -1275,6 +1277,7 @@ export interface MapTechHQCluster {
|
||||
|
||||
export interface MapTechEventCluster {
|
||||
id: string;
|
||||
_clusterId?: number;
|
||||
lat: number;
|
||||
lon: number;
|
||||
count: number;
|
||||
@@ -1288,6 +1291,7 @@ export interface MapTechEventCluster {
|
||||
|
||||
export interface MapDatacenterCluster {
|
||||
id: string;
|
||||
_clusterId?: number;
|
||||
lat: number;
|
||||
lon: number;
|
||||
count: number;
|
||||
|
||||
Reference in New Issue
Block a user