-
Notifications
You must be signed in to change notification settings - Fork 5
Add random colors and multi-layer popups to PMTiles control #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import { | |
| type MapMouseEvent, | ||
| Popup, | ||
| } from "maplibre-gl"; | ||
| import { generateDistinctColors } from "../utils/color"; | ||
| import type { | ||
| PMTilesLayerControlOptions, | ||
| PMTilesLayerControlState, | ||
|
|
@@ -449,40 +450,60 @@ export class PMTilesLayerControl implements IControl { | |
|
|
||
| if (pickableLayerIds.length === 0) return; | ||
|
|
||
| // Query features at click point | ||
| const features = this._map.queryRenderedFeatures(e.point, { | ||
| layers: pickableLayerIds, | ||
| }); | ||
| // Query all features at click point, grouped by source layer | ||
| const seenSourceLayers = new Set<string>(); | ||
| const layerFeatures: Array<{ | ||
| sourceLayer: string; | ||
| properties: Record<string, unknown>; | ||
| }> = []; | ||
|
|
||
| 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 || {}, | ||
| }); | ||
| } | ||
|
Comment on lines
+460
to
+473
|
||
| } | ||
| } | ||
|
|
||
| if (features.length === 0) { | ||
| // Close popup if clicking on empty area | ||
| if (layerFeatures.length === 0) { | ||
| if (this._popup) { | ||
| this._popup.remove(); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // Get the first feature | ||
| const feature = features[0]; | ||
| const properties = feature.properties || {}; | ||
|
|
||
| // Build popup content | ||
| // Build combined popup content for all layers at this point | ||
| let html = '<div class="maplibre-gl-pmtiles-popup">'; | ||
| html += `<div class="maplibre-gl-pmtiles-popup-header">${feature.sourceLayer || "Feature"}</div>`; | ||
| html += '<div class="maplibre-gl-pmtiles-popup-content">'; | ||
|
|
||
| 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>`; | ||
| 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>`; | ||
|
Comment on lines
+488
to
+503
|
||
| } | ||
| html += "</table>"; | ||
| } | ||
| html += "</table>"; | ||
| } | ||
|
|
||
| html += "</div></div>"; | ||
|
|
@@ -1054,15 +1075,25 @@ export class PMTilesLayerControl implements IControl { | |
| ? this._options.beforeId | ||
| : undefined; | ||
|
|
||
| // Track per-source-layer colors for this PMTiles source | ||
| const sourceLayerColors: Record<string, string> = {}; | ||
|
|
||
| if (tileType === "vector") { | ||
| // Use selected source layers if available, otherwise use all | ||
| const layersToRender = | ||
| this._state.selectedSourceLayers.length > 0 | ||
| ? this._state.selectedSourceLayers | ||
| : sourceLayers; | ||
|
|
||
| // Generate distinct colors for each source layer | ||
| const colors = generateDistinctColors(layersToRender.length); | ||
|
|
||
| // Add layers for each source layer | ||
| for (const sourceLayer of layersToRender) { | ||
| for (let i = 0; i < layersToRender.length; i++) { | ||
| const sourceLayer = layersToRender[i]; | ||
| const color = colors[i]; | ||
| sourceLayerColors[sourceLayer] = color; | ||
|
|
||
| // Determine layer type based on geometry (simplified - add all types) | ||
| const fillLayerId = `${sourceId}-${sourceLayer}-fill`; | ||
| const lineLayerId = `${sourceId}-${sourceLayer}-line`; | ||
|
|
@@ -1076,7 +1107,7 @@ export class PMTilesLayerControl implements IControl { | |
| source: sourceId, | ||
| "source-layer": sourceLayer, | ||
| paint: { | ||
| "fill-color": this._options.defaultFillColor, | ||
| "fill-color": color, | ||
| "fill-opacity": this._state.layerOpacity * 0.6, | ||
| }, | ||
| filter: ["==", ["geometry-type"], "Polygon"], | ||
|
|
@@ -1093,7 +1124,7 @@ export class PMTilesLayerControl implements IControl { | |
| source: sourceId, | ||
| "source-layer": sourceLayer, | ||
| paint: { | ||
| "line-color": this._options.defaultLineColor, | ||
| "line-color": color, | ||
| "line-opacity": this._state.layerOpacity, | ||
| "line-width": 1, | ||
| }, | ||
|
|
@@ -1115,9 +1146,11 @@ export class PMTilesLayerControl implements IControl { | |
| source: sourceId, | ||
| "source-layer": sourceLayer, | ||
| paint: { | ||
| "circle-color": this._options.defaultCircleColor, | ||
| "circle-color": color, | ||
| "circle-opacity": this._state.layerOpacity, | ||
| "circle-radius": 4, | ||
| "circle-radius": 2, | ||
| "circle-stroke-color": color, | ||
| "circle-stroke-width": 0.5, | ||
| }, | ||
| filter: ["==", ["geometry-type"], "Point"], | ||
| }, | ||
|
|
@@ -1180,6 +1213,7 @@ export class PMTilesLayerControl implements IControl { | |
| layerIds, | ||
| opacity: this._state.layerOpacity, | ||
| pickable: this._state.pickable, | ||
| sourceLayerColors, | ||
| }; | ||
| this._pmtilesLayers.set(sourceId, layerInfo); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,66 @@ | ||
| import type { ColorStop } from "../core/types"; | ||
|
|
||
| /** | ||
| * Converts an HSL color to a hex string. | ||
| * | ||
| * @param h - Hue (0-360). | ||
| * @param s - Saturation (0-100). | ||
| * @param l - Lightness (0-100). | ||
| * @returns Hex color string with #. | ||
| */ | ||
| function hslToHex(h: number, s: number, l: number): string { | ||
| const sNorm = s / 100; | ||
| const lNorm = l / 100; | ||
| const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm; | ||
| const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); | ||
| const m = lNorm - c / 2; | ||
| let r = 0, | ||
| g = 0, | ||
| b = 0; | ||
| if (h < 60) { | ||
| r = c; | ||
| g = x; | ||
| } else if (h < 120) { | ||
| r = x; | ||
| g = c; | ||
| } else if (h < 180) { | ||
| g = c; | ||
| b = x; | ||
| } else if (h < 240) { | ||
| g = x; | ||
| b = c; | ||
| } else if (h < 300) { | ||
| r = x; | ||
| b = c; | ||
| } else { | ||
| r = c; | ||
| b = x; | ||
| } | ||
| return rgbToHex( | ||
| Math.round((r + m) * 255), | ||
| Math.round((g + m) * 255), | ||
| Math.round((b + m) * 255), | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Generates an array of visually distinct colors using the golden angle | ||
| * hue distribution for maximum separation. | ||
| * | ||
| * @param count - Number of distinct colors to generate. | ||
| * @returns Array of hex color strings. | ||
| */ | ||
| 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; | ||
| } | ||
|
Comment on lines
+53
to
+62
|
||
|
|
||
| /** | ||
| * Converts a hex color to RGB values. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
queryRenderedFeaturesis called once perlayerIdinpickableLayerIds. 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 singlequeryRenderedFeatures(e.point, { layers: pickableLayerIds })call and then group the results in-memory bysourceLayer(and feature).