Skip to content

Add random colors and multi-layer popups to PMTiles control#77

Merged
giswqs merged 1 commit intomainfrom
random-colors-multi-layer-popup
Mar 9, 2026
Merged

Add random colors and multi-layer popups to PMTiles control#77
giswqs merged 1 commit intomainfrom
random-colors-multi-layer-popup

Conversation

@giswqs
Copy link
Copy Markdown
Member

@giswqs giswqs commented Mar 9, 2026

Summary

  • Generate distinct colors per source layer using golden angle hue distribution instead of using the same default color (steelblue/#333) for all layers, making it easy to visually distinguish different layers
  • Show all overlapping features at a click point in a single popup grouped by source layer name, instead of only showing the first feature found
  • Use smaller circle radius (2px with stroke) for point layers for cleaner visualization

Changes

  • src/lib/utils/color.ts: Add generateDistinctColors() and hslToHex() functions
  • src/lib/utils/index.ts: Export generateDistinctColors
  • src/lib/core/PMTilesLayer.ts: Use distinct colors per source layer in _addLayer(), rewrite _setupClickHandler() to query all layers and group features by source layer
  • src/lib/core/types.ts: Add sourceLayerColors field to PMTilesLayerInfo
  • src/lib/styles/pmtiles-layer.css: Update popup styles for multi-layer display with section headers, proper table layout, and last-row border removal

Test plan

  • Load a vector PMTiles file and verify each source layer gets a unique color
  • Click on an area where multiple layers overlap and verify all layers appear in one popup
  • Verify popup section headers show friendly layer names
  • Verify raster PMTiles still work correctly (no color changes)
  • Verify the layer control adapter still groups layers correctly

- 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
Copilot AI review requested due to automatic review settings March 9, 2026 01:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +460 to +473
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 || {},
});
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +460 to +465
for (const layerId of pickableLayerIds) {
if (!this._map.getLayer(layerId)) continue;
const features = this._map.queryRenderedFeatures(e.point, {
layers: [layerId],
});
if (features.length > 0) {
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +488 to +503
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>`;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/utils/color.ts
Comment on lines +53 to +62
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;
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@giswqs giswqs merged commit 8b156a6 into main Mar 9, 2026
6 checks passed
@giswqs giswqs deleted the random-colors-multi-layer-popup branch March 9, 2026 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants