Skip to content
3 changes: 2 additions & 1 deletion src/renderer/Clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const canCopy = id =>
ID.isLayerId(id) ||
ID.isFeatureId(id) ||
ID.isMarkerId(id) ||
ID.isTileServiceId(id)
ID.isTileServiceId(id) ||
ID.isSSEServiceId(id)

/**
* @async implicit
Expand Down
75 changes: 72 additions & 3 deletions src/renderer/components/map/SSEVectorSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class SSEVectorSource extends VectorSource {
strategy: function (extent, resolution) {
if (state.resolution !== resolution) {
state.resolution = resolution
this.getFeatures().map(feature => feature.$.centerResolution(resolution))
this.getFeatures().forEach(feature => feature.$?.centerResolution(resolution))
}
return [extent]
}
Expand Down Expand Up @@ -128,6 +128,30 @@ class SSEVectorSource extends VectorSource {
*/
this.useFeatureIds = options.useFeatureIds !== false

/**
* Render mode: 'vector' for normal rendering, 'heatmap' for density visualization
* @type {string}
*/
this.renderMode = options.renderMode || 'vector'

/**
* Rate limiting interval for heatmap accumulation in milliseconds
* @type {number}
*/
this.heatmapInterval = options.heatmapInterval || 1000

/**
* Timestamp of last heatmap accumulation (ms since epoch)
* @type {number}
*/
this.lastHeatmapUpdate = 0

/**
* Maximum number of accumulated heatmap features before oldest are dropped
* @type {number}
*/
this.maxHeatmapFeatures = options.maxHeatmapFeatures || 50000

/**
* Prefix added to feature IDs
* @type {string}
Expand Down Expand Up @@ -233,7 +257,13 @@ class SSEVectorSource extends VectorSource {
try {
/** @type {GeoJSONData} */
const data = JSON.parse(event.data)
if (data.id) {
if (data.type === 'FeatureCollection' && data.features) {
data.features.forEach(feature => {
if (feature.id) {
feature.id = `${this.idPrefix}${feature.id}`
}
})
} else if (data.id) {
data.id = `${this.idPrefix}${data.id}`
}
if (this.onMessage) {
Expand Down Expand Up @@ -279,6 +309,8 @@ class SSEVectorSource extends VectorSource {
this.timerId = null
}

this.lastHeatmapUpdate = 0

this.dispatchEvent({
type: 'sse-disconnected',
source: this
Expand Down Expand Up @@ -330,7 +362,13 @@ class SSEVectorSource extends VectorSource {
console.warn('Unknown feature type ' + this.pendingData.type)
}

if (this.useFeatureIds) {
if (this.renderMode === 'heatmap') {
const now = Date.now()
if (now - this.lastHeatmapUpdate >= this.heatmapInterval) {
this.accumulateFeatures(features)
this.lastHeatmapUpdate = now
}
} else if (this.useFeatureIds) {
this.updateFeaturesById(features)
} else {
this.replaceAllFeatures(features)
Expand Down Expand Up @@ -395,6 +433,37 @@ class SSEVectorSource extends VectorSource {
this.addFeatures(olFeatures)
}

/**
* Accumulates features without clearing existing ones (for heatmap mode).
* Uses lightweight format.readFeature() instead of signal-based featureReader.
* Maps properties.confidence to a 'weight' property for heatmap rendering.
*
* @param {GeoJSONFeature[]} features - Array of GeoJSON features
* @returns {void}
*/
accumulateFeatures (features) {
const olFeatures = features.map(feature => {
const reprojectedFeature = {
...feature,
geometry: this.reprojectGeometry(feature.geometry)
}
const olFeature = format.readFeature(reprojectedFeature)
const confidence = feature.properties?.confidence
olFeature.set('weight', confidence != null ? confidence : 0.5)
return olFeature
})
this.addFeatures(olFeatures)

// Evict oldest features when cap is exceeded
const all = this.getFeatures()
const excess = all.length - this.maxHeatmapFeatures
if (excess > 0) {
for (let i = 0; i < excess; i++) {
this.removeFeature(all[i])
}
}
}

/**
* Updates existing features or adds new ones based on feature ID.
* Features without IDs are skipped with a warning.
Expand Down
29 changes: 29 additions & 0 deletions src/renderer/components/properties/SSEServiceProperties.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
.sse-section-label {
font-size: 0.85rem;
font-weight: 500;
color: #666;
}

.sse-render-mode {
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px 0;
}

.sse-radio-group {
display: flex;
gap: 16px;
}

.sse-radio-label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
}

.sse-radio-label input:disabled + span {
color: #999;
}

.sse-option-row {
display: flex;
align-items: center;
Expand Down
Loading
Loading