Add random colors and multi-layer popups to PMTiles control#77
Conversation
- Generate distinct colors per source layer using golden angle hue distribution instead of using the same default color for all layers - Show all overlapping features in a single popup grouped by source layer name, instead of only showing the first feature - Update popup CSS for multi-layer display with section headers - Add generateDistinctColors utility function - Track sourceLayerColors in PMTilesLayerInfo - Use smaller circle radius (2px) with stroke for point layers
There was a problem hiding this comment.
Pull request overview
This PR enhances the PMTiles layer control’s visualization and feature inspection by assigning distinct colors per vector source layer and improving click popups to display information from multiple overlapping layers.
Changes:
- Added distinct color generation utilities using golden-angle hue distribution.
- Updated PMTiles vector styling to apply per-source-layer colors and adjusted point styling to smaller circles with stroke.
- Reworked click popup rendering and updated popup CSS for multi-layer sectioned display.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/utils/index.ts | Re-exports the new distinct color generator from utils. |
| src/lib/utils/color.ts | Adds generateDistinctColors() (and internal HSL→hex conversion) to produce visually distinct layer colors. |
| src/lib/core/PMTilesLayer.ts | Applies distinct colors per vector source layer; updates click handler to build a combined popup across layers. |
| src/lib/core/types.ts | Extends PMTilesLayerInfo with optional sourceLayerColors for tracking assigned colors. |
| src/lib/styles/pmtiles-layer.css | Updates popup styling for multi-layer/sectioned layout and improved table formatting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (const layerId of pickableLayerIds) { | ||
| if (!this._map.getLayer(layerId)) continue; | ||
| const features = this._map.queryRenderedFeatures(e.point, { | ||
| layers: [layerId], | ||
| }); | ||
| if (features.length > 0) { | ||
| const sl = features[0].sourceLayer || layerId; | ||
| if (!seenSourceLayers.has(sl)) { | ||
| seenSourceLayers.add(sl); | ||
| layerFeatures.push({ | ||
| sourceLayer: sl, | ||
| properties: features[0].properties || {}, | ||
| }); | ||
| } |
There was a problem hiding this comment.
The click handler only captures features[0] per style layer and then de-dupes by sourceLayer, so multiple overlapping features (even within the same source layer) at the click point will be dropped. Consider collecting all returned features and grouping them (e.g., by feature.id/properties + sourceLayer) so the popup truly shows all overlapping features per source layer.
| for (const layerId of pickableLayerIds) { | ||
| if (!this._map.getLayer(layerId)) continue; | ||
| const features = this._map.queryRenderedFeatures(e.point, { | ||
| layers: [layerId], | ||
| }); | ||
| if (features.length > 0) { |
There was a problem hiding this comment.
queryRenderedFeatures is called once per layerId in pickableLayerIds. This turns a single click into N queries (3× per source-layer), which can become noticeably expensive as more PMTiles sources/layers are added. Prefer a single queryRenderedFeatures(e.point, { layers: pickableLayerIds }) call and then group the results in-memory by sourceLayer (and feature).
| for (const { sourceLayer, properties } of layerFeatures) { | ||
| const friendlyName = sourceLayer.replace(/[-_]/g, " "); | ||
| html += `<div class="maplibre-gl-pmtiles-popup-header">${friendlyName}</div>`; | ||
|
|
||
| const propEntries = Object.entries(properties); | ||
| if (propEntries.length === 0) { | ||
| html += | ||
| '<div class="maplibre-gl-pmtiles-popup-empty">No properties</div>'; | ||
| } else { | ||
| html += '<table class="maplibre-gl-pmtiles-popup-table">'; | ||
| for (const [key, value] of propEntries) { | ||
| const displayValue = | ||
| typeof value === "object" | ||
| ? JSON.stringify(value) | ||
| : String(value); | ||
| html += `<tr><td class="maplibre-gl-pmtiles-popup-key">${key}</td><td class="maplibre-gl-pmtiles-popup-value">${displayValue}</td></tr>`; |
There was a problem hiding this comment.
Popup HTML is constructed via string interpolation with unescaped sourceLayer, property keys, and property values, then passed to Popup#setHTML. If any attribute contains </& etc., this can lead to HTML injection/XSS. Please escape these fields (there are _escapeHtml helpers elsewhere in the codebase) or build the popup DOM using textContent instead of raw HTML.
| export function generateDistinctColors(count: number): string[] { | ||
| const colors: string[] = []; | ||
| for (let i = 0; i < count; i++) { | ||
| const hue = (i * 137.508) % 360; | ||
| const saturation = 55 + (i % 3) * 15; | ||
| const lightness = 45 + (i % 2) * 10; | ||
| colors.push(hslToHex(hue, saturation, lightness)); | ||
| } | ||
| return colors; | ||
| } |
There was a problem hiding this comment.
generateDistinctColors() is newly added and exported, but there are no unit tests covering its output shape/validity (e.g., count handling, hex format, uniqueness expectations). Since tests/utils.test.ts already covers the other color utilities, please add tests for this function there.
Summary
steelblue/#333) for all layers, making it easy to visually distinguish different layersChanges
src/lib/utils/color.ts: AddgenerateDistinctColors()andhslToHex()functionssrc/lib/utils/index.ts: ExportgenerateDistinctColorssrc/lib/core/PMTilesLayer.ts: Use distinct colors per source layer in_addLayer(), rewrite_setupClickHandler()to query all layers and group features by source layersrc/lib/core/types.ts: AddsourceLayerColorsfield toPMTilesLayerInfosrc/lib/styles/pmtiles-layer.css: Update popup styles for multi-layer display with section headers, proper table layout, and last-row border removalTest plan