@phila/layerboard 3.0.0-beta.20 → 3.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ import { LayerConfig } from './layer';
2
+ export interface LayerboardConfig {
3
+ /**
4
+ * ArcGIS Online WebMap ID to fetch layer configuration from
5
+ */
6
+ webMapId: string;
7
+ /**
8
+ * Mode for layer configuration
9
+ * - 'dynamic': Fetch from ArcGIS Online at runtime (recommended)
10
+ * - 'static': Use pre-generated static config files
11
+ */
12
+ mode?: "dynamic" | "static";
13
+ /**
14
+ * Layer panel display mode
15
+ * - 'flat': Simple list of all layers (OpenMaps style)
16
+ * - 'topics': Grouped by topic in accordion (StreetSmartPHL style)
17
+ */
18
+ panelMode?: "flat" | "topics";
19
+ /**
20
+ * Topic configuration (required when panelMode is 'topics')
21
+ */
22
+ topics?: TopicConfig[];
23
+ /**
24
+ * Static layer configurations (used when mode is 'static')
25
+ */
26
+ staticLayers?: LayerConfig[];
27
+ /**
28
+ * Initial map view settings
29
+ */
30
+ initialView?: InitialViewConfig;
31
+ /**
32
+ * Feature flags to enable/disable framework features
33
+ */
34
+ features?: FeatureFlags;
35
+ /**
36
+ * Optional callback when map is ready
37
+ */
38
+ onMapReady?: (map: unknown) => void;
39
+ }
40
+ /**
41
+ * Initial map view configuration
42
+ */
43
+ export interface InitialViewConfig {
44
+ /** Center coordinates [longitude, latitude] */
45
+ center: [number, number];
46
+ /** Initial zoom level */
47
+ zoom: number;
48
+ /** Optional min zoom constraint */
49
+ minZoom?: number;
50
+ /** Optional max zoom constraint */
51
+ maxZoom?: number;
52
+ }
53
+ /**
54
+ * Feature flags for enabling/disabling framework features
55
+ */
56
+ export interface FeatureFlags {
57
+ /** Enable address search (default: true) */
58
+ search?: boolean;
59
+ /** Enable geolocation/locate me button (default: true) */
60
+ geolocation?: boolean;
61
+ /** Enable Cyclomedia street-level imagery integration */
62
+ cyclomedia?: boolean;
63
+ /** Enable Pictometry oblique imagery integration */
64
+ pictometry?: boolean;
65
+ /** Enable layer opacity controls (default: true) */
66
+ opacitySliders?: boolean;
67
+ /** Enable layer legends (default: true) */
68
+ legends?: boolean;
69
+ }
70
+ export interface TopicConfig {
71
+ id: string;
72
+ title: string;
73
+ icon?: string;
74
+ layerIds: string[];
75
+ defaultExpanded?: boolean;
76
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * DataSource Types
3
+ *
4
+ * Configuration and state types for fetching external data from APIs.
5
+ * Used by topic components to display dynamic content like notices, status, etc.
6
+ */
7
+ /**
8
+ * Configuration for a data source
9
+ */
10
+ export interface DataSourceConfig {
11
+ /** Unique identifier for the data source */
12
+ id: string;
13
+ /** URL to fetch data from */
14
+ url: string;
15
+ /** Type of data source - determines how data is fetched */
16
+ type: "http-get" | "http-post" | "esri";
17
+ /** Optional polling interval in milliseconds (for auto-refresh) */
18
+ pollInterval?: number;
19
+ /** Optional request options (headers, etc.) */
20
+ options?: RequestInit;
21
+ /** Optional transform function to process the response data */
22
+ transform?: (data: unknown) => unknown;
23
+ }
24
+ /**
25
+ * State for a single data source
26
+ */
27
+ export interface DataSourceState<T = unknown> {
28
+ /** The fetched data (null if not yet loaded or error) */
29
+ data: T | null;
30
+ /** Whether the data is currently being fetched */
31
+ loading: boolean;
32
+ /** Error message if fetch failed */
33
+ error: string | null;
34
+ /** Timestamp of last successful fetch */
35
+ lastFetched: number | null;
36
+ }
37
+ /**
38
+ * Combined state for all data sources
39
+ */
40
+ export type DataSourcesState = Record<string, DataSourceState>;
@@ -0,0 +1,3 @@
1
+ export * from './config';
2
+ export * from './layer';
3
+ export * from './dataSource';
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Layer Configuration Types
3
+ */
4
+ export interface LegendItem {
5
+ type: "fill" | "line" | "circle";
6
+ color: string;
7
+ label: string;
8
+ width?: number;
9
+ radius?: number;
10
+ }
11
+ export interface PopupFieldFormat {
12
+ dateFormat?: string;
13
+ digitSeparator?: boolean;
14
+ places?: number;
15
+ }
16
+ export interface PopupField {
17
+ field: string;
18
+ label: string;
19
+ format?: PopupFieldFormat;
20
+ }
21
+ export interface PopupConfig {
22
+ title: string;
23
+ fields: PopupField[];
24
+ showTime?: boolean;
25
+ }
26
+ /**
27
+ * Display options for layer controls in the sidebar
28
+ * These match the original Layerboard's topicLayers options
29
+ */
30
+ export interface LayerDisplayOptions {
31
+ /** Whether to show a checkbox for this layer (default: true) */
32
+ shouldShowCheckbox?: boolean;
33
+ /** Whether to show an opacity slider for this layer (default: true) */
34
+ shouldShowSlider?: boolean;
35
+ /** Whether to show the legend for this layer (default: true) */
36
+ shouldShowLegendBox?: boolean;
37
+ /** Alternative display name (overrides title in UI) */
38
+ layerNameChange?: string;
39
+ }
40
+ export interface LayerConfig {
41
+ id: string;
42
+ title: string;
43
+ type: "fill" | "line" | "circle";
44
+ url: string;
45
+ where?: string;
46
+ minZoom?: number;
47
+ maxZoom?: number;
48
+ opacity: number;
49
+ paint?: Record<string, unknown>;
50
+ outlinePaint?: Record<string, unknown>;
51
+ legend: LegendItem[];
52
+ popup: PopupConfig | null;
53
+ /** Display options for sidebar controls */
54
+ displayOptions?: LayerDisplayOptions;
55
+ }
56
+ /**
57
+ * Override configuration for popup behavior
58
+ */
59
+ export interface PopupOverride {
60
+ showTime?: boolean;
61
+ }
62
+ /**
63
+ * Override configuration for layer styles
64
+ * Allows overriding the paint and legend from WebMap with custom values
65
+ */
66
+ export interface LayerStyleOverride {
67
+ /** MapLibre paint properties to override */
68
+ paint?: Record<string, unknown>;
69
+ /** MapLibre outline paint properties (for polygons) */
70
+ outlinePaint?: Record<string, unknown>;
71
+ /** Legend items to override */
72
+ legend?: LegendItem[];
73
+ /** Override the layer type (fill, line, circle) */
74
+ type?: "fill" | "line" | "circle";
75
+ }
76
+ export interface CirclePaint {
77
+ "circle-radius": number | unknown[];
78
+ "circle-color": string | unknown[];
79
+ "circle-opacity"?: number;
80
+ "circle-stroke-width"?: number;
81
+ "circle-stroke-color"?: string;
82
+ }
83
+ export interface LinePaint {
84
+ "line-width": number | unknown[];
85
+ "line-color": string | unknown[];
86
+ "line-opacity"?: number;
87
+ }
88
+ export interface FillPaint {
89
+ "fill-color": string | unknown[];
90
+ "fill-opacity"?: number;
91
+ "fill-outline-color"?: string;
92
+ }
93
+ /**
94
+ * Configuration for ESRI tiled map layers (MapServer tiles)
95
+ * These are separate from WebMap feature layers and rendered as raster tiles.
96
+ * Used by topics like PickupPHL (collection day) and PlowPHL (plow status).
97
+ */
98
+ export interface TiledLayerConfig {
99
+ /** Unique identifier for the tiled layer */
100
+ id: string;
101
+ /** Display title for the layer */
102
+ title: string;
103
+ /** ESRI MapServer tile URL (e.g., https://tiles.arcgis.com/.../MapServer) */
104
+ url: string;
105
+ /** Z-index for layer ordering (higher = on top) */
106
+ zIndex?: number;
107
+ /** Attribution text to display on the map */
108
+ attribution?: string;
109
+ /** Minimum zoom level at which the layer is visible */
110
+ minZoom?: number;
111
+ /** Maximum zoom level at which the layer is visible */
112
+ maxZoom?: number;
113
+ /** Initial opacity (0-1, default 1) */
114
+ opacity?: number;
115
+ /**
116
+ * Enable scale-based switching between tiled and dynamic rendering.
117
+ * When enabled, uses pre-rendered tiles when zoomed out (better performance)
118
+ * and switches to dynamic /export/ rendering when zoomed in (sharper imagery).
119
+ * The switch threshold is fetched from the MapServer's layer definitions.
120
+ */
121
+ scaleBasedRendering?: boolean;
122
+ }
@@ -0,0 +1 @@
1
+ export { transformWebMapToLayerConfigs } from './webmap-transformer';
@@ -0,0 +1,222 @@
1
+ import { LayerConfig, LegendItem, PopupConfig } from '../types';
2
+ export interface EsriWebMap {
3
+ operationalLayers?: EsriOperationalLayer[];
4
+ [key: string]: unknown;
5
+ }
6
+ export interface EsriOperationalLayer {
7
+ id?: string;
8
+ title: string;
9
+ url?: string;
10
+ opacity?: number;
11
+ layerDefinition?: {
12
+ drawingInfo?: EsriDrawingInfo;
13
+ definitionExpression?: string;
14
+ minScale?: number;
15
+ maxScale?: number;
16
+ };
17
+ popupInfo?: EsriPopupInfo;
18
+ [key: string]: unknown;
19
+ }
20
+ export interface EsriDrawingInfo {
21
+ renderer?: EsriRenderer;
22
+ [key: string]: unknown;
23
+ }
24
+ export interface EsriRenderer {
25
+ type: "simple" | "uniqueValue" | "classBreaks";
26
+ symbol?: EsriSymbol;
27
+ label?: string;
28
+ field?: string;
29
+ field1?: string;
30
+ defaultSymbol?: EsriSymbol;
31
+ /** Minimum value for classBreaks renderer (used for label generation) */
32
+ minValue?: number;
33
+ uniqueValueInfos?: Array<{
34
+ value: string | number;
35
+ label?: string;
36
+ symbol?: EsriSymbol;
37
+ }>;
38
+ classBreakInfos?: Array<{
39
+ classMaxValue: number;
40
+ classMinValue?: number;
41
+ label?: string;
42
+ symbol?: EsriSymbol;
43
+ }>;
44
+ visualVariables?: Array<{
45
+ type: "colorInfo" | "sizeInfo" | "opacityInfo" | "rotationInfo";
46
+ field?: string;
47
+ stops?: Array<{
48
+ value: number;
49
+ color?: number[];
50
+ size?: number;
51
+ label?: string;
52
+ }>;
53
+ [key: string]: unknown;
54
+ }>;
55
+ [key: string]: unknown;
56
+ }
57
+ export interface EsriSymbol {
58
+ type: "esriSFS" | "esriSLS" | "esriSMS" | "esriPMS" | "esriPFS";
59
+ color?: number[];
60
+ size?: number;
61
+ width?: number;
62
+ outline?: {
63
+ color?: number[];
64
+ width?: number;
65
+ style?: string;
66
+ };
67
+ [key: string]: unknown;
68
+ }
69
+ export interface EsriPopupInfo {
70
+ title?: string;
71
+ fieldInfos?: Array<{
72
+ fieldName: string;
73
+ label?: string;
74
+ visible?: boolean;
75
+ format?: {
76
+ dateFormat?: string;
77
+ digitSeparator?: boolean;
78
+ places?: number;
79
+ };
80
+ }>;
81
+ [key: string]: unknown;
82
+ }
83
+ interface RendererResult {
84
+ paint: Record<string, unknown>;
85
+ legend: LegendItem[];
86
+ geomType: "fill" | "line" | "circle";
87
+ outlinePaint: Record<string, unknown> | null;
88
+ }
89
+ /**
90
+ * Convert Esri RGBA array to CSS color string
91
+ *
92
+ * Transforms Esri's RGBA color format [r, g, b, a] to CSS rgba() or hex.
93
+ * Esri uses 0-255 for alpha, CSS uses 0-1.
94
+ *
95
+ * @param color - Esri RGBA array [red, green, blue, alpha] where each value is 0-255
96
+ * @returns CSS color string (hex for opaque colors, rgba() for transparent)
97
+ * @example
98
+ * esriColorToCSS([255, 0, 0, 255]) // Returns "#ff0000"
99
+ * esriColorToCSS([255, 0, 0, 128]) // Returns "rgba(255, 0, 0, 0.50)"
100
+ */
101
+ export declare function esriColorToCSS(color: number[] | null | undefined): string;
102
+ /**
103
+ * Convert Esri opacity (0-1) to MapLibre opacity
104
+ */
105
+ export declare function convertOpacity(opacity: number | undefined): number;
106
+ /**
107
+ * Convert Esri minScale/maxScale to MapLibre minzoom/maxzoom
108
+ *
109
+ * Note: The relationship is inverted!
110
+ * - Esri minScale (large number) = zoomed out limit → MapLibre minzoom (small zoom level)
111
+ * - Esri maxScale (small number) = zoomed in limit → MapLibre maxzoom (large zoom level)
112
+ *
113
+ * Uses the formula: zoom ≈ log2(559082264 / scale)
114
+ * where 559082264 is the approximate scale at zoom level 0 at the equator.
115
+ *
116
+ * @param minScale - Esri minScale (layer disappears when zoomed out beyond this)
117
+ * @param maxScale - Esri maxScale (layer disappears when zoomed in beyond this)
118
+ * @returns Object with optional minZoom and maxZoom properties for MapLibre
119
+ * @example
120
+ * convertScaleToZoom(100000, 5000) // Returns { minZoom: 12.45, maxZoom: 17.11 }
121
+ */
122
+ export declare function convertScaleToZoom(minScale?: number, maxScale?: number): {
123
+ minZoom?: number;
124
+ maxZoom?: number;
125
+ };
126
+ /**
127
+ * Transform Esri renderer to MapLibre paint styles
128
+ *
129
+ * Main renderer conversion function that handles all Esri renderer types:
130
+ * - Simple: Single symbol for all features
131
+ * - UniqueValue: Different symbols based on attribute field values
132
+ * - ClassBreaks: Graduated colors based on numeric ranges
133
+ *
134
+ * Converts Esri symbols and colors to MapLibre paint properties, generates
135
+ * legend entries, and handles outline styling for fill layers.
136
+ *
137
+ * The customLabelMap parameter (optional, only for unique value renderers):
138
+ * Enables hybrid approach where you can use renderer from one source (e.g., service)
139
+ * for field/colors/symbols, but labels from another source (e.g., service description)
140
+ * for human-readable names. This solves cases where:
141
+ * - Renderer has correct colors but only numeric labels (e.g., "11", "22")
142
+ * - Description field has detailed labels (e.g., "11 Residential Low Density")
143
+ * - Example: Land Use layer uses service renderer (c_dig2 field, correct colors) with
144
+ * parsed description labels ("Residential Low Density" instead of just "11")
145
+ *
146
+ * @param drawingInfo - Esri drawing info containing renderer configuration
147
+ * @param layerOpacity - Layer opacity (0-1), defaults to 1
148
+ * @param customLabelMap - Optional map of field values to custom labels (unique value renderers only)
149
+ * @returns Object containing paint styles, legend items, geometry type, and optional outline paint
150
+ *
151
+ * @example Basic usage
152
+ * const result = transformEsriRenderer(layer.drawingInfo, 0.8);
153
+ * // result.paint = { 'fill-color': '#ff0000', 'fill-opacity': 0.8 }
154
+ * // result.legend = [{ type: 'fill', color: '#ff0000', label: 'Feature' }]
155
+ * // result.geomType = 'fill'
156
+ *
157
+ * @example With custom labels (hybrid approach for unique value renderers)
158
+ * const labelMap = new Map([
159
+ * ['11', 'Residential Low Density'],
160
+ * ['22', 'Commercial Business/Professional']
161
+ * ]);
162
+ * const result = transformEsriRenderer(serviceDrawingInfo, 1.0, labelMap);
163
+ * // result uses renderer's field/colors but labelMap's descriptive labels
164
+ */
165
+ export declare function transformEsriRenderer(drawingInfo?: EsriDrawingInfo, layerOpacity?: number, customLabelMap?: Map<string, string>, layerTitle?: string): RendererResult;
166
+ /**
167
+ * Transform Esri popupInfo to application popup configuration
168
+ *
169
+ * Converts Esri's popupInfo structure to the app's PopupConfig format.
170
+ * Filters to visible fields only and preserves field formatting options
171
+ * (date formats, number formatting, etc.).
172
+ *
173
+ * @param popupInfo - Esri popup info from layer definition
174
+ * @returns PopupConfig object with title and visible fields, or null if no popup info
175
+ * @example
176
+ * const popup = transformPopupConfig(layer.popupInfo);
177
+ * // Returns: { title: "{NAME}", fields: [{ field: "name", label: "Name" }] }
178
+ */
179
+ export declare function transformPopupConfig(popupInfo?: EsriPopupInfo): PopupConfig | null;
180
+ /**
181
+ * Generate legend entries from Esri renderer
182
+ *
183
+ * Convenience function that extracts just the legend items from the renderer
184
+ * transformation. This is the same as the legend array returned by
185
+ * transformEsriRenderer().
186
+ *
187
+ * @param drawingInfo - Esri drawing info containing renderer configuration
188
+ * @param layerOpacity - Layer opacity (0-1), defaults to 1
189
+ * @param customLabelMap - Optional map of field values to custom labels
190
+ * @returns Array of legend items with type, color, and label
191
+ * @example
192
+ * const legend = transformLegendConfig(layer.drawingInfo);
193
+ * // Returns: [{ type: 'fill', color: '#ff0000', label: 'Parks' }]
194
+ */
195
+ export declare function transformLegendConfig(drawingInfo?: EsriDrawingInfo, layerOpacity?: number, customLabelMap?: Map<string, string>): LegendItem[];
196
+ /**
197
+ * Extract Esri definitionExpression as where clause
198
+ *
199
+ * Simple extraction of the definitionExpression from Esri layer definition.
200
+ * This SQL-like expression filters which features are displayed on the map.
201
+ *
202
+ * @param layerDefinition - Esri layer definition object
203
+ * @returns Definition expression string, or undefined if not present
204
+ * @example
205
+ * const where = buildWhereClause(layer.layerDefinition);
206
+ * // Returns: "STATUS = 'Active' AND YEAR >= 2020"
207
+ */
208
+ export declare function buildWhereClause(layerDefinition?: {
209
+ definitionExpression?: string;
210
+ }): string | undefined;
211
+ /**
212
+ * Transform Esri WebMap JSON to array of LayerConfig objects
213
+ *
214
+ * This is the main entry point for runtime transformation.
215
+ * Takes a WebMap JSON object and returns an array of layer configurations
216
+ * compatible with the app's LayerConfig interface.
217
+ *
218
+ * @param webMapJson The Esri WebMap JSON object
219
+ * @returns Array of LayerConfig objects ready to use with MapLibre
220
+ */
221
+ export declare function transformWebMapToLayerConfigs(webMapJson: EsriWebMap): Promise<LayerConfig[]>;
222
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phila/layerboard",
3
- "version": "3.0.0-beta.20",
3
+ "version": "3.0.0-beta.21",
4
4
  "type": "module",
5
5
  "description": "Vue 3 + MapLibre mapping framework for City of Philadelphia applications",
6
6
  "main": "./dist/index.js",