From 9f160ec35b4263f8a20c1a9ab191d6a85f1ba1bc Mon Sep 17 00:00:00 2001 From: jussi-sa Date: Wed, 17 Sep 2025 10:49:58 +0300 Subject: [PATCH 1/8] standard style support --- .../create-style-tool/CreateStyleTool.ts | 6 +- .../retrieve-style-tool/RetrieveStyleTool.ts | 4 +- .../StyleBuilderTool.schema.ts | 27 +- .../style-builder-tool/StyleBuilderTool.ts | 651 +++++++++++++++--- .../update-style-tool/UpdateStyleTool.ts | 4 +- src/types/mapbox-style.ts | 6 + src/utils/styleUtils.ts | 31 + .../tool-naming-convention.test.ts.snap | 35 +- .../StyleBuilderTool.test.ts | 232 ++++++- 9 files changed, 864 insertions(+), 132 deletions(-) create mode 100644 src/utils/styleUtils.ts diff --git a/src/tools/create-style-tool/CreateStyleTool.ts b/src/tools/create-style-tool/CreateStyleTool.ts index 7419db0..2c3c96f 100644 --- a/src/tools/create-style-tool/CreateStyleTool.ts +++ b/src/tools/create-style-tool/CreateStyleTool.ts @@ -1,4 +1,5 @@ import { fetchClient } from '../../utils/fetchRequest.js'; +import { filterExpandedMapboxStyles } from '../../utils/styleUtils.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { CreateStyleSchema, @@ -18,7 +19,7 @@ export class CreateStyleTool extends MapboxApiBasedTool< protected async execute( input: CreateStyleInput, accessToken?: string - ): Promise { + ): Promise { const username = MapboxApiBasedTool.getUserNameFromToken(accessToken); const url = `${MapboxApiBasedTool.mapboxApiEndpoint}styles/v1/${username}?access_token=${accessToken}`; @@ -42,6 +43,7 @@ export class CreateStyleTool extends MapboxApiBasedTool< } const data = await response.json(); - return data; + // Return full style but filter out expanded Mapbox styles + return filterExpandedMapboxStyles(data); } } diff --git a/src/tools/retrieve-style-tool/RetrieveStyleTool.ts b/src/tools/retrieve-style-tool/RetrieveStyleTool.ts index b1fe316..a1e1336 100644 --- a/src/tools/retrieve-style-tool/RetrieveStyleTool.ts +++ b/src/tools/retrieve-style-tool/RetrieveStyleTool.ts @@ -1,3 +1,4 @@ +import { filterExpandedMapboxStyles } from '../../utils/styleUtils.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { RetrieveStyleSchema, @@ -30,6 +31,7 @@ export class RetrieveStyleTool extends MapboxApiBasedTool< } const data = await response.json(); - return data; + // Always filter out expanded Mapbox styles to prevent token overflow + return filterExpandedMapboxStyles(data); } } diff --git a/src/tools/style-builder-tool/StyleBuilderTool.schema.ts b/src/tools/style-builder-tool/StyleBuilderTool.schema.ts index 9d1594d..551f5f6 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.schema.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.schema.ts @@ -81,16 +81,35 @@ const LayerConfigSchema = z.object({ z.record(z.unknown()) ]) .optional() - .describe('Custom Mapbox expression for advanced styling') + .describe('Custom Mapbox expression for advanced styling'), + + // Slot for Standard styles + slot: z + .enum(['bottom', 'middle', 'top']) + .optional() + .describe( + 'Layer slot for Mapbox Standard styles. Controls layer stacking order. ' + + 'Bottom: below most map features, Middle: between base and labels, Top: above all base map features (default)' + ) }); export const StyleBuilderToolSchema = z.object({ style_name: z.string().default('Custom Style').describe('Name for the style'), base_style: z - .enum(['streets', 'light', 'dark', 'satellite', 'outdoors', 'blank']) - .default('streets') - .describe('Base style template to start from'), + .enum([ + 'standard', + 'streets', + 'light', + 'dark', + 'satellite', + 'outdoors', + 'blank' + ]) + .default('standard') + .describe( + 'Base style template to start from (standard uses new Mapbox Standard, classic styles are deprecated)' + ), layers: z .array(LayerConfigSchema) diff --git a/src/tools/style-builder-tool/StyleBuilderTool.ts b/src/tools/style-builder-tool/StyleBuilderTool.ts index e6430c5..6620d3e 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.ts @@ -9,27 +9,64 @@ import type { MapboxStyle, Layer, Filter } from '../../types/mapbox-style.js'; export class StyleBuilderTool extends BaseTool { name = 'style_builder_tool'; - description = `Build custom Mapbox styles with precise control over layers and visual properties, including zoom-based and data-driven expressions. - -HOW TO CREATE A STYLE: -1. First, consult resource://mapbox-style-layers to see all available layer types -2. Use this tool to generate a style configuration -3. Apply the style using create_style_tool or update_style_tool + description = `Generate Mapbox style JSON for creating new styles or updating existing ones. Supports Mapbox Standard (default), Classic, and Blank base styles with full control over layers, expressions, and visual properties. + +USAGE: +1. Use this tool to generate style JSON configuration +2. For NEW styles: Use the generated JSON with create_style_tool +3. For EXISTING styles: Use portions of the JSON with update_style_tool to modify specific layers + +BASE STYLES: +• standard (default): Modern Mapbox Standard with imports, requires slot property +• streets/light/dark/satellite/outdoors: Classic styles (deprecated but supported) +• blank: Empty style for full customization + +LAYER ORDERING: +• In ALL styles: Later layers in array render on top of earlier layers +• Standard style: 'slot' determines which section, array order matters within each slot +• Classic/Blank: Array order is the only control for layer stacking +Example: [background, water, roads, labels] = labels render on top + +MAPBOX STANDARD - SLOT PROPERTY: +When using Standard base style, each layer needs a 'slot' to control stacking: +• bottom: Below most map features (land, water) +• middle: Between base features and labels +• top: Above all base map features (default for visibility) +Within each slot, array order still applies - later layers render on top + +RESOURCE GUIDE: +The resource://mapbox-style-layers contains comprehensive documentation including: +• All available layer types with descriptions +• Paint and layout properties for each layer type +• Common filters and expressions +• Example configurations AVAILABLE LAYER TYPES: • water, waterway - Oceans, lakes, rivers • landuse, parks - Land areas like parks, hospitals, schools • buildings, building_3d - Building footprints and 3D extrusions -• roads (motorways, primary_roads, secondary_roads, streets, paths, railways) +• ROAD TYPES (use these specific types for best results): + - motorways - Highway/freeway roads (class: motorway) + - primary_roads - Major roads (class: primary, trunk) + - secondary_roads - Secondary roads (class: secondary) + - streets - Local streets (class: street, street_limited) + - paths - Walking/cycling paths (class: path, pedestrian) + - railways - Rail lines + - roads - Generic/all roads (avoid using - use specific types above instead) • country_boundaries, state_boundaries - Administrative borders • place_labels, road_labels, poi_labels - Text labels • landcover - Natural features like forests, grass • airports - Airport features • transit - Bus stops, subway entrances, rail stations (filter by maki: bus, entrance, rail-metro) +IMPORTANT FOR ROADS: +• Always use specific road layer types (motorways, primary_roads, etc.) instead of generic 'roads' +• Each road type automatically includes proper filters and zoom-based width interpolation +• Don't specify fixed widths - the tool automatically applies appropriate zoom-based scaling + ACTIONS YOU CAN APPLY: -• color - Set the layer's color -• highlight - Make layer prominent with color/width +• color - Set the layer's color (roads will use smart defaults if not specified) +• highlight - Make layer prominent with enhanced color/width • hide - Remove layer from view • show - Display layer with default styling @@ -112,19 +149,25 @@ ${JSON.stringify(style, null, 2)} private buildStyle(input: StyleBuilderToolInput): MapboxStyle { const layers: Layer[] = []; + const isUsingStandard = input.base_style === 'standard'; + + // Only add background layer for non-Standard styles + // Standard style provides its own background through imports + if (!isUsingStandard) { + const bgColor = + input.global_settings?.background_color || + (input.global_settings?.mode === 'dark' ? '#1a1a1a' : '#f8f4f0'); + + const backgroundLayer: Layer = { + id: 'background', + type: 'background', + paint: { + 'background-color': bgColor + } + }; - // Add background layer - const bgColor = - input.global_settings?.background_color || - (input.global_settings?.mode === 'dark' ? '#1a1a1a' : '#f8f4f0'); - - layers.push({ - id: 'background', - type: 'background', - paint: { - 'background-color': bgColor - } - }); + layers.push(backgroundLayer); + } // Build each configured layer for (const config of input.layers) { @@ -136,60 +179,119 @@ ${JSON.stringify(style, null, 2)} continue; } - const layer = this.createLayer(layerDef, config, input.global_settings); + const layer = this.createLayer( + layerDef, + config, + input.global_settings, + isUsingStandard + ); if (layer) { layers.push(layer); } } - // Add default essential layers if not specified - const configuredTypes = new Set(input.layers.map((l) => l.layer_type)); - const essentialLayers = ['water']; - - for (const layerType of essentialLayers) { - if (!configuredTypes.has(layerType)) { - const layerDef = MAPBOX_STYLE_LAYERS[layerType]; - if (layerDef) { - const layer = this.createLayer( - layerDef, - { - layer_type: layerType, - action: 'show' - }, - input.global_settings - ); - if (layer) { - layers.push(layer); - } - } - } - } + // Note: We no longer automatically add layers that weren't explicitly requested + // The user should specify all desired layers in the input - return { + // Create the base style object with minimal properties + // Additional properties will be added based on base style type + const style: MapboxStyle = { version: 8, - name: input.style_name, - sources: { + name: input.style_name + } as MapboxStyle; + + // Determine which base style to use + const isClassicStyle = [ + 'streets', + 'light', + 'dark', + 'satellite', + 'outdoors' + ].includes(input.base_style); + + // For standard style, use imports to inherit from Mapbox Standard + if (input.base_style === 'standard') { + // Follow the exact order from the working Mapbox Studio example + style.metadata = { + 'mapbox:autocomposite': true, + 'mapbox:uiParadigm': 'imports', + 'mapbox:sdk-support': { + js: '3.14.0', + android: '11.14.0', + ios: '11.14.0' + }, + 'mapbox:groups': {} + }; + style.center = [0, 0]; + style.zoom = 2; + style.imports = [ + { + id: 'basemap', + url: 'mapbox://styles/mapbox/standard' + } + ]; + style.sources = { + composite: { + url: 'mapbox://mapbox.mapbox-streets-v8', + type: 'vector' + } + }; + style.sprite = 'mapbox://sprites/mapbox/streets-v12'; + style.glyphs = 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf'; + style.projection = { name: 'globe' }; + style.layers = layers; + + // Explicitly set terrain to null for API compatibility + // @ts-expect-error - The API expects null but TypeScript type doesn't allow it + style.terrain = null; + } else if (isClassicStyle) { + // For classic styles (being deprecated), use traditional sources + style.center = [0, 0]; + style.zoom = 2; + style.sources = { composite: { type: 'vector', url: 'mapbox://mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2' } - }, - sprite: 'mapbox://sprites/mapbox/streets-v12', - glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf', - layers - }; + }; + style.sprite = 'mapbox://sprites/mapbox/streets-v12'; + style.glyphs = 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf'; + style.layers = layers; + } else if (input.base_style === 'blank') { + // Blank style - no imports, just basic sources + style.center = [0, 0]; + style.zoom = 2; + style.sources = { + composite: { + type: 'vector', + url: 'mapbox://mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2' + } + }; + style.sprite = 'mapbox://sprites/mapbox/streets-v12'; + style.glyphs = 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf'; + style.layers = layers; + } + + return style; } private createLayer( layerDef: (typeof MAPBOX_STYLE_LAYERS)[keyof typeof MAPBOX_STYLE_LAYERS], config: StyleBuilderToolInput['layers'][0], - globalSettings?: StyleBuilderToolInput['global_settings'] + globalSettings?: StyleBuilderToolInput['global_settings'], + isUsingStandard?: boolean ): Layer | null { const layer: Layer = { id: `${layerDef.id}-custom`, type: layerDef.type as Layer['type'] }; + // Add slot for Standard style + if (isUsingStandard) { + // Use custom slot if provided, otherwise default to 'top' for visibility + layer.slot = config.slot || 'top'; + } + // Add source configuration if (layerDef.sourceLayer) { layer.source = 'composite'; @@ -205,77 +307,183 @@ ${JSON.stringify(style, null, 2)} // Build paint properties const paint: Record = {}; + // Use the user-provided color if available, otherwise use defaults + let effectiveColor = config.color; + + // Ensure hex colors have # prefix + if ( + effectiveColor && + !effectiveColor.startsWith('#') && + !effectiveColor.startsWith('rgb') && + !effectiveColor.startsWith('hsl') + ) { + effectiveColor = '#' + effectiveColor; + } + + // Only provide a default color if none was specified + if ( + !effectiveColor && + (config.action === 'color' || config.action === 'highlight') + ) { + effectiveColor = this.getHarmoniousColor( + config.layer_type, + config.action + ); + } + // Apply color based on action if ( (config.action === 'color' || config.action === 'highlight') && - config.color + effectiveColor ) { const colorProp = this.getColorProperty(layerDef.type); if (colorProp) { paint[colorProp] = this.generateExpression( - config.color, + effectiveColor, config, 'color' ); } } - // Apply opacity if specified - if (config.opacity !== undefined) { - const opacityProp = this.getOpacityProperty(layerDef.type); - if (opacityProp) { + // Apply opacity - use specified value or smart defaults + const opacityProp = this.getOpacityProperty(layerDef.type); + if (opacityProp) { + // For Standard style overlays, use higher opacity by default + // This keeps colors vibrant and easily distinguishable + const opacity = + config.opacity !== undefined + ? config.opacity + : isUsingStandard + ? 0.75 + : this.getDefaultOpacity(config.layer_type, layerDef.type); + + // Only apply if not full opacity (to keep styles cleaner) + if (opacity < 1.0) { paint[opacityProp] = this.generateExpression( - config.opacity, + opacity, config, 'opacity' ); } } - // Apply width for line layers - if (config.width !== undefined && layerDef.type === 'line') { - paint['line-width'] = this.generateExpression( - config.width, - config, - 'width' - ); + // Apply width for line layers with better defaults + if (layerDef.type === 'line') { + if (config.width !== undefined) { + // Use the user-provided width + const width = config.width; + + // Always use zoom interpolation for roads + if (typeof width === 'number' && width > 0) { + // Create zoom-based interpolation that respects the provided width + // but ensures it scales properly with zoom + paint['line-width'] = [ + 'interpolate', + ['linear'], + ['zoom'], + 5, + width * 0.4, // Thinner at low zoom + 10, + width * 0.6, // Building up + 14, + width * 0.85, // Near full width at city zoom + 18, + width // Full width at high zoom + ]; + } else { + paint['line-width'] = this.generateExpression(width, config, 'width'); + } + } else { + // Apply smart default widths based on road type with zoom interpolation + const defaultWidth = this.getDefaultLineWidth(config.layer_type); + if (defaultWidth) { + paint['line-width'] = defaultWidth; + } + } } - // For highlight action, make it prominent + // For highlight action, make it prominent but refined if (config.action === 'highlight') { - if (!config.color) { + if (!effectiveColor) { const colorProp = this.getColorProperty(layerDef.type); if (colorProp) { paint[colorProp] = this.generateExpression( - '#ff0000', + this.getHarmoniousColor(config.layer_type, 'highlight'), config, 'color' ); } } - if (!config.width && layerDef.type === 'line') { - paint['line-width'] = this.generateExpression(3, config, 'width'); + if (!config.width && layerDef.type === 'line' && !paint['line-width']) { + // Use refined highlight width + const highlightWidth = this.getDefaultLineWidth( + config.layer_type, + true + ); + paint['line-width'] = highlightWidth || 1.8; } - if (config.opacity === undefined) { + // For highlights, use moderately higher opacity + if ( + config.opacity === undefined && + !paint[this.getOpacityProperty(layerDef.type) || ''] + ) { const opacityProp = this.getOpacityProperty(layerDef.type); if (opacityProp) { - paint[opacityProp] = this.generateExpression(1, config, 'opacity'); + // Use 0.6 for road highlights, 0.8 for other features + const highlightOpacity = + config.layer_type.includes('road') || + config.layer_type.includes('street') || + config.layer_type.includes('motorway') + ? 0.6 + : 0.8; + paint[opacityProp] = this.generateExpression( + highlightOpacity, + config, + 'opacity' + ); } } } - // Apply defaults from layer definition + // Apply defaults from layer definition with harmonious colors for (const prop of layerDef.paintProperties) { if (!(prop.property in paint)) { - // Use a reasonable default - if (prop.property.includes('color') && !prop.example) { - paint[prop.property] = '#808080'; // Default gray + // Use harmonious defaults + if (prop.property.includes('color')) { + if ( + prop.example && + typeof prop.example === 'string' && + prop.example.startsWith('#') + ) { + paint[prop.property] = prop.example; + } else { + paint[prop.property] = this.getHarmoniousColor( + config.layer_type, + 'default' + ); + } + } else if (prop.property === 'line-width') { + // Skip line-width defaults, we handle those with smart zoom scaling above + continue; } else if (prop.example !== undefined) { paint[prop.property] = prop.example; } } } + // Special handling for symbol layers to ensure better text readability + if (layer.type === 'symbol') { + // Ensure text has proper halo for readability + if (!paint['text-halo-color']) { + paint['text-halo-color'] = + globalSettings?.mode === 'dark' ? '#000000' : '#ffffff'; + } + if (!paint['text-halo-width']) { + paint['text-halo-width'] = 1.5; + } + } + // Adjust for dark mode if (globalSettings?.mode === 'dark') { if (layer.type === 'symbol') { @@ -288,14 +496,52 @@ ${JSON.stringify(style, null, 2)} layer.paint = paint; } - // Add layout properties if needed + // Add layout properties with better defaults for specific layer types if (layerDef.layoutProperties && layerDef.layoutProperties.length > 0) { const layout: Record = {}; - for (const prop of layerDef.layoutProperties) { - if (prop.example !== undefined) { - layout[prop.property] = prop.example; + + // Special handling for transit and POI layers + if ( + config.layer_type === 'transit' || + config.layer_type === 'poi_labels' + ) { + layout['text-field'] = ['get', 'name']; + layout['icon-image'] = [ + 'get', + config.layer_type === 'transit' ? 'network' : 'maki' + ]; + layout['text-anchor'] = 'top'; + layout['text-offset'] = [0, 0.8]; + layout['icon-size'] = 1; + layout['text-font'] = ['DIN Pro Regular', 'Arial Unicode MS Regular']; + layout['text-size'] = 12; + } else if (config.layer_type === 'place_labels') { + layout['text-field'] = ['get', 'name']; + layout['text-font'] = ['DIN Pro Medium', 'Arial Unicode MS Regular']; + layout['text-size'] = [ + 'interpolate', + ['linear'], + ['zoom'], + 10, + 12, + 18, + 24 + ]; + } else if (config.layer_type === 'road_labels') { + layout['symbol-placement'] = 'line'; + layout['text-field'] = ['get', 'name']; + layout['text-font'] = ['DIN Pro Regular', 'Arial Unicode MS Regular']; + layout['text-size'] = 12; + layout['text-rotation-alignment'] = 'map'; + } else { + // Default layout from definition + for (const prop of layerDef.layoutProperties) { + if (prop.example !== undefined) { + layout[prop.property] = prop.example; + } } } + if (Object.keys(layout).length > 0) { layer.layout = layout; } @@ -573,4 +819,245 @@ ${JSON.stringify(style, null, 2)} return null; } + + private getDefaultLineWidth( + layerType: string, + isHighlight: boolean = false + ): unknown | null { + // Reasonable default line widths with zoom interpolation (25% thicker) + const roadWidths: Record = { + roads: [ + 'interpolate', + ['linear'], + ['zoom'], + 5, + 0.6, + 10, + 1.9, + 14, + 3.8, + 18, + 5.0 + ], + motorways: [ + 'interpolate', + ['linear'], + ['zoom'], + 5, + 1.0, + 10, + 2.5, + 14, + 4.4, + 18, + 6.3 + ], + primary_roads: [ + 'interpolate', + ['linear'], + ['zoom'], + 7, + 0.8, + 11, + 1.9, + 14, + 3.1, + 18, + 4.4 + ], + secondary_roads: [ + 'interpolate', + ['linear'], + ['zoom'], + 10, + 0.6, + 12, + 1.3, + 14, + 2.5, + 18, + 3.1 + ], + streets: [ + 'interpolate', + ['linear'], + ['zoom'], + 12, + 0.4, + 14, + 1.0, + 16, + 1.9, + 18, + 2.5 + ], + paths: [ + 'interpolate', + ['linear'], + ['zoom'], + 13, + 0.5, + 15, + 0.8, + 17, + 1.0, + 19, + 1.2 + ], + railways: [ + 'interpolate', + ['linear'], + ['zoom'], + 8, + 0.8, + 12, + 1.2, + 16, + 1.8, + 20, + 2.5 + ], + waterway: [ + 'interpolate', + ['exponential', 1.3], + ['zoom'], + 8, + 1.0, + 20, + 4.0 + ], + + // Administrative boundaries + country_boundaries: 2.0, + state_boundaries: 1.5 + }; + + // If highlighting, slightly increase the widths + if (isHighlight && roadWidths[layerType]) { + const baseExpression = roadWidths[layerType] as unknown[]; + const modifiedExpression = [...baseExpression]; + // Increase each width value by 20% + for (let i = 0; i < modifiedExpression.length; i++) { + if (typeof modifiedExpression[i] === 'number' && i % 2 === 0 && i > 3) { + modifiedExpression[i] = (modifiedExpression[i] as number) * 1.2; + } + } + return modifiedExpression; + } + + return roadWidths[layerType] || null; + } + + private getDefaultOpacity(layerType: string, layerDefType: string): number { + // Sophisticated opacity values for different layer types + const opacityMap: Record = { + // Water features - slightly transparent for depth + water: 0.85, + waterway: 0.75, + + // Natural features - soft and subtle + parks: 0.65, + landuse: 0.45, + landcover: 0.4, + + // Roads - much more subtle opacity + motorways: 0.4, + primary_roads: 0.35, + secondary_roads: 0.3, + streets: 0.25, + paths: 0.2, + railways: 0.3, + roads: 0.3, // General roads + + // Buildings - subtle presence + buildings: 0.6, + building_3d: 0.7, + + // Administrative - very subtle + country_boundaries: 0.8, + state_boundaries: 0.6, + + // Infrastructure + airports: 0.7, + transit: 0.75, + + // Labels should be fully opaque for readability + place_labels: 1.0, + road_labels: 1.0, + poi_labels: 1.0 + }; + + // Symbol layers should always be fully opaque + if (layerDefType === 'symbol') { + return 1.0; + } + + return opacityMap[layerType] || 0.7; + } + + private getHarmoniousColor(layerType: string, action: string): string { + // Define default colors for when user doesn't specify + const colorPalette = { + // Transportation colors - vibrant defaults + motorways: '#ff0000', // Pure red + primary_roads: '#ff6600', // Orange + secondary_roads: '#ffaa00', // Yellow-orange + streets: '#999999', // Medium gray + paths: '#666666', // Dark gray + railways: '#555555', // Very dark gray + roads: '#ff3333', // Generic road red + + // Water features (nice blues) + water: '#4A90E2', // Nice blue + waterway: '#5BA0F2', // Lighter blue + + // Natural features (greens) + parks: '#90C090', // Park green + landuse: '#A0D0A0', // Light green + landcover: '#B0E0B0', // Pale green + + // Administrative (purples) + country_boundaries: '#9966CC', // Purple + state_boundaries: '#B399D4', // Light purple + + // Labels (dark tones for readability) + place_labels: '#333333', // Dark gray + road_labels: '#444444', // Medium dark gray + poi_labels: '#555555', // Gray + + // Infrastructure + buildings: '#D4C4B0', // Tan + building_3d: '#C4B4A0', // Darker tan + airports: '#CC99CC', // Light purple + transit: '#6699CC', // Blue-gray + + // Default/highlight colors + default: '#808080', // Neutral gray + highlight: '#FF0000' // Red for highlights + }; + + // Return color based on layer type and action + if (action === 'highlight') { + // Use slightly more saturated versions for highlights + const highlightColors: Record = { + motorways: '#C0C0C0', // Medium gray for highlights + primary_roads: '#CCCCCC', // Slightly darker gray + secondary_roads: '#D0D0D0', // Light-medium gray + streets: '#D8D8D8', // Light gray + roads: '#CCCCCC', // Generic road highlight + water: '#7FA3CC', + parks: '#88B889', + country_boundaries: '#9B7FB8', + state_boundaries: '#B299CC', + transit: '#6B8FAA', + airports: '#D09099' + }; + return highlightColors[layerType] || colorPalette.highlight; + } + + return ( + colorPalette[layerType as keyof typeof colorPalette] || + colorPalette.default + ); + } } diff --git a/src/tools/update-style-tool/UpdateStyleTool.ts b/src/tools/update-style-tool/UpdateStyleTool.ts index 6fc3e41..2e9487a 100644 --- a/src/tools/update-style-tool/UpdateStyleTool.ts +++ b/src/tools/update-style-tool/UpdateStyleTool.ts @@ -1,3 +1,4 @@ +import { filterExpandedMapboxStyles } from '../../utils/styleUtils.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { UpdateStyleSchema, @@ -40,6 +41,7 @@ export class UpdateStyleTool extends MapboxApiBasedTool< } const data = await response.json(); - return data; + // Return full style but filter out expanded Mapbox styles + return filterExpandedMapboxStyles(data); } } diff --git a/src/types/mapbox-style.ts b/src/types/mapbox-style.ts index 55bfead..edd0773 100644 --- a/src/types/mapbox-style.ts +++ b/src/types/mapbox-style.ts @@ -142,6 +142,7 @@ export interface BaseLayer { filter?: Filter; layout?: Record; paint?: Record; + slot?: 'bottom' | 'middle' | 'top'; } export interface BackgroundLayer extends BaseLayer { @@ -442,6 +443,11 @@ export interface MapboxStyle { version: 8; name?: string; metadata?: Record; + imports?: Array<{ + id: string; + url: string; + data?: Record; + }>; center?: [number, number]; zoom?: number; bearing?: number; diff --git a/src/utils/styleUtils.ts b/src/utils/styleUtils.ts new file mode 100644 index 0000000..545f6bd --- /dev/null +++ b/src/utils/styleUtils.ts @@ -0,0 +1,31 @@ +/** + * Filters out expanded Mapbox styles from imports to reduce response size. + * This preserves the reference to the style (e.g., mapbox://styles/mapbox/standard) + * but removes the expanded style data that causes token overflow. + */ +export function filterExpandedMapboxStyles(style: T): T { + // Create a shallow copy + const filtered = { ...style } as T & { + imports?: Array<{ url: string; data?: unknown }>; + }; + + // Filter out the expanded data from Mapbox style imports + if (filtered.imports && Array.isArray(filtered.imports)) { + filtered.imports = filtered.imports.map((importItem) => { + // Keep the import reference but remove expanded data for Mapbox styles + if ( + importItem.url && + importItem.url.startsWith('mapbox://styles/mapbox/') + ) { + // Return only the reference, not the expanded data + return { + id: (importItem as Record).id, + url: importItem.url + }; + } + return importItem; + }); + } + + return filtered as T; +} diff --git a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap index ac7e99f..bc6c715 100644 --- a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -64,12 +64,37 @@ exports[`Tool Naming Convention > should maintain consistent tool list (snapshot }, { "className": "StyleBuilderTool", - "description": "Build custom Mapbox styles with precise control over layers and visual properties, including zoom-based and data-driven expressions. + "description": "Generate Mapbox style JSON for creating new styles or updating existing ones. Supports Mapbox Standard (default), Classic, and Blank base styles with full control over layers, expressions, and visual properties. -HOW TO CREATE A STYLE: -1. First, consult resource://mapbox-style-layers to see all available layer types -2. Use this tool to generate a style configuration -3. Apply the style using create_style_tool or update_style_tool +USAGE: +1. Use this tool to generate style JSON configuration +2. For NEW styles: Use the generated JSON with create_style_tool +3. For EXISTING styles: Use portions of the JSON with update_style_tool to modify specific layers + +BASE STYLES: +• standard (default): Modern Mapbox Standard with imports, requires slot property +• streets/light/dark/satellite/outdoors: Classic styles (deprecated but supported) +• blank: Empty style for full customization + +LAYER ORDERING: +• In ALL styles: Later layers in array render on top of earlier layers +• Standard style: 'slot' determines which section, array order matters within each slot +• Classic/Blank: Array order is the only control for layer stacking +Example: [background, water, roads, labels] = labels render on top + +MAPBOX STANDARD - SLOT PROPERTY: +When using Standard base style, each layer needs a 'slot' to control stacking: +• bottom: Below most map features (land, water) +• middle: Between base features and labels +• top: Above all base map features (default for visibility) +Within each slot, array order still applies - later layers render on top + +RESOURCE GUIDE: +The resource://mapbox-style-layers contains comprehensive documentation including: +• All available layer types with descriptions +• Paint and layout properties for each layer type +• Common filters and expressions +• Example configurations AVAILABLE LAYER TYPES: • water, waterway - Oceans, lakes, rivers diff --git a/test/tools/style-builder-tool/StyleBuilderTool.test.ts b/test/tools/style-builder-tool/StyleBuilderTool.test.ts index c838360..f9ab8b6 100644 --- a/test/tools/style-builder-tool/StyleBuilderTool.test.ts +++ b/test/tools/style-builder-tool/StyleBuilderTool.test.ts @@ -12,13 +12,13 @@ describe('StyleBuilderTool', () => { describe('basic functionality', () => { it('should have correct name and description', () => { expect(tool.name).toBe('style_builder_tool'); - expect(tool.description).toContain('Build custom Mapbox styles'); + expect(tool.description).toContain('Generate Mapbox style JSON'); }); it('should build a basic style with water layer', async () => { const input: StyleBuilderToolInput = { style_name: 'Test Style', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'water', @@ -42,7 +42,7 @@ describe('StyleBuilderTool', () => { it('should handle dark mode', async () => { const input: StyleBuilderToolInput = { style_name: 'Dark Mode Style', - base_style: 'streets-v12', + base_style: 'streets', // Use classic style to test background color layers: [], global_settings: { mode: 'dark', @@ -63,7 +63,7 @@ describe('StyleBuilderTool', () => { it('should handle color action', async () => { const input: StyleBuilderToolInput = { style_name: 'Color Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'primary_roads', @@ -83,7 +83,7 @@ describe('StyleBuilderTool', () => { it('should handle highlight action', async () => { const input: StyleBuilderToolInput = { style_name: 'Highlight Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'railways', @@ -105,7 +105,7 @@ describe('StyleBuilderTool', () => { it('should handle hide action', async () => { const input: StyleBuilderToolInput = { style_name: 'Hide Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'place_labels', @@ -124,7 +124,7 @@ describe('StyleBuilderTool', () => { it('should handle show action', async () => { const input: StyleBuilderToolInput = { style_name: 'Show Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'buildings', @@ -145,7 +145,7 @@ describe('StyleBuilderTool', () => { it('should handle country boundaries with correct filters', async () => { const input: StyleBuilderToolInput = { style_name: 'Country Boundaries Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'country_boundaries', @@ -185,7 +185,7 @@ describe('StyleBuilderTool', () => { it('should handle state boundaries', async () => { const input: StyleBuilderToolInput = { style_name: 'State Boundaries Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'state_boundaries', @@ -220,7 +220,7 @@ describe('StyleBuilderTool', () => { it('should generate valid Mapbox style JSON', async () => { const input: StyleBuilderToolInput = { style_name: 'Valid Style Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'water', @@ -246,24 +246,28 @@ describe('StyleBuilderTool', () => { // Check basic style structure expect(style.version).toBe(8); expect(style.name).toBe('Valid Style Test'); - expect(style.sources).toBeTruthy(); - expect(style.sources.composite).toBeTruthy(); - expect(style.sources.composite.url).toBe( - 'mapbox://mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2' - ); - expect(style.sprite).toContain('streets-v12'); - expect(style.glyphs).toContain('mapbox://fonts'); + // For standard style, check imports instead of sources + expect(style.imports).toBeTruthy(); + expect(Array.isArray(style.imports)).toBe(true); + expect(style.imports[0]).toEqual({ + id: 'basemap', + url: 'mapbox://styles/mapbox/standard' + }); expect(Array.isArray(style.layers)).toBe(true); - // Check background layer is always added - const bgLayer = style.layers.find((l: any) => l.id === 'background'); - expect(bgLayer).toBeTruthy(); + // Standard styles don't have background layers (provided by import) + // Only check for background in non-standard styles + if (input.base_style !== 'standard') { + const bgLayer = style.layers.find((l: any) => l.id === 'background'); + expect(bgLayer).toBeTruthy(); + } }); - it('should include essential layers by default', async () => { + it('should include only background layer when no layers specified', async () => { + // Test with classic style const input: StyleBuilderToolInput = { style_name: 'Essential Layers Test', - base_style: 'streets-v12', + base_style: 'streets', // Use classic style layers: [] // No layers specified }; @@ -273,14 +277,11 @@ describe('StyleBuilderTool', () => { const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); - // Should have at least background and water - expect(style.layers.length).toBeGreaterThanOrEqual(2); + // Classic styles should only have background when no layers specified + expect(style.layers.length).toBe(1); const bgLayer = style.layers.find((l: any) => l.id === 'background'); - const waterLayer = style.layers.find((l: any) => l.id === 'water-custom'); - expect(bgLayer).toBeTruthy(); - expect(waterLayer).toBeTruthy(); }); }); @@ -288,7 +289,7 @@ describe('StyleBuilderTool', () => { it('should handle unknown layer types gracefully', async () => { const input: StyleBuilderToolInput = { style_name: 'Unknown Layer Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'unknown_layer' as any, @@ -309,7 +310,7 @@ describe('StyleBuilderTool', () => { it('should handle custom filters', async () => { const input: StyleBuilderToolInput = { style_name: 'Custom Filter Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'motorways', @@ -340,7 +341,7 @@ describe('StyleBuilderTool', () => { it('should generate zoom-based expressions', async () => { const input: StyleBuilderToolInput = { style_name: 'Zoom Expression Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'motorways', @@ -376,7 +377,7 @@ describe('StyleBuilderTool', () => { it('should generate data-driven expressions', async () => { const input: StyleBuilderToolInput = { style_name: 'Data Driven Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'primary_roads', @@ -414,7 +415,7 @@ describe('StyleBuilderTool', () => { it('should handle custom expressions', async () => { const input: StyleBuilderToolInput = { style_name: 'Custom Expression Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'buildings', @@ -453,7 +454,7 @@ describe('StyleBuilderTool', () => { it('should generate opacity interpolation with zoom', async () => { const input: StyleBuilderToolInput = { style_name: 'Opacity Zoom Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'buildings', @@ -561,7 +562,7 @@ describe('StyleBuilderTool', () => { it('should filter roads by class', async () => { const input: StyleBuilderToolInput = { style_name: 'Motorway Filter Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'motorways', @@ -592,7 +593,7 @@ describe('StyleBuilderTool', () => { it('should filter by multiple properties', async () => { const input: StyleBuilderToolInput = { style_name: 'Bridge Motorways Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'motorways', @@ -626,7 +627,7 @@ describe('StyleBuilderTool', () => { it('should filter admin boundaries correctly', async () => { const input: StyleBuilderToolInput = { style_name: 'Undisputed Countries Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'country_boundaries', @@ -659,11 +660,168 @@ describe('StyleBuilderTool', () => { }); }); + describe('style types', () => { + it('should generate Standard style with imports', async () => { + const input: StyleBuilderToolInput = { + style_name: 'Standard Style Test', + base_style: 'standard', + layers: [ + { + layer_type: 'water', + action: 'color', + color: '#0099ff' + } + ] + }; + + const result = await tool.execute(input); + const text = result.content[0].text; + + const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); + const style = JSON.parse(jsonMatch![1]); + + // Check that Standard style uses imports + expect(style.imports).toBeTruthy(); + expect(Array.isArray(style.imports)).toBe(true); + expect(style.imports[0]).toEqual({ + id: 'basemap', + url: 'mapbox://styles/mapbox/standard' + }); + // Should have sources defined (required by spec) + // With custom layers, it needs composite source + expect(style.sources).toBeDefined(); + expect(style.sources.composite).toBeDefined(); + + // Check that layers have slot property for Standard style + style.layers.forEach((layer: any) => { + expect(layer.slot).toBe('top'); + }); + }); + + it('should generate Classic style with sources', async () => { + const input: StyleBuilderToolInput = { + style_name: 'Classic Style Test', + base_style: 'streets', + layers: [ + { + layer_type: 'water', + action: 'color', + color: '#0099ff' + } + ] + }; + + const result = await tool.execute(input); + const text = result.content[0].text; + + const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); + const style = JSON.parse(jsonMatch![1]); + + // Check that Classic style uses traditional sources + expect(style.sources).toBeTruthy(); + expect(style.sources.composite).toBeTruthy(); + expect(style.sources.composite.url).toBe( + 'mapbox://mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2' + ); + expect(style.sprite).toContain('streets-v12'); + expect(style.glyphs).toContain('mapbox://fonts'); + // Should not have imports for classic styles + expect(style.imports).toBeUndefined(); + + // Classic styles should not have slot property + style.layers.forEach((layer: any) => { + expect(layer.slot).toBeUndefined(); + }); + }); + + it('should use custom slot for Standard style layers', async () => { + const input: StyleBuilderToolInput = { + style_name: 'Custom Slot Test', + base_style: 'standard', + layers: [ + { + layer_type: 'water', + action: 'color', + color: '#0099ff', + slot: 'bottom' + }, + { + layer_type: 'parks', + action: 'color', + color: '#00ff00', + slot: 'middle' + }, + { + layer_type: 'poi_labels', + action: 'show', + slot: 'top' + } + ] + }; + + const result = await tool.execute(input); + const text = result.content[0].text; + + const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); + const style = JSON.parse(jsonMatch![1]); + + // Check that layers have correct custom slots + // Note: layer IDs include the original layer ID plus '-custom' suffix + const waterLayer = style.layers.find((l: any) => l.id === 'water-custom'); + const parksLayer = style.layers.find( + (l: any) => l.id === 'landuse_park-custom' + ); + const poiLayer = style.layers.find( + (l: any) => l.id === 'poi-label-custom' + ); + + expect(waterLayer).toBeTruthy(); + expect(parksLayer).toBeTruthy(); + expect(poiLayer).toBeTruthy(); + + expect(waterLayer.slot).toBe('bottom'); + expect(parksLayer.slot).toBe('middle'); + expect(poiLayer.slot).toBe('top'); + }); + + it('should generate Blank style with sources but no imports', async () => { + const input: StyleBuilderToolInput = { + style_name: 'Blank Style Test', + base_style: 'blank', + layers: [ + { + layer_type: 'water', + action: 'color', + color: '#0099ff' + } + ] + }; + + const result = await tool.execute(input); + const text = result.content[0].text; + + const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); + const style = JSON.parse(jsonMatch![1]); + + // Check that Blank style has sources but no imports + expect(style.sources).toBeTruthy(); + expect(style.sources.composite).toBeTruthy(); + expect(style.sprite).toContain('streets-v12'); + expect(style.glyphs).toContain('mapbox://fonts'); + expect(style.imports).toBeUndefined(); + + // Blank styles should not have slot property + style.layers.forEach((layer: any) => { + expect(layer.slot).toBeUndefined(); + }); + }); + }); + describe('multiple layers', () => { it('should handle multiple layers with different actions', async () => { const input: StyleBuilderToolInput = { style_name: 'Multi Layer Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { layer_type: 'water', From f996e9e1838eabfa94925eb1edb22e6848986326 Mon Sep 17 00:00:00 2001 From: jussi-sa Date: Wed, 17 Sep 2025 11:24:27 +0300 Subject: [PATCH 2/8] fixing boundaries --- .../style-builder-tool/StyleBuilderTool.ts | 7 ++++++- .../tool-naming-convention.test.ts.snap | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/tools/style-builder-tool/StyleBuilderTool.ts b/src/tools/style-builder-tool/StyleBuilderTool.ts index 6620d3e..c10a469 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.ts @@ -562,7 +562,12 @@ ${JSON.stringify(style, null, 2)} const [property, values] = condition.split(':').map((s) => s.trim()); const valueList = values.split('|').map((v) => { const trimmed = v.trim(); - // Handle boolean strings + // Special handling for admin layer properties that use string booleans + // maritime and disputed use "true"/"false" as strings, not booleans + if (property === 'maritime' || property === 'disputed') { + return trimmed; // Keep as string "true" or "false" + } + // Handle boolean strings for other properties if (trimmed === 'true') return true; if (trimmed === 'false') return false; // Try to parse as number diff --git a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap index bc6c715..bce6f10 100644 --- a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -100,16 +100,28 @@ AVAILABLE LAYER TYPES: • water, waterway - Oceans, lakes, rivers • landuse, parks - Land areas like parks, hospitals, schools • buildings, building_3d - Building footprints and 3D extrusions -• roads (motorways, primary_roads, secondary_roads, streets, paths, railways) +• ROAD TYPES (use these specific types for best results): + - motorways - Highway/freeway roads (class: motorway) + - primary_roads - Major roads (class: primary, trunk) + - secondary_roads - Secondary roads (class: secondary) + - streets - Local streets (class: street, street_limited) + - paths - Walking/cycling paths (class: path, pedestrian) + - railways - Rail lines + - roads - Generic/all roads (avoid using - use specific types above instead) • country_boundaries, state_boundaries - Administrative borders • place_labels, road_labels, poi_labels - Text labels • landcover - Natural features like forests, grass • airports - Airport features • transit - Bus stops, subway entrances, rail stations (filter by maki: bus, entrance, rail-metro) +IMPORTANT FOR ROADS: +• Always use specific road layer types (motorways, primary_roads, etc.) instead of generic 'roads' +• Each road type automatically includes proper filters and zoom-based width interpolation +• Don't specify fixed widths - the tool automatically applies appropriate zoom-based scaling + ACTIONS YOU CAN APPLY: -• color - Set the layer's color -• highlight - Make layer prominent with color/width +• color - Set the layer's color (roads will use smart defaults if not specified) +• highlight - Make layer prominent with enhanced color/width • hide - Remove layer from view • show - Display layer with default styling From c52f03040f5de66a0bd714a4c4dc484d99deffef Mon Sep 17 00:00:00 2001 From: jussi-sa Date: Wed, 17 Sep 2025 16:52:04 +0300 Subject: [PATCH 3/8] fine tuning for standard styles --- src/constants/mapboxStreetsV8Fields.ts | 4022 ++++++++++++++++- src/constants/mapboxStyleLayers.ts | 54 +- .../StyleBuilderTool.schema.ts | 179 +- .../style-builder-tool/StyleBuilderTool.ts | 642 ++- src/utils/styleUtils.ts | 34 +- .../tool-naming-convention.test.ts.snap | 20 + .../StyleBuilderTool.test.ts | 111 +- 7 files changed, 4698 insertions(+), 364 deletions(-) diff --git a/src/constants/mapboxStreetsV8Fields.ts b/src/constants/mapboxStreetsV8Fields.ts index 16201a8..726cff3 100644 --- a/src/constants/mapboxStreetsV8Fields.ts +++ b/src/constants/mapboxStreetsV8Fields.ts @@ -1,118 +1,14 @@ /** - * Complete field definitions for Mapbox Streets v8 source layers - * This provides all available properties for filtering + * Complete Mapbox Streets v8 source layer field definitions + * Extracted from actual Streets v8 tileset data + * AUTO-GENERATED - DO NOT EDIT MANUALLY */ export const STREETS_V8_FIELDS = { - road: { - class: { - description: 'Road classification', - values: [ - 'motorway', - 'motorway_link', - 'trunk', - 'trunk_link', - 'primary', - 'primary_link', - 'secondary', - 'secondary_link', - 'tertiary', - 'tertiary_link', - 'street', - 'street_limited', - 'pedestrian', - 'construction', - 'track', - 'service', - 'ferry', - 'path', - 'golf', - 'level_crossing', - 'turning_circle', - 'roundabout', - 'mini_roundabout', - 'turning_loop', - 'traffic_signals', - 'major_rail', - 'minor_rail', - 'service_rail', - 'aerialway' - ] as const - }, - structure: { - description: 'Physical structure', - values: ['none', 'bridge', 'tunnel', 'ford'] as const - }, - type: { - description: 'Specific road type from OSM tags', - values: [ - 'steps', - 'corridor', - 'parking_aisle', - 'platform', - 'piste' - ] as const - }, - oneway: { - description: 'One-way traffic', - values: ['true', 'false'] as const - }, - dual_carriageway: { - description: 'Part of dual carriageway', - values: ['true', 'false'] as const - }, - surface: { - description: 'Road surface', - values: ['paved', 'unpaved'] as const - }, - toll: { - description: 'Toll road', - values: ['true', 'false'] as const - }, - layer: { - description: 'Z-ordering layer (-5 to 5)', - type: 'number' as const - }, - lane_count: { - description: 'Number of lanes', - type: 'number' as const - } - }, - - admin: { - admin_level: { - description: 'Administrative level', - values: [0, 1, 2] as const // 0=country, 1=state/province, 2=county - }, - disputed: { - description: 'Disputed boundary', - values: ['true', 'false'] as const - }, - maritime: { - description: 'Maritime boundary', - values: ['true', 'false'] as const - }, - worldview: { - description: 'Worldview perspective', - values: [ - 'all', - 'CN', - 'IN', - 'US', - 'JP', - 'AR', - 'MA', - 'RS', - 'RU', - 'TR', - 'VN' - ] as const - } - }, - + // ============ landuse ============ landuse: { class: { - description: 'Landuse classification', + description: 'class field', values: [ 'aboriginal_lands', 'agriculture', @@ -135,42 +31,227 @@ export const STREETS_V8_FIELDS = { 'scrub', 'wood' ] as const - } - }, - - landuse_overlay: { - class: { - description: 'Overlay classification', - values: ['national_park', 'wetland', 'wetland_noveg'] as const - } - }, - - building: { - extrude: { - description: 'Should be extruded in 3D', - values: ['true', 'false'] as const - }, - underground: { - description: 'Underground building', - values: ['true', 'false'] as const }, - height: { - description: 'Building height', - type: 'number' as const - }, - min_height: { - description: 'Building base height', - type: 'number' as const + type: { + description: 'type field', + values: [ + 'wood', + 'farmland', + 'forest', + 'grass', + 'meadow', + 'scrub', + 'parking', + 'surface', + 'park', + 'farmyard', + 'orchard', + 'grassland', + 'garden', + 'school', + 'soccer', + 'vineyard', + 'playground', + 'tennis', + 'bare_rock', + 'allotments', + 'pitch', + 'heath', + 'baseball', + 'bunker', + 'quarry', + 'beach', + 'basketball', + 'scree', + 'village_green', + 'recreation_ground', + 'sports_centre', + 'common', + 'christian', + 'tee', + 'sand', + 'green', + 'multi', + 'hospital', + 'greenhouse_horticulture', + 'glacier', + 'fairway', + 'farm', + 'golf_course', + 'camp_site', + 'university', + 'college', + 'plant_nursery', + 'equestrian', + 'fell', + 'beachvolleyball', + 'volleyball', + 'american_football', + 'athletics', + 'caravan_site', + 'rock', + 'muslim', + 'skateboard', + 'wetland', + 'bowls', + 'picnic_site', + 'boules', + 'cricket', + 'dog_park', + 'running', + 'conservation', + 'track', + 'netball', + 'underground', + 'lane', + 'rugby_union', + 'zoo', + 'hockey', + 'shooting', + 'downhill', + 'jewish', + 'field', + 'football', + 'table_tennis', + 'handball', + 'rough', + 'field_hockey', + 'team_handball', + 'carports', + 'pelota', + 'rugby', + 'paddle_tennis', + 'archery', + 'horse_racing', + 'gaelic_games', + 'softball', + 'golf', + 'ice_hockey', + 'basin', + 'coastline', + 'badminton', + 'driving_range', + 'bog', + 'cricket_nets', + 'swimming', + 'futsal' + ] as const } }, - water: { - // Water has no filterable fields - }, - + // ============ waterway ============ waterway: { + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const + }, + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string + }, class: { - description: 'Waterway classification', + description: 'class field', values: [ 'river', 'canal', @@ -181,132 +262,3611 @@ export const STREETS_V8_FIELDS = { ] as const }, type: { - description: 'Waterway type', + description: 'type field', values: ['river', 'canal', 'stream', 'ditch', 'drain'] as const } }, - aeroway: { - type: { - description: 'Aeroway type', - values: ['runway', 'taxiway', 'apron', 'helipad'] as const - } - }, + // ============ water ============ + water: {}, - place_label: { - class: { - description: 'Place classification', + // ============ aeroway ============ + aeroway: { + iso_3166_1: { + description: 'iso_3166_1 field', values: [ - 'country', - 'state', - 'settlement', - 'settlement_subdivision' + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' ] as const }, - capital: { - description: 'Capital admin level', - values: [2, 3, 4, 5, 6] as const + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string }, - filterrank: { - description: 'Priority for label density', - type: 'number' as const // 0-5 + type: { + description: 'type field', + values: ['runway', 'taxiway', 'apron', 'helipad'] as const }, - symbolrank: { - description: 'Symbol ranking', - type: 'number' as const + ref: { + description: 'ref field', + values: [] as const // string } }, - poi_label: { - class: { - description: 'POI thematic grouping', - type: 'string' as const + // ============ structure ============ + structure: { + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const }, - filterrank: { - description: 'Priority for label density', - type: 'number' as const // 0-5 + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string }, - maki: { - description: 'Icon to use (e.g., airport, hospital, restaurant, park)', - type: 'string' as const - } - }, - - natural_label: { class: { - description: 'Natural feature classification', + description: 'class field', values: [ - 'glacier', - 'landform', - 'water_feature', - 'wetland', - 'ocean', - 'sea', - 'river', - 'water', - 'reservoir', - 'dock', - 'canal', - 'drain', - 'ditch', - 'stream', - 'continent' + 'cliff', + 'crosswalk', + 'entrance', + 'fence', + 'gate', + 'hedge', + 'land' ] as const }, - elevation_m: { - description: 'Elevation in meters', - type: 'number' as const + type: { + description: 'type field', + values: [ + 'bollard', + 'breakwater', + 'bridge', + 'city_wall', + 'cliff', + 'crosswalk', + 'earth_bank', + 'entrance', + 'fence', + 'gate', + 'hedge', + 'home', + 'kissing_gate', + 'lift_gate', + 'main', + 'pier', + 'retaining_wall', + 'sliding_gate', + 'spikes', + 'staircase', + 'swing_gate', + 'wall', + 'wire_fence', + 'yes' + ] as const } }, - transit_stop_label: { - mode: { - description: 'Transit mode', + // ============ building ============ + building: { + iso_3166_1: { + description: 'iso_3166_1 field', values: [ - 'rail', - 'metro_rail', - 'light_rail', - 'tram', - 'bus', - 'monorail', - 'funicular', - 'bicycle', - 'ferry', - 'narrow_gauge', - 'preserved', - 'miniature' + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' ] as const }, - maki: { - description: 'Icon type (visual representation of transit type)', + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string + }, + extrude: { + description: 'extrude field', + values: ['true', 'false'] as const + }, + building_id: { + description: 'building_id field', + values: [] as const // number - Range: 0 to 100000000000 + }, + height: { + description: 'height field', + values: [] as const // number - Range: 0 to 1500 + }, + min_height: { + description: 'min_height field', + values: [] as const // number - Range: 0 to 1500 + }, + type: { + description: 'type field', values: [ - 'rail', - 'rail-metro', - 'rail-light', - 'entrance', - 'bus', - 'bicycle-share', - 'ferry' + 'building', + 'house', + 'residential', + 'garage', + 'apartments', + 'industrial', + 'hut', + 'detached', + 'shed', + 'roof', + 'commercial', + 'terrace', + 'garages', + 'school', + 'building:part', + 'retail', + 'construction', + 'greenhouse', + 'barn', + 'farm_auxiliary', + 'church', + 'warehouse', + 'service', + 'farm', + 'civic', + 'cabin', + 'manufacture', + 'university', + 'office', + 'static_caravan', + 'hangar', + 'public', + 'collapsed', + 'hospital', + 'semidetached_house', + 'hotel', + 'bungalow', + 'chapel', + 'ger', + 'kindergarten', + 'ruins', + 'parking', + 'storage_tank', + 'dormitory', + 'mosque', + 'commercial;residential', + 'transportation', + 'stable', + 'train_station', + 'damaged', + 'college', + 'semi', + 'transformer_tower', + 'houseboat', + 'trullo', + 'bunker', + 'station', + 'slurry_tank', + 'shop', + 'cowshed', + 'silo', + 'supermarket', + 'pajaru', + 'terminal', + 'carport', + 'residence', + 'dam', + 'temple', + 'duplex', + 'factory', + 'agricultural', + 'constructie', + 'allotment_house', + 'chalet', + 'kiosk', + 'tower', + 'tank', + 'shelter', + 'dwelling_house', + 'pavilion', + 'grandstand', + 'Residence', + 'ruin', + 'boathouse', + 'store', + 'summer_cottage', + 'mobile_home', + 'government_office', + 'outbuilding', + 'beach_hut', + 'pub', + 'glasshouse', + 'apartment', + 'storage', + 'community_group_office', + 'clinic', + 'residences', + 'cathedral', + 'bangunan', + 'semi-detached' ] as const + }, + underground: { + description: 'underground field', + values: ['true', 'false'] as const } }, - airport_label: { + // ============ landuse_overlay ============ + landuse_overlay: { class: { - description: 'Airport classification', - values: ['military', 'civil'] as const + description: 'class field', + values: ['national_park', 'wetland', 'wetland_noveg'] as const }, - maki: { - description: 'Icon type (visual representation)', - values: ['airport', 'heliport', 'rocket'] as const + type: { + description: 'type field', + values: [ + 'wetland', + 'bog', + 'basin', + 'marsh', + 'swamp', + 'nature_reserve', + 'protected_area', + 'reedbed', + 'wet_meadow', + 'tidalflat', + 'mangrove', + 'mud', + 'saltmarsh', + 'national_park', + 'string_bog', + 'saltern', + 'fen', + 'palsa_bog', + 'tundra_pond', + 'peat_bog', + 'reed', + 'raised_bog', + 'reef' + ] as const + }, + name: { + description: 'name field', + values: [] as const // string + }, + name_de: { + description: 'name_de field', + values: [] as const // string + }, + name_en: { + description: 'name_en field', + values: [] as const // string + }, + name_es: { + description: 'name_es field', + values: [] as const // string + }, + name_fr: { + description: 'name_fr field', + values: [] as const // string + }, + name_ru: { + description: 'name_ru field', + values: [] as const // string + }, + 'name_zh-Hant': { + description: 'name_zh-Hant field', + values: [] as const // string + }, + 'name_zh-Hans': { + description: 'name_zh-Hans field', + values: [] as const // string + }, + name_pt: { + description: 'name_pt field', + values: [] as const // string + }, + name_ar: { + description: 'name_ar field', + values: [] as const // string + }, + name_vi: { + description: 'name_vi field', + values: [] as const // string + }, + name_it: { + description: 'name_it field', + values: [] as const // string + }, + name_ja: { + description: 'name_ja field', + values: [] as const // string + }, + name_ko: { + description: 'name_ko field', + values: [] as const // string + } + }, + + // ============ road ============ + road: { + name: { + description: 'name field', + values: [] as const // string + }, + name_de: { + description: 'name_de field', + values: [] as const // string + }, + name_en: { + description: 'name_en field', + values: [] as const // string + }, + name_es: { + description: 'name_es field', + values: [] as const // string + }, + name_fr: { + description: 'name_fr field', + values: [] as const // string + }, + name_ru: { + description: 'name_ru field', + values: [] as const // string + }, + 'name_zh-Hant': { + description: 'name_zh-Hant field', + values: [] as const // string + }, + 'name_zh-Hans': { + description: 'name_zh-Hans field', + values: [] as const // string + }, + name_pt: { + description: 'name_pt field', + values: [] as const // string + }, + name_ar: { + description: 'name_ar field', + values: [] as const // string + }, + name_vi: { + description: 'name_vi field', + values: [] as const // string + }, + name_it: { + description: 'name_it field', + values: [] as const // string + }, + name_ja: { + description: 'name_ja field', + values: [] as const // string + }, + name_ko: { + description: 'name_ko field', + values: [] as const // string + }, + name_script: { + description: 'name_script field', + values: [ + 'Arabic', + 'Armenian', + 'Bengali', + 'Bopomofo', + 'Canadian_Aboriginal', + 'Common', + 'Cyrillic', + 'Devanagari', + 'Ethiopic', + 'Georgian', + 'Glagolitic', + 'Greek', + 'Gujarati', + 'Gurmukhi', + 'Han', + 'Hangul', + 'Hebrew', + 'Hiragana', + 'Kannada', + 'Katakana', + 'Khmer', + 'Lao', + 'Latin', + 'Malayalam', + 'Mongolian', + 'Myanmar', + 'Nko', + 'Sinhala', + 'Syriac', + 'Tamil', + 'Telugu', + 'Thaana', + 'Thai', + 'Tibetan', + 'Tifinagh', + 'Unknown' + ] as const + }, + oneway: { + description: 'oneway field', + values: ['true', 'false'] as const + }, + bike_lane: { + description: 'bike_lane field', + values: ['left', 'right', 'both', 'no', 'yes'] as const + }, + layer: { + description: 'layer field', + values: [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5] as const + }, + access: { + description: 'access field', + values: ['restricted'] as const + }, + dual_carriageway: { + description: 'dual_carriageway field', + values: ['true', 'false'] as const + }, + structure: { + description: 'structure field', + values: ['none', 'bridge', 'tunnel', 'ford'] as const + }, + surface: { + description: 'surface field', + values: ['paved', 'unpaved'] as const + }, + len: { + description: 'len field', + values: [] as const // number - Range: 0 to 99999 + }, + ref: { + description: 'ref field', + values: [] as const // string + }, + reflen: { + description: 'reflen field', + values: [] as const // number - Range: 0 to 250 + }, + shield: { + description: 'shield field', + values: [ + 'default', + 'rectangle-white', + 'rectangle-red', + 'rectangle-yellow', + 'rectangle-green', + 'rectangle-blue', + 'circle-white', + 'ae-national', + 'ae-d-route', + 'ae-f-route', + 'ae-s-route', + 'au-national-highway', + 'au-national-route', + 'au-state', + 'au-tourist', + 'br-federal', + 'br-state', + 'ch-motorway', + 'cn-nths-expy', + 'cn-provincial-expy', + 'de-motorway', + 'gr-motorway', + 'hk-strategic-route', + 'hr-motorway', + 'hu-motorway', + 'hu-main', + 'in-national', + 'in-state', + 'kr-natl-expy', + 'kr-natl-hwy', + 'kr-metro-expy', + 'kr-metropolitan', + 'kr-local', + 'mx-federal', + 'mx-state', + 'nz-state', + 'pe-national', + 'pe-regional', + 'ro-national', + 'ro-county', + 'ro-communal', + 'si-motorway', + 'tw-national', + 'tw-provincial-expy', + 'tw-provincial', + 'tw-county-township', + 'us-interstate', + 'us-interstate-duplex', + 'us-interstate-business', + 'us-interstate-truck', + 'us-highway', + 'us-highway-duplex', + 'us-highway-alternate', + 'us-highway-business', + 'us-highway-bypass', + 'us-highway-truck', + 'us-bia', + 'za-national', + 'za-provincial' + ] as const + }, + shield_beta: { + description: 'shield_beta field', + values: [ + 'default', + 'rectangle-white', + 'rectangle-red', + 'rectangle-yellow', + 'rectangle-green', + 'rectangle-blue', + 'circle-white', + 'ae-national', + 'ae-d-route', + 'ae-f-route', + 'ae-s-route', + 'au-national-highway', + 'au-national-route', + 'au-state', + 'au-tourist', + 'br-federal', + 'br-state', + 'ch-motorway', + 'cn-nths-expy', + 'cn-provincial-expy', + 'de-motorway', + 'gr-motorway', + 'hk-strategic-route', + 'hr-motorway', + 'hu-motorway', + 'hu-main', + 'in-national', + 'in-state', + 'kr-natl-expy', + 'kr-natl-hwy', + 'kr-metro-expy', + 'kr-metropolitan', + 'kr-local', + 'mx-federal', + 'mx-state', + 'nz-state', + 'pe-national', + 'pe-regional', + 'ro-national', + 'ro-county', + 'ro-communal', + 'si-motorway', + 'tw-national', + 'tw-provincial-expy', + 'tw-provincial', + 'tw-county-township', + 'us-interstate', + 'us-interstate-duplex', + 'us-interstate-business', + 'us-interstate-truck', + 'us-highway', + 'us-highway-duplex', + 'us-highway-alternate', + 'us-highway-business', + 'us-highway-bypass', + 'us-highway-truck', + 'us-bia', + 'za-national', + 'za-provincial', + 'al-motorway', + 'ar-national', + 'cl-highway', + 'co-national', + 'cy-motorway', + 'il-highway-black', + 'il-highway-blue', + 'il-highway-green', + 'il-highway-red', + 'it-motorway', + 'md-local', + 'md-main', + 'my-expressway', + 'my-federal', + 'nz-urban', + 'ph-expressway', + 'ph-primary', + 'qa-main', + 'sa-highway', + 'th-highway', + 'th-motorway-toll', + 'tr-motorway' + ] as const + }, + shield_text_color: { + description: 'shield_text_color field', + values: ['black', 'blue', 'white', 'yellow', 'orange'] as const + }, + shield_text_color_beta: { + description: 'shield_text_color_beta field', + values: ['black', 'blue', 'white', 'yellow', 'red', 'green'] as const + }, + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const + }, + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string + }, + class: { + description: 'class field', + values: [ + 'motorway', + 'motorway_link', + 'trunk', + 'trunk_link', + 'primary', + 'primary_link', + 'secondary', + 'secondary_link', + 'tertiary', + 'tertiary_link', + 'level_crossing', + 'street', + 'street_limited', + 'pedestrian', + 'construction', + 'track', + 'service', + 'ferry', + 'path', + 'major_rail', + 'minor_rail', + 'service_rail', + 'aerialway', + 'golf', + 'turning_circle', + 'roundabout', + 'mini_roundabout', + 'turning_loop', + 'traffic_signals', + 'intersection' + ] as const + }, + type: { + description: 'type field', + values: [ + 'motorway', + 'motorway_link', + 'trunk', + 'primary', + 'secondary', + 'tertiary', + 'trunk_link', + 'primary_link', + 'secondary_link', + 'tertiary_link', + 'residential', + 'unclassified', + 'road', + 'living_street', + 'level_crossing', + 'raceway', + 'pedestrian', + 'platform', + 'construction:motorway', + 'construction:motorway_link', + 'construction:trunk', + 'construction:trunk_link', + 'construction:primary', + 'construction:primary_link', + 'construction:secondary', + 'construction:secondary_link', + 'construction:tertiary', + 'construction:tertiary_link', + 'construction:unclassified', + 'construction:residential', + 'construction:road', + 'construction:living_street', + 'construction:pedestrian', + 'construction', + 'track:grade1', + 'track:grade2', + 'track:grade3', + 'track:grade4', + 'track:grade5', + 'track', + 'service:alley', + 'service:emergency_access', + 'service:drive_through', + 'service:driveway', + 'service:parking_aisle', + 'service', + 'ferry', + 'ferry_auto', + 'steps', + 'corridor', + 'sidewalk', + 'crossing', + 'piste', + 'mountain_bike', + 'hiking', + 'trail', + 'cycleway', + 'footway', + 'path', + 'bridleway', + 'rail', + 'subway', + 'narrow_gauge', + 'funicular', + 'light_rail', + 'miniature', + 'monorail', + 'preserved', + 'tram', + 'aerialway:cable_car', + 'aerialway:gondola', + 'aerialway:mixed_lift', + 'aerialway:chair_lift', + 'aerialway:drag_lift', + 'aerialway:magic_carpet', + 'aerialway', + 'hole', + 'turning_circle', + 'mini_roundabout', + 'traffic_signals' + ] as const + }, + toll: { + description: 'toll field', + values: ['true'] as const + }, + lane_count: { + description: 'lane_count field', + values: [] as const // number - Range: 0 to 20 + } + }, + + // ============ admin ============ + admin: { + admin_level: { + description: 'admin_level field', + values: [0, 1, 2] as const + }, + disputed: { + description: 'disputed field', + values: ['true', 'false'] as const + }, + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const + }, + maritime: { + description: 'maritime field', + values: ['true', 'false'] as const + }, + worldview: { + description: 'worldview field', + values: ['JP', 'CN', 'IN', 'US', 'all'] as const + } + }, + + // ============ place_label ============ + place_label: { + class: { + description: 'class field', + values: [ + 'country', + 'disputed_country', + 'state', + 'disputed_state', + 'settlement', + 'settlement_subdivision' + ] as const + }, + abbr: { + description: 'abbr field', + values: [] as const // string + }, + name: { + description: 'name field', + values: [] as const // string + }, + name_de: { + description: 'name_de field', + values: [] as const // string + }, + name_en: { + description: 'name_en field', + values: [] as const // string + }, + name_es: { + description: 'name_es field', + values: [] as const // string + }, + name_fr: { + description: 'name_fr field', + values: [] as const // string + }, + name_ru: { + description: 'name_ru field', + values: [] as const // string + }, + 'name_zh-Hant': { + description: 'name_zh-Hant field', + values: [] as const // string + }, + 'name_zh-Hans': { + description: 'name_zh-Hans field', + values: [] as const // string + }, + name_pt: { + description: 'name_pt field', + values: [] as const // string + }, + name_ar: { + description: 'name_ar field', + values: [] as const // string + }, + name_vi: { + description: 'name_vi field', + values: [] as const // string + }, + name_it: { + description: 'name_it field', + values: [] as const // string + }, + name_ja: { + description: 'name_ja field', + values: [] as const // string + }, + name_ko: { + description: 'name_ko field', + values: [] as const // string + }, + name_script: { + description: 'name_script field', + values: [ + 'Arabic', + 'Armenian', + 'Bengali', + 'Bopomofo', + 'Canadian_Aboriginal', + 'Common', + 'Cyrillic', + 'Devanagari', + 'Ethiopic', + 'Georgian', + 'Glagolitic', + 'Greek', + 'Gujarati', + 'Gurmukhi', + 'Han', + 'Hangul', + 'Hebrew', + 'Hiragana', + 'Kannada', + 'Katakana', + 'Khmer', + 'Lao', + 'Latin', + 'Malayalam', + 'Mongolian', + 'Myanmar', + 'Nko', + 'Sinhala', + 'Syriac', + 'Tamil', + 'Telugu', + 'Thaana', + 'Thai', + 'Tibetan', + 'Tifinagh', + 'Unknown' + ] as const + }, + filterrank: { + description: 'filterrank field', + values: [0, 1, 2, 3, 4, 5] as const + }, + capital: { + description: 'capital field', + values: [2, 3, 4, 5, 6] as const + }, + text_anchor: { + description: 'text_anchor field', + values: [ + 'left', + 'right', + 'top', + 'top-left', + 'top-right', + 'bottom', + 'bottom-left', + 'bottom-right' + ] as const + }, + symbolrank: { + description: 'symbolrank field', + values: [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 + ] as const + }, + type: { + description: 'type field', + values: [ + 'country', + 'territory', + 'sar', + 'disputed_territory', + 'state', + 'city', + 'town', + 'village', + 'hamlet', + 'suburb', + 'neighbourhood', + 'quarter' + ] as const + }, + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const + }, + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string + }, + worldview: { + description: 'worldview field', + values: ['JP', 'CN', 'IN', 'US', 'all'] as const + } + }, + + // ============ airport_label ============ + airport_label: { + class: { + description: 'class field', + values: ['military', 'civil'] as const + }, + name: { + description: 'name field', + values: [] as const // string + }, + name_de: { + description: 'name_de field', + values: [] as const // string + }, + name_en: { + description: 'name_en field', + values: [] as const // string + }, + name_es: { + description: 'name_es field', + values: [] as const // string + }, + name_fr: { + description: 'name_fr field', + values: [] as const // string + }, + name_ru: { + description: 'name_ru field', + values: [] as const // string + }, + 'name_zh-Hant': { + description: 'name_zh-Hant field', + values: [] as const // string + }, + 'name_zh-Hans': { + description: 'name_zh-Hans field', + values: [] as const // string + }, + name_pt: { + description: 'name_pt field', + values: [] as const // string + }, + name_ar: { + description: 'name_ar field', + values: [] as const // string + }, + name_vi: { + description: 'name_vi field', + values: [] as const // string + }, + name_it: { + description: 'name_it field', + values: [] as const // string + }, + name_ja: { + description: 'name_ja field', + values: [] as const // string + }, + name_ko: { + description: 'name_ko field', + values: [] as const // string + }, + name_script: { + description: 'name_script field', + values: [ + 'Arabic', + 'Armenian', + 'Bengali', + 'Bopomofo', + 'Canadian_Aboriginal', + 'Common', + 'Cyrillic', + 'Devanagari', + 'Ethiopic', + 'Georgian', + 'Glagolitic', + 'Greek', + 'Gujarati', + 'Gurmukhi', + 'Han', + 'Hangul', + 'Hebrew', + 'Hiragana', + 'Kannada', + 'Katakana', + 'Khmer', + 'Lao', + 'Latin', + 'Malayalam', + 'Mongolian', + 'Myanmar', + 'Nko', + 'Sinhala', + 'Syriac', + 'Tamil', + 'Telugu', + 'Thaana', + 'Thai', + 'Tibetan', + 'Tifinagh', + 'Unknown' + ] as const + }, + maki: { + description: 'maki field', + values: ['airport', 'heliport', 'rocket', 'airfield'] as const + }, + ref: { + description: 'ref field', + values: [] as const // string + }, + sizerank: { + description: 'sizerank field', + values: [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 + ] as const + }, + worldview: { + description: 'worldview field', + values: ['JP', 'CN', 'IN', 'US', 'all'] as const + }, + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const + }, + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string + } + }, + + // ============ transit_stop_label ============ + transit_stop_label: { + name: { + description: 'name field', + values: [] as const // string + }, + name_de: { + description: 'name_de field', + values: [] as const // string + }, + name_en: { + description: 'name_en field', + values: [] as const // string + }, + name_es: { + description: 'name_es field', + values: [] as const // string + }, + name_fr: { + description: 'name_fr field', + values: [] as const // string + }, + name_ru: { + description: 'name_ru field', + values: [] as const // string + }, + 'name_zh-Hant': { + description: 'name_zh-Hant field', + values: [] as const // string + }, + 'name_zh-Hans': { + description: 'name_zh-Hans field', + values: [] as const // string + }, + name_pt: { + description: 'name_pt field', + values: [] as const // string + }, + name_ar: { + description: 'name_ar field', + values: [] as const // string + }, + name_vi: { + description: 'name_vi field', + values: [] as const // string + }, + name_it: { + description: 'name_it field', + values: [] as const // string + }, + name_ja: { + description: 'name_ja field', + values: [] as const // string + }, + name_ko: { + description: 'name_ko field', + values: [] as const // string + }, + name_script: { + description: 'name_script field', + values: [ + 'Arabic', + 'Armenian', + 'Bengali', + 'Bopomofo', + 'Canadian_Aboriginal', + 'Common', + 'Cyrillic', + 'Devanagari', + 'Ethiopic', + 'Georgian', + 'Glagolitic', + 'Greek', + 'Gujarati', + 'Gurmukhi', + 'Han', + 'Hangul', + 'Hebrew', + 'Hiragana', + 'Kannada', + 'Katakana', + 'Khmer', + 'Lao', + 'Latin', + 'Malayalam', + 'Mongolian', + 'Myanmar', + 'Nko', + 'Sinhala', + 'Syriac', + 'Tamil', + 'Telugu', + 'Thaana', + 'Thai', + 'Tibetan', + 'Tifinagh', + 'Unknown' + ] as const + }, + mode: { + description: 'mode field', + values: [ + 'metro_rail', + 'rail', + 'light_rail', + 'tram', + 'monorail', + 'funicular', + 'bicycle', + 'bus', + 'ferry', + 'narrow_gauge', + 'preserved', + 'miniature' + ] as const + }, + stop_type: { + description: 'stop_type field', + values: ['stop', 'station', 'entrance'] as const + }, + maki: { + description: 'maki field', + values: [ + 'bus', + 'rail', + 'rail-light', + 'entrance', + 'ferry', + 'bicycle-share', + 'rail-metro' + ] as const + }, + network: { + description: 'network field', + values: [ + 'barcelona-metro', + 'boston-t', + 'chongqing-rail-transit', + 'de-s-bahn', + 'de-s-bahn.de-u-bahn', + 'de-u-bahn', + 'delhi-metro', + 'gb-national-rail', + 'gb-national-rail.london-dlr', + 'gb-national-rail.london-dlr.london-overground.london-tfl-rail.london-underground', + 'gb-national-rail.london-dlr.london-overground.london-underground', + 'gb-national-rail.london-dlr.london-underground', + 'gb-national-rail.london-overground', + 'gb-national-rail.london-overground.london-underground', + 'gb-national-rail.london-overground.london-tfl-rail.london-underground', + 'gb-national-rail.london-tfl-rail', + 'gb-national-rail.london-tfl-rail.london-overground', + 'gb-national-rail.london-tfl-rail.london-underground', + 'gb-national-rail.london-underground', + 'hong-kong-mtr', + 'kiev-metro', + 'london-dlr', + 'london-dlr.london-tfl-rail', + 'london-dlr.london-tfl-rail.london-underground', + 'london-dlr.london-underground', + 'london-overground', + 'london-overground.london-tfl-rail', + 'london-overground.london-tfl-rail.london-underground', + 'london-overground.london-underground', + 'london-tfl-rail', + 'london-tfl-rail.london-underground', + 'london-underground', + 'madrid-metro', + 'mexico-city-metro', + 'milan-metro', + 'moscow-metro', + 'new-york-subway', + 'osaka-subway', + 'oslo-metro', + 'paris-metro', + 'paris-metro.paris-rer', + 'paris-rer', + 'paris-rer.paris-transilien', + 'paris-transilien', + 'philadelphia-septa', + 'san-francisco-bart', + 'singapore-mrt', + 'stockholm-metro', + 'taipei-metro', + 'tokyo-metro', + 'vienna-u-bahn', + 'washington-metro', + 'rail', + 'rail-metro', + 'rail-light', + 'entrance', + 'bus', + 'ferry', + 'bicycle-share' + ] as const + }, + network_beta: { + description: 'network_beta field', + values: [ + 'jp-shinkansen', + 'jp-shinkansen.jp-jr', + 'jp-shinkansen.tokyo-metro', + 'jp-shinkansen.osaka-subway', + 'jp-shinkansen.jp-jr.tokyo-metro', + 'jp-shinkansen.jp-jr.osaka-subway', + 'jp-jr', + 'jp-jr.tokyo-metro', + 'jp-jr.osaka-subway' + ] as const + }, + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const + }, + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string + }, + filterrank: { + description: 'filterrank field', + values: [0, 1, 2, 3, 4, 5] as const + } + }, + + // ============ natural_label ============ + natural_label: { + name: { + description: 'name field', + values: [] as const // string + }, + name_de: { + description: 'name_de field', + values: [] as const // string + }, + name_en: { + description: 'name_en field', + values: [] as const // string + }, + name_es: { + description: 'name_es field', + values: [] as const // string + }, + name_fr: { + description: 'name_fr field', + values: [] as const // string + }, + name_ru: { + description: 'name_ru field', + values: [] as const // string + }, + 'name_zh-Hant': { + description: 'name_zh-Hant field', + values: [] as const // string + }, + 'name_zh-Hans': { + description: 'name_zh-Hans field', + values: [] as const // string + }, + name_pt: { + description: 'name_pt field', + values: [] as const // string + }, + name_ar: { + description: 'name_ar field', + values: [] as const // string + }, + name_vi: { + description: 'name_vi field', + values: [] as const // string + }, + name_it: { + description: 'name_it field', + values: [] as const // string + }, + name_ja: { + description: 'name_ja field', + values: [] as const // string + }, + name_ko: { + description: 'name_ko field', + values: [] as const // string + }, + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const + }, + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string + }, + name_script: { + description: 'name_script field', + values: [ + 'Arabic', + 'Armenian', + 'Bengali', + 'Bopomofo', + 'Canadian_Aboriginal', + 'Common', + 'Cyrillic', + 'Devanagari', + 'Ethiopic', + 'Georgian', + 'Glagolitic', + 'Greek', + 'Gujarati', + 'Gurmukhi', + 'Han', + 'Hangul', + 'Hebrew', + 'Hiragana', + 'Kannada', + 'Katakana', + 'Khmer', + 'Lao', + 'Latin', + 'Malayalam', + 'Mongolian', + 'Myanmar', + 'Nko', + 'Sinhala', + 'Syriac', + 'Tamil', + 'Telugu', + 'Thaana', + 'Thai', + 'Tibetan', + 'Tifinagh', + 'Unknown' + ] as const + }, + sizerank: { + description: 'sizerank field', + values: [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 + ] as const + }, + filterrank: { + description: 'filterrank field', + values: [] as const // number - Range: 0 to 5 + }, + class: { + description: 'class field', + values: [ + 'ocean', + 'sea', + 'disputed_sea', + 'water', + 'reservoir', + 'river', + 'bay', + 'dock', + 'river', + 'canal', + 'stream', + 'landform', + 'wetland', + 'water_feature', + 'glacier', + 'continent' + ] as const + }, + maki: { + description: 'maki field', + values: ['marker', 'waterfall', 'volcano', 'mountain'] as const + }, + elevation_m: { + description: 'elevation_m field', + values: [] as const // number - Range: -15000 to 21114 + }, + elevation_ft: { + description: 'elevation_ft field', + values: [] as const // number - Range: -49215 to 69276 + }, + worldview: { + description: 'worldview field', + values: ['JP', 'CN', 'IN', 'US', 'all'] as const + } + }, + + // ============ poi_label ============ + poi_label: { + name: { + description: 'name field', + values: [] as const // string + }, + name_de: { + description: 'name_de field', + values: [] as const // string + }, + name_en: { + description: 'name_en field', + values: [] as const // string + }, + name_es: { + description: 'name_es field', + values: [] as const // string + }, + name_fr: { + description: 'name_fr field', + values: [] as const // string + }, + name_ru: { + description: 'name_ru field', + values: [] as const // string + }, + 'name_zh-Hant': { + description: 'name_zh-Hant field', + values: [] as const // string + }, + 'name_zh-Hans': { + description: 'name_zh-Hans field', + values: [] as const // string + }, + name_pt: { + description: 'name_pt field', + values: [] as const // string + }, + name_ar: { + description: 'name_ar field', + values: [] as const // string + }, + name_vi: { + description: 'name_vi field', + values: [] as const // string + }, + name_it: { + description: 'name_it field', + values: [] as const // string + }, + name_ja: { + description: 'name_ja field', + values: [] as const // string + }, + name_ko: { + description: 'name_ko field', + values: [] as const // string + }, + name_script: { + description: 'name_script field', + values: [ + 'Arabic', + 'Armenian', + 'Bengali', + 'Bopomofo', + 'Canadian_Aboriginal', + 'Common', + 'Cyrillic', + 'Devanagari', + 'Ethiopic', + 'Georgian', + 'Glagolitic', + 'Greek', + 'Gujarati', + 'Gurmukhi', + 'Han', + 'Hangul', + 'Hebrew', + 'Hiragana', + 'Kannada', + 'Katakana', + 'Khmer', + 'Lao', + 'Latin', + 'Malayalam', + 'Mongolian', + 'Myanmar', + 'Nko', + 'Sinhala', + 'Syriac', + 'Tamil', + 'Telugu', + 'Thaana', + 'Thai', + 'Tibetan', + 'Tifinagh', + 'Unknown' + ] as const + }, + filterrank: { + description: 'filterrank field', + values: [] as const // number - Range: 1 to 5 + }, + maki: { + description: 'maki field', + values: [ + 'amusement-park', + 'aquarium', + 'art-gallery', + 'attraction', + 'cinema', + 'casino', + 'museum', + 'stadium', + 'theatre', + 'zoo', + 'marker', + 'bank', + 'bicycle', + 'car-rental', + 'laundry', + 'suitcase', + 'veterinary', + 'college', + 'school', + 'bar', + 'beer', + 'cafe', + 'fast-food', + 'ice-cream', + 'restaurant', + 'restaurant-noodle', + 'restaurant-pizza', + 'restaurant-seafood', + 'alcohol-shop', + 'bakery', + 'grocery', + 'convenience', + 'confectionery', + 'castle', + 'monument', + 'harbor', + 'farm', + 'bridge', + 'communications-tower', + 'watermill', + 'windmill', + 'lodging', + 'dentist', + 'doctor', + 'hospital', + 'pharmacy', + 'fuel', + 'car-repair', + 'charging-station', + 'parking', + 'parking-garage', + 'campsite', + 'cemetery', + 'dog-park', + 'garden', + 'golf', + 'park', + 'picnic-site', + 'playground', + 'embassy', + 'fire-station', + 'library', + 'police', + 'post', + 'prison', + 'town-hall', + 'place-of-worship', + 'religious-buddhist', + 'religious-christian', + 'religious-jewish', + 'religious-muslim', + 'viewpoint', + 'horse-riding', + 'swimming', + 'beach', + 'american-football', + 'basketball', + 'tennis', + 'table-tennis', + 'volleyball', + 'bowling-alley', + 'slipway', + 'pitch', + 'fitness-centre', + 'skateboard', + 'car', + 'clothing-store', + 'furniture', + 'hardware', + 'globe', + 'jewelry-store', + 'mobile-phone', + 'optician', + 'shoe', + 'watch', + 'shop', + 'music', + 'drinking-water', + 'information', + 'toilet', + 'ranger-station' + ] as const + }, + maki_beta: { + description: 'maki_beta field', + values: [ + 'baseball', + 'lighthouse', + 'landmark', + 'industry', + 'highway-services', + 'highway-rest-area', + 'racetrack-cycling', + 'racetrack-horse', + 'racetrack-boat', + 'racetrack', + 'religious-shinto', + 'observation-tower', + 'restaurant-bbq', + 'tunnel' + ] as const + }, + maki_modifier: { + description: 'maki_modifier field', + values: ['JP'] as const + }, + class: { + description: 'class field', + values: [ + 'arts_and_entertainment', + 'building', + 'commercial_services', + 'education', + 'food_and_drink', + 'food_and_drink_stores', + 'historic', + 'industrial', + 'landmark', + 'lodging', + 'medical', + 'motorist', + 'park_like', + 'place_like', + 'public_facilities', + 'religion', + 'sport_and_leisure', + 'store_like', + 'visitor_amenities', + 'general' + ] as const + }, + type: { + description: 'type field', + values: [ + 'Parking', + 'Locality', + 'Yes', + 'School', + 'Restaurant', + 'Place Of Worship', + 'Pitch', + 'Swimming Pool', + 'Retail', + 'Playground', + 'Convenience', + 'Residential', + 'Park', + 'Fuel', + 'Fast Food', + 'Isolated Dwelling', + 'Cafe', + 'Supermarket', + 'Cemetery', + 'Hotel', + 'Bank', + 'Industrial', + 'Pharmacy', + 'Clothes', + 'Guidepost', + 'Allotments', + 'Hospital', + 'Apartments', + 'Kindergarten', + 'Toilets', + 'Memorial', + 'Hairdresser', + 'Car Repair', + 'Bar', + 'Commercial', + 'Bakery', + 'Government', + 'Board', + 'Bridge', + 'House', + 'Company', + 'Grave Yard', + 'Drinking Water', + 'Post Office', + 'Pub', + 'Clinic', + 'Beach', + 'Guest House', + 'Sports Centre', + 'Attraction', + 'Viewpoint', + 'Doctors', + 'Car', + 'Townhall', + 'Police', + 'Fire Station', + 'University', + 'Camp Site', + 'Picnic Site', + 'Beauty', + 'Community Centre', + 'Dentist', + 'Works', + 'Library', + 'Shinto', + 'Museum', + 'Social Facility', + 'Wood', + 'Nature Reserve', + 'Mobile Phone', + 'Information', + 'Hardware', + 'Furniture', + 'Buddhist', + 'Chalet', + 'Electronics', + 'Marketplace', + 'Butcher', + 'College', + 'Forest', + 'Mall', + 'Estate Agent', + 'Shoes', + 'Alcohol', + 'Florist', + 'Archaeological Site', + 'Picnic Table', + 'Ruins', + 'Doityourself', + 'Fitness Centre', + 'Car Parts', + 'Monument', + 'Map', + 'Optician', + 'Office', + 'Jewelry', + 'Variety Store', + 'Hostel', + 'Construction', + 'Insurance' + ] as const + }, + brand: { + description: 'brand field', + values: [ + '21rentacar', + '2nd-street', + '31-ice-cream', + '7-eleven', + 'aen', + 'aeon', + 'aiya', + 'alpen', + 'aoki', + 'aoyama', + 'asakuma', + 'atom', + 'audi', + 'autobacs', + 'b-kids', + 'bamiyan', + 'barneys-newyork', + 'benz', + 'best-denki', + 'big-boy', + 'bikkuri-donkey', + 'bmw', + 'bon-belta', + 'book-off', + 'budget', + 'carenex', + 'casa', + 'citroen', + 'cockpit', + 'coco-ichibanya', + 'cocos', + 'community-store', + 'cosmo-oil', + 'costco', + 'daiei', + 'daihatsu', + 'daily-store', + 'daimaru', + 'daiwa', + 'dennys', + 'dio', + 'doutor-coffee', + 'eki-rent-a-car', + 'eneos', + 'f-rent-a-car', + 'familymart', + 'ferrari', + 'fiat', + 'forus', + 'fukudaya-department-store', + 'fukuya', + 'futata', + 'garage-off', + 'general-motors', + 'gmdat', + 'grache-gardens', + 'gulliver', + 'gusto', + 'hamacho', + 'hamazushi', + 'hamburg-restaurant-bell', + 'hankyu-department-store', + 'hanshin', + 'hard-off', + 'haruyama', + 'heisei-car', + 'heiwado', + 'hihirose', + 'hino', + 'hobby-off', + 'hokuren', + 'honda', + 'honda-cars', + 'ichibata-department-store', + 'idemitsu-oil', + 'inageya', + 'isetan', + 'isuzu', + 'ito-yokado', + 'iwataya', + 'izumi', + 'izumiya', + 'izutsuya', + 'j-net-rentcar', + 'ja-ss', + 'jaguar', + 'japan-post-bank', + 'japan-post-insurance', + 'japan-rent-a-car', + 'jolly-ox', + 'jolly-pasta', + 'jonathans', + 'joyfull', + 'jumble-store', + 'kaisen-misakiko', + 'kasumi', + 'kawatoku', + 'keihan-department-store', + 'keio-department-store', + 'kfc', + 'kintetsu-department-store', + 'kygnus-oil', + 'kyubeiya', + 'laforet-harajuku', + 'lamborghini', + 'lamu', + 'landrover', + 'lawson', + 'lexus', + 'life', + 'lotteria', + 'lumine', + 'maruetsu', + 'maruetsupetit', + 'maruhiro-department-store', + 'maruhoncowboy', + 'marui', + 'marunen-me', + 'matsubishi', + 'matsuya', + 'matsuya-department-store', + 'matsuyadenki', + 'matsuzakaya', + 'mazda-autozam', + 'mazda-enfini', + 'mcdonalds', + 'meitetsu-pare-department-store', + 'melsa', + 'michi-no-eki', + 'milky-way', + 'mini', + 'mini-piago', + 'ministop', + 'mitsubishi-corporation-energy', + 'mitsubishi-fuso', + 'mitsubishi-motors', + 'mitsukoshi', + 'mizuho-bank', + 'mode-off', + 'mos-burger', + 'mufg-bank', + 'my-basket', + 'nagasakiya', + 'nakago', + 'nakasan', + 'nakau', + 'natural-lawson', + 'navi', + 'netz-toyota', + 'niconicorentacar', + 'nippo-rent-a-car-system', + 'nippon-rent-a-car', + 'nissan', + 'nissan-cherry', + 'nissan-motor', + 'nissan-parts', + 'nissan-prince', + 'nissan-rent-a-car', + 'nissan-satio', + 'odakyu-department-store', + 'off-house', + 'ohsho', + 'oita-rental', + 'ok', + 'okajima', + 'okuno', + 'okuwa', + 'onuma', + 'orix-rent-a-car', + 'osaka-ohsho', + 'ots-rentacar', + 'palty-fuji', + 'parco', + 'petras', + 'peugeot', + 'plaka', + 'poplar', + 'popolo', + 'pork-cutlet-hamakatsu', + 'porsche', + 'ralse', + 'recycle-mart', + 'red-cabbage', + 'red-lobster', + 'renault', + 'resona-bank', + 'ringer-hut', + 'rolls-royce', + 'royal-host', + 'saga-rent-lease', + 'saijo-department-store', + 'saikaya', + 'saint-marc', + 'saitama-resona-bank', + 'saizeriya', + 'sanbangai', + 'sanei', + 'santa-no-souko', + 'sato', + 'seibu', + 'seicomart', + 'seiyu', + 'shabushabu-dontei', + 'shinkin-bank', + 'showa-shell-oil', + 'sizzler', + 'sky-rentallease', + 'smile-santa', + 'sogo', + 'sokoseikatsukan', + 'solato', + 'starbucks-coffee', + 'steak-hamburg-ken', + 'steak-miya', + 'steak-no-don', + 'store100', + 'subaru', + 'suehiro', + 'sukiya', + 'sumitomo-mitsui-banking-corporation', + 'sunpiazza', + 'sushihan', + 'suzuki', + 'suzuran-department-store', + 'tachiya', + 'taiyakan', + 'takarajima', + 'takashimaya', + 'tamaya', + 'tenmaya', + 'times-car-rental', + 'tobu-department-store', + 'tokiwa', + 'tokyu-department-store', + 'tokyu-store', + 'tomato-onion', + 'tonden', + 'toyopet', + 'toyota', + 'toyota-corolla', + 'toyota-parts', + 'toyota-rent-a-car', + 'tsuruya-department-store', + 'tullys-coffee', + 'ud-trucks', + 'victoria', + 'victoria-station', + 'vivre', + 'volks', + 'volkswagen', + 'volvo', + 'yamakataya', + 'yamazaki-shop', + 'yanase', + 'yao-department-store', + 'yayoiken', + 'yellow-hat', + 'york-benimaru', + 'yoshinoya', + 'you-me-mart', + 'yumean', + 'zenrin' + ] as const + }, + category_en: { + description: 'category_en field', + values: [ + 'Locality', + 'School Grounds', + 'Swimming Pool', + 'Restaurant', + 'Park', + 'Church', + 'Shop', + 'Playground', + 'Sport Pitch', + 'Convenience Store', + 'Gas Station', + 'Supermarket', + 'Cafe', + 'Cemetery', + 'Bank', + 'Fast Food', + 'Hotel', + 'Isolated Dwelling', + 'Retail Building', + 'Guidepost', + 'Pharmacy', + 'Residential Area', + 'Community Garden', + 'Clothing Store', + 'Kindergarten', + 'Information Board', + 'Graveyard', + 'Memorial', + 'Apartments', + 'Hospital Grounds', + 'Pub', + 'Post Office', + 'Bar', + 'House', + 'Bakery', + 'Industrial Area', + 'Car Repair Shop', + 'Mosque', + 'Place of Worship', + 'Viewpoint', + 'Sports Complex', + 'Police', + 'Beach', + 'Picnic Site', + 'Tourist Attraction', + 'Guest House', + 'Town Hall', + 'Car Parking', + 'Fire Station', + 'Campground', + 'Car Dealership', + 'Doctor\u2019s Office', + 'Residential Building', + 'Community Center', + 'Library', + 'Museum', + 'Clinic', + 'Information', + 'Dentist', + 'Social Facility', + 'Monument', + 'Hardware Store', + 'Butcher', + 'Wood', + 'Furniture Store', + 'Florist', + 'Marketplace', + 'University Grounds', + 'Electronics Store', + 'DIY Store', + 'Mall', + 'College Grounds', + 'Shoe Store', + 'Mobile Phone Store', + 'University Building', + 'Archaeological Site', + 'Liquor Store', + 'Quarry', + 'Stadium', + 'Commercial Area', + 'Tower', + 'Buddhist Temple', + 'Hostel', + 'Castle', + 'Factory', + 'Bridge', + 'Ruins', + 'Department Store', + 'Motel', + 'Book Store', + 'Jeweler', + 'Optician', + 'Golf Course', + 'Holiday Cottage', + 'Gift Shop', + 'Farmland', + 'Bicycle Shop', + 'Greengrocer', + 'Theater', + 'Retail Area' + ] as const + }, + 'category_zh-Hans': { + description: 'category_zh-Hans field', + values: [ + '\u5730\u65b9', + '\u5b66\u6821', + '\u6e38\u6cf3\u6c60', + '\u9910\u9986', + '\u516c\u56ed', + '\u57fa\u7763\u6559\u5802', + '\u5546\u5e97', + '\u5893\u5730', + '\u513f\u7ae5\u6e38\u4e50\u573a', + '\u8fd0\u52a8\u573a\u5730', + '\u4fbf\u5229\u5e97', + '\u52a0\u6cb9\u7ad9', + '\u8d85\u5e02', + '\u5496\u5561\u9986', + '\u94f6\u884c', + '\u5feb\u9910\u5e97', + '\u5bbe\u9986', + '\u5b64\u7acb\u5c45\u6240', + '\u96f6\u552e\u4e1a\u5efa\u7b51', + '\u8def\u6807', + '\u836f\u623f', + '\u5c45\u6c11\u533a', + '\u516c\u5171\u82b1\u56ed', + '\u670d\u88c5\u5e97', + '\u5e7c\u513f\u56ed', + '\u4fe1\u606f\u677f', + '\u7eaa\u5ff5\u7891', + '\u4f4f\u5b85\u697c', + '\u8bca\u6240', + '\u533b\u9662', + '\u9152\u9986', + '\u90ae\u5c40', + '\u9152\u5427', + '\u623f\u5c4b', + '\u9762\u5305\u5e97', + '\u5de5\u4e1a\u533a', + '\u6c7d\u8f66\u4fee\u7406\u5e97', + '\u6e05\u771f\u5bfa', + '\u793c\u62dc\u573a\u6240', + '\u89c2\u666f\u70b9', + '\u4f53\u80b2\u4e2d\u5fc3/\u7efc\u5408\u4f53\u80b2\u573a', + '\u8b66\u5bdf\u5c40', + '\u6d77\u6ee9', + '\u91ce\u9910\u5730', + '\u65c5\u6e38\u540d\u80dc', + '\u5c0f\u65c5\u9986', + '\u653f\u5e9c\u529e\u516c\u5927\u697c', + '\u505c\u8f66\u573a', + '\u6d88\u9632\u7ad9', + '\u5bbf\u8425\u573a\u5730', + '\u6c7d\u8f66\u5e97', + '\u4f4f\u5b85\u5efa\u7b51\u7269', + '\u793e\u533a\u4e2d\u5fc3', + '\u56fe\u4e66\u9986', + '\u535a\u7269\u9986', + '\u6e38\u5ba2\u4e2d\u5fc3', + '\u7259\u79d1\u533b\u9662', + '\u793e\u4f1a\u670d\u52a1\u8bbe\u65bd', + '\u7eaa\u5ff5\u5802', + '\u4e94\u91d1\u5e97', + '\u8089\u5e97', + '\u6811\u6797', + '\u5bb6\u5177\u5e97', + '\u82b1\u5e97', + '\u978b\u5e97', + '\u5e02\u573a', + '\u5927\u5b66', + '\u7535\u5b50\u4ea7\u54c1\u5e97', + 'DIY\u5e97', + '\u8d2d\u7269\u4e2d\u5fc3', + '\u5b66\u9662', + '\u624b\u673a\u5e97', + '\u5927\u5b66\u5efa\u7b51', + '\u8003\u53e4\u9057\u5740', + '\u5916\u5356\u9152\u5e97', + '\u9732\u5929\u77ff\u573a', + '\u4f53\u80b2\u573a', + '\u5546\u4e1a\u533a', + '\u5854', + '\u4f5b\u6559\u5bfa\u5e99', + '\u65c5\u820d', + '\u57ce\u5821', + '\u5de5\u5382', + '\u6865\u6881', + '\u9057\u8ff9', + '\u767e\u8d27\u5546\u573a', + '\u6c7d\u8f66\u65c5\u9986', + '\u4e66\u5e97', + '\u73e0\u5b9d\u5e97', + '\u773c\u955c\u5e97', + '\u9ad8\u5c14\u592b\u7403\u573a', + '\u5ea6\u5047\u5c4b', + '\u793c\u54c1\u5e97', + '\u81ea\u884c\u8f66\u5e97', + '\u852c\u679c\u5e97', + '\u5267\u9662', + '\u96f6\u552e\u5546\u5e97', + '\u517d\u533b\u9662', + '\u65c5\u884c\u793e', + '\u7eff\u5730' + ] as const + }, + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const + }, + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string + }, + sizerank: { + description: 'sizerank field', + values: [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 + ] as const + } + }, + + // ============ motorway_junction ============ + motorway_junction: { + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const + }, + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string + }, + ref: { + description: 'ref field', + values: [] as const // string + }, + reflen: { + description: 'reflen field', + values: [] as const // number - Range: 0 to 50 + }, + name: { + description: 'name field', + values: [] as const // string + }, + class: { + description: 'class field', + values: [ + 'motorway', + 'motorway_link', + 'trunk', + 'trunk_link', + 'primary', + 'secondary', + 'tertiary', + 'primary_link', + 'secondary_link', + 'tertiary_link', + 'street', + 'street_limited', + 'construction', + 'track', + 'service', + 'path', + 'major_rail', + 'minor_rail', + 'service_rail' + ] as const + }, + type: { + description: 'type field', + values: [ + 'motorway', + 'trunk', + 'motorway_link', + 'primary', + 'trunk_link', + 'secondary', + 'tertiary', + 'primary_link', + 'secondary_link', + 'tertiary_link' + ] as const + }, + maki_beta: { + description: 'maki_beta field', + values: ['interchange', 'junction'] as const + }, + filterrank: { + description: 'filterrank field', + values: [0, 1, 2, 3, 4, 5] as const + } + }, + + // ============ housenum_label ============ + housenum_label: { + iso_3166_1: { + description: 'iso_3166_1 field', + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const + }, + iso_3166_2: { + description: 'iso_3166_2 field', + values: [] as const // string + }, + house_num: { + description: 'house_num field', + values: [] as const // string } } } as const; export type SourceLayer = keyof typeof STREETS_V8_FIELDS; -export type FieldValues< - L extends SourceLayer, - F extends keyof (typeof STREETS_V8_FIELDS)[L] -> = (typeof STREETS_V8_FIELDS)[L][F] extends { values: readonly any[] } - ? (typeof STREETS_V8_FIELDS)[L][F]['values'][number] - : any; diff --git a/src/constants/mapboxStyleLayers.ts b/src/constants/mapboxStyleLayers.ts index 655adb7..04cd08e 100644 --- a/src/constants/mapboxStyleLayers.ts +++ b/src/constants/mapboxStyleLayers.ts @@ -92,7 +92,7 @@ export const MAPBOX_STYLE_LAYERS: Record = { description: 'Line layer for rivers, streams, and canals', sourceLayer: 'waterway', type: 'line', - commonFilters: ['class: river, stream, canal'], + commonFilters: ['class: river|stream|canal'], paintProperties: [ { property: 'line-color', @@ -118,7 +118,7 @@ export const MAPBOX_STYLE_LAYERS: Record = { description: 'Fill layer for parks, gardens, and green spaces', sourceLayer: 'landuse', type: 'fill', - commonFilters: ['class: park, cemetery, golf_course'], + commonFilters: ['class: park|cemetery|golf_course'], paintProperties: [ { property: 'fill-color', @@ -197,7 +197,7 @@ export const MAPBOX_STYLE_LAYERS: Record = { description: 'Line layer for railway tracks and rail lines', sourceLayer: 'road', type: 'line', - commonFilters: ['class: major_rail, minor_rail, service_rail'], + commonFilters: ['class: major_rail|minor_rail|service_rail'], paintProperties: [ { property: 'line-color', @@ -224,12 +224,48 @@ export const MAPBOX_STYLE_LAYERS: Record = { ] }, + road: { + id: 'road', + description: + 'Generic road layer for custom filtering (toll roads, bridges, tunnels, bike lanes, etc.)', + sourceLayer: 'road', + type: 'line', + commonFilters: [], + paintProperties: [ + { + property: 'line-color', + description: 'Color of roads', + example: '#ccc' + }, + { + property: 'line-width', + description: 'Width of road lines', + example: 4 + }, + { + property: 'line-opacity', + description: 'Opacity of road lines', + example: 1 + } + ], + layoutProperties: [], + examples: [ + 'Show toll roads with filter_properties: { toll: true }', + 'Show bridges with filter_properties: { structure: "bridge" }', + 'Show tunnels with filter_properties: { structure: "tunnel" }', + 'Show paved roads with filter_properties: { surface: "paved" }', + 'Show roads with bike lanes with filter_properties: { bike_lane: ["left", "right", "both"] }', + 'Show one-way roads with filter_properties: { oneway: "true" }', + 'Show restricted roads with filter_properties: { access: "restricted" }' + ] + }, + motorways: { id: 'road-motorway', description: 'Line layer for highways and motorways', sourceLayer: 'road', type: 'line', - commonFilters: ['class: motorway, trunk'], + commonFilters: ['class: motorway|trunk'], paintProperties: [ { property: 'line-color', @@ -275,7 +311,7 @@ export const MAPBOX_STYLE_LAYERS: Record = { description: 'Line layer for secondary roads', sourceLayer: 'road', type: 'line', - commonFilters: ['class: secondary, tertiary'], + commonFilters: ['class: secondary|tertiary'], paintProperties: [ { property: 'line-color', @@ -304,7 +340,7 @@ export const MAPBOX_STYLE_LAYERS: Record = { description: 'Line layer for local streets', sourceLayer: 'road', type: 'line', - commonFilters: ['class: street, street_limited, residential, service'], + commonFilters: ['class: street|street_limited|residential|service'], paintProperties: [ { property: 'line-color', @@ -337,7 +373,7 @@ export const MAPBOX_STYLE_LAYERS: Record = { description: 'Line layer for pedestrian paths, footways, and trails', sourceLayer: 'road', type: 'line', - commonFilters: ['class: path, pedestrian'], + commonFilters: ['class: path|pedestrian'], paintProperties: [ { property: 'line-color', @@ -527,7 +563,7 @@ export const MAPBOX_STYLE_LAYERS: Record = { description: 'Symbol layer for city, town, and place name labels', sourceLayer: 'place_label', type: 'symbol', - commonFilters: ['class: settlement, city, town, village'], + commonFilters: ['class: settlement|city|town|village'], layoutProperties: [ { property: 'text-field', @@ -625,7 +661,7 @@ export const MAPBOX_STYLE_LAYERS: Record = { description: 'Symbol layer for points of interest (POI) labels', sourceLayer: 'poi_label', type: 'symbol', - commonFilters: ['class: park, hospital, school, museum, etc.'], + commonFilters: [], layoutProperties: [ { property: 'text-field', diff --git a/src/tools/style-builder-tool/StyleBuilderTool.schema.ts b/src/tools/style-builder-tool/StyleBuilderTool.schema.ts index 551f5f6..1428f93 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.schema.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.schema.ts @@ -108,7 +108,9 @@ export const StyleBuilderToolSchema = z.object({ ]) .default('standard') .describe( - 'Base style template to start from (standard uses new Mapbox Standard, classic styles are deprecated)' + 'Base style template. ALWAYS defaults to "standard" for new styles. ' + + 'Use "standard" for all new styles unless explicitly requested otherwise. ' + + 'Classic styles (streets/light/dark) should only be used when working with existing Classic styles or upon explicit request.' ), layers: z @@ -122,7 +124,180 @@ export const StyleBuilderToolSchema = z.object({ mode: z.enum(['light', 'dark']).optional().describe('Light or dark mode') }) .optional() - .describe('Global style settings') + .describe('Global style settings'), + + standard_config: z + .object({ + // Boolean configuration properties + showPedestrianRoads: z + .boolean() + .optional() + .describe( + 'Show/hide the base pedestrian roads and paths from the Standard style' + ), + showPlaceLabels: z + .boolean() + .optional() + .describe( + 'Show/hide the base place label layers from the Standard style' + ), + showPointOfInterestLabels: z + .boolean() + .optional() + .describe( + 'Show/hide the base POI icons and text from the Standard style' + ), + showRoadLabels: z + .boolean() + .optional() + .describe( + 'Show/hide the base road labels and shields from the Standard style' + ), + showTransitLabels: z + .boolean() + .optional() + .describe( + 'Show/hide the base transit icons and text from the Standard style' + ), + show3dObjects: z + .boolean() + .optional() + .describe( + 'Show/hide the base 3D objects like buildings and landmarks from the Standard style' + ), + showLandmarkIcons: z + .boolean() + .optional() + .describe('Show/hide the base landmark icons from the Standard style'), + showLandmarkIconLabels: z + .boolean() + .optional() + .describe( + 'Show/hide the base landmark icon labels from the Standard style' + ), + showAdminBoundaries: z + .boolean() + .optional() + .describe( + 'Show/hide the base administrative boundaries from the Standard style' + ), + showRoadsAndTransit: z + .boolean() + .optional() + .describe( + 'Show/hide the base roads and transit networks from the Standard style (Standard-Satellite)' + ), + + // String configuration properties + theme: z + .enum(['default', 'faded', 'monochrome', 'custom']) + .optional() + .describe('Theme for the base Standard style layers'), + 'theme-data': z + .string() + .optional() + .describe('Custom color theme for the base style via Base64 LUT image'), + lightPreset: z + .enum(['dusk', 'dawn', 'day', 'night']) + .optional() + .describe('Time-of-day lighting for the base Standard style'), + font: z + .string() + .optional() + .describe('Font family for the base Standard style text'), + colorModePointOfInterestLabels: z + .string() + .optional() + .describe('Color mode for the base POI labels'), + backgroundPointOfInterestLabels: z + .string() + .optional() + .describe('Background style for the base POI labels'), + + // Numeric configuration properties + densityPointOfInterestLabels: z + .number() + .min(1) + .max(5) + .optional() + .describe('Density of base POI labels (1-5, default 3)'), + + // Color override properties + colorPlaceLabels: z + .string() + .optional() + .describe('Override color for the base place labels in Standard style'), + colorRoadLabels: z + .string() + .optional() + .describe('Override color for the base road labels in Standard style'), + colorGreenspace: z + .string() + .optional() + .describe( + 'Override color for the base greenspace areas in Standard style' + ), + colorWater: z + .string() + .optional() + .describe( + 'Override color for the base water features in Standard style' + ), + colorAdminBoundaries: z + .string() + .optional() + .describe( + 'Override color for the base administrative boundaries in Standard style' + ), + colorPointOfInterestLabels: z + .string() + .optional() + .describe('Override color for the base POI labels in Standard style'), + colorMotorways: z + .string() + .optional() + .describe( + 'Override color for the base motorways/highways in Standard style' + ), + colorTrunks: z + .string() + .optional() + .describe('Override color for the base trunk roads in Standard style'), + colorRoads: z + .string() + .optional() + .describe( + 'Override color for the base regular roads in Standard style' + ), + colorBuildingHighlight: z + .string() + .optional() + .describe( + 'Override color for the base highlighted buildings in Standard style' + ), + colorBuildingSelect: z + .string() + .optional() + .describe( + 'Override color for the base selected buildings in Standard style' + ), + colorPlaceLabelHighlight: z + .string() + .optional() + .describe( + 'Override color for the base highlighted place labels in Standard style' + ), + colorPlaceLabelSelect: z + .string() + .optional() + .describe( + 'Override color for the base selected place labels in Standard style' + ) + }) + .optional() + .describe( + 'Configuration for the base Mapbox Standard style. These properties customize the underlying Standard style features - you can still add your own custom layers on top using the layers parameter. The Standard style provides a rich basemap that you can configure and enhance with additional layers.' + ) }); export type StyleBuilderToolInput = z.infer; diff --git a/src/tools/style-builder-tool/StyleBuilderTool.ts b/src/tools/style-builder-tool/StyleBuilderTool.ts index c10a469..5babae0 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.ts @@ -9,17 +9,35 @@ import type { MapboxStyle, Layer, Filter } from '../../types/mapbox-style.js'; export class StyleBuilderTool extends BaseTool { name = 'style_builder_tool'; - description = `Generate Mapbox style JSON for creating new styles or updating existing ones. Supports Mapbox Standard (default), Classic, and Blank base styles with full control over layers, expressions, and visual properties. + description = `Generate Mapbox style JSON for creating new styles or updating existing ones. Uses Mapbox Standard for all NEW styles, and preserves the base style type when working with EXISTING styles. USAGE: 1. Use this tool to generate style JSON configuration 2. For NEW styles: Use the generated JSON with create_style_tool 3. For EXISTING styles: Use portions of the JSON with update_style_tool to modify specific layers +BASE STYLE SELECTION: +• FOR NEW STYLES: ALWAYS use base_style: 'standard' unless user explicitly requests otherwise + - Standard is the modern, recommended approach with best performance + - Example: "Create a style showing toll roads" → use 'standard' + - Example: "Create a dark style showing parks" → use 'standard' with dark theme config + +• FOR EXISTING STYLES: Auto-detect and preserve the current base style + - If working on a Classic style (retrieved via retrieve_style_tool), keep using Classic + - If working on a Standard style, keep using Standard + - Look for 'imports' field to identify Standard styles + BASE STYLES: -• standard (default): Modern Mapbox Standard with imports, requires slot property -• streets/light/dark/satellite/outdoors: Classic styles (deprecated but supported) -• blank: Empty style for full customization +• standard (DEFAULT FOR NEW STYLES): Modern Mapbox Standard with imports, best performance +• streets/light/dark/satellite/outdoors: Classic styles (use only for existing Classic styles or explicit request) +• blank: Empty style for full customization (only when explicitly needed) + +WHEN TO USE EACH BASE STYLE: +• "Create a style showing toll roads" → base_style: 'standard' +• "Create a dark style with parks" → base_style: 'standard' + standard_config: { theme: 'monochrome' } +• "Build a navigation style" → base_style: 'standard' +• "Working on style cmfnxfroh..." (if it's Classic) → preserve its base_style +• "Create a streets-v11 style" (explicit request) → base_style: 'streets' LAYER ORDERING: • In ALL styles: Later layers in array render on top of earlier layers @@ -34,6 +52,26 @@ When using Standard base style, each layer needs a 'slot' to control stacking: • top: Above all base map features (default for visibility) Within each slot, array order still applies - later layers render on top +MAPBOX STANDARD - CONFIGURATION: +Standard style provides a rich basemap that can be customized using the standard_config parameter. +These settings control the BASE Standard style features - you can still add custom layers on top! + +• Visibility toggles: Control which base features are shown + - showPlaceLabels, showRoadLabels, showTransitLabels (base labels) + - showPedestrianRoads, show3dObjects, showAdminBoundaries (base features) +• Theme options: Adjust the overall look of the base style + - theme: default/faded/monochrome/custom + - lightPreset: day/night/dawn/dusk +• Color overrides: Change colors of base Standard style elements + - Roads: colorMotorways, colorTrunks, colorRoads + - Nature: colorWater, colorGreenspace + - Labels: colorPlaceLabels, colorRoadLabels, colorPointOfInterestLabels + - Admin: colorAdminBoundaries +• Density controls: densityPointOfInterestLabels (1-5) + +IMPORTANT: These configurations modify the underlying Standard basemap. +Your custom layers (defined in 'layers' parameter) are added ON TOP of this configured basemap. + RESOURCE GUIDE: The resource://mapbox-style-layers contains comprehensive documentation including: • All available layer types with descriptions @@ -43,26 +81,30 @@ The resource://mapbox-style-layers contains comprehensive documentation includin AVAILABLE LAYER TYPES: • water, waterway - Oceans, lakes, rivers -• landuse, parks - Land areas like parks, hospitals, schools +• landuse - General land use areas +• parks - Parks, cemeteries, golf courses (pre-filtered for class: park|cemetery|golf_course) • buildings, building_3d - Building footprints and 3D extrusions -• ROAD TYPES (use these specific types for best results): - - motorways - Highway/freeway roads (class: motorway) - - primary_roads - Major roads (class: primary, trunk) - - secondary_roads - Secondary roads (class: secondary) - - streets - Local streets (class: street, street_limited) - - paths - Walking/cycling paths (class: path, pedestrian) - - railways - Rail lines - - roads - Generic/all roads (avoid using - use specific types above instead) +• ROAD TYPES (each automatically includes the correct road classes): + - road - Generic road layer for CUSTOM filtering (use for toll roads, bridges, tunnels, etc.) + - motorways - Highways/freeways (includes: motorway, trunk) + - primary_roads - Major arterial roads (includes: primary) + - secondary_roads - Secondary & tertiary roads (includes: secondary, tertiary) + - streets - Local/residential streets (includes: street, street_limited, residential, service) + - paths - Pedestrian paths & walkways (includes: path, pedestrian) + - railways - Rail lines (includes: major_rail, minor_rail, service_rail) • country_boundaries, state_boundaries - Administrative borders -• place_labels, road_labels, poi_labels - Text labels +• place_labels - City/town/village labels (pre-filtered) +• road_labels - Street name labels +• poi_labels - Points of interest with icons • landcover - Natural features like forests, grass -• airports - Airport features -• transit - Bus stops, subway entrances, rail stations (filter by maki: bus, entrance, rail-metro) +• airports - Airport features (runways, terminals) +• transit - Transit stops with icons (bus, subway, rail) IMPORTANT FOR ROADS: -• Always use specific road layer types (motorways, primary_roads, etc.) instead of generic 'roads' -• Each road type automatically includes proper filters and zoom-based width interpolation -• Don't specify fixed widths - the tool automatically applies appropriate zoom-based scaling +• Use 'road' layer type for custom filtering (toll roads, bridges, tunnels, bike lanes, etc.) +• Use specific road types (motorways, primary_roads, etc.) for pre-filtered road classes +• Each specific road type automatically includes proper filters and zoom-based width interpolation +• The generic 'road' layer requires filter_properties for filtering ACTIONS YOU CAN APPLY: • color - Set the layer's color (roads will use smart defaults if not specified) @@ -77,23 +119,37 @@ EXPRESSION FEATURES: • Interpolated values - "Fade buildings in between zoom 14 and 16" ADVANCED FILTERING: -• "Show only motorways and trunk roads" -• "Display only bridges, not tunnels" -• "Show only paved roads" -• "Display only disputed boundaries" -• "Show only major rail lines, not service rails" -• "Filter POIs by maki icon type (restaurants, hospitals, etc.)" -• "Show only bus stops (transit layer with maki: bus)" -• "Display subway entrances (transit with maki: entrance)" +• "Show only motorways and trunk roads" - Use motorways layer type +• "Display only toll roads" - Use road layer with filter_properties: { toll: true } +• "Display only bridges" - Use road layer with filter_properties: { structure: 'bridge' } +• "Show only tunnels" - Use road layer with filter_properties: { structure: 'tunnel' } +• "Show only paved roads" - Use road layer with filter_properties: { surface: 'paved' } +• "Show roads with bike lanes" - Use road layer with filter_properties: { bike_lane: ['left', 'right', 'both'] } +• "Display only disputed boundaries" - Use filter_properties: { disputed: 'true' } +• "Filter POIs by maki icon type" - Use poi_labels with filter_properties: { maki: 'restaurant' } +• "Show only bus stops" - Use transit layer with filter_properties: { maki: 'bus' } +• "Display subway entrances" - Use transit layer with filter_properties: { maki: 'entrance' } COMPREHENSIVE EXAMPLES: -• "Show only motorways that are bridges" -• "Display major rails but exclude tunnels" -• "Color roads: motorways red, primary orange, secondary yellow" -• "Show only toll roads that are paved" -• "Display only civil airports, not military" -• "Show country boundaries excluding maritime ones" -• "Color bus stops red and subway entrances blue (transit with different maki values)" +• "Color all roads differently": + - Use motorways (red), primary_roads (orange), secondary_roads (yellow), streets (green), paths (purple) + - Each layer type automatically includes the correct road classes +• "Show only toll roads" - Use road layer with filter_properties: { toll: true } +• "Show toll motorways" - Use road layer with filter_properties: { toll: true, class: ['motorway', 'trunk'] } +• "Display bridges" - Use road layer with filter_properties: { structure: 'bridge' } +• "Show paved roads" - Use road layer with filter_properties: { surface: 'paved' } +• "Display one-way streets" - Use road layer with filter_properties: { oneway: 'true', class: ['street'] } +• "Show roads with bike lanes" - Use road layer with filter_properties: { bike_lane: ['left', 'right', 'both', 'yes'] } +• "Show all labels" - Use place_labels, road_labels, poi_labels layers +• "Display boundaries" - Use country_boundaries and state_boundaries layers + +IMPORTANT FOR ROAD FILTERING: +• The 'toll' property uses 'true' as a string value when present +• Structure values: 'none', 'bridge', 'tunnel', 'ford' +• Surface values: 'paved', 'unpaved' +• Bike lane values: 'left', 'right', 'both', 'yes', 'no' +• Oneway and dual_carriageway use string values: 'true' or 'false' +• Access can be 'restricted' when limitations exist For detailed layer properties and filters, check resource://mapbox-style-layers @@ -118,6 +174,7 @@ To show multiple transit types: filter_properties: { maki: ['bus', 'entrance', ' **Name:** ${input.style_name} **Base:** ${input.base_style} **Layers Configured:** ${input.layers.length} +${input.standard_config ? `**Standard Config:** ${Object.keys(input.standard_config).length} properties set` : ''} ${this.generateSummary(input)} @@ -224,12 +281,22 @@ ${JSON.stringify(style, null, 2)} }; style.center = [0, 0]; style.zoom = 2; - style.imports = [ - { - id: 'basemap', - url: 'mapbox://styles/mapbox/standard' - } - ]; + + // Build the import configuration + const importConfig: any = { + id: 'basemap', + url: 'mapbox://styles/mapbox/standard' + }; + + // Add Standard style configuration if provided + if ( + input.standard_config && + Object.keys(input.standard_config).length > 0 + ) { + importConfig.config = input.standard_config; + } + + style.imports = [importConfig]; style.sources = { composite: { url: 'mapbox://mapbox.mapbox-streets-v8', @@ -281,15 +348,50 @@ ${JSON.stringify(style, null, 2)} globalSettings?: StyleBuilderToolInput['global_settings'], isUsingStandard?: boolean ): Layer | null { + // Generate a unique ID for the layer based on its properties + let layerId = `${layerDef.id}-custom`; + + // If there are filter properties, create a unique suffix from them + if (config.filter_properties) { + // Create a deterministic hash from the filter properties + const filterKeys = Object.entries(config.filter_properties) + .map(([key, value]) => `${key}-${value}`) + .join('-'); + layerId = `${layerDef.id}-${filterKeys}`; + } + const layer: Layer = { - id: `${layerDef.id}-custom`, + id: layerId, type: layerDef.type as Layer['type'] }; // Add slot for Standard style if (isUsingStandard) { - // Use custom slot if provided, otherwise default to 'top' for visibility - layer.slot = config.slot || 'top'; + // Smart slot assignment if not explicitly provided: + // - Layers with filter_properties go to 'top' for visibility + // - Regular roads go to 'middle' + // - Labels go to 'top' + + if (config.slot) { + // User explicitly set the slot - respect their choice + layer.slot = config.slot; + } else if ( + config.filter_properties && + Object.keys(config.filter_properties).length > 0 + ) { + // ANY layer with filter properties should be on top for visibility + // This includes: toll roads, bridges, tunnels, bike lanes, restricted roads, etc. + layer.slot = 'top'; + } else if (config.layer_type.includes('label')) { + // Labels should always be on top + layer.slot = 'top'; + } else if (this.isRoadLayer(config.layer_type)) { + // Regular roads without filters in the middle + layer.slot = 'middle'; + } else { + // Default for other layers + layer.slot = 'middle'; + } } // Add source configuration @@ -349,22 +451,96 @@ ${JSON.stringify(style, null, 2)} // Apply opacity - use specified value or smart defaults const opacityProp = this.getOpacityProperty(layerDef.type); if (opacityProp) { - // For Standard style overlays, use higher opacity by default - // This keeps colors vibrant and easily distinguishable - const opacity = - config.opacity !== undefined - ? config.opacity - : isUsingStandard - ? 0.75 + // Special handling for boundaries - fade at higher zooms + if ( + config.layer_type === 'country_boundaries' || + config.layer_type === 'state_boundaries' + ) { + const baseOpacity = + config.opacity !== undefined + ? config.opacity : this.getDefaultOpacity(config.layer_type, layerDef.type); - // Only apply if not full opacity (to keep styles cleaner) - if (opacity < 1.0) { - paint[opacityProp] = this.generateExpression( - opacity, - config, - 'opacity' - ); + // Create zoom-based interpolation for boundaries + paint[opacityProp] = [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + baseOpacity, // Full opacity at world view + 6, + baseOpacity * 0.8, // Slightly faded at country view + 10, + baseOpacity * 0.6, // More faded at region view + 14, + baseOpacity * 0.4, // Very faded at city view + 18, + baseOpacity * 0.2 // Almost invisible at street level + ]; + } else if (this.isRoadLayer(config.layer_type)) { + // Special handling for roads - more subtle at lower zooms + const baseOpacity = + config.opacity !== undefined + ? config.opacity + : this.getDefaultOpacity(config.layer_type, layerDef.type); + + // For highlighted/navigation roads, use higher opacity + const isNavigationHighlight = + config.action === 'highlight' || config.layer_type === 'motorways'; + + if (isNavigationHighlight) { + // Navigation-focused roads should be more prominent + paint[opacityProp] = [ + 'interpolate', + ['linear'], + ['zoom'], + 5, + Math.max(baseOpacity * 0.6, 0.6), // More visible at country view + 8, + Math.max(baseOpacity * 0.75, 0.75), // Good visibility at region view + 11, + Math.max(baseOpacity * 0.85, 0.85), // Strong at city level + 14, + Math.max(baseOpacity * 0.95, 0.95), // Nearly full at neighborhood + 16, + 1.0 // Full opacity at street level + ]; + } else { + // Regular roads - subtle at low zooms + paint[opacityProp] = [ + 'interpolate', + ['linear'], + ['zoom'], + 5, + baseOpacity * 0.3, // Very subtle at country view + 8, + baseOpacity * 0.5, // Half opacity at region view + 11, + baseOpacity * 0.7, // More visible at city level + 14, + baseOpacity * 0.85, // Nearly full at neighborhood level + 16, + baseOpacity // Full opacity at street level + ]; + } + } else { + // For Standard style overlays, use higher opacity by default + // This keeps colors vibrant and easily distinguishable + const opacity = + config.opacity !== undefined + ? config.opacity + : isUsingStandard + ? 0.75 + : this.getDefaultOpacity(config.layer_type, layerDef.type); + + // Only apply if not full opacity (to keep styles cleaner) + if (opacity < 1.0) { + paint[opacityProp] = this.generateExpression( + opacity, + config, + 'opacity' + ); + } } } @@ -396,7 +572,11 @@ ${JSON.stringify(style, null, 2)} } } else { // Apply smart default widths based on road type with zoom interpolation - const defaultWidth = this.getDefaultLineWidth(config.layer_type); + const defaultWidth = this.getDefaultLineWidth( + config.layer_type, + config.action === 'highlight' + ); + if (defaultWidth) { paint['line-width'] = defaultWidth; } @@ -551,33 +731,62 @@ ${JSON.stringify(style, null, 2)} } private parseFilterString(filterStr: string): unknown | null { - // Parse filter strings like "class: park, cemetery" or "admin_level: 0, maritime: false" + // Parse filter strings like "class: motorway|trunk" or "admin_level: 0, maritime: false" const filters: unknown[] = []; - // Split by comma if there are multiple conditions - const conditions = filterStr.split(',').map((s) => s.trim()); - - for (const condition of conditions) { - if (condition.includes(':')) { - const [property, values] = condition.split(':').map((s) => s.trim()); - const valueList = values.split('|').map((v) => { - const trimmed = v.trim(); - // Special handling for admin layer properties that use string booleans - // maritime and disputed use "true"/"false" as strings, not booleans - if (property === 'maritime' || property === 'disputed') { - return trimmed; // Keep as string "true" or "false" - } - // Handle boolean strings for other properties - if (trimmed === 'true') return true; - if (trimmed === 'false') return false; - // Try to parse as number - const num = Number(trimmed); - return isNaN(num) ? trimmed : num; - }); - - if (valueList.length === 1) { - filters.push(['==', ['get', property], valueList[0]]); - } else { + // Check if this contains multiple properties (multiple colons not within quotes) + const colonMatches = filterStr.match(/:/g) || []; + + if (colonMatches.length === 1) { + // Single property, possibly with multiple values separated by | + const [property, values] = filterStr.split(':').map((s) => s.trim()); + const valueList = values.split('|').map((v) => { + const trimmed = v.trim(); + // Special handling for admin layer properties that use string booleans + // maritime and disputed use "true"/"false" as strings, not booleans + if ( + property === 'maritime' || + property === 'disputed' || + property === 'toll' + ) { + return trimmed; // Keep as string "true" or "false" + } + // Handle boolean strings for other properties + if (trimmed === 'true') return true; + if (trimmed === 'false') return false; + // Try to parse as number + const num = Number(trimmed); + return isNaN(num) ? trimmed : num; + }); + + // Always use match format for consistency with Mapbox Studio + filters.push(['match', ['get', property], valueList, true, false]); + } else { + // Multiple properties separated by comma + const conditions = filterStr.split(',').map((s) => s.trim()); + + for (const condition of conditions) { + if (condition.includes(':')) { + const [property, values] = condition.split(':').map((s) => s.trim()); + const valueList = values.split('|').map((v) => { + const trimmed = v.trim(); + // Special handling for properties that use string booleans + if ( + property === 'maritime' || + property === 'disputed' || + property === 'toll' + ) { + return trimmed; // Keep as string + } + // Handle boolean strings for other properties + if (trimmed === 'true') return true; + if (trimmed === 'false') return false; + // Try to parse as number + const num = Number(trimmed); + return isNaN(num) ? trimmed : num; + }); + + // Always use match format for consistency with Mapbox Studio filters.push(['match', ['get', property], valueList, true, false]); } } @@ -643,6 +852,77 @@ ${JSON.stringify(style, null, 2)} parts.push(`\n**Mode:** ${input.global_settings.mode}`); } + // Add Standard style configuration summary if present + if ( + input.standard_config && + Object.keys(input.standard_config).length > 0 + ) { + parts.push(`\n**Standard Style Configuration:**`); + const config = input.standard_config; + + // Visibility settings + const visibilitySettings = []; + if (config.showPlaceLabels !== undefined) + visibilitySettings.push( + `Place labels: ${config.showPlaceLabels ? 'shown' : 'hidden'}` + ); + if (config.showRoadLabels !== undefined) + visibilitySettings.push( + `Road labels: ${config.showRoadLabels ? 'shown' : 'hidden'}` + ); + if (config.showPointOfInterestLabels !== undefined) + visibilitySettings.push( + `POI labels: ${config.showPointOfInterestLabels ? 'shown' : 'hidden'}` + ); + if (config.showTransitLabels !== undefined) + visibilitySettings.push( + `Transit labels: ${config.showTransitLabels ? 'shown' : 'hidden'}` + ); + if (config.showPedestrianRoads !== undefined) + visibilitySettings.push( + `Pedestrian roads: ${config.showPedestrianRoads ? 'shown' : 'hidden'}` + ); + if (config.show3dObjects !== undefined) + visibilitySettings.push( + `3D objects: ${config.show3dObjects ? 'shown' : 'hidden'}` + ); + if (config.showAdminBoundaries !== undefined) + visibilitySettings.push( + `Admin boundaries: ${config.showAdminBoundaries ? 'shown' : 'hidden'}` + ); + + if (visibilitySettings.length > 0) { + parts.push(`• Visibility: ${visibilitySettings.join(', ')}`); + } + + // Theme settings + if (config.theme) parts.push(`• Theme: ${config.theme}`); + if (config.lightPreset) + parts.push(`• Light preset: ${config.lightPreset}`); + + // Color overrides + const colorOverrides = []; + if (config.colorMotorways) + colorOverrides.push(`motorways: ${config.colorMotorways}`); + if (config.colorTrunks) + colorOverrides.push(`trunks: ${config.colorTrunks}`); + if (config.colorRoads) colorOverrides.push(`roads: ${config.colorRoads}`); + if (config.colorWater) colorOverrides.push(`water: ${config.colorWater}`); + if (config.colorGreenspace) + colorOverrides.push(`greenspace: ${config.colorGreenspace}`); + if (config.colorAdminBoundaries) + colorOverrides.push(`admin boundaries: ${config.colorAdminBoundaries}`); + + if (colorOverrides.length > 0) { + parts.push(`• Color overrides: ${colorOverrides.join(', ')}`); + } + + // Other settings + if (config.densityPointOfInterestLabels !== undefined) { + parts.push(`• POI density: ${config.densityPointOfInterestLabels}`); + } + } + return parts.join('\n'); } @@ -777,20 +1057,99 @@ ${JSON.stringify(style, null, 2)} for (const [property, value] of Object.entries(filterConfig)) { if (value === undefined || value === null) continue; - const fieldDef = layerFields[property as keyof typeof layerFields]; + const fieldDef = layerFields[property as keyof typeof layerFields] as any; + + // Special handling for toll property - it's a presence check, not a value check + // The toll field only has 'true' when present, otherwise it's not in the data + if ( + property === 'toll' && + (value === true || value === 'true' || value === 1 || value === '1') + ) { + // Use "has" expression to check if toll property exists + filters.push(['has', 'toll']); + continue; + } + if (!fieldDef) continue; - // Handle array of values (multiple selections) - if (Array.isArray(value)) { - if (value.length === 1) { - filters.push(['==', ['get', property], value[0]]); - } else if (value.length > 1) { - filters.push(['match', ['get', property], value, true, false]); + // Check if this field uses string booleans by looking at its defined values + const isStringBooleanField = + fieldDef && + 'values' in fieldDef && + Array.isArray(fieldDef.values) && + fieldDef.values.length > 0 && + (fieldDef.values.includes('true') || fieldDef.values.includes('false')); + + // Convert values for properties that expect string booleans + let processedValue = value; + if (isStringBooleanField) { + if (Array.isArray(value)) { + processedValue = value.map((v) => { + // Handle all truthy values + if (v === true || v === 1 || v === '1' || v === 'true') + return 'true'; + // Handle all falsy values + if (v === false || v === 0 || v === '0' || v === 'false') + return 'false'; + return String(v); + }); + } else { + // Handle all truthy values + if ( + value === true || + value === 1 || + value === '1' || + value === 'true' + ) { + processedValue = 'true'; + } else if ( + value === false || + value === 0 || + value === '0' || + value === 'false' + ) { + processedValue = 'false'; + } else { + processedValue = String(value); + } } } - // Handle single value - else { - filters.push(['==', ['get', property], value]); + + // Optional: Validate values against defined values (only for fields with enumerated values) + if ( + fieldDef && + 'values' in fieldDef && + Array.isArray(fieldDef.values) && + fieldDef.values.length > 0 + ) { + const validValues = fieldDef.values; + const valuesToCheck = Array.isArray(processedValue) + ? processedValue + : [processedValue]; + + for (const val of valuesToCheck) { + if (!validValues.includes(val as any)) { + console.warn( + `Warning: Value "${val}" is not a valid option for field "${property}" in layer "${sourceLayer}". Valid values are: ${validValues.join(', ')}` + ); + } + } + } + + // Use Mapbox Studio's match format for all property filters + // For presence-based fields like 'toll', we already handled them above + if (Array.isArray(processedValue) && processedValue.length > 0) { + // Array of values - use as is + filters.push(['match', ['get', property], processedValue, true, false]); + } else if (processedValue !== undefined && processedValue !== null) { + // Single value - wrap in array for consistent match format + filters.push([ + 'match', + ['get', property], + [processedValue], + true, + false + ]); } } @@ -808,21 +1167,46 @@ ${JSON.stringify(style, null, 2)} return config.filter as Filter; } - // If filter_properties is provided, build from that + const filters: Filter[] = []; + + // First, add common filters from layer definition + if (layerDef.commonFilters && layerDef.commonFilters.length > 0) { + // Handle multiple filter conditions - join with comma for multiple properties + // Each element can be either a single property or a property with pipe-separated values + const filterStr = layerDef.commonFilters.join(', '); + const commonFilter = this.parseFilterString(filterStr); + if (commonFilter) { + filters.push(commonFilter as Filter); + } + } + + // Then, add filter_properties if provided if (config.filter_properties && layerDef.sourceLayer) { - return this.buildAdvancedFilter( + const propertyFilter = this.buildAdvancedFilter( layerDef.sourceLayer, config.filter_properties ); + if (propertyFilter) { + filters.push(propertyFilter); + } } - // Otherwise, use common filters from layer definition - if (layerDef.commonFilters && layerDef.commonFilters.length > 0) { - const filterStr = layerDef.commonFilters.join(', '); - return this.parseFilterString(filterStr) as Filter; - } + // Combine filters if there are multiple + if (filters.length === 0) return null; + if (filters.length === 1) return filters[0]; + return ['all', ...filters] as Filter; + } - return null; + private isRoadLayer(layerType: string): boolean { + return [ + 'roads', + 'motorways', + 'primary_roads', + 'secondary_roads', + 'streets', + 'paths', + 'railways' + ].includes(layerType); } private getDefaultLineWidth( @@ -932,9 +1316,37 @@ ${JSON.stringify(style, null, 2)} 4.0 ], - // Administrative boundaries - country_boundaries: 2.0, - state_boundaries: 1.5 + // Administrative boundaries - thinner with zoom interpolation + country_boundaries: [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + 0.5, // Very thin at world view + 4, + 0.8, // Slightly thicker at country level + 8, + 1.2, // Moderate at region level + 12, + 1.5, // Slightly thicker at city level + 16, + 1.8 // Maximum thickness at street level + ], + state_boundaries: [ + 'interpolate', + ['linear'], + ['zoom'], + 2, + 0.3, // Almost invisible at world view + 6, + 0.6, // Thin at country level + 10, + 1.0, // Moderate at region level + 14, + 1.3, // Slightly thicker at city level + 18, + 1.5 // Maximum thickness at street level + ] }; // If highlighting, slightly increase the widths @@ -965,22 +1377,22 @@ ${JSON.stringify(style, null, 2)} landuse: 0.45, landcover: 0.4, - // Roads - much more subtle opacity - motorways: 0.4, - primary_roads: 0.35, - secondary_roads: 0.3, - streets: 0.25, - paths: 0.2, - railways: 0.3, - roads: 0.3, // General roads + // Roads - varied opacity for navigation clarity + motorways: 0.85, // High visibility for navigation + primary_roads: 0.75, // Clear but not dominant + secondary_roads: 0.65, // Visible but subdued + streets: 0.55, // Background context + paths: 0.45, // Subtle + railways: 0.7, // Clear but distinct + roads: 0.6, // General roads // Buildings - subtle presence buildings: 0.6, building_3d: 0.7, - // Administrative - very subtle - country_boundaries: 0.8, - state_boundaries: 0.6, + // Administrative - very subtle, fading at higher zooms + country_boundaries: 0.5, // More subtle base opacity + state_boundaries: 0.4, // Even more subtle for states // Infrastructure airports: 0.7, diff --git a/src/utils/styleUtils.ts b/src/utils/styleUtils.ts index 545f6bd..2a8cd40 100644 --- a/src/utils/styleUtils.ts +++ b/src/utils/styleUtils.ts @@ -1,12 +1,23 @@ /** * Filters out expanded Mapbox styles from imports to reduce response size. * This preserves the reference to the style (e.g., mapbox://styles/mapbox/standard) - * but removes the expanded style data that causes token overflow. + * and any configuration settings, but removes the expanded style data that causes token overflow. + * + * Preserved properties: + * - id: The import identifier + * - url: The Mapbox style URL reference + * - config: Standard style configuration (colors, visibility, themes, etc.) */ export function filterExpandedMapboxStyles(style: T): T { // Create a shallow copy const filtered = { ...style } as T & { - imports?: Array<{ url: string; data?: unknown }>; + imports?: Array<{ + id?: unknown; + url: string; + data?: unknown; + config?: Record; + [key: string]: unknown; + }>; }; // Filter out the expanded data from Mapbox style imports @@ -17,11 +28,24 @@ export function filterExpandedMapboxStyles(style: T): T { importItem.url && importItem.url.startsWith('mapbox://styles/mapbox/') ) { - // Return only the reference, not the expanded data - return { - id: (importItem as Record).id, + // Preserve the essential properties including config for Standard style customization + const result: { + id?: unknown; + url: string; + config?: Record; + [key: string]: unknown; + } = { + id: importItem.id, url: importItem.url }; + + // IMPORTANT: Preserve the config property which contains Standard style configuration + // This includes visibility settings, color overrides, themes, etc. + if (importItem.config) { + result.config = importItem.config; + } + + return result; } return importItem; }); diff --git a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap index bce6f10..3fa87b6 100644 --- a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -89,6 +89,26 @@ When using Standard base style, each layer needs a 'slot' to control stacking: • top: Above all base map features (default for visibility) Within each slot, array order still applies - later layers render on top +MAPBOX STANDARD - CONFIGURATION: +Standard style provides a rich basemap that can be customized using the standard_config parameter. +These settings control the BASE Standard style features - you can still add custom layers on top! + +• Visibility toggles: Control which base features are shown + - showPlaceLabels, showRoadLabels, showTransitLabels (base labels) + - showPedestrianRoads, show3dObjects, showAdminBoundaries (base features) +• Theme options: Adjust the overall look of the base style + - theme: default/faded/monochrome/custom + - lightPreset: day/night/dawn/dusk +• Color overrides: Change colors of base Standard style elements + - Roads: colorMotorways, colorTrunks, colorRoads + - Nature: colorWater, colorGreenspace + - Labels: colorPlaceLabels, colorRoadLabels, colorPointOfInterestLabels + - Admin: colorAdminBoundaries +• Density controls: densityPointOfInterestLabels (1-5) + +IMPORTANT: These configurations modify the underlying Standard basemap. +Your custom layers (defined in 'layers' parameter) are added ON TOP of this configured basemap. + RESOURCE GUIDE: The resource://mapbox-style-layers contains comprehensive documentation including: • All available layer types with descriptions diff --git a/test/tools/style-builder-tool/StyleBuilderTool.test.ts b/test/tools/style-builder-tool/StyleBuilderTool.test.ts index f9ab8b6..8d46d4a 100644 --- a/test/tools/style-builder-tool/StyleBuilderTool.test.ts +++ b/test/tools/style-builder-tool/StyleBuilderTool.test.ts @@ -518,7 +518,13 @@ describe('StyleBuilderTool', () => { l.id.includes('transit') ); expect(transitLayer).toBeDefined(); - expect(transitLayer.filter).toEqual(['==', ['get', 'maki'], 'bus']); + expect(transitLayer.filter).toEqual([ + 'match', + ['get', 'maki'], + ['bus'], + true, + false + ]); }); it('should filter multiple transit types', async () => { @@ -559,6 +565,39 @@ describe('StyleBuilderTool', () => { }); describe('comprehensive filtering', () => { + it('should filter toll roads correctly', async () => { + const tool = new StyleBuilderTool(); + const input: StyleBuilderToolInput = { + style_name: 'Toll Roads Test', + base_style: 'standard', + layers: [ + { + layer_type: 'road', + action: 'highlight', + color: '#9370DB', + filter_properties: { + toll: true + } + } + ] + }; + + const result = await tool.execute(input); + expect(result.isError).toBe(false); + + const styleJson = JSON.parse( + result.content[0].text.match(/```json\n([\s\S]*?)\n```/)![1] + ); + + const roadsLayer = styleJson.layers.find((l: any) => + l.id.includes('road-toll-true') + ); + expect(roadsLayer).toBeDefined(); + // Should have 'has' filter for toll + expect(roadsLayer.filter).toEqual(['has', 'toll']); + expect(roadsLayer.paint['line-color']).toBe('#9370DB'); + }); + it('should filter roads by class', async () => { const input: StyleBuilderToolInput = { style_name: 'Motorway Filter Test', @@ -694,10 +733,78 @@ describe('StyleBuilderTool', () => { // Check that layers have slot property for Standard style style.layers.forEach((layer: any) => { - expect(layer.slot).toBe('top'); + expect(layer.slot).toBeDefined(); + // Water is not a road or label, so it should default to 'middle' + expect(layer.slot).toBe('middle'); }); }); + it('should generate Standard style with configuration', async () => { + const input: StyleBuilderToolInput = { + style_name: 'Standard Config Test', + base_style: 'standard', + layers: [ + { + layer_type: 'water', + action: 'color', + color: '#0099ff' + } + ], + standard_config: { + // Visibility settings + showPlaceLabels: false, + showRoadLabels: false, + showTransitLabels: true, + showPedestrianRoads: false, + show3dObjects: true, + showAdminBoundaries: true, + + // Theme settings + theme: 'faded', + lightPreset: 'dusk', + + // Color overrides + colorMotorways: '#ff0000', + colorTrunks: '#ff6600', + colorRoads: '#ffaa00', + colorWater: '#0066cc', + colorGreenspace: '#00cc00', + colorAdminBoundaries: '#9966cc', + + // Density settings + densityPointOfInterestLabels: 5 + } + }; + + const result = await tool.execute(input); + const text = result.content[0].text; + + expect(text).toContain('Standard Config:** 15 properties set'); + expect(text).toContain('Theme: faded'); + expect(text).toContain('Light preset: dusk'); + + const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); + const style = JSON.parse(jsonMatch![1]); + + // Check that Standard style uses imports with config + expect(style.imports).toBeTruthy(); + expect(Array.isArray(style.imports)).toBe(true); + expect(style.imports[0].id).toBe('basemap'); + expect(style.imports[0].url).toBe('mapbox://styles/mapbox/standard'); + + // Check that config properties are included + const config = style.imports[0].config; + expect(config).toBeTruthy(); + expect(config.showPlaceLabels).toBe(false); + expect(config.showRoadLabels).toBe(false); + expect(config.showTransitLabels).toBe(true); + expect(config.theme).toBe('faded'); + expect(config.lightPreset).toBe('dusk'); + expect(config.colorMotorways).toBe('#ff0000'); + expect(config.colorWater).toBe('#0066cc'); + expect(config.densityPointOfInterestLabels).toBe(5); + }); + it('should generate Classic style with sources', async () => { const input: StyleBuilderToolInput = { style_name: 'Classic Style Test', From f058da5e88c6fa5d3ff17e751a8742cccb025e02 Mon Sep 17 00:00:00 2001 From: jussi-sa Date: Wed, 17 Sep 2025 18:21:55 +0300 Subject: [PATCH 4/8] snapshot test --- .../tool-naming-convention.test.ts.snap | 104 ++++++++++++------ 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap index 3fa87b6..cf41e2b 100644 --- a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -64,17 +64,35 @@ exports[`Tool Naming Convention > should maintain consistent tool list (snapshot }, { "className": "StyleBuilderTool", - "description": "Generate Mapbox style JSON for creating new styles or updating existing ones. Supports Mapbox Standard (default), Classic, and Blank base styles with full control over layers, expressions, and visual properties. + "description": "Generate Mapbox style JSON for creating new styles or updating existing ones. Uses Mapbox Standard for all NEW styles, and preserves the base style type when working with EXISTING styles. USAGE: 1. Use this tool to generate style JSON configuration 2. For NEW styles: Use the generated JSON with create_style_tool 3. For EXISTING styles: Use portions of the JSON with update_style_tool to modify specific layers +BASE STYLE SELECTION: +• FOR NEW STYLES: ALWAYS use base_style: 'standard' unless user explicitly requests otherwise + - Standard is the modern, recommended approach with best performance + - Example: "Create a style showing toll roads" → use 'standard' + - Example: "Create a dark style showing parks" → use 'standard' with dark theme config + +• FOR EXISTING STYLES: Auto-detect and preserve the current base style + - If working on a Classic style (retrieved via retrieve_style_tool), keep using Classic + - If working on a Standard style, keep using Standard + - Look for 'imports' field to identify Standard styles + BASE STYLES: -• standard (default): Modern Mapbox Standard with imports, requires slot property -• streets/light/dark/satellite/outdoors: Classic styles (deprecated but supported) -• blank: Empty style for full customization +• standard (DEFAULT FOR NEW STYLES): Modern Mapbox Standard with imports, best performance +• streets/light/dark/satellite/outdoors: Classic styles (use only for existing Classic styles or explicit request) +• blank: Empty style for full customization (only when explicitly needed) + +WHEN TO USE EACH BASE STYLE: +• "Create a style showing toll roads" → base_style: 'standard' +• "Create a dark style with parks" → base_style: 'standard' + standard_config: { theme: 'monochrome' } +• "Build a navigation style" → base_style: 'standard' +• "Working on style cmfnxfroh..." (if it's Classic) → preserve its base_style +• "Create a streets-v11 style" (explicit request) → base_style: 'streets' LAYER ORDERING: • In ALL styles: Later layers in array render on top of earlier layers @@ -118,26 +136,30 @@ The resource://mapbox-style-layers contains comprehensive documentation includin AVAILABLE LAYER TYPES: • water, waterway - Oceans, lakes, rivers -• landuse, parks - Land areas like parks, hospitals, schools +• landuse - General land use areas +• parks - Parks, cemeteries, golf courses (pre-filtered for class: park|cemetery|golf_course) • buildings, building_3d - Building footprints and 3D extrusions -• ROAD TYPES (use these specific types for best results): - - motorways - Highway/freeway roads (class: motorway) - - primary_roads - Major roads (class: primary, trunk) - - secondary_roads - Secondary roads (class: secondary) - - streets - Local streets (class: street, street_limited) - - paths - Walking/cycling paths (class: path, pedestrian) - - railways - Rail lines - - roads - Generic/all roads (avoid using - use specific types above instead) +• ROAD TYPES (each automatically includes the correct road classes): + - road - Generic road layer for CUSTOM filtering (use for toll roads, bridges, tunnels, etc.) + - motorways - Highways/freeways (includes: motorway, trunk) + - primary_roads - Major arterial roads (includes: primary) + - secondary_roads - Secondary & tertiary roads (includes: secondary, tertiary) + - streets - Local/residential streets (includes: street, street_limited, residential, service) + - paths - Pedestrian paths & walkways (includes: path, pedestrian) + - railways - Rail lines (includes: major_rail, minor_rail, service_rail) • country_boundaries, state_boundaries - Administrative borders -• place_labels, road_labels, poi_labels - Text labels +• place_labels - City/town/village labels (pre-filtered) +• road_labels - Street name labels +• poi_labels - Points of interest with icons • landcover - Natural features like forests, grass -• airports - Airport features -• transit - Bus stops, subway entrances, rail stations (filter by maki: bus, entrance, rail-metro) +• airports - Airport features (runways, terminals) +• transit - Transit stops with icons (bus, subway, rail) IMPORTANT FOR ROADS: -• Always use specific road layer types (motorways, primary_roads, etc.) instead of generic 'roads' -• Each road type automatically includes proper filters and zoom-based width interpolation -• Don't specify fixed widths - the tool automatically applies appropriate zoom-based scaling +• Use 'road' layer type for custom filtering (toll roads, bridges, tunnels, bike lanes, etc.) +• Use specific road types (motorways, primary_roads, etc.) for pre-filtered road classes +• Each specific road type automatically includes proper filters and zoom-based width interpolation +• The generic 'road' layer requires filter_properties for filtering ACTIONS YOU CAN APPLY: • color - Set the layer's color (roads will use smart defaults if not specified) @@ -152,23 +174,37 @@ EXPRESSION FEATURES: • Interpolated values - "Fade buildings in between zoom 14 and 16" ADVANCED FILTERING: -• "Show only motorways and trunk roads" -• "Display only bridges, not tunnels" -• "Show only paved roads" -• "Display only disputed boundaries" -• "Show only major rail lines, not service rails" -• "Filter POIs by maki icon type (restaurants, hospitals, etc.)" -• "Show only bus stops (transit layer with maki: bus)" -• "Display subway entrances (transit with maki: entrance)" +• "Show only motorways and trunk roads" - Use motorways layer type +• "Display only toll roads" - Use road layer with filter_properties: { toll: true } +• "Display only bridges" - Use road layer with filter_properties: { structure: 'bridge' } +• "Show only tunnels" - Use road layer with filter_properties: { structure: 'tunnel' } +• "Show only paved roads" - Use road layer with filter_properties: { surface: 'paved' } +• "Show roads with bike lanes" - Use road layer with filter_properties: { bike_lane: ['left', 'right', 'both'] } +• "Display only disputed boundaries" - Use filter_properties: { disputed: 'true' } +• "Filter POIs by maki icon type" - Use poi_labels with filter_properties: { maki: 'restaurant' } +• "Show only bus stops" - Use transit layer with filter_properties: { maki: 'bus' } +• "Display subway entrances" - Use transit layer with filter_properties: { maki: 'entrance' } COMPREHENSIVE EXAMPLES: -• "Show only motorways that are bridges" -• "Display major rails but exclude tunnels" -• "Color roads: motorways red, primary orange, secondary yellow" -• "Show only toll roads that are paved" -• "Display only civil airports, not military" -• "Show country boundaries excluding maritime ones" -• "Color bus stops red and subway entrances blue (transit with different maki values)" +• "Color all roads differently": + - Use motorways (red), primary_roads (orange), secondary_roads (yellow), streets (green), paths (purple) + - Each layer type automatically includes the correct road classes +• "Show only toll roads" - Use road layer with filter_properties: { toll: true } +• "Show toll motorways" - Use road layer with filter_properties: { toll: true, class: ['motorway', 'trunk'] } +• "Display bridges" - Use road layer with filter_properties: { structure: 'bridge' } +• "Show paved roads" - Use road layer with filter_properties: { surface: 'paved' } +• "Display one-way streets" - Use road layer with filter_properties: { oneway: 'true', class: ['street'] } +• "Show roads with bike lanes" - Use road layer with filter_properties: { bike_lane: ['left', 'right', 'both', 'yes'] } +• "Show all labels" - Use place_labels, road_labels, poi_labels layers +• "Display boundaries" - Use country_boundaries and state_boundaries layers + +IMPORTANT FOR ROAD FILTERING: +• The 'toll' property uses 'true' as a string value when present +• Structure values: 'none', 'bridge', 'tunnel', 'ford' +• Surface values: 'paved', 'unpaved' +• Bike lane values: 'left', 'right', 'both', 'yes', 'no' +• Oneway and dual_carriageway use string values: 'true' or 'false' +• Access can be 'restricted' when limitations exist For detailed layer properties and filters, check resource://mapbox-style-layers From ff3a656a9341cfef418058b9316b439c713abfb6 Mon Sep 17 00:00:00 2001 From: jussi-sa Date: Thu, 18 Sep 2025 15:31:37 +0300 Subject: [PATCH 5/8] layer matching --- .../style-builder-tool/StyleBuilderTool.ts | 1099 ++++++++++++++--- .../tool-naming-convention.test.ts.snap | 175 +-- .../StyleBuilderTool.test.ts | 68 + 3 files changed, 999 insertions(+), 343 deletions(-) diff --git a/src/tools/style-builder-tool/StyleBuilderTool.ts b/src/tools/style-builder-tool/StyleBuilderTool.ts index 5babae0..65fadb3 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.ts @@ -5,157 +5,83 @@ import { } from './StyleBuilderTool.schema.js'; import { MAPBOX_STYLE_LAYERS } from '../../constants/mapboxStyleLayers.js'; import { STREETS_V8_FIELDS } from '../../constants/mapboxStreetsV8Fields.js'; -import type { MapboxStyle, Layer, Filter } from '../../types/mapbox-style.js'; +import type { Layer, Filter, MapboxStyle } from '../../types/mapbox-style.js'; + +// Type for dynamically created layer definitions +type DynamicLayerDefinition = { + id: string; + type: 'fill' | 'line' | 'symbol' | 'circle' | 'fill-extrusion'; + sourceLayer: string; + description: string; + paintProperties: Array<{ + property: string; + description: string; + example: unknown; + }>; + commonFilters: string[]; + examples: string[]; +}; + +// Geometry types from Mapbox tilestats API for Streets v8 +// This maps actual source-layer names to their geometry types +const SOURCE_LAYER_GEOMETRY: Record< + string, + 'Point' | 'LineString' | 'Polygon' +> = { + landuse: 'Polygon', + waterway: 'LineString', + water: 'Polygon', + aeroway: 'LineString', + structure: 'LineString', + building: 'Polygon', + landuse_overlay: 'Polygon', + road: 'LineString', + admin: 'LineString', + place_label: 'Point', + airport_label: 'Point', + transit_stop_label: 'Point', + natural_label: 'LineString', // Note: Can be both Point and LineString, but primarily LineString + poi_label: 'Point', + motorway_junction: 'Point', + housenum_label: 'Point' +}; export class StyleBuilderTool extends BaseTool { name = 'style_builder_tool'; - description = `Generate Mapbox style JSON for creating new styles or updating existing ones. Uses Mapbox Standard for all NEW styles, and preserves the base style type when working with EXISTING styles. + description = `Generate Mapbox style JSON for creating new styles or updating existing ones. -USAGE: -1. Use this tool to generate style JSON configuration -2. For NEW styles: Use the generated JSON with create_style_tool -3. For EXISTING styles: Use portions of the JSON with update_style_tool to modify specific layers - -BASE STYLE SELECTION: -• FOR NEW STYLES: ALWAYS use base_style: 'standard' unless user explicitly requests otherwise - - Standard is the modern, recommended approach with best performance - - Example: "Create a style showing toll roads" → use 'standard' - - Example: "Create a dark style showing parks" → use 'standard' with dark theme config - -• FOR EXISTING STYLES: Auto-detect and preserve the current base style - - If working on a Classic style (retrieved via retrieve_style_tool), keep using Classic - - If working on a Standard style, keep using Standard - - Look for 'imports' field to identify Standard styles +The tool intelligently resolves layer types and filter properties using Streets v8 data. +You don't need exact layer names - the tool automatically finds the correct layer based on your filters. BASE STYLES: -• standard (DEFAULT FOR NEW STYLES): Modern Mapbox Standard with imports, best performance -• streets/light/dark/satellite/outdoors: Classic styles (use only for existing Classic styles or explicit request) -• blank: Empty style for full customization (only when explicitly needed) - -WHEN TO USE EACH BASE STYLE: -• "Create a style showing toll roads" → base_style: 'standard' -• "Create a dark style with parks" → base_style: 'standard' + standard_config: { theme: 'monochrome' } -• "Build a navigation style" → base_style: 'standard' -• "Working on style cmfnxfroh..." (if it's Classic) → preserve its base_style -• "Create a streets-v11 style" (explicit request) → base_style: 'streets' - -LAYER ORDERING: -• In ALL styles: Later layers in array render on top of earlier layers -• Standard style: 'slot' determines which section, array order matters within each slot -• Classic/Blank: Array order is the only control for layer stacking -Example: [background, water, roads, labels] = labels render on top - -MAPBOX STANDARD - SLOT PROPERTY: -When using Standard base style, each layer needs a 'slot' to control stacking: -• bottom: Below most map features (land, water) -• middle: Between base features and labels -• top: Above all base map features (default for visibility) -Within each slot, array order still applies - later layers render on top - -MAPBOX STANDARD - CONFIGURATION: -Standard style provides a rich basemap that can be customized using the standard_config parameter. -These settings control the BASE Standard style features - you can still add custom layers on top! - -• Visibility toggles: Control which base features are shown - - showPlaceLabels, showRoadLabels, showTransitLabels (base labels) - - showPedestrianRoads, show3dObjects, showAdminBoundaries (base features) -• Theme options: Adjust the overall look of the base style - - theme: default/faded/monochrome/custom - - lightPreset: day/night/dawn/dusk -• Color overrides: Change colors of base Standard style elements - - Roads: colorMotorways, colorTrunks, colorRoads - - Nature: colorWater, colorGreenspace - - Labels: colorPlaceLabels, colorRoadLabels, colorPointOfInterestLabels - - Admin: colorAdminBoundaries -• Density controls: densityPointOfInterestLabels (1-5) - -IMPORTANT: These configurations modify the underlying Standard basemap. -Your custom layers (defined in 'layers' parameter) are added ON TOP of this configured basemap. - -RESOURCE GUIDE: -The resource://mapbox-style-layers contains comprehensive documentation including: -• All available layer types with descriptions -• Paint and layout properties for each layer type -• Common filters and expressions -• Example configurations - -AVAILABLE LAYER TYPES: -• water, waterway - Oceans, lakes, rivers -• landuse - General land use areas -• parks - Parks, cemeteries, golf courses (pre-filtered for class: park|cemetery|golf_course) -• buildings, building_3d - Building footprints and 3D extrusions -• ROAD TYPES (each automatically includes the correct road classes): - - road - Generic road layer for CUSTOM filtering (use for toll roads, bridges, tunnels, etc.) - - motorways - Highways/freeways (includes: motorway, trunk) - - primary_roads - Major arterial roads (includes: primary) - - secondary_roads - Secondary & tertiary roads (includes: secondary, tertiary) - - streets - Local/residential streets (includes: street, street_limited, residential, service) - - paths - Pedestrian paths & walkways (includes: path, pedestrian) - - railways - Rail lines (includes: major_rail, minor_rail, service_rail) -• country_boundaries, state_boundaries - Administrative borders -• place_labels - City/town/village labels (pre-filtered) -• road_labels - Street name labels -• poi_labels - Points of interest with icons -• landcover - Natural features like forests, grass -• airports - Airport features (runways, terminals) -• transit - Transit stops with icons (bus, subway, rail) - -IMPORTANT FOR ROADS: -• Use 'road' layer type for custom filtering (toll roads, bridges, tunnels, bike lanes, etc.) -• Use specific road types (motorways, primary_roads, etc.) for pre-filtered road classes -• Each specific road type automatically includes proper filters and zoom-based width interpolation -• The generic 'road' layer requires filter_properties for filtering - -ACTIONS YOU CAN APPLY: -• color - Set the layer's color (roads will use smart defaults if not specified) -• highlight - Make layer prominent with enhanced color/width -• hide - Remove layer from view -• show - Display layer with default styling - -EXPRESSION FEATURES: -• Zoom-based styling - "Make roads wider at higher zoom levels" -• Data-driven styling - "Color roads based on their class" -• Property-based filters - "Show only international airports" -• Interpolated values - "Fade buildings in between zoom 14 and 16" - -ADVANCED FILTERING: -• "Show only motorways and trunk roads" - Use motorways layer type -• "Display only toll roads" - Use road layer with filter_properties: { toll: true } -• "Display only bridges" - Use road layer with filter_properties: { structure: 'bridge' } -• "Show only tunnels" - Use road layer with filter_properties: { structure: 'tunnel' } -• "Show only paved roads" - Use road layer with filter_properties: { surface: 'paved' } -• "Show roads with bike lanes" - Use road layer with filter_properties: { bike_lane: ['left', 'right', 'both'] } -• "Display only disputed boundaries" - Use filter_properties: { disputed: 'true' } -• "Filter POIs by maki icon type" - Use poi_labels with filter_properties: { maki: 'restaurant' } -• "Show only bus stops" - Use transit layer with filter_properties: { maki: 'bus' } -• "Display subway entrances" - Use transit layer with filter_properties: { maki: 'entrance' } - -COMPREHENSIVE EXAMPLES: -• "Color all roads differently": - - Use motorways (red), primary_roads (orange), secondary_roads (yellow), streets (green), paths (purple) - - Each layer type automatically includes the correct road classes -• "Show only toll roads" - Use road layer with filter_properties: { toll: true } -• "Show toll motorways" - Use road layer with filter_properties: { toll: true, class: ['motorway', 'trunk'] } -• "Display bridges" - Use road layer with filter_properties: { structure: 'bridge' } -• "Show paved roads" - Use road layer with filter_properties: { surface: 'paved' } -• "Display one-way streets" - Use road layer with filter_properties: { oneway: 'true', class: ['street'] } -• "Show roads with bike lanes" - Use road layer with filter_properties: { bike_lane: ['left', 'right', 'both', 'yes'] } -• "Show all labels" - Use place_labels, road_labels, poi_labels layers -• "Display boundaries" - Use country_boundaries and state_boundaries layers - -IMPORTANT FOR ROAD FILTERING: -• The 'toll' property uses 'true' as a string value when present -• Structure values: 'none', 'bridge', 'tunnel', 'ford' -• Surface values: 'paved', 'unpaved' -• Bike lane values: 'left', 'right', 'both', 'yes', 'no' -• Oneway and dual_carriageway use string values: 'true' or 'false' -• Access can be 'restricted' when limitations exist - -For detailed layer properties and filters, check resource://mapbox-style-layers - -TRANSIT FILTERING EXAMPLE: -To show only bus stops: use layer_type: 'transit' with filter_properties: { maki: 'bus' } -To show multiple transit types: filter_properties: { maki: ['bus', 'entrance', 'rail-metro'] }`; +• standard (DEFAULT): Modern Mapbox Standard with best performance +• streets/light/dark/satellite/outdoors: Classic styles +• blank: Empty canvas for full customization + +STANDARD STYLE CONFIG: +Use standard_config to customize the basemap: +• Theme: default/faded/monochrome +• Light: day/night/dawn/dusk +• Show/hide: labels, roads, 3D buildings +• Colors: water, roads, parks, etc. + +LAYER ACTIONS: +• color: Apply a specific color +• highlight: Make prominent +• hide: Remove from view +• show: Display with defaults + +AUTO-DETECTION: +The tool automatically finds the correct layer from your filter_properties. +Examples: +• { class: 'park' } → finds 'landuse' layer +• { type: 'wetland' } → finds 'landuse_overlay' layer +• { maki: 'restaurant' } → finds 'poi_label' layer +• { toll: true } → finds 'road' layer + +Invalid layer names are auto-corrected based on the filter properties you provide. + +For detailed documentation: resource://mapbox-style-layers`; constructor() { super({ inputSchema: StyleBuilderToolSchema }); @@ -163,7 +89,14 @@ To show multiple transit types: filter_properties: { maki: ['bus', 'entrance', ' protected async execute(input: StyleBuilderToolInput) { try { - const style = this.buildStyle(input); + const result = this.buildStyle(input); + const { style, corrections } = result; + + // Build corrections message if any + const correctionsMessage = + corrections.length > 0 + ? `\n**Auto-corrections Applied:**\n${corrections.join('\n')}\n` + : ''; return { content: [ @@ -175,7 +108,7 @@ To show multiple transit types: filter_properties: { maki: ['bus', 'entrance', ' **Base:** ${input.base_style} **Layers Configured:** ${input.layers.length} ${input.standard_config ? `**Standard Config:** ${Object.keys(input.standard_config).length} properties set` : ''} - +${correctionsMessage} ${this.generateSummary(input)} **Generated Style JSON:** @@ -204,8 +137,12 @@ ${JSON.stringify(style, null, 2)} } } - private buildStyle(input: StyleBuilderToolInput): MapboxStyle { + private buildStyle(input: StyleBuilderToolInput): { + style: MapboxStyle; + corrections: string[]; + } { const layers: Layer[] = []; + const allCorrections: string[] = []; const isUsingStandard = input.base_style === 'standard'; // Only add background layer for non-Standard styles @@ -230,20 +167,55 @@ ${JSON.stringify(style, null, 2)} for (const config of input.layers) { if (config.action === 'hide') continue; - const layerDef = MAPBOX_STYLE_LAYERS[config.layer_type]; + // Try to get layer from MAPBOX_STYLE_LAYERS first + let layerDef: + | (typeof MAPBOX_STYLE_LAYERS)[keyof typeof MAPBOX_STYLE_LAYERS] + | DynamicLayerDefinition + | null = MAPBOX_STYLE_LAYERS[config.layer_type]; + + // If not found, try to create a dynamic layer definition if (!layerDef) { - console.warn(`Unknown layer type: ${config.layer_type}`); - continue; + layerDef = this.createDynamicLayerDefinition(config.layer_type); + if (!layerDef) { + // Try to find the correct layer based on filter properties + const correctLayer = this.findCorrectLayerForFilters(config); + if (correctLayer) { + allCorrections.push( + `• Layer type "${config.layer_type}" not found. Using "${correctLayer}" instead (contains the filtered fields).` + ); + // Update the config with the correct layer type + config.layer_type = correctLayer; + // Try again with the correct layer + layerDef = + MAPBOX_STYLE_LAYERS[correctLayer] || + this.createDynamicLayerDefinition(correctLayer); + } else { + // If no filter properties or can't find a match, skip with warning + console.warn(`Unknown layer type: "${config.layer_type}"`); + if ( + config.filter_properties && + Object.keys(config.filter_properties).length > 0 + ) { + // Only provide suggestions if they had filter properties + const availableLayers = this.getAvailableLayersInfo(config); + console.warn(availableLayers); + } + continue; + } + } } - const layer = this.createLayer( + const result = this.createLayer( layerDef, config, input.global_settings, isUsingStandard ); - if (layer) { - layers.push(layer); + if (result.layer) { + layers.push(result.layer); + } + if (result.corrections.length > 0) { + allCorrections.push(...result.corrections); } } @@ -324,8 +296,8 @@ ${JSON.stringify(style, null, 2)} style.sprite = 'mapbox://sprites/mapbox/streets-v12'; style.glyphs = 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf'; style.layers = layers; - } else if (input.base_style === 'blank') { - // Blank style - no imports, just basic sources + } else { + // Blank style or no base style specified - no imports, just basic sources style.center = [0, 0]; style.zoom = 2; style.sources = { @@ -339,17 +311,19 @@ ${JSON.stringify(style, null, 2)} style.layers = layers; } - return style; + return { style, corrections: allCorrections }; } private createLayer( - layerDef: (typeof MAPBOX_STYLE_LAYERS)[keyof typeof MAPBOX_STYLE_LAYERS], + layerDef: + | (typeof MAPBOX_STYLE_LAYERS)[keyof typeof MAPBOX_STYLE_LAYERS] + | NonNullable>, config: StyleBuilderToolInput['layers'][0], globalSettings?: StyleBuilderToolInput['global_settings'], isUsingStandard?: boolean - ): Layer | null { + ): { layer: Layer | null; corrections: string[] } { // Generate a unique ID for the layer based on its properties - let layerId = `${layerDef.id}-custom`; + let layerId = `${layerDef.id || config.layer_type}-custom`; // If there are filter properties, create a unique suffix from them if (config.filter_properties) { @@ -400,10 +374,10 @@ ${JSON.stringify(style, null, 2)} layer['source-layer'] = layerDef.sourceLayer; } - // Generate comprehensive filter - const filter = this.generateComprehensiveFilter(config, layerDef); - if (filter) { - layer.filter = filter; + // Generate comprehensive filter with auto-correction + const filterResult = this.generateComprehensiveFilter(config, layerDef); + if (filterResult.filter) { + layer.filter = filterResult.filter; } // Build paint properties @@ -677,7 +651,11 @@ ${JSON.stringify(style, null, 2)} } // Add layout properties with better defaults for specific layer types - if (layerDef.layoutProperties && layerDef.layoutProperties.length > 0) { + if ( + 'layoutProperties' in layerDef && + layerDef.layoutProperties && + layerDef.layoutProperties.length > 0 + ) { const layout: Record = {}; // Special handling for transit and POI layers @@ -713,7 +691,7 @@ ${JSON.stringify(style, null, 2)} layout['text-font'] = ['DIN Pro Regular', 'Arial Unicode MS Regular']; layout['text-size'] = 12; layout['text-rotation-alignment'] = 'map'; - } else { + } else if ('layoutProperties' in layerDef && layerDef.layoutProperties) { // Default layout from definition for (const prop of layerDef.layoutProperties) { if (prop.example !== undefined) { @@ -727,7 +705,7 @@ ${JSON.stringify(style, null, 2)} } } - return layer; + return { layer, corrections: filterResult.corrections }; } private parseFilterString(filterStr: string): unknown | null { @@ -827,7 +805,9 @@ ${JSON.stringify(style, null, 2)} const parts: string[] = ['**Layer Configurations:**']; for (const config of input.layers) { - const layerDef = MAPBOX_STYLE_LAYERS[config.layer_type]; + const layerDef = + MAPBOX_STYLE_LAYERS[config.layer_type] || + this.createDynamicLayerDefinition(config.layer_type); const description = layerDef?.description || config.layer_type; switch (config.action) { @@ -1039,24 +1019,300 @@ ${JSON.stringify(style, null, 2)} ]; } + /** + * Calculate similarity between two strings (simple Levenshtein-like score) + */ + private calculateSimilarity(str1: string, str2: string): number { + const s1 = str1.toLowerCase(); + const s2 = str2.toLowerCase(); + + // Exact match + if (s1 === s2) return 1; + + // Substring match - high score if one contains the other + if (s1.includes(s2) || s2.includes(s1)) { + const lengthRatio = + Math.min(s1.length, s2.length) / Math.max(s1.length, s2.length); + return 0.7 + 0.2 * lengthRatio; + } + + // Calculate common characters + let common = 0; + for (let i = 0; i < Math.min(s1.length, s2.length); i++) { + if (s1[i] === s2[i]) common++; + } + + return common / Math.max(s1.length, s2.length); + } + + /** + * Find the closest matching value for a field using intelligent matching + */ + private findClosestFieldValue( + fieldName: string, + inputValue: string | number | boolean, + validValues: readonly any[] + ): { value: any; corrected: boolean; message?: string } { + // For non-string values, just check if it's valid + if (typeof inputValue !== 'string') { + const isValid = validValues.includes(inputValue); + return { + value: inputValue, + corrected: false, + message: isValid + ? undefined + : `Invalid ${fieldName} value: ${inputValue}. Valid values: ${validValues.slice(0, 10).join(', ')}${validValues.length > 10 ? '...' : ''}` + }; + } + + // 1. Check for exact match (case-insensitive) + const exactMatch = validValues.find( + (v) => + typeof v === 'string' && v.toLowerCase() === inputValue.toLowerCase() + ); + if (exactMatch) { + return { + value: exactMatch, + corrected: exactMatch !== inputValue, + message: + exactMatch !== inputValue + ? `Auto-corrected casing: "${inputValue}" → "${exactMatch}"` + : undefined + }; + } + + // 2. Try common variations (only if they result in a valid value) + const variations = [ + inputValue.replace(/\s+/g, '_'), // spaces to underscores + inputValue.replace(/\s+/g, '-'), // spaces to hyphens + inputValue.replace(/_/g, '-'), // underscores to hyphens + inputValue.replace(/-/g, '_'), // hyphens to underscores + inputValue.replace(/[\s_-]+/g, '') // remove all separators + ]; + + for (const variation of variations) { + const match = validValues.find( + (v) => + typeof v === 'string' && v.toLowerCase() === variation.toLowerCase() + ); + if (match) { + return { + value: match, + corrected: true, + message: `Auto-corrected: "${inputValue}" → "${match}"` + }; + } + } + + // 3. Find best match using similarity scoring + const stringValues = validValues.filter( + (v) => typeof v === 'string' + ) as string[]; + if (stringValues.length > 0) { + const scores = stringValues.map((v) => ({ + value: v, + score: this.calculateSimilarity(inputValue, v) + })); + + // Sort by score descending + scores.sort((a, b) => b.score - a.score); + + // If we have a good match (>70% similarity), use it + if (scores[0].score > 0.7) { + return { + value: scores[0].value, + corrected: true, + message: `Auto-corrected: "${inputValue}" → "${scores[0].value}" (${Math.round(scores[0].score * 100)}% match)` + }; + } + + // If we have a decent match (>50% similarity) and it's significantly better than the next one + if ( + scores[0].score > 0.5 && + (!scores[1] || scores[0].score > scores[1].score * 1.5) + ) { + return { + value: scores[0].value, + corrected: true, + message: `Auto-corrected: "${inputValue}" → "${scores[0].value}" (best guess)` + }; + } + } + + // 4. No good match found - return original with error message + const suggestions = validValues.slice(0, 10).join(', '); + return { + value: inputValue, + corrected: false, + message: `Warning: "${inputValue}" is not a valid ${fieldName} value. Valid values include: ${suggestions}${validValues.length > 10 ? '...' : ''}` + }; + } + + /** + * Intelligently resolve filter properties by checking if they're field names or values + */ + private resolveFilterProperty( + sourceLayer: string, + property: string, + value: any + ): { + resolvedProperty: string; + resolvedValue: any; + correction?: string; + } { + const layerFields = STREETS_V8_FIELDS[ + sourceLayer as keyof typeof STREETS_V8_FIELDS + ] as any; + if (!layerFields) { + return { resolvedProperty: property, resolvedValue: value }; + } + + // Case 1: Property is an actual field name in this layer (e.g., "toll", "oneway", "bike_lane") + if (property in layerFields) { + const fieldDef = layerFields[property]; + + // Validate/correct the value for this field + if (fieldDef && 'values' in fieldDef && Array.isArray(fieldDef.values)) { + const result = this.findClosestFieldValue( + property, + value, + fieldDef.values + ); + return { + resolvedProperty: property, + resolvedValue: result.value, + correction: result.message + }; + } + return { resolvedProperty: property, resolvedValue: value }; + } + + // Case 2: Property might be a value that belongs to a field (e.g., "wetland" should be type: "wetland") + // Priority order for searching fields + const fieldPriority = [ + 'type', + 'class', + 'maki', + 'structure', + 'surface', + 'mode', + 'stop_type' + ]; + + // First, try the priority fields + for (const fieldName of fieldPriority) { + const fieldDef = layerFields[fieldName]; + if ( + !fieldDef || + !('values' in fieldDef) || + !Array.isArray(fieldDef.values) + ) + continue; + + // Check if our property name matches a value in this field + for (const validValue of fieldDef.values) { + if ( + String(validValue).toLowerCase() === String(property).toLowerCase() + ) { + return { + resolvedProperty: fieldName, + resolvedValue: validValue, + correction: `Interpreted "${property}" as ${fieldName}="${validValue}"` + }; + } + } + + // Check for partial matches + for (const validValue of fieldDef.values) { + const propLower = String(property).toLowerCase(); + const valLower = String(validValue).toLowerCase(); + if (valLower.includes(propLower) || propLower.includes(valLower)) { + return { + resolvedProperty: fieldName, + resolvedValue: validValue, + correction: `Interpreted "${property}" as ${fieldName}="${validValue}" (partial match)` + }; + } + } + } + + // Case 3: Search all other fields if no match in priority fields + for (const [fieldName, fieldDef] of Object.entries(layerFields)) { + if (fieldPriority.includes(fieldName)) continue; // Already checked + if (!fieldDef || typeof fieldDef !== 'object') continue; + if (!('values' in fieldDef) || !Array.isArray((fieldDef as any).values)) + continue; + + const values = (fieldDef as any).values; + for (const validValue of values) { + if ( + String(validValue).toLowerCase() === String(property).toLowerCase() + ) { + return { + resolvedProperty: fieldName, + resolvedValue: validValue, + correction: `Interpreted "${property}" as ${fieldName}="${validValue}"` + }; + } + } + } + + // Case 4: No match found - keep original but warn + return { + resolvedProperty: property, + resolvedValue: value, + correction: `Warning: "${property}" not found as field or value in ${sourceLayer} layer` + }; + } + private buildAdvancedFilter( sourceLayer: string, filterConfig: Record< string, string | number | boolean | (string | number | boolean)[] > - ): Filter | null { + ): { filter: Filter | null; corrections: string[] } { const filters: unknown[] = []; + const corrections: string[] = []; // Get field definitions for this source layer const layerFields = STREETS_V8_FIELDS[sourceLayer as keyof typeof STREETS_V8_FIELDS]; - if (!layerFields) return null; + if (!layerFields) return { filter: null, corrections: [] }; + + // Resolve each property to determine if it's a field name or value + const resolvedConfig: Record = {}; - // Build filter expressions for each property for (const [property, value] of Object.entries(filterConfig)) { if (value === undefined || value === null) continue; + const resolved = this.resolveFilterProperty(sourceLayer, property, value); + + if (resolved.correction) { + corrections.push(resolved.correction); + } + + // Accumulate values for the same property + if (resolvedConfig[resolved.resolvedProperty]) { + // If we already have this property, combine values into array + const existing = resolvedConfig[resolved.resolvedProperty]; + if (Array.isArray(existing)) { + existing.push(resolved.resolvedValue); + } else { + resolvedConfig[resolved.resolvedProperty] = [ + existing, + resolved.resolvedValue + ]; + } + } else { + resolvedConfig[resolved.resolvedProperty] = resolved.resolvedValue; + } + } + + // Now build filters from resolved config + for (const [property, value] of Object.entries(resolvedConfig)) { + if (value === undefined || value === null) continue; + const fieldDef = layerFields[property as keyof typeof layerFields] as any; // Special handling for toll property - it's a presence check, not a value check @@ -1070,7 +1326,12 @@ ${JSON.stringify(style, null, 2)} continue; } - if (!fieldDef) continue; + if (!fieldDef) { + console.warn( + `Warning: Field "${property}" does not exist in layer "${sourceLayer}". Skipping filter.` + ); + continue; + } // Check if this field uses string booleans by looking at its defined values const isStringBooleanField = @@ -1115,7 +1376,7 @@ ${JSON.stringify(style, null, 2)} } } - // Optional: Validate values against defined values (only for fields with enumerated values) + // Validate and auto-correct values against defined values if ( fieldDef && 'values' in fieldDef && @@ -1123,16 +1384,33 @@ ${JSON.stringify(style, null, 2)} fieldDef.values.length > 0 ) { const validValues = fieldDef.values; - const valuesToCheck = Array.isArray(processedValue) - ? processedValue - : [processedValue]; - - for (const val of valuesToCheck) { - if (!validValues.includes(val as any)) { - console.warn( - `Warning: Value "${val}" is not a valid option for field "${property}" in layer "${sourceLayer}". Valid values are: ${validValues.join(', ')}` + + if (Array.isArray(processedValue)) { + // For arrays, validate and correct each value + const correctedValues = []; + for (const val of processedValue) { + const result = this.findClosestFieldValue( + property, + val, + validValues ); + if (result.message) { + corrections.push(` ${property}: ${result.message}`); + } + correctedValues.push(result.value); } + processedValue = correctedValues; + } else { + // For single values, validate and correct + const result = this.findClosestFieldValue( + property, + processedValue, + validValues + ); + if (result.message) { + corrections.push(` ${property}: ${result.message}`); + } + processedValue = result.value; } } @@ -1153,24 +1431,53 @@ ${JSON.stringify(style, null, 2)} } } - if (filters.length === 0) return null; - if (filters.length === 1) return filters[0] as Filter; - return ['all', ...filters] as Filter; + const filter = + filters.length === 0 + ? null + : filters.length === 1 + ? (filters[0] as Filter) + : (['all', ...filters] as Filter); + + return { filter, corrections }; } private generateComprehensiveFilter( config: StyleBuilderToolInput['layers'][0], - layerDef: (typeof MAPBOX_STYLE_LAYERS)[keyof typeof MAPBOX_STYLE_LAYERS] - ): Filter | null { - // If custom filter is provided, use it - if (config.filter) { - return config.filter as Filter; + layerDef: + | (typeof MAPBOX_STYLE_LAYERS)[keyof typeof MAPBOX_STYLE_LAYERS] + | DynamicLayerDefinition + | null + ): { filter: Filter | null; corrections: string[] } { + // If custom filter is provided, process it through buildAdvancedFilter + if ( + config.filter && + typeof config.filter === 'object' && + !Array.isArray(config.filter) + ) { + // It's a simple object like {type: 'wetland'}, process it + if (layerDef && 'sourceLayer' in layerDef && layerDef.sourceLayer) { + return this.buildAdvancedFilter( + layerDef.sourceLayer, + config.filter as Record< + string, + string | number | boolean | (string | number | boolean)[] + > + ); + } + } else if (config.filter && Array.isArray(config.filter)) { + // It's already a Mapbox expression, use it as-is + return { filter: config.filter as Filter, corrections: [] }; } const filters: Filter[] = []; + const allCorrections: string[] = []; // First, add common filters from layer definition - if (layerDef.commonFilters && layerDef.commonFilters.length > 0) { + if ( + layerDef && + layerDef.commonFilters && + layerDef.commonFilters.length > 0 + ) { // Handle multiple filter conditions - join with comma for multiple properties // Each element can be either a single property or a property with pipe-separated values const filterStr = layerDef.commonFilters.join(', '); @@ -1181,20 +1488,33 @@ ${JSON.stringify(style, null, 2)} } // Then, add filter_properties if provided - if (config.filter_properties && layerDef.sourceLayer) { - const propertyFilter = this.buildAdvancedFilter( + if ( + config.filter_properties && + layerDef && + 'sourceLayer' in layerDef && + layerDef.sourceLayer + ) { + const result = this.buildAdvancedFilter( layerDef.sourceLayer, config.filter_properties ); - if (propertyFilter) { - filters.push(propertyFilter); + if (result.filter) { + filters.push(result.filter); + } + if (result.corrections.length > 0) { + allCorrections.push(...result.corrections); } } // Combine filters if there are multiple - if (filters.length === 0) return null; - if (filters.length === 1) return filters[0]; - return ['all', ...filters] as Filter; + const filter = + filters.length === 0 + ? null + : filters.length === 1 + ? filters[0] + : (['all', ...filters] as Filter); + + return { filter, corrections: allCorrections }; } private isRoadLayer(layerType: string): boolean { @@ -1375,7 +1695,6 @@ ${JSON.stringify(style, null, 2)} // Natural features - soft and subtle parks: 0.65, landuse: 0.45, - landcover: 0.4, // Roads - varied opacity for navigation clarity motorways: 0.85, // High visibility for navigation @@ -1412,6 +1731,389 @@ ${JSON.stringify(style, null, 2)} return opacityMap[layerType] || 0.7; } + private createDynamicLayerDefinition(layerType: string) { + // Check if this layer type exists as a source-layer + // No conversion needed - source-layer names already use underscores + const sourceLayer = layerType; + + // Check if this source-layer exists in STREETS_V8_FIELDS or our geometry mapping + const hasInStreetsV8 = sourceLayer in STREETS_V8_FIELDS; + const hasInGeometry = sourceLayer in SOURCE_LAYER_GEOMETRY; + + if (!hasInStreetsV8 && !hasInGeometry) { + return null; + } + + // Get geometry type from our hardcoded mapping + const geometry = SOURCE_LAYER_GEOMETRY[sourceLayer]; + if (!geometry) { + // Source-layer exists in STREETS_V8_FIELDS but not in our geometry mapping + // This shouldn't happen with Streets v8, but let's handle it gracefully + console.warn(`No geometry type found for source-layer: ${sourceLayer}`); + return null; + } + + // Determine layer type based on geometry + let type: 'fill' | 'line' | 'symbol' | 'circle' | 'fill-extrusion'; + let paintProperties: Array<{ + property: string; + description: string; + example: any; + }> = []; + + switch (geometry) { + case 'Polygon': + // Special case for buildings with 3D + if (sourceLayer === 'building' && layerType.includes('3d')) { + type = 'fill-extrusion'; + paintProperties = [ + { + property: 'fill-extrusion-color', + description: 'Building color', + example: '#AAAAAA' + }, + { + property: 'fill-extrusion-height', + description: 'Building height', + example: ['get', 'height'] + }, + { + property: 'fill-extrusion-base', + description: 'Building base height', + example: ['get', 'min_height'] + }, + { + property: 'fill-extrusion-opacity', + description: 'Building opacity', + example: 0.8 + } + ]; + } else { + type = 'fill'; + paintProperties = [ + { + property: 'fill-color', + description: 'Fill color', + example: '#000000' + }, + { + property: 'fill-opacity', + description: 'Fill opacity', + example: 0.5 + }, + { + property: 'fill-outline-color', + description: 'Outline color', + example: '#000000' + } + ]; + } + break; + + case 'LineString': + // Admin boundaries and natural features are often rendered as lines + type = 'line'; + paintProperties = [ + { + property: 'line-color', + description: 'Line color', + example: '#000000' + }, + { property: 'line-width', description: 'Line width', example: 2 }, + { + property: 'line-opacity', + description: 'Line opacity', + example: 0.8 + } + ]; + break; + + case 'Point': + // Points can be either circle or symbol layers + // Labels and text-based layers should be symbols + if ( + sourceLayer.includes('label') || + sourceLayer === 'motorway_junction' + ) { + type = 'symbol'; + paintProperties = [ + { + property: 'text-color', + description: 'Text color', + example: '#000000' + }, + { + property: 'text-halo-color', + description: 'Text halo color', + example: '#FFFFFF' + }, + { + property: 'text-halo-width', + description: 'Text halo width', + example: 1 + } + ]; + } else { + // Default to circle for point features without labels + type = 'circle'; + paintProperties = [ + { + property: 'circle-color', + description: 'Circle color', + example: '#000000' + }, + { + property: 'circle-radius', + description: 'Circle radius', + example: 5 + }, + { + property: 'circle-opacity', + description: 'Circle opacity', + example: 0.8 + } + ]; + } + break; + + default: + // This shouldn't happen with our hardcoded geometry mapping + console.warn(`Unknown geometry type: ${geometry}`); + type = 'fill'; + paintProperties = [ + { + property: 'fill-color', + description: 'Fill color', + example: '#000000' + }, + { + property: 'fill-opacity', + description: 'Fill opacity', + example: 0.5 + } + ]; + } + + return { + id: sourceLayer, // Use source-layer name as the id + type: type as any, + sourceLayer: sourceLayer, + description: `${sourceLayer} layer (${geometry} geometry)`, + paintProperties, + commonFilters: [], + examples: [] + }; + } + + private findCorrectLayerForFilters( + config: StyleBuilderToolInput['layers'][0] + ): string | null { + // If no filter properties, we can't determine the correct layer + if (!config.filter_properties) { + return null; + } + + // Score each source layer based on how well it matches the filter properties + const layerScores: Record = {}; + const filterEntries = Object.entries(config.filter_properties); + + for (const [sourceLayer, fields] of Object.entries(STREETS_V8_FIELDS)) { + let score = 0; + const layerFields = fields as Record< + string, + { values?: readonly string[]; description?: string } + >; + + for (const [fieldName, fieldValue] of filterEntries) { + // Check if this field exists in this layer + if (fieldName in layerFields) { + score += 10; // Base score for having the field + + const fieldDef = layerFields[fieldName]; + if (fieldDef.values && Array.isArray(fieldDef.values)) { + // Check if the value exists in this field's allowed values + const valuesToCheck = Array.isArray(fieldValue) + ? fieldValue + : [fieldValue]; + + for (const val of valuesToCheck) { + const normalizedVal = String(val) + .toLowerCase() + .replace(/[_-\s]/g, ''); + + // Exact match (after normalization) + const exactMatch = fieldDef.values.some( + (v) => + String(v) + .toLowerCase() + .replace(/[_-\s]/g, '') === normalizedVal + ); + + if (exactMatch) { + score += 20; // High score for exact value match + } else { + // Partial match + const partialMatch = fieldDef.values.some( + (v) => + String(v) + .toLowerCase() + .replace(/[_-\s]/g, '') + .includes(normalizedVal) || + normalizedVal.includes( + String(v) + .toLowerCase() + .replace(/[_-\s]/g, '') + ) + ); + + if (partialMatch) { + score += 5; // Lower score for partial match + } + } + } + } else { + // Field exists but has no predefined values (like name fields) + // Still counts but with lower score + score += 5; + } + } + } + + if (score > 0) { + layerScores[sourceLayer] = score; + } + } + + // Find the layer with the highest score + let bestLayer: string | null = null; + let bestScore = 0; + + for (const [layer, score] of Object.entries(layerScores)) { + if (score > bestScore) { + bestScore = score; + bestLayer = layer; + } + } + + // If we found a match and it's different from what was requested, return it + if (bestLayer && bestLayer !== config.layer_type) { + return bestLayer; + } + + return null; + } + + private getAvailableLayersInfo( + config: StyleBuilderToolInput['layers'][0] + ): string { + const messages: string[] = []; + + // First, list the valid layer types from MAPBOX_STYLE_LAYERS + const validPreDefinedLayers = Object.keys(MAPBOX_STYLE_LAYERS); + messages.push( + `Valid pre-defined layer types: ${validPreDefinedLayers.join(', ')}` + ); + + // List valid source layers from STREETS_V8_FIELDS + const validSourceLayers = Object.keys(STREETS_V8_FIELDS); + messages.push( + `Valid Streets v8 source layers: ${validSourceLayers.join(', ')}` + ); + + // If they specified filter properties, suggest layers that contain those fields + if (config.filter_properties) { + const suggestions: string[] = []; + + for (const [fieldName, fieldValue] of Object.entries( + config.filter_properties + )) { + // Find which layers contain this field + const layersWithField: string[] = []; + + for (const [sourceLayer, fields] of Object.entries(STREETS_V8_FIELDS)) { + const layerFields = fields as Record< + string, + { values?: readonly string[]; description?: string } + >; + if (fieldName in layerFields) { + const fieldDef = layerFields[fieldName]; + if (fieldDef.values && fieldDef.values.length > 0) { + // Show some example values for this field in this layer + const exampleValues = fieldDef.values.slice(0, 5).join(', '); + const moreValues = + fieldDef.values.length > 5 + ? `, ... (${fieldDef.values.length} total)` + : ''; + layersWithField.push( + `${sourceLayer} (${fieldName}: ${exampleValues}${moreValues})` + ); + } else { + layersWithField.push(`${sourceLayer} (has ${fieldName} field)`); + } + } + } + + if (layersWithField.length > 0) { + suggestions.push( + `Layers with field "${fieldName}": ${layersWithField.join(', ')}` + ); + } + + // Also look for value matches + const layersWithValue: string[] = []; + const valueStr = String(fieldValue) + .toLowerCase() + .replace(/[_-\s]/g, ''); + + for (const [sourceLayer, fields] of Object.entries(STREETS_V8_FIELDS)) { + const layerFields = fields as Record< + string, + { values?: readonly string[]; description?: string } + >; + + for (const [layerFieldName, fieldDef] of Object.entries( + layerFields + )) { + if (fieldDef.values && Array.isArray(fieldDef.values)) { + const matchingValues = fieldDef.values.filter( + (v) => + String(v) + .toLowerCase() + .replace(/[_-\s]/g, '') + .includes(valueStr) || + valueStr.includes( + String(v) + .toLowerCase() + .replace(/[_-\s]/g, '') + ) + ); + + if (matchingValues.length > 0) { + layersWithValue.push( + `${sourceLayer}.${layerFieldName} has: ${matchingValues.slice(0, 3).join(', ')}` + ); + } + } + } + } + + if (layersWithValue.length > 0) { + suggestions.push( + `Layers with value similar to "${fieldValue}": ${layersWithValue.slice(0, 5).join(', ')}` + ); + } + } + + if (suggestions.length > 0) { + messages.push( + `\nSuggestions based on your filters:\n${suggestions.join('\n')}` + ); + } + } + + return messages.join('\n'); + } + private getHarmoniousColor(layerType: string, action: string): string { // Define default colors for when user doesn't specify const colorPalette = { @@ -1431,7 +2133,6 @@ ${JSON.stringify(style, null, 2)} // Natural features (greens) parks: '#90C090', // Park green landuse: '#A0D0A0', // Light green - landcover: '#B0E0B0', // Pale green // Administrative (purples) country_boundaries: '#9966CC', // Purple diff --git a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap index cf41e2b..e64a816 100644 --- a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -64,153 +64,40 @@ exports[`Tool Naming Convention > should maintain consistent tool list (snapshot }, { "className": "StyleBuilderTool", - "description": "Generate Mapbox style JSON for creating new styles or updating existing ones. Uses Mapbox Standard for all NEW styles, and preserves the base style type when working with EXISTING styles. + "description": "Generate Mapbox style JSON for creating new styles or updating existing ones. -USAGE: -1. Use this tool to generate style JSON configuration -2. For NEW styles: Use the generated JSON with create_style_tool -3. For EXISTING styles: Use portions of the JSON with update_style_tool to modify specific layers - -BASE STYLE SELECTION: -• FOR NEW STYLES: ALWAYS use base_style: 'standard' unless user explicitly requests otherwise - - Standard is the modern, recommended approach with best performance - - Example: "Create a style showing toll roads" → use 'standard' - - Example: "Create a dark style showing parks" → use 'standard' with dark theme config - -• FOR EXISTING STYLES: Auto-detect and preserve the current base style - - If working on a Classic style (retrieved via retrieve_style_tool), keep using Classic - - If working on a Standard style, keep using Standard - - Look for 'imports' field to identify Standard styles +The tool intelligently resolves layer types and filter properties using Streets v8 data. +You don't need exact layer names - the tool automatically finds the correct layer based on your filters. BASE STYLES: -• standard (DEFAULT FOR NEW STYLES): Modern Mapbox Standard with imports, best performance -• streets/light/dark/satellite/outdoors: Classic styles (use only for existing Classic styles or explicit request) -• blank: Empty style for full customization (only when explicitly needed) - -WHEN TO USE EACH BASE STYLE: -• "Create a style showing toll roads" → base_style: 'standard' -• "Create a dark style with parks" → base_style: 'standard' + standard_config: { theme: 'monochrome' } -• "Build a navigation style" → base_style: 'standard' -• "Working on style cmfnxfroh..." (if it's Classic) → preserve its base_style -• "Create a streets-v11 style" (explicit request) → base_style: 'streets' - -LAYER ORDERING: -• In ALL styles: Later layers in array render on top of earlier layers -• Standard style: 'slot' determines which section, array order matters within each slot -• Classic/Blank: Array order is the only control for layer stacking -Example: [background, water, roads, labels] = labels render on top - -MAPBOX STANDARD - SLOT PROPERTY: -When using Standard base style, each layer needs a 'slot' to control stacking: -• bottom: Below most map features (land, water) -• middle: Between base features and labels -• top: Above all base map features (default for visibility) -Within each slot, array order still applies - later layers render on top - -MAPBOX STANDARD - CONFIGURATION: -Standard style provides a rich basemap that can be customized using the standard_config parameter. -These settings control the BASE Standard style features - you can still add custom layers on top! - -• Visibility toggles: Control which base features are shown - - showPlaceLabels, showRoadLabels, showTransitLabels (base labels) - - showPedestrianRoads, show3dObjects, showAdminBoundaries (base features) -• Theme options: Adjust the overall look of the base style - - theme: default/faded/monochrome/custom - - lightPreset: day/night/dawn/dusk -• Color overrides: Change colors of base Standard style elements - - Roads: colorMotorways, colorTrunks, colorRoads - - Nature: colorWater, colorGreenspace - - Labels: colorPlaceLabels, colorRoadLabels, colorPointOfInterestLabels - - Admin: colorAdminBoundaries -• Density controls: densityPointOfInterestLabels (1-5) - -IMPORTANT: These configurations modify the underlying Standard basemap. -Your custom layers (defined in 'layers' parameter) are added ON TOP of this configured basemap. - -RESOURCE GUIDE: -The resource://mapbox-style-layers contains comprehensive documentation including: -• All available layer types with descriptions -• Paint and layout properties for each layer type -• Common filters and expressions -• Example configurations - -AVAILABLE LAYER TYPES: -• water, waterway - Oceans, lakes, rivers -• landuse - General land use areas -• parks - Parks, cemeteries, golf courses (pre-filtered for class: park|cemetery|golf_course) -• buildings, building_3d - Building footprints and 3D extrusions -• ROAD TYPES (each automatically includes the correct road classes): - - road - Generic road layer for CUSTOM filtering (use for toll roads, bridges, tunnels, etc.) - - motorways - Highways/freeways (includes: motorway, trunk) - - primary_roads - Major arterial roads (includes: primary) - - secondary_roads - Secondary & tertiary roads (includes: secondary, tertiary) - - streets - Local/residential streets (includes: street, street_limited, residential, service) - - paths - Pedestrian paths & walkways (includes: path, pedestrian) - - railways - Rail lines (includes: major_rail, minor_rail, service_rail) -• country_boundaries, state_boundaries - Administrative borders -• place_labels - City/town/village labels (pre-filtered) -• road_labels - Street name labels -• poi_labels - Points of interest with icons -• landcover - Natural features like forests, grass -• airports - Airport features (runways, terminals) -• transit - Transit stops with icons (bus, subway, rail) - -IMPORTANT FOR ROADS: -• Use 'road' layer type for custom filtering (toll roads, bridges, tunnels, bike lanes, etc.) -• Use specific road types (motorways, primary_roads, etc.) for pre-filtered road classes -• Each specific road type automatically includes proper filters and zoom-based width interpolation -• The generic 'road' layer requires filter_properties for filtering - -ACTIONS YOU CAN APPLY: -• color - Set the layer's color (roads will use smart defaults if not specified) -• highlight - Make layer prominent with enhanced color/width -• hide - Remove layer from view -• show - Display layer with default styling - -EXPRESSION FEATURES: -• Zoom-based styling - "Make roads wider at higher zoom levels" -• Data-driven styling - "Color roads based on their class" -• Property-based filters - "Show only international airports" -• Interpolated values - "Fade buildings in between zoom 14 and 16" - -ADVANCED FILTERING: -• "Show only motorways and trunk roads" - Use motorways layer type -• "Display only toll roads" - Use road layer with filter_properties: { toll: true } -• "Display only bridges" - Use road layer with filter_properties: { structure: 'bridge' } -• "Show only tunnels" - Use road layer with filter_properties: { structure: 'tunnel' } -• "Show only paved roads" - Use road layer with filter_properties: { surface: 'paved' } -• "Show roads with bike lanes" - Use road layer with filter_properties: { bike_lane: ['left', 'right', 'both'] } -• "Display only disputed boundaries" - Use filter_properties: { disputed: 'true' } -• "Filter POIs by maki icon type" - Use poi_labels with filter_properties: { maki: 'restaurant' } -• "Show only bus stops" - Use transit layer with filter_properties: { maki: 'bus' } -• "Display subway entrances" - Use transit layer with filter_properties: { maki: 'entrance' } - -COMPREHENSIVE EXAMPLES: -• "Color all roads differently": - - Use motorways (red), primary_roads (orange), secondary_roads (yellow), streets (green), paths (purple) - - Each layer type automatically includes the correct road classes -• "Show only toll roads" - Use road layer with filter_properties: { toll: true } -• "Show toll motorways" - Use road layer with filter_properties: { toll: true, class: ['motorway', 'trunk'] } -• "Display bridges" - Use road layer with filter_properties: { structure: 'bridge' } -• "Show paved roads" - Use road layer with filter_properties: { surface: 'paved' } -• "Display one-way streets" - Use road layer with filter_properties: { oneway: 'true', class: ['street'] } -• "Show roads with bike lanes" - Use road layer with filter_properties: { bike_lane: ['left', 'right', 'both', 'yes'] } -• "Show all labels" - Use place_labels, road_labels, poi_labels layers -• "Display boundaries" - Use country_boundaries and state_boundaries layers - -IMPORTANT FOR ROAD FILTERING: -• The 'toll' property uses 'true' as a string value when present -• Structure values: 'none', 'bridge', 'tunnel', 'ford' -• Surface values: 'paved', 'unpaved' -• Bike lane values: 'left', 'right', 'both', 'yes', 'no' -• Oneway and dual_carriageway use string values: 'true' or 'false' -• Access can be 'restricted' when limitations exist - -For detailed layer properties and filters, check resource://mapbox-style-layers - -TRANSIT FILTERING EXAMPLE: -To show only bus stops: use layer_type: 'transit' with filter_properties: { maki: 'bus' } -To show multiple transit types: filter_properties: { maki: ['bus', 'entrance', 'rail-metro'] }", +• standard (DEFAULT): Modern Mapbox Standard with best performance +• streets/light/dark/satellite/outdoors: Classic styles +• blank: Empty canvas for full customization + +STANDARD STYLE CONFIG: +Use standard_config to customize the basemap: +• Theme: default/faded/monochrome +• Light: day/night/dawn/dusk +• Show/hide: labels, roads, 3D buildings +• Colors: water, roads, parks, etc. + +LAYER ACTIONS: +• color: Apply a specific color +• highlight: Make prominent +• hide: Remove from view +• show: Display with defaults + +AUTO-DETECTION: +The tool automatically finds the correct layer from your filter_properties. +Examples: +• { class: 'park' } → finds 'landuse' layer +• { type: 'wetland' } → finds 'landuse_overlay' layer +• { maki: 'restaurant' } → finds 'poi_label' layer +• { toll: true } → finds 'road' layer + +Invalid layer names are auto-corrected based on the filter properties you provide. + +For detailed documentation: resource://mapbox-style-layers", "toolName": "style_builder_tool", }, { diff --git a/test/tools/style-builder-tool/StyleBuilderTool.test.ts b/test/tools/style-builder-tool/StyleBuilderTool.test.ts index 8d46d4a..eccac65 100644 --- a/test/tools/style-builder-tool/StyleBuilderTool.test.ts +++ b/test/tools/style-builder-tool/StyleBuilderTool.test.ts @@ -924,6 +924,74 @@ describe('StyleBuilderTool', () => { }); }); + describe('layer auto-correction', () => { + it('should auto-correct landcover to landuse_overlay for wetlands', async () => { + const input: StyleBuilderToolInput = { + style_name: 'Wetlands Test', + base_style: 'standard', + layers: [ + { + layer_type: 'landcover', // Wrong layer type + action: 'color', + color: '#00ff00', + filter_properties: { + type: ['wetland', 'swamp'] + } + } + ] + }; + + const result = await tool.execute(input); + + expect(result.isError).toBe(false); + const text = result.content[0].text; + expect(text).toContain('Auto-corrections Applied'); + expect(text).toContain('Using "landuse_overlay" instead'); + + // Check the generated style JSON + const jsonMatch = text.match(/```json\n([\s\S]+?)\n```/); + expect(jsonMatch).toBeTruthy(); + const style = JSON.parse(jsonMatch![1]); + + // Find the generated layer + const wetlandLayer = style.layers.find( + (l: any) => l['source-layer'] === 'landuse_overlay' + ); + expect(wetlandLayer).toBeTruthy(); + + // The filter should be a Mapbox expression like ['match', ['get', 'type'], ['wetland', 'swamp'], true, false] + expect(wetlandLayer.filter).toBeTruthy(); + expect(wetlandLayer.filter[0]).toBe('match'); // Expression type + expect(wetlandLayer.filter[1]).toEqual(['get', 'type']); // Field accessor + expect(wetlandLayer.filter[2]).toContain('wetland'); // Values to match + expect(wetlandLayer.filter[2]).toContain('swamp'); + }); + + it('should find correct layer based on filter field and value', async () => { + const input: StyleBuilderToolInput = { + style_name: 'Field Resolution Test', + base_style: 'standard', + layers: [ + { + layer_type: 'unknown', // Completely unknown layer + action: 'color', + color: '#ff0000', + filter_properties: { + maki: 'restaurant' // This field only exists in poi_label + } + } + ] + }; + + const result = await tool.execute(input); + + expect(result.isError).toBe(false); + const text = result.content[0].text; + expect(text).toContain('Auto-corrections Applied'); + expect(text).toContain('Using "poi_label" instead'); + }); + }); + describe('multiple layers', () => { it('should handle multiple layers with different actions', async () => { const input: StyleBuilderToolInput = { From 29ccf381ec02a417bd925ca5d3f7af71998691b6 Mon Sep 17 00:00:00 2001 From: jussi-sa Date: Fri, 19 Sep 2025 14:32:24 +0300 Subject: [PATCH 6/8] fixing layer types, tool description, filtering --- src/constants/mapboxStyleLayers.ts | 820 ---------- .../MapboxStyleLayersResource.ts | 612 +++++--- .../StyleBuilderTool.schema.ts | 48 +- .../style-builder-tool/StyleBuilderTool.ts | 1367 +++++++++-------- .../MapboxStyleLayersResource.test.ts | 4 +- .../tool-naming-convention.test.ts.snap | 35 +- .../StyleBuilderTool.test.ts | 126 +- 7 files changed, 1226 insertions(+), 1786 deletions(-) delete mode 100644 src/constants/mapboxStyleLayers.ts diff --git a/src/constants/mapboxStyleLayers.ts b/src/constants/mapboxStyleLayers.ts deleted file mode 100644 index 04cd08e..0000000 --- a/src/constants/mapboxStyleLayers.ts +++ /dev/null @@ -1,820 +0,0 @@ -/** - * Mapbox Style Layer Definitions - * - * Comprehensive descriptions of all Mapbox style layers to guide LLMs in creating styles. - * Based on Mapbox Streets v12 specification. - */ - -export interface LayerDefinition { - id: string; - description: string; - sourceLayer?: string; - type: - | 'background' - | 'fill' - | 'line' - | 'symbol' - | 'circle' - | 'raster' - | 'hillshade' - | 'heatmap' - | 'fill-extrusion' - | 'sky'; - commonFilters?: string[]; - availableProperties?: Record< - string, - { - description: string; - values?: string[]; - type?: 'string' | 'number' | 'boolean'; - } - >; - paintProperties: { - property: string; - description: string; - example: unknown; - }[]; - layoutProperties?: { - property: string; - description: string; - example: unknown; - }[]; - examples: string[]; -} - -export const MAPBOX_STYLE_LAYERS: Record = { - // Background layers - land: { - id: 'land', - description: - 'Background layer for land/terrain. Sets the base color of the map.', - type: 'background', - paintProperties: [ - { - property: 'background-color', - description: 'Color of the land/background', - example: '#f8f4f0' - } - ], - examples: [ - 'Create a dark mode map with black land', - 'Make the background beige' - ] - }, - - // Water features - water: { - id: 'water', - description: 'Fill layer for water bodies like oceans, lakes, and rivers', - sourceLayer: 'water', - type: 'fill', - paintProperties: [ - { - property: 'fill-color', - description: 'Color of water bodies', - example: '#73b6e6' - }, - { - property: 'fill-opacity', - description: 'Opacity of water (0-1)', - example: 1 - } - ], - examples: [ - 'Change water to yellow', - 'Make oceans dark blue', - 'Set lakes to turquoise' - ] - }, - - waterway: { - id: 'waterway', - description: 'Line layer for rivers, streams, and canals', - sourceLayer: 'waterway', - type: 'line', - commonFilters: ['class: river|stream|canal'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of waterways', - example: '#73b6e6' - }, - { - property: 'line-width', - description: 'Width of waterway lines', - example: ['interpolate', ['exponential', 1.3], ['zoom'], 8, 0.5, 20, 6] - } - ], - examples: [ - 'Highlight rivers in bright blue', - 'Make streams wider', - 'Show canals in green' - ] - }, - - // Landuse and land cover - parks: { - id: 'landuse_park', - description: 'Fill layer for parks, gardens, and green spaces', - sourceLayer: 'landuse', - type: 'fill', - commonFilters: ['class: park|cemetery|golf_course'], - paintProperties: [ - { - property: 'fill-color', - description: 'Color of parks and green spaces', - example: '#d8e8c8' - }, - { - property: 'fill-opacity', - description: 'Opacity of parks', - example: 0.9 - } - ], - examples: [ - 'Highlight parks in bright green', - 'Make parks darker', - 'Show golf courses in different shade' - ] - }, - - buildings: { - id: 'building', - description: 'Fill or fill-extrusion layer for buildings', - sourceLayer: 'building', - type: 'fill', - paintProperties: [ - { - property: 'fill-color', - description: 'Color of buildings', - example: '#e0d8ce' - }, - { - property: 'fill-opacity', - description: 'Opacity of buildings', - example: ['interpolate', ['linear'], ['zoom'], 15, 0, 16, 1] - } - ], - examples: [ - 'Show buildings in red', - 'Make buildings semi-transparent', - 'Hide buildings at low zoom' - ] - }, - - building_3d: { - id: 'building-3d', - description: '3D extrusion layer for buildings', - sourceLayer: 'building', - type: 'fill-extrusion', - paintProperties: [ - { - property: 'fill-extrusion-color', - description: 'Color of 3D buildings', - example: '#e0d8ce' - }, - { - property: 'fill-extrusion-height', - description: 'Height of buildings', - example: ['get', 'height'] - }, - { - property: 'fill-extrusion-base', - description: 'Base height of buildings', - example: ['get', 'min_height'] - } - ], - examples: [ - 'Create 3D buildings', - 'Make buildings taller', - 'Color buildings by height' - ] - }, - - // Transportation - railways: { - id: 'road-rail', - description: 'Line layer for railway tracks and rail lines', - sourceLayer: 'road', - type: 'line', - commonFilters: ['class: major_rail|minor_rail|service_rail'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of railway lines', - example: '#bbb' - }, - { - property: 'line-width', - description: 'Width of railway lines', - example: ['interpolate', ['exponential', 1.5], ['zoom'], 14, 0.5, 20, 2] - } - ], - layoutProperties: [ - { - property: 'line-join', - description: 'Line join style', - example: 'round' - } - ], - examples: [ - 'Highlight railways in red', - 'Make train tracks thicker', - 'Show metro lines differently' - ] - }, - - road: { - id: 'road', - description: - 'Generic road layer for custom filtering (toll roads, bridges, tunnels, bike lanes, etc.)', - sourceLayer: 'road', - type: 'line', - commonFilters: [], - paintProperties: [ - { - property: 'line-color', - description: 'Color of roads', - example: '#ccc' - }, - { - property: 'line-width', - description: 'Width of road lines', - example: 4 - }, - { - property: 'line-opacity', - description: 'Opacity of road lines', - example: 1 - } - ], - layoutProperties: [], - examples: [ - 'Show toll roads with filter_properties: { toll: true }', - 'Show bridges with filter_properties: { structure: "bridge" }', - 'Show tunnels with filter_properties: { structure: "tunnel" }', - 'Show paved roads with filter_properties: { surface: "paved" }', - 'Show roads with bike lanes with filter_properties: { bike_lane: ["left", "right", "both"] }', - 'Show one-way roads with filter_properties: { oneway: "true" }', - 'Show restricted roads with filter_properties: { access: "restricted" }' - ] - }, - - motorways: { - id: 'road-motorway', - description: 'Line layer for highways and motorways', - sourceLayer: 'road', - type: 'line', - commonFilters: ['class: motorway|trunk'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of highways', - example: '#fc8' - }, - { - property: 'line-width', - description: 'Width of highway lines', - example: ['interpolate', ['exponential', 1.5], ['zoom'], 5, 0.5, 18, 30] - } - ], - examples: [ - 'Make highways orange', - 'Widen motorways', - 'Highlight major roads' - ] - }, - - primary_roads: { - id: 'road-primary', - description: 'Line layer for primary/main roads', - sourceLayer: 'road', - type: 'line', - commonFilters: ['class: primary'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of primary roads', - example: '#fea' - }, - { - property: 'line-width', - description: 'Width of primary roads', - example: ['interpolate', ['exponential', 1.5], ['zoom'], 5, 0.5, 18, 26] - } - ], - examples: ['Color main roads yellow', 'Make primary roads prominent'] - }, - - secondary_roads: { - id: 'road-secondary', - description: 'Line layer for secondary roads', - sourceLayer: 'road', - type: 'line', - commonFilters: ['class: secondary|tertiary'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of secondary roads', - example: '#fff' - }, - { - property: 'line-width', - description: 'Width of secondary roads', - example: [ - 'interpolate', - ['exponential', 1.5], - ['zoom'], - 11, - 0.5, - 18, - 20 - ] - } - ], - examples: ['Show secondary roads in gray', 'Make minor roads thinner'] - }, - - streets: { - id: 'road-street', - description: 'Line layer for local streets', - sourceLayer: 'road', - type: 'line', - commonFilters: ['class: street|street_limited|residential|service'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of streets', - example: '#fff' - }, - { - property: 'line-width', - description: 'Width of streets', - example: [ - 'interpolate', - ['exponential', 1.5], - ['zoom'], - 12, - 0.5, - 18, - 12 - ] - } - ], - examples: [ - 'Color residential streets', - 'Hide small streets', - 'Make local roads visible' - ] - }, - - paths: { - id: 'road-path', - description: 'Line layer for pedestrian paths, footways, and trails', - sourceLayer: 'road', - type: 'line', - commonFilters: ['class: path|pedestrian'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of paths', - example: '#cba' - }, - { - property: 'line-width', - description: 'Width of paths', - example: ['interpolate', ['exponential', 1.5], ['zoom'], 15, 1, 18, 4] - }, - { - property: 'line-dasharray', - description: 'Dash pattern for paths', - example: [1, 1] - } - ], - examples: [ - 'Show walking paths as dotted lines', - 'Highlight hiking trails', - 'Color bike paths green' - ] - }, - - tunnels: { - id: 'tunnel', - description: 'Line layers for roads in tunnels (with special styling)', - sourceLayer: 'road', - type: 'line', - commonFilters: ['structure: tunnel'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of tunnel roads', - example: '#fff' - }, - { - property: 'line-opacity', - description: 'Opacity of tunnel roads (usually reduced)', - example: 0.5 - }, - { - property: 'line-dasharray', - description: 'Dash pattern for tunnels', - example: [0.4, 0.4] - } - ], - examples: ['Make tunnels semi-transparent', 'Show tunnels as dashed lines'] - }, - - bridges: { - id: 'bridge', - description: 'Line layers for roads on bridges (with special casing)', - sourceLayer: 'road', - type: 'line', - commonFilters: ['structure: bridge'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of bridge roads', - example: '#fff' - }, - { - property: 'line-width', - description: 'Width of bridges (usually wider than regular roads)', - example: ['interpolate', ['exponential', 1.5], ['zoom'], 12, 1, 18, 30] - } - ], - examples: [ - 'Highlight bridges', - 'Make bridge outlines thicker', - 'Color bridges differently' - ] - }, - - airports: { - id: 'aeroway', - description: 'Fill and line layers for airport runways and taxiways', - sourceLayer: 'aeroway', - type: 'fill', - paintProperties: [ - { - property: 'fill-color', - description: 'Color of airport areas', - example: '#ddd' - }, - { - property: 'fill-opacity', - description: 'Opacity of airport areas', - example: 1 - } - ], - examples: [ - 'Show airports in gray', - 'Highlight runways', - 'Make airport areas visible' - ] - }, - - // Administrative boundaries - country_boundaries: { - id: 'admin-0-boundary', - description: - 'Line layer for country/nation boundaries (from admin source-layer)', - sourceLayer: 'admin', - type: 'line', - commonFilters: ['admin_level: 0', 'maritime: false', 'disputed: false'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of country borders', - example: '#8b8aba' - }, - { - property: 'line-width', - description: 'Width of country borders', - example: ['interpolate', ['linear'], ['zoom'], 3, 0.5, 10, 2] - }, - { - property: 'line-dasharray', - description: 'Dash pattern for disputed borders', - example: [2, 2] - } - ], - examples: [ - 'Make country borders red', - 'Show disputed boundaries as dashed', - 'Thicken international borders' - ] - }, - - state_boundaries: { - id: 'admin-1-boundary', - description: - 'Line layer for state/province boundaries (from admin source-layer)', - sourceLayer: 'admin', - type: 'line', - commonFilters: ['admin_level: 1', 'maritime: false'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of state borders', - example: '#9e9cab' - }, - { - property: 'line-width', - description: 'Width of state borders', - example: ['interpolate', ['linear'], ['zoom'], 3, 0.3, 10, 1.5] - } - ], - examples: [ - 'Show state boundaries', - 'Make province borders visible', - 'Color regional boundaries' - ] - }, - - disputed_boundaries: { - id: 'admin-disputed', - description: 'Line layer for disputed boundaries', - sourceLayer: 'admin', - type: 'line', - commonFilters: ['disputed: 1'], - paintProperties: [ - { - property: 'line-color', - description: 'Color of disputed borders', - example: '#ff0000' - }, - { - property: 'line-width', - description: 'Width of disputed borders', - example: 2 - }, - { - property: 'line-dasharray', - description: 'Dash pattern for disputed borders', - example: [2, 4] - } - ], - examples: ['Show disputed territories', 'Highlight contested borders'] - }, - - // Labels - place_labels: { - id: 'place-label', - description: 'Symbol layer for city, town, and place name labels', - sourceLayer: 'place_label', - type: 'symbol', - commonFilters: ['class: settlement|city|town|village'], - layoutProperties: [ - { - property: 'text-field', - description: 'Text to display', - example: ['get', 'name'] - }, - { - property: 'text-font', - description: 'Font family', - example: ['DIN Pro Medium', 'Arial Unicode MS Regular'] - }, - { - property: 'text-size', - description: 'Text size', - example: ['interpolate', ['linear'], ['zoom'], 10, 12, 18, 24] - } - ], - paintProperties: [ - { - property: 'text-color', - description: 'Color of place labels', - example: '#333' - }, - { - property: 'text-halo-color', - description: 'Color of text halo/outline', - example: '#fff' - }, - { - property: 'text-halo-width', - description: 'Width of text halo', - example: 1.5 - } - ], - examples: [ - 'Hide city names', - 'Make town labels larger', - 'Color place names blue' - ] - }, - - road_labels: { - id: 'road-label', - description: 'Symbol layer for road name labels', - sourceLayer: 'road', - type: 'symbol', - layoutProperties: [ - { - property: 'symbol-placement', - description: 'Label placement strategy', - example: 'line' - }, - { - property: 'text-field', - description: 'Road name text', - example: ['get', 'name'] - }, - { - property: 'text-font', - description: 'Font for road names', - example: ['DIN Pro Regular', 'Arial Unicode MS Regular'] - }, - { - property: 'text-size', - description: 'Size of road labels', - example: 12 - }, - { - property: 'text-rotation-alignment', - description: 'Text rotation alignment', - example: 'map' - } - ], - paintProperties: [ - { - property: 'text-color', - description: 'Color of road labels', - example: '#666' - }, - { - property: 'text-halo-color', - description: 'Halo color for road labels', - example: '#fff' - } - ], - examples: [ - 'Show street names', - 'Hide road labels', - 'Make road names bigger' - ] - }, - - poi_labels: { - id: 'poi-label', - description: 'Symbol layer for points of interest (POI) labels', - sourceLayer: 'poi_label', - type: 'symbol', - commonFilters: [], - layoutProperties: [ - { - property: 'text-field', - description: 'POI name', - example: ['get', 'name'] - }, - { - property: 'icon-image', - description: 'Icon for POI', - example: ['get', 'maki'] - }, - { - property: 'text-anchor', - description: 'Text anchor position', - example: 'top' - } - ], - paintProperties: [ - { - property: 'text-color', - description: 'Color of POI labels', - example: '#666' - }, - { - property: 'icon-opacity', - description: 'Opacity of POI icons', - example: 1 - } - ], - examples: [ - 'Show restaurant names', - 'Hide POI labels', - 'Display park names in green' - ] - }, - - transit: { - id: 'transit', - description: 'Symbol layer for transit stations and stops', - sourceLayer: 'transit_stop_label', - type: 'symbol', - layoutProperties: [ - { - property: 'text-field', - description: 'Station name', - example: ['get', 'name'] - }, - { - property: 'icon-image', - description: 'Transit icon', - example: ['get', 'network'] - } - ], - paintProperties: [ - { - property: 'text-color', - description: 'Color of transit labels', - example: '#4898ff' - } - ], - examples: [ - 'Show subway stations', - 'Highlight bus stops', - 'Display train stations prominently' - ] - } -}; - -// Helper function to get layer suggestions based on user input -export function getLayerSuggestions(userPrompt: string): string[] { - const prompt = userPrompt.toLowerCase(); - const suggestions: string[] = []; - - Object.entries(MAPBOX_STYLE_LAYERS).forEach(([key, layer]) => { - // Check if the prompt mentions this layer type - const keywords = [ - key, - layer.id, - layer.sourceLayer, - ...layer.examples.join(' ').toLowerCase().split(' ') - ].filter(Boolean); - - if (keywords.some((keyword) => prompt.includes(keyword as string))) { - suggestions.push(key); - } - }); - - // Add specific keyword mappings - if ( - prompt.includes('water') || - prompt.includes('ocean') || - prompt.includes('sea') || - prompt.includes('lake') - ) { - suggestions.push('water', 'waterway'); - } - if ( - prompt.includes('park') || - prompt.includes('green') || - prompt.includes('garden') - ) { - suggestions.push('parks'); - } - if ( - prompt.includes('railway') || - prompt.includes('train') || - prompt.includes('rail') || - prompt.includes('metro') - ) { - suggestions.push('railways'); - } - if ( - prompt.includes('road') || - prompt.includes('street') || - prompt.includes('highway') || - prompt.includes('motorway') - ) { - suggestions.push( - 'motorways', - 'primary_roads', - 'secondary_roads', - 'streets' - ); - } - if ( - prompt.includes('building') || - prompt.includes('house') || - prompt.includes('3d') - ) { - suggestions.push('buildings', 'building_3d'); - } - if ( - prompt.includes('label') || - prompt.includes('name') || - prompt.includes('text') - ) { - suggestions.push('place_labels', 'road_labels', 'poi_labels'); - } - if ( - prompt.includes('country') || - prompt.includes('border') || - prompt.includes('boundary') - ) { - suggestions.push('country_boundaries', 'state_boundaries'); - } - if ( - prompt.includes('transit') || - prompt.includes('subway') || - prompt.includes('bus') || - prompt.includes('station') - ) { - suggestions.push('transit'); - } - - return [...new Set(suggestions)]; -} diff --git a/src/resources/mapbox-style-layers-resource/MapboxStyleLayersResource.ts b/src/resources/mapbox-style-layers-resource/MapboxStyleLayersResource.ts index b7d0e61..67fd994 100644 --- a/src/resources/mapbox-style-layers-resource/MapboxStyleLayersResource.ts +++ b/src/resources/mapbox-style-layers-resource/MapboxStyleLayersResource.ts @@ -1,18 +1,14 @@ import { BaseResource } from '../BaseResource.js'; -import { - MAPBOX_STYLE_LAYERS, - getLayerSuggestions -} from '../../constants/mapboxStyleLayers.js'; /** - * Resource providing comprehensive Mapbox style layer definitions - * to guide LLMs in creating and modifying Mapbox styles + * Resource providing Mapbox GL JS style specification guidance + * to help LLMs understand layer types, properties, and how to use them */ export class MapboxStyleLayersResource extends BaseResource { - readonly name = 'Mapbox Style Layers Guide'; + readonly name = 'Mapbox Style Specification Guide'; readonly uri = 'resource://mapbox-style-layers'; readonly description = - 'Comprehensive guide for Mapbox style layers including types, properties, and examples'; + 'Mapbox GL JS style specification reference for layer types, paint/layout properties, and Streets v8 source layers'; readonly mimeType = 'text/markdown'; protected async readCallback(uri: URL) { @@ -34,294 +30,446 @@ export class MapboxStyleLayersResource extends BaseResource { const sections: string[] = []; // Header - sections.push('# Mapbox Style Creation Guide'); + sections.push('# Mapbox Style Specification Guide'); sections.push(''); - sections.push('## How to Create a Custom Mapbox Style'); + sections.push( + 'This guide provides the Mapbox GL JS style specification for creating custom map styles.' + ); + sections.push(''); + + // Source layers and geometry types + sections.push('## Streets v8 Source Layers'); sections.push(''); - sections.push('### Step-by-Step Process:'); + sections.push('### Source Layer → Geometry Type Mapping'); + sections.push(''); + sections.push('**Polygon layers:**'); sections.push( - '1. **Understand the request** - What layers should be visible? What colors/styling?' + '- `landuse` - Land use areas (parks, residential, industrial, etc.)' ); sections.push( - '2. **Use style_builder_tool** - This tool generates the style JSON configuration' + '- `water` - Water bodies (oceans, lakes, rivers as polygons)' ); + sections.push('- `building` - Building footprints with height data'); sections.push( - '3. **Apply the style** - Use create_style_tool to create a new style or update_style_tool to modify existing' + '- `landuse_overlay` - Overlay features (wetlands, national parks)' ); sections.push(''); - sections.push('### Example Workflow:'); - sections.push('```'); + sections.push('**LineString layers:**'); + sections.push('- `road` - All roads, paths, railways'); + sections.push('- `admin` - Administrative boundaries'); + sections.push('- `waterway` - Rivers, streams, canals as lines'); + sections.push('- `aeroway` - Airport runways and taxiways'); + sections.push('- `structure` - Bridges, tunnels, fences'); + sections.push('- `natural_label` - Natural feature label placement paths'); + sections.push(''); + sections.push('**Point layers:**'); + sections.push('- `place_label` - City, state, country labels'); + sections.push('- `poi_label` - Points of interest'); + sections.push('- `airport_label` - Airport labels'); + sections.push('- `transit_stop_label` - Transit stops'); + sections.push('- `motorway_junction` - Highway exits'); + sections.push('- `housenum_label` - House numbers'); + sections.push(''); + sections.push('## Layer Types and Properties'); + sections.push(''); + + sections.push('### fill'); sections.push( - 'User: "Create a dark mode style with blue water and hidden labels"' + 'Used for: Polygon features (landuse, water, building, landuse_overlay)' ); - sections.push('Assistant: '); - sections.push('1. Uses style_builder_tool with:'); - sections.push(' - global_settings: { mode: "dark" }'); - sections.push(' - layers: ['); + sections.push(''); + sections.push('**Paint properties:**'); sections.push( - ' { layer_type: "water", action: "color", color: "#0066ff" },' + '- `fill-color` - The color of the filled area (default: `#000000`)' + ); + sections.push( + '- `fill-opacity` - Opacity of the entire fill layer, 0-1 (default: `1`)' + ); + sections.push( + '- `fill-outline-color` - Color of the outline (disabled if unset)' + ); + sections.push( + '- `fill-pattern` - Name of image in sprite to use for fill pattern' + ); + sections.push( + '- `fill-antialias` - Whether to antialias the fill (default: `true`)' + ); + sections.push( + '- `fill-translate` - Geometry translation [x, y] in pixels (default: `[0, 0]`)' + ); + sections.push( + '- `fill-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)' ); - sections.push(' { layer_type: "place_labels", action: "hide" },'); - sections.push(' { layer_type: "road_labels", action: "hide" }'); - sections.push(' ]'); - sections.push('2. Uses create_style_tool with the generated JSON'); - sections.push('```'); sections.push(''); - sections.push('## Quick Reference'); + sections.push('**No layout properties for fill layers**'); sections.push(''); - sections.push('### Common User Requests → Layer Mappings'); + + sections.push('### line'); + sections.push( + 'Used for: LineString features (road, admin, waterway, aeroway, structure, natural_label)' + ); sections.push(''); - sections.push('- **"change water color"** → `water`, `waterway`'); + sections.push('**Paint properties:**'); + sections.push( + '- `line-color` - The color of the line (default: `#000000`)' + ); + sections.push( + '- `line-width` - Width of the line in pixels (default: `1`)' + ); + sections.push('- `line-opacity` - Opacity of the line, 0-1 (default: `1`)'); sections.push( - '- **"highlight parks"** → `parks` (landuse with class=park)' + '- `line-blur` - Blur applied to the line in pixels (default: `0`)' ); sections.push( - '- **"show railways"** → `railways` (road with class=major_rail)' + '- `line-dasharray` - Dash pattern [dash, gap, dash, gap...] (solid if unset)' ); sections.push( - '- **"color roads"** → `motorways`, `primary_roads`, `secondary_roads`, `streets`' + '- `line-gap-width` - Width of inner gap in line (default: `0`)' ); - sections.push('- **"3D buildings"** → `building_3d` (fill-extrusion)'); sections.push( - '- **"hide labels"** → `place_labels`, `road_labels`, `poi_labels`' + '- `line-offset` - Line offset perpendicular to direction (default: `0`)' ); sections.push( - '- **"show borders"** → `country_boundaries`, `state_boundaries`' + '- `line-pattern` - Name of image in sprite for line pattern' ); - sections.push('- **"transit/subway"** → `transit`, `railways`'); sections.push( - '- **"country boundaries"** → `country_boundaries` (admin layer, admin_level=0)' + '- `line-gradient` - Gradient along the line (requires `lineMetrics: true` in source)' ); sections.push( - '- **"state boundaries"** → `state_boundaries` (admin layer, admin_level=1)' + '- `line-translate` - Geometry translation [x, y] in pixels (default: `[0, 0]`)' + ); + sections.push( + '- `line-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)' ); sections.push(''); - - // Layer categories - sections.push('## Layer Categories'); + sections.push('**Layout properties:**'); + sections.push( + '- `line-cap` - Display of line ends: `butt`, `round`, `square` (default: `butt`)' + ); + sections.push( + '- `line-join` - Display of line joins: `bevel`, `round`, `miter` (default: `miter`)' + ); + sections.push('- `line-miter-limit` - Maximum miter length (default: `2`)'); + sections.push( + '- `line-round-limit` - Maximum round join radius (default: `1.05`)' + ); + sections.push('- `line-sort-key` - Sort key for layer ordering'); sections.push(''); - const categories = { - 'Background & Base': ['land'], - 'Water Features': ['water', 'waterway'], - 'Land Use': ['parks', 'buildings', 'building_3d'], - Transportation: [ - 'railways', - 'motorways', - 'primary_roads', - 'secondary_roads', - 'streets', - 'paths', - 'tunnels', - 'bridges' - ], - Aviation: ['airports'], - Boundaries: ['country_boundaries', 'state_boundaries'], - Labels: ['place_labels', 'road_labels', 'poi_labels', 'transit'] - }; - - Object.entries(categories).forEach(([category, layers]) => { - sections.push(`### ${category}`); - layers.forEach((layerKey) => { - const layer = MAPBOX_STYLE_LAYERS[layerKey]; - if (layer) { - sections.push(`- **${layerKey}**: ${layer.description}`); - } - }); - sections.push(''); - }); - - // Detailed layer specifications - sections.push('## Detailed Layer Specifications'); + sections.push('### symbol'); + sections.push( + 'Used for: Point and LineString labels (all *_label layers, natural_label, motorway_junction)' + ); + sections.push(''); + sections.push('**Layout properties (text):**'); + sections.push('- `text-field` - Text to display, e.g., `["get", "name"]`'); + sections.push( + '- `text-font` - Font stack, e.g., `["DIN Pro Regular", "Arial Unicode MS Regular"]`' + ); + sections.push('- `text-size` - Font size in pixels (default: `16`)'); + sections.push( + '- `text-max-width` - Maximum text width in ems (default: `10`)' + ); + sections.push( + '- `text-line-height` - Text line height in ems (default: `1.2`)' + ); + sections.push( + '- `text-letter-spacing` - Letter spacing in ems (default: `0`)' + ); + sections.push( + '- `text-justify` - Text justification: `auto`, `left`, `center`, `right` (default: `center`)' + ); + sections.push( + '- `text-anchor` - Text anchor: `center`, `left`, `right`, `top`, `bottom`, `top-left`, etc.' + ); + sections.push( + '- `text-max-angle` - Maximum angle for curved text (default: `45`)' + ); + sections.push('- `text-rotate` - Text rotation in degrees (default: `0`)'); + sections.push( + '- `text-padding` - Padding around text for collision (default: `2`)' + ); + sections.push( + '- `text-keep-upright` - Keep text upright when map rotates (default: `true`)' + ); + sections.push( + '- `text-transform` - Text case: `none`, `uppercase`, `lowercase` (default: `none`)' + ); + sections.push( + '- `text-offset` - Text offset [x, y] in ems (default: `[0, 0]`)' + ); + sections.push( + '- `text-allow-overlap` - Allow text to overlap (default: `false`)' + ); + sections.push( + '- `text-ignore-placement` - Ignore placement collisions (default: `false`)' + ); + sections.push( + '- `text-optional` - Hide text if icon collides (default: `false`)' + ); + sections.push(''); + sections.push('**Layout properties (icon):**'); + sections.push( + '- `icon-image` - Name of icon in sprite, e.g., `["get", "maki"]`' + ); + sections.push('- `icon-size` - Scale factor for icon (default: `1`)'); + sections.push('- `icon-rotate` - Icon rotation in degrees (default: `0`)'); + sections.push( + '- `icon-padding` - Padding around icon for collision (default: `2`)' + ); + sections.push( + '- `icon-keep-upright` - Keep icon upright (default: `false`)' + ); + sections.push( + '- `icon-offset` - Icon offset [x, y] in ems (default: `[0, 0]`)' + ); + sections.push( + '- `icon-anchor` - Icon anchor: `center`, `left`, `right`, `top`, `bottom`, etc.' + ); + sections.push( + '- `icon-pitch-alignment` - Icon alignment: `map`, `viewport`, `auto` (default: `auto`)' + ); + sections.push( + '- `icon-text-fit` - Scale icon to text: `none`, `width`, `height`, `both` (default: `none`)' + ); + sections.push( + '- `icon-text-fit-padding` - Padding for icon-text-fit [top, right, bottom, left]' + ); + sections.push( + '- `icon-allow-overlap` - Allow icon to overlap (default: `false`)' + ); + sections.push( + '- `icon-ignore-placement` - Ignore icon collisions (default: `false`)' + ); + sections.push( + '- `icon-optional` - Hide icon if text collides (default: `false`)' + ); + sections.push(''); + sections.push('**Layout properties (symbol):**'); + sections.push( + '- `symbol-placement` - Symbol placement: `point`, `line`, `line-center` (default: `point`)' + ); + sections.push( + '- `symbol-spacing` - Distance between symbols on line (default: `250`)' + ); + sections.push( + '- `symbol-avoid-edges` - Avoid symbols at tile edges (default: `false`)' + ); + sections.push('- `symbol-sort-key` - Sort key for symbol ordering'); + sections.push( + '- `symbol-z-order` - Z-order: `auto`, `viewport-y`, `source` (default: `auto`)' + ); + sections.push(''); + sections.push('**Paint properties (text):**'); + sections.push('- `text-color` - Color of the text (default: `#000000`)'); + sections.push( + '- `text-halo-color` - Color of the halo around text (default: `rgba(0, 0, 0, 0)`)' + ); + sections.push('- `text-halo-width` - Width of the halo (default: `0`)'); + sections.push('- `text-halo-blur` - Blur of the halo (default: `0`)'); + sections.push('- `text-opacity` - Opacity of the text, 0-1 (default: `1`)'); + sections.push( + '- `text-translate` - Text translation [x, y] in pixels (default: `[0, 0]`)' + ); + sections.push( + '- `text-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)' + ); + sections.push(''); + sections.push('**Paint properties (icon):**'); + sections.push('- `icon-color` - Tint color for SDF icons'); + sections.push('- `icon-halo-color` - Color of icon halo for SDF icons'); + sections.push('- `icon-halo-width` - Width of icon halo (default: `0`)'); + sections.push('- `icon-halo-blur` - Blur of icon halo (default: `0`)'); + sections.push('- `icon-opacity` - Opacity of the icon, 0-1 (default: `1`)'); + sections.push( + '- `icon-translate` - Icon translation [x, y] in pixels (default: `[0, 0]`)' + ); + sections.push( + '- `icon-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)' + ); sections.push(''); - Object.entries(MAPBOX_STYLE_LAYERS).forEach(([key, layer]) => { - sections.push(`### ${key}`); - sections.push(''); - sections.push(`**Description:** ${layer.description}`); - sections.push(''); - - if (layer.sourceLayer) { - sections.push(`**Source Layer:** \`${layer.sourceLayer}\``); - sections.push(''); - } - - sections.push(`**Type:** \`${layer.type}\``); - sections.push(''); - - if (layer.commonFilters && layer.commonFilters.length > 0) { - sections.push('**Common Filters:**'); - layer.commonFilters.forEach((filter) => { - sections.push(`- ${filter}`); - }); - sections.push(''); - } - - if (layer.paintProperties.length > 0) { - sections.push('**Paint Properties:**'); - sections.push(''); - layer.paintProperties.forEach((prop) => { - sections.push(`- \`${prop.property}\`: ${prop.description}`); - sections.push(` - Example: \`${JSON.stringify(prop.example)}\``); - }); - sections.push(''); - } - - if (layer.layoutProperties && layer.layoutProperties.length > 0) { - sections.push('**Layout Properties:**'); - sections.push(''); - layer.layoutProperties.forEach((prop) => { - sections.push(`- \`${prop.property}\`: ${prop.description}`); - sections.push(` - Example: \`${JSON.stringify(prop.example)}\``); - }); - sections.push(''); - } - - if (layer.examples.length > 0) { - sections.push('**Example User Requests:**'); - layer.examples.forEach((example) => { - sections.push(`- "${example}"`); - }); - sections.push(''); - } - - sections.push('---'); - sections.push(''); - }); + sections.push('### circle'); + sections.push( + 'Used for: Point features (can be used with POI or custom point data)' + ); + sections.push(''); + sections.push('**Paint properties:**'); + sections.push( + '- `circle-color` - The color of the circle (default: `#000000`)' + ); + sections.push('- `circle-radius` - Circle radius in pixels (default: `5`)'); + sections.push( + '- `circle-opacity` - Opacity of the circle, 0-1 (default: `1`)' + ); + sections.push('- `circle-blur` - Amount to blur the circle (default: `0`)'); + sections.push('- `circle-stroke-color` - Color of the circle stroke'); + sections.push( + '- `circle-stroke-width` - Width of the circle stroke (default: `0`)' + ); + sections.push( + '- `circle-stroke-opacity` - Opacity of the circle stroke, 0-1 (default: `1`)' + ); + sections.push( + '- `circle-translate` - Circle translation [x, y] in pixels (default: `[0, 0]`)' + ); + sections.push( + '- `circle-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)' + ); + sections.push( + '- `circle-pitch-scale` - Circle scaling: `map` or `viewport` (default: `map`)' + ); + sections.push( + '- `circle-pitch-alignment` - Circle alignment: `map` or `viewport` (default: `viewport`)' + ); + sections.push(''); + sections.push('**Layout properties:**'); + sections.push('- `circle-sort-key` - Sort key for circle ordering'); + sections.push(''); - // Usage examples - sections.push('## Complete Style Examples'); - sections.push(''); - sections.push('### Example 1: Highlight Railways and Parks, Yellow Water'); - sections.push(''); - sections.push('```javascript'); - sections.push('layers: ['); - sections.push(' {'); - sections.push(' id: "water",'); - sections.push(' type: "fill",'); - sections.push(' source: "composite",'); - sections.push(' "source-layer": "water",'); - sections.push(' paint: {'); - sections.push(' "fill-color": "#ffff00" // Yellow'); - sections.push(' }'); - sections.push(' },'); - sections.push(' {'); - sections.push(' id: "parks",'); - sections.push(' type: "fill",'); - sections.push(' source: "composite",'); - sections.push(' "source-layer": "landuse",'); - sections.push(' filter: ["==", ["get", "class"], "park"],'); - sections.push(' paint: {'); - sections.push(' "fill-color": "#00ff00", // Bright green'); - sections.push(' "fill-opacity": 0.9'); - sections.push(' }'); - sections.push(' },'); - sections.push(' {'); - sections.push(' id: "railways",'); - sections.push(' type: "line",'); - sections.push(' source: "composite",'); - sections.push(' "source-layer": "road",'); + sections.push('### fill-extrusion'); sections.push( - ' filter: ["match", ["get", "class"], ["major_rail", "minor_rail"], true, false],' + 'Used for: 3D buildings (building layer with height/min_height attributes)' ); - sections.push(' paint: {'); - sections.push(' "line-color": "#ff0000", // Red'); + sections.push(''); + sections.push('**Paint properties:**'); sections.push( - ' "line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 14, 2, 20, 8]' + '- `fill-extrusion-color` - Base color of the extrusion (default: `#000000`)' + ); + sections.push( + '- `fill-extrusion-height` - Height in meters, e.g., `["get", "height"]` (default: `0`)' + ); + sections.push( + '- `fill-extrusion-base` - Base height in meters, e.g., `["get", "min_height"]` (default: `0`)' + ); + sections.push( + '- `fill-extrusion-opacity` - Opacity of the extrusion, 0-1 (default: `1`)' + ); + sections.push( + '- `fill-extrusion-pattern` - Name of image in sprite for pattern' + ); + sections.push( + '- `fill-extrusion-translate` - Geometry translation [x, y] in pixels (default: `[0, 0]`)' + ); + sections.push( + '- `fill-extrusion-translate-anchor` - Reference: `map` or `viewport` (default: `map`)' + ); + sections.push( + '- `fill-extrusion-vertical-gradient` - Use vertical gradient (default: `true`)' ); - sections.push(' }'); - sections.push(' }'); - sections.push(']'); - sections.push('```'); + sections.push(''); + sections.push('**No layout properties for fill-extrusion layers**'); sections.push(''); - // Expression examples - sections.push('## Common Expression Patterns'); - sections.push(''); - sections.push('### Zoom-based Interpolation'); - sections.push('```javascript'); - sections.push('"line-width": ['); - sections.push(' "interpolate",'); - sections.push(' ["exponential", 1.5],'); - sections.push(' ["zoom"],'); - sections.push(' 12, 0.5, // At zoom 12, width is 0.5'); - sections.push(' 18, 20 // At zoom 18, width is 20'); - sections.push(']'); + sections.push('## Common Patterns'); + sections.push(''); + sections.push('### Filtering Examples'); + sections.push(''); + sections.push('**Parks only (not cemeteries or golf courses):**'); + sections.push('```json'); + sections.push('{'); + sections.push(' "layer_type": "landuse",'); + sections.push(' "filter_properties": { "class": "park" }'); + sections.push('}'); sections.push('```'); sections.push(''); - - sections.push('### Feature Property Matching'); - sections.push('```javascript'); - sections.push('filter: ['); - sections.push(' "match",'); - sections.push(' ["get", "class"],'); - sections.push(' ["motorway", "trunk"], true, // Match these values'); - sections.push(' false // Default'); - sections.push(']'); + sections.push('**Major roads:**'); + sections.push('```json'); + sections.push('{'); + sections.push(' "layer_type": "road",'); + sections.push( + ' "filter_properties": { "class": ["motorway", "trunk", "primary"] }' + ); + sections.push('}'); sections.push('```'); sections.push(''); - - sections.push('### Conditional Styling'); - sections.push('```javascript'); - sections.push('"fill-color": ['); - sections.push(' "case",'); - sections.push(' ["==", ["get", "type"], "hospital"], "#ff0000",'); - sections.push(' ["==", ["get", "type"], "school"], "#0000ff",'); - sections.push(' "#cccccc" // Default color'); - sections.push(']'); + sections.push('**Country boundaries:**'); + sections.push('```json'); + sections.push('{'); + sections.push(' "layer_type": "admin",'); + sections.push( + ' "filter_properties": { "admin_level": 0, "maritime": "false" }' + ); + sections.push('}'); + sections.push('```'); + sections.push(''); + sections.push('**3D Buildings:**'); + sections.push('```json'); + sections.push('{'); + sections.push(' "layer_type": "building",'); + sections.push(' "filter_properties": { "extrude": "true" }'); + sections.push('}'); sections.push('```'); sections.push(''); - // Tips - sections.push('## Tips for LLM Usage'); + // Available fields reference + sections.push('## Available Filter Fields'); sections.push(''); sections.push( - '1. **Layer Order Matters**: Layers are drawn in the order they appear (first = bottom)' + 'For detailed field values in each source layer, use the style_builder_tool.' ); sections.push( - '2. **Use Filters**: Filter by `class`, `type`, or other properties to target specific features' + 'The tool will provide specific guidance when a layer is not recognized.' ); + sections.push(''); + sections.push('### Key Fields by Layer:'); + sections.push(''); + sections.push('**landuse:** class, type'); + sections.push('**road:** class, type, structure, toll, oneway'); + sections.push('**admin:** admin_level, disputed, maritime'); + sections.push('**building:** type, height, min_height, extrude'); + sections.push('**water:** (no filter fields - all water features)'); + sections.push('**waterway:** class, type'); + sections.push('**place_label:** class, type, capital'); + sections.push('**poi_label:** maki, class, filterrank'); + sections.push('**transit_stop_label:** mode, stop_type, network'); + sections.push(''); + + sections.push('## Working with Styles'); + sections.push(''); + sections.push('### Using style_builder_tool'); + sections.push(''); sections.push( - '3. **Zoom Levels**: Use interpolation for smooth transitions across zoom levels' + 'The style_builder_tool is the primary way to create Mapbox styles. It:' ); sections.push( - '4. **Source Layers**: Most features come from `composite` source with specific `source-layer`' + '- Automatically determines the correct geometry type for each source layer' ); sections.push( - '5. **Color Formats**: Use hex colors (#rrggbb), rgb(), hsl(), or named colors' + '- Applies appropriate paint properties based on the action (color, highlight, hide, show)' ); + sections.push('- Generates proper filters from filter_properties'); sections.push( - '6. **Opacity**: Use opacity properties for transparency (0 = transparent, 1 = opaque)' + '- Provides helpful suggestions when layers are not recognized' ); sections.push(''); + sections.push('### Example Usage'); + sections.push(''); + sections.push('```'); + sections.push('style_builder_tool({'); + sections.push(' style_name: "Custom Style",'); + sections.push(' base_style: "standard",'); + sections.push(' layers: ['); + sections.push(' {'); + sections.push(' layer_type: "water",'); + sections.push(' action: "color",'); + sections.push(' color: "#0099ff"'); + sections.push(' },'); + sections.push(' {'); + sections.push(' layer_type: "landuse",'); + sections.push(' filter_properties: { class: "park" },'); + sections.push(' action: "color",'); + sections.push(' color: "#00ff00"'); + sections.push(' },'); + sections.push(' {'); + sections.push(' layer_type: "road",'); + sections.push(' filter_properties: { class: ["motorway", "trunk"] },'); + sections.push(' action: "highlight"'); + sections.push(' }'); + sections.push(' ]'); + sections.push('})'); + sections.push('```'); return sections.join('\n'); } } - -// Helper function to interpret user requests -export function interpretStyleRequest(userPrompt: string): { - suggestedLayers: string[]; - interpretation: string; -} { - const suggestions = getLayerSuggestions(userPrompt); - - let interpretation = 'Based on your request, you may want to modify: '; - - if (suggestions.length > 0) { - interpretation += suggestions - .map((s) => { - const layer = MAPBOX_STYLE_LAYERS[s]; - return `${s} (${layer?.description || 'unknown'})`; - }) - .join(', '); - } else { - interpretation += - 'No specific layers identified. Please provide more details.'; - } - - return { - suggestedLayers: suggestions, - interpretation - }; -} diff --git a/src/tools/style-builder-tool/StyleBuilderTool.schema.ts b/src/tools/style-builder-tool/StyleBuilderTool.schema.ts index 1428f93..8730516 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.schema.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.schema.ts @@ -6,6 +6,31 @@ const LayerConfigSchema = z.object({ .describe( 'Layer type from the resource (e.g., "water", "railways", "parks")' ), + + render_type: z + .enum([ + 'fill', + 'line', + 'symbol', + 'circle', + 'fill-extrusion', + 'heatmap', + 'auto' + ]) + .optional() + .default('auto') + .describe( + 'How to render this layer visually. Default "auto" chooses based on geometry type.\n' + + 'Override to achieve specific visual effects:\n' + + '• "line" - For outlines, borders, strokes (e.g., building outlines, road borders)\n' + + '• "fill" - For solid filled areas (e.g., solid color buildings, water bodies)\n' + + '• "fill-extrusion" - For 3D extrusions (e.g., 3D buildings)\n' + + '• "symbol" - For text labels or icons\n' + + '• "circle" - For dot visualization (e.g., POI dots, data points)\n' + + '• "heatmap" - For density maps (points only)\n' + + 'IMPORTANT: Use "line" for outlines even on polygon features like buildings.' + ), + action: z .enum(['show', 'hide', 'color', 'highlight']) .describe('What to do with this layer'), @@ -14,7 +39,10 @@ const LayerConfigSchema = z.object({ .optional() .describe('Color value if action is "color" or "highlight"'), opacity: z.number().min(0).max(1).optional().describe('Opacity value'), - width: z.number().optional().describe('Width for line layers'), + width: z + .number() + .optional() + .describe('Width for line layers or outline thickness'), filter: z .union([ z.string(), @@ -99,18 +127,18 @@ export const StyleBuilderToolSchema = z.object({ base_style: z .enum([ 'standard', - 'streets', - 'light', - 'dark', - 'satellite', - 'outdoors', - 'blank' + 'streets-v11', + 'light-v10', + 'dark-v10', + 'satellite-v9', + 'satellite-streets-v11', + 'outdoors-v11' ]) .default('standard') .describe( - 'Base style template. ALWAYS defaults to "standard" for new styles. ' + - 'Use "standard" for all new styles unless explicitly requested otherwise. ' + - 'Classic styles (streets/light/dark) should only be used when working with existing Classic styles or upon explicit request.' + 'Base style template. ALWAYS use "standard" as the default for all new styles. ' + + 'Standard style provides the best performance and modern features. ' + + 'Only use Classic styles (streets/light/dark/satellite/outdoors) when explicitly requested with "create a classic style" or when working with an existing Classic style.' ), layers: z diff --git a/src/tools/style-builder-tool/StyleBuilderTool.ts b/src/tools/style-builder-tool/StyleBuilderTool.ts index 65fadb3..0d6e8af 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.ts @@ -3,14 +3,14 @@ import { StyleBuilderToolSchema, type StyleBuilderToolInput } from './StyleBuilderTool.schema.js'; -import { MAPBOX_STYLE_LAYERS } from '../../constants/mapboxStyleLayers.js'; +// Using STREETS_V8_FIELDS as single source of truth instead of MAPBOX_STYLE_LAYERS import { STREETS_V8_FIELDS } from '../../constants/mapboxStreetsV8Fields.js'; import type { Layer, Filter, MapboxStyle } from '../../types/mapbox-style.js'; // Type for dynamically created layer definitions type DynamicLayerDefinition = { id: string; - type: 'fill' | 'line' | 'symbol' | 'circle' | 'fill-extrusion'; + type: 'fill' | 'line' | 'symbol' | 'circle' | 'fill-extrusion' | 'heatmap'; sourceLayer: string; description: string; paintProperties: Array<{ @@ -18,8 +18,12 @@ type DynamicLayerDefinition = { description: string; example: unknown; }>; + layoutProperties?: Array<{ + property: string; + description: string; + example: unknown; + }>; commonFilters: string[]; - examples: string[]; }; // Geometry types from Mapbox tilestats API for Streets v8 @@ -48,15 +52,16 @@ const SOURCE_LAYER_GEOMETRY: Record< export class StyleBuilderTool extends BaseTool { name = 'style_builder_tool'; + private currentSourceLayer?: string; // Track current source layer for better error messages description = `Generate Mapbox style JSON for creating new styles or updating existing ones. The tool intelligently resolves layer types and filter properties using Streets v8 data. You don't need exact layer names - the tool automatically finds the correct layer based on your filters. BASE STYLES: -• standard (DEFAULT): Modern Mapbox Standard with best performance -• streets/light/dark/satellite/outdoors: Classic styles -• blank: Empty canvas for full customization +• standard: ALWAYS THE DEFAULT - Modern Mapbox Standard with best performance +• Classic styles: streets-v11/light-v10/dark-v10/satellite-v9/outdoors-v11 + Only use Classic when user explicitly says "create a classic style" or working with existing Classic style STANDARD STYLE CONFIG: Use standard_config to customize the basemap: @@ -65,6 +70,25 @@ Use standard_config to customize the basemap: • Show/hide: labels, roads, 3D buildings • Colors: water, roads, parks, etc. +LAYER ORDERING: +• Layers are rendered in order - last layer in the array appears visually on top +• The 'slot' parameter is OPTIONAL - by default, layer order in the array determines visibility +• For Standard style, you can optionally use 'slot' to control placement: + - No slot (default): Above all existing layers in the style + - 'top': Behind Place and Transit labels + - 'middle': Between basemap and labels + - 'bottom': Below most basemap features + +LAYER RENDERING: +• render_type controls HOW to visualize the layer (line, fill, symbol, etc.) +• Most important: Use render_type:"line" for outlines/borders even on polygon features +• Default "auto" picks based on geometry, but override for specific effects: + - Building outlines → render_type:"line" (not fill!) + - Solid buildings → render_type:"fill" or "fill-extrusion" (3D) + - Road lines → render_type:"line" (auto works too) + - POI dots → render_type:"circle" + - Labels → render_type:"symbol" + LAYER ACTIONS: • color: Apply a specific color • highlight: Make prominent @@ -78,10 +102,18 @@ Examples: • { type: 'wetland' } → finds 'landuse_overlay' layer • { maki: 'restaurant' } → finds 'poi_label' layer • { toll: true } → finds 'road' layer +• { admin_level: 0 } → finds 'admin' layer (for country boundaries) +• { admin_level: 1 } → finds 'admin' layer (for state/province boundaries) -Invalid layer names are auto-corrected based on the filter properties you provide. +IMPORTANT LAYER NAMES: +• Use "admin" for all boundaries (countries, states, etc.) +• Use "building" (singular, not "buildings") +• Use "road" for all streets, highways, paths -For detailed documentation: resource://mapbox-style-layers`; +If a layer type is not recognized, the tool will provide helpful suggestions showing: +• All available source layers from Streets v8 +• Which fields are available in each layer +• Examples of how to properly specify layers and filters`; constructor() { super({ inputSchema: StyleBuilderToolSchema }); @@ -90,7 +122,20 @@ For detailed documentation: resource://mapbox-style-layers`; protected async execute(input: StyleBuilderToolInput) { try { const result = this.buildStyle(input); - const { style, corrections } = result; + const { style, corrections, layerHelp, availableProperties } = result; + + // If we need layer help, return guidance to the model + if (layerHelp) { + return { + content: [ + { + type: 'text' as const, + text: layerHelp + } + ], + isError: false // Return as guidance, not error + }; + } // Build corrections message if any const correctionsMessage = @@ -98,6 +143,21 @@ For detailed documentation: resource://mapbox-style-layers`; ? `\n**Auto-corrections Applied:**\n${corrections.join('\n')}\n` : ''; + // Build available properties message + let propertiesMessage = ''; + if (availableProperties && Object.keys(availableProperties).length > 0) { + propertiesMessage = '\n**Available Properties for Your Layers:**\n'; + for (const [layerType, props] of Object.entries(availableProperties)) { + propertiesMessage += `\n**${layerType} layers:**\n`; + if (props.paint && props.paint.length > 0) { + propertiesMessage += `- Paint: ${props.paint.slice(0, 8).join(', ')}${props.paint.length > 8 ? '...' : ''}\n`; + } + if (props.layout && props.layout.length > 0) { + propertiesMessage += `- Layout: ${props.layout.slice(0, 8).join(', ')}${props.layout.length > 8 ? '...' : ''}\n`; + } + } + } + return { content: [ { @@ -105,10 +165,11 @@ For detailed documentation: resource://mapbox-style-layers`; text: `**Style Built Successfully** **Name:** ${input.style_name} -**Base:** ${input.base_style} +**Base:** ${input.base_style || 'standard'} **Layers Configured:** ${input.layers.length} ${input.standard_config ? `**Standard Config:** ${Object.keys(input.standard_config).length} properties set` : ''} ${correctionsMessage} +${propertiesMessage} ${this.generateSummary(input)} **Generated Style JSON:** @@ -140,10 +201,18 @@ ${JSON.stringify(style, null, 2)} private buildStyle(input: StyleBuilderToolInput): { style: MapboxStyle; corrections: string[]; + layerHelp?: string; + availableProperties?: Record; } { const layers: Layer[] = []; const allCorrections: string[] = []; - const isUsingStandard = input.base_style === 'standard'; + const availableProperties: Record< + string, + { paint: string[]; layout: string[] } + > = {}; + // Apply default base_style if not specified + const baseStyle = input.base_style || 'standard'; + const isUsingStandard = baseStyle === 'standard'; // Only add background layer for non-Standard styles // Standard style provides its own background through imports @@ -167,44 +236,41 @@ ${JSON.stringify(style, null, 2)} for (const config of input.layers) { if (config.action === 'hide') continue; - // Try to get layer from MAPBOX_STYLE_LAYERS first - let layerDef: - | (typeof MAPBOX_STYLE_LAYERS)[keyof typeof MAPBOX_STYLE_LAYERS] - | DynamicLayerDefinition - | null = MAPBOX_STYLE_LAYERS[config.layer_type]; + // Determine the source layer for this config + let sourceLayer = config.layer_type; + let layerDef: DynamicLayerDefinition | null = null; - // If not found, try to create a dynamic layer definition - if (!layerDef) { - layerDef = this.createDynamicLayerDefinition(config.layer_type); - if (!layerDef) { - // Try to find the correct layer based on filter properties - const correctLayer = this.findCorrectLayerForFilters(config); - if (correctLayer) { - allCorrections.push( - `• Layer type "${config.layer_type}" not found. Using "${correctLayer}" instead (contains the filtered fields).` - ); - // Update the config with the correct layer type - config.layer_type = correctLayer; - // Try again with the correct layer - layerDef = - MAPBOX_STYLE_LAYERS[correctLayer] || - this.createDynamicLayerDefinition(correctLayer); - } else { - // If no filter properties or can't find a match, skip with warning - console.warn(`Unknown layer type: "${config.layer_type}"`); - if ( - config.filter_properties && - Object.keys(config.filter_properties).length > 0 - ) { - // Only provide suggestions if they had filter properties - const availableLayers = this.getAvailableLayersInfo(config); - console.warn(availableLayers); - } - continue; - } + // Check if layer_type is a valid source layer + if (sourceLayer in STREETS_V8_FIELDS) { + layerDef = this.createDynamicLayerDefinition(sourceLayer, config); + } else if ( + config.filter_properties && + Object.keys(config.filter_properties).length > 0 + ) { + // Try to find the correct source layer based on filter properties + const bestMatch = this.findSourceLayerByFilterProperties( + config.filter_properties + ); + if (bestMatch) { + sourceLayer = bestMatch; + allCorrections.push( + `• Determined source layer "${sourceLayer}" from filter properties (original: "${config.layer_type}")` + ); + layerDef = this.createDynamicLayerDefinition(sourceLayer, config); } } + // If still no match, return helpful information + if (!layerDef) { + const helpMessage = this.generateLayerHelp(config); + return { + style: {} as MapboxStyle, + corrections: [], + layerHelp: helpMessage, + availableProperties: {} + }; + } + const result = this.createLayer( layerDef, config, @@ -213,8 +279,37 @@ ${JSON.stringify(style, null, 2)} ); if (result.layer) { layers.push(result.layer); + + // Collect available properties for this layer type + if (layerDef.type && !availableProperties[layerDef.type]) { + availableProperties[layerDef.type] = { + paint: layerDef.paintProperties + .filter((p) => p.example !== undefined) + .map((p) => p.property), + layout: layerDef.layoutProperties + ? layerDef.layoutProperties + .filter((p) => p.example !== undefined) + .map((p) => p.property) + : [] + }; + } } if (result.corrections.length > 0) { + // Check for critical errors that need immediate attention + const criticalError = result.corrections.find((c) => + c.startsWith('ERROR:') + ); + if (criticalError) { + // Return helpful guidance for the model to retry with correct field + return { + style: {} as MapboxStyle, + corrections: [], + layerHelp: + criticalError + + '\n\n**Please retry with the corrected filter_properties.**', + availableProperties: {} + }; + } allCorrections.push(...result.corrections); } } @@ -230,16 +325,16 @@ ${JSON.stringify(style, null, 2)} } as MapboxStyle; // Determine which base style to use - const isClassicStyle = [ - 'streets', - 'light', - 'dark', - 'satellite', - 'outdoors' - ].includes(input.base_style); + // const isClassicStyle = [ + // 'streets', + // 'light', + // 'dark', + // 'satellite', + // 'outdoors' + // ].includes(baseStyle); // For standard style, use imports to inherit from Mapbox Standard - if (input.base_style === 'standard') { + if (baseStyle === 'standard') { // Follow the exact order from the working Mapbox Studio example style.metadata = { 'mapbox:autocomposite': true, @@ -283,21 +378,8 @@ ${JSON.stringify(style, null, 2)} // Explicitly set terrain to null for API compatibility // @ts-expect-error - The API expects null but TypeScript type doesn't allow it style.terrain = null; - } else if (isClassicStyle) { - // For classic styles (being deprecated), use traditional sources - style.center = [0, 0]; - style.zoom = 2; - style.sources = { - composite: { - type: 'vector', - url: 'mapbox://mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2' - } - }; - style.sprite = 'mapbox://sprites/mapbox/streets-v12'; - style.glyphs = 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf'; - style.layers = layers; } else { - // Blank style or no base style specified - no imports, just basic sources + // Classic styles - use traditional sources style.center = [0, 0]; style.zoom = 2; style.sources = { @@ -311,13 +393,170 @@ ${JSON.stringify(style, null, 2)} style.layers = layers; } - return { style, corrections: allCorrections }; + return { style, corrections: allCorrections, availableProperties }; + } + + private findSourceLayerByFilterProperties( + filterProperties: Record + ): string | null { + let bestMatch: { layer: string; score: number } | null = null; + + for (const [sourceLayer, fields] of Object.entries(STREETS_V8_FIELDS)) { + let score = 0; + const layerFields = fields as any; + + for (const [filterKey, filterValue] of Object.entries(filterProperties)) { + // Check if this field exists in this source layer + if (filterKey in layerFields) { + score += 10; + + // Check if the value is valid for this field + if (layerFields[filterKey].values) { + const validValues = layerFields[filterKey].values; + const valuesToCheck = Array.isArray(filterValue) + ? filterValue + : [filterValue]; + + for (const val of valuesToCheck) { + const normalizedVal = String(val).toLowerCase(); + if ( + validValues.some( + (v: any) => String(v).toLowerCase() === normalizedVal + ) + ) { + score += 20; // High score for exact match + } + } + } + } + } + + if (score > 0 && (!bestMatch || score > bestMatch.score)) { + bestMatch = { layer: sourceLayer, score }; + } + } + + return bestMatch?.layer || null; + } + + private generateLayerHelp( + config: StyleBuilderToolInput['layers'][0] + ): string { + // Generate all possible layer/filter combinations from STREETS_V8_FIELDS + const combinations: string[] = []; + + for (const [sourceLayer, fields] of Object.entries(STREETS_V8_FIELDS)) { + const layerFields = fields as any; + const fieldExamples: string[] = []; + + // Get up to 3 example fields with their values + let fieldCount = 0; + for (const [fieldName, fieldDef] of Object.entries(layerFields)) { + if (fieldCount >= 3) break; + if ( + fieldDef && + typeof fieldDef === 'object' && + 'values' in fieldDef && + fieldDef.values + ) { + const values = (fieldDef.values as any[]) + .slice(0, 3) + .map((v) => `"${v}"`) + .join(', '); + fieldExamples.push(`${fieldName}: ${values}`); + fieldCount++; + } + } + + if (fieldExamples.length > 0) { + combinations.push(`**${sourceLayer}**: ${fieldExamples.join(' | ')}`); + } + } + + // Create helpful message for the model + let helpText = `**Layer "${config.layer_type}" not found.**\n\n`; + + helpText += `**IMPORTANT:** Keep the same base_style and other settings, just correct the layer_type.\n\n`; + + helpText += `**Available source layers you can use:**\n`; + + // List all available source layers with helpful clarifications + const allLayers = Object.keys(SOURCE_LAYER_GEOMETRY); + const layerDescriptions: Record = { + admin: 'admin (administrative boundaries - countries, states, etc.)', + building: 'building (building footprints)', + landuse: 'landuse (parks, residential, industrial areas)', + landuse_overlay: 'landuse_overlay (wetlands, national parks)', + road: 'road (all roads, streets, paths, railways)', + water: 'water (oceans, lakes, rivers as polygons)', + waterway: 'waterway (rivers, streams as lines)', + place_label: 'place_label (city, state, country labels)', + poi_label: 'poi_label (points of interest)', + transit_stop_label: 'transit_stop_label (bus, train stops)', + natural_label: 'natural_label (natural feature labels)', + motorway_junction: 'motorway_junction (highway exits)', + housenum_label: 'housenum_label (house numbers)', + airport_label: 'airport_label (airport labels)', + aeroway: 'aeroway (runways, taxiways)', + structure: 'structure (bridges, tunnels, fences)' + }; + + helpText += allLayers + .map((layer) => `• ${layerDescriptions[layer] || layer}`) + .join('\n'); + helpText += '\n\n'; + + // Add common confusion clarifications + helpText += `**Note:** Looking for boundaries? Use "admin" with filter_properties like {admin_level: 0} for countries.\n\n`; + + if ( + config.filter_properties && + Object.keys(config.filter_properties).length > 0 + ) { + helpText += `You specified filter_properties: ${JSON.stringify(config.filter_properties)}\n\n`; + + // Check which layers have these fields + const matchingLayers: string[] = []; + for (const [filterKey] of Object.entries(config.filter_properties)) { + for (const [sourceLayer, fields] of Object.entries(STREETS_V8_FIELDS)) { + if (filterKey in (fields as any)) { + matchingLayers.push(`${sourceLayer} (has field: ${filterKey})`); + } + } + } + + if (matchingLayers.length > 0) { + helpText += `**Layers with your filter fields:**\n${matchingLayers.map((l) => `• ${l}`).join('\n')}\n\n`; + } + } + + helpText += `**Try again with the correct layer_type from the list above.**\n\n`; + + helpText += `**Example for parks:** +\`\`\`json +{ + "layer_type": "landuse", + "filter_properties": { "class": "park" }, + "action": "color", + "color": "#90C090" +} +\`\`\` + +**Example for only cemeteries:** +\`\`\`json +{ + "layer_type": "landuse", + "filter_properties": { "class": "cemetery" }, + "action": "color", + "color": "#D0D0D0" +} +\`\`\``; + + return helpText; } private createLayer( - layerDef: - | (typeof MAPBOX_STYLE_LAYERS)[keyof typeof MAPBOX_STYLE_LAYERS] - | NonNullable>, + layerDef: DynamicLayerDefinition, config: StyleBuilderToolInput['layers'][0], globalSettings?: StyleBuilderToolInput['global_settings'], isUsingStandard?: boolean @@ -339,34 +578,18 @@ ${JSON.stringify(style, null, 2)} type: layerDef.type as Layer['type'] }; - // Add slot for Standard style - if (isUsingStandard) { - // Smart slot assignment if not explicitly provided: - // - Layers with filter_properties go to 'top' for visibility - // - Regular roads go to 'middle' - // - Labels go to 'top' - - if (config.slot) { - // User explicitly set the slot - respect their choice - layer.slot = config.slot; - } else if ( - config.filter_properties && - Object.keys(config.filter_properties).length > 0 - ) { - // ANY layer with filter properties should be on top for visibility - // This includes: toll roads, bridges, tunnels, bike lanes, restricted roads, etc. - layer.slot = 'top'; - } else if (config.layer_type.includes('label')) { - // Labels should always be on top - layer.slot = 'top'; - } else if (this.isRoadLayer(config.layer_type)) { - // Regular roads without filters in the middle - layer.slot = 'middle'; - } else { - // Default for other layers - layer.slot = 'middle'; - } + // Add slot for Standard style if explicitly provided + if (isUsingStandard && config.slot) { + // User explicitly set the slot - respect their choice + // Available slots: + // - no slot (undefined): Above all existing layers in the style + // - 'top': Behind Place and Transit labels + // - 'middle': Between basemap and labels + // - 'bottom': Below most basemap features + layer.slot = config.slot; } + // Note: If no slot is specified, the layer will appear above all existing layers + // Layers are rendered in order - last layer in the array appears visually on top // Add source configuration if (layerDef.sourceLayer) { @@ -654,6 +877,7 @@ ${JSON.stringify(style, null, 2)} if ( 'layoutProperties' in layerDef && layerDef.layoutProperties && + Array.isArray(layerDef.layoutProperties) && layerDef.layoutProperties.length > 0 ) { const layout: Record = {}; @@ -691,7 +915,10 @@ ${JSON.stringify(style, null, 2)} layout['text-font'] = ['DIN Pro Regular', 'Arial Unicode MS Regular']; layout['text-size'] = 12; layout['text-rotation-alignment'] = 'map'; - } else if ('layoutProperties' in layerDef && layerDef.layoutProperties) { + } else if ( + 'layoutProperties' in layerDef && + Array.isArray(layerDef.layoutProperties) + ) { // Default layout from definition for (const prop of layerDef.layoutProperties) { if (prop.example !== undefined) { @@ -708,73 +935,6 @@ ${JSON.stringify(style, null, 2)} return { layer, corrections: filterResult.corrections }; } - private parseFilterString(filterStr: string): unknown | null { - // Parse filter strings like "class: motorway|trunk" or "admin_level: 0, maritime: false" - const filters: unknown[] = []; - - // Check if this contains multiple properties (multiple colons not within quotes) - const colonMatches = filterStr.match(/:/g) || []; - - if (colonMatches.length === 1) { - // Single property, possibly with multiple values separated by | - const [property, values] = filterStr.split(':').map((s) => s.trim()); - const valueList = values.split('|').map((v) => { - const trimmed = v.trim(); - // Special handling for admin layer properties that use string booleans - // maritime and disputed use "true"/"false" as strings, not booleans - if ( - property === 'maritime' || - property === 'disputed' || - property === 'toll' - ) { - return trimmed; // Keep as string "true" or "false" - } - // Handle boolean strings for other properties - if (trimmed === 'true') return true; - if (trimmed === 'false') return false; - // Try to parse as number - const num = Number(trimmed); - return isNaN(num) ? trimmed : num; - }); - - // Always use match format for consistency with Mapbox Studio - filters.push(['match', ['get', property], valueList, true, false]); - } else { - // Multiple properties separated by comma - const conditions = filterStr.split(',').map((s) => s.trim()); - - for (const condition of conditions) { - if (condition.includes(':')) { - const [property, values] = condition.split(':').map((s) => s.trim()); - const valueList = values.split('|').map((v) => { - const trimmed = v.trim(); - // Special handling for properties that use string booleans - if ( - property === 'maritime' || - property === 'disputed' || - property === 'toll' - ) { - return trimmed; // Keep as string - } - // Handle boolean strings for other properties - if (trimmed === 'true') return true; - if (trimmed === 'false') return false; - // Try to parse as number - const num = Number(trimmed); - return isNaN(num) ? trimmed : num; - }); - - // Always use match format for consistency with Mapbox Studio - filters.push(['match', ['get', property], valueList, true, false]); - } - } - } - - if (filters.length === 0) return null; - if (filters.length === 1) return filters[0]; - return ['all', ...filters]; - } - private getColorProperty(layerType: string): string | null { const colorProps: Record = { fill: 'fill-color', @@ -805,9 +965,10 @@ ${JSON.stringify(style, null, 2)} const parts: string[] = ['**Layer Configurations:**']; for (const config of input.layers) { - const layerDef = - MAPBOX_STYLE_LAYERS[config.layer_type] || - this.createDynamicLayerDefinition(config.layer_type); + const layerDef = this.createDynamicLayerDefinition( + config.layer_type, + config + ); const description = layerDef?.description || config.layer_type; switch (config.action) { @@ -982,43 +1143,6 @@ ${JSON.stringify(style, null, 2)} return value; } - private generateDataDrivenExpression( - property: string, - valueMap: Record, - defaultValue: unknown - ): unknown { - const expression: unknown[] = ['match', ['get', property]]; - - for (const [key, value] of Object.entries(valueMap)) { - expression.push(key); - expression.push(value); - } - - expression.push(defaultValue); - return expression; - } - - private generateZoomInterpolation( - minZoom: number, - maxZoom: number, - minValue: number, - maxValue: number, - interpolationType: 'linear' | 'exponential' = 'linear' - ): unknown { - const interpolation = - interpolationType === 'exponential' ? ['exponential', 1.5] : ['linear']; - - return [ - 'interpolate', - interpolation, - ['zoom'], - minZoom, - minValue, - maxZoom, - maxValue - ]; - } - /** * Calculate similarity between two strings (simple Levenshtein-like score) */ @@ -1051,7 +1175,8 @@ ${JSON.stringify(style, null, 2)} private findClosestFieldValue( fieldName: string, inputValue: string | number | boolean, - validValues: readonly any[] + validValues: readonly any[], + sourceLayer?: string ): { value: any; corrected: boolean; message?: string } { // For non-string values, just check if it's valid if (typeof inputValue !== 'string') { @@ -1139,7 +1264,44 @@ ${JSON.stringify(style, null, 2)} } } - // 4. No good match found - return original with error message + // 4. No good match found - check if this value exists in other fields + // This helps when user specifies class:"golf_course" but it should be type:"golf_course" + if (sourceLayer) { + const layerFields = STREETS_V8_FIELDS[ + sourceLayer as keyof typeof STREETS_V8_FIELDS + ] as any; + if (layerFields) { + // Check all other fields to see if this value exists there + for (const [otherFieldName, otherFieldDef] of Object.entries( + layerFields + )) { + if (otherFieldName === fieldName) continue; // Skip the current field + if (!otherFieldDef || typeof otherFieldDef !== 'object') continue; + if ( + !('values' in otherFieldDef) || + !Array.isArray((otherFieldDef as any).values) + ) + continue; + + const otherValues = (otherFieldDef as any).values; + const exactMatch = otherValues.find( + (v: any) => + typeof v === 'string' && + v.toLowerCase() === inputValue.toLowerCase() + ); + + if (exactMatch) { + return { + value: inputValue, + corrected: false, + message: `ERROR: "${inputValue}" is not a valid ${fieldName} value. Did you mean ${otherFieldName}:"${exactMatch}"? Use filter_properties: {${otherFieldName}: "${exactMatch}"} instead.` + }; + } + } + } + } + + // 5. Really no match anywhere - return original with error message const suggestions = validValues.slice(0, 10).join(', '); return { value: inputValue, @@ -1176,7 +1338,8 @@ ${JSON.stringify(style, null, 2)} const result = this.findClosestFieldValue( property, value, - fieldDef.values + fieldDef.values, + sourceLayer ); return { resolvedProperty: property, @@ -1275,6 +1438,9 @@ ${JSON.stringify(style, null, 2)} const filters: unknown[] = []; const corrections: string[] = []; + // Set current source layer for better error messages + this.currentSourceLayer = sourceLayer; + // Get field definitions for this source layer const layerFields = STREETS_V8_FIELDS[sourceLayer as keyof typeof STREETS_V8_FIELDS]; @@ -1392,9 +1558,16 @@ ${JSON.stringify(style, null, 2)} const result = this.findClosestFieldValue( property, val, - validValues + validValues, + sourceLayer ); if (result.message) { + // If it's a critical error (wrong field), we should stop and guide the model + if (result.message.startsWith('ERROR:')) { + corrections.push(result.message); + // Don't continue with invalid filter - return early + return { filter: null, corrections: [result.message] }; + } corrections.push(` ${property}: ${result.message}`); } correctedValues.push(result.value); @@ -1405,9 +1578,16 @@ ${JSON.stringify(style, null, 2)} const result = this.findClosestFieldValue( property, processedValue, - validValues + validValues, + sourceLayer ); if (result.message) { + // If it's a critical error (wrong field), we should stop and guide the model + if (result.message.startsWith('ERROR:')) { + corrections.push(result.message); + // Don't continue with invalid filter - return early + return { filter: null, corrections: [result.message] }; + } corrections.push(` ${property}: ${result.message}`); } processedValue = result.value; @@ -1443,10 +1623,7 @@ ${JSON.stringify(style, null, 2)} private generateComprehensiveFilter( config: StyleBuilderToolInput['layers'][0], - layerDef: - | (typeof MAPBOX_STYLE_LAYERS)[keyof typeof MAPBOX_STYLE_LAYERS] - | DynamicLayerDefinition - | null + layerDef: DynamicLayerDefinition | null ): { filter: Filter | null; corrections: string[] } { // If custom filter is provided, process it through buildAdvancedFilter if ( @@ -1472,22 +1649,7 @@ ${JSON.stringify(style, null, 2)} const filters: Filter[] = []; const allCorrections: string[] = []; - // First, add common filters from layer definition - if ( - layerDef && - layerDef.commonFilters && - layerDef.commonFilters.length > 0 - ) { - // Handle multiple filter conditions - join with comma for multiple properties - // Each element can be either a single property or a property with pipe-separated values - const filterStr = layerDef.commonFilters.join(', '); - const commonFilter = this.parseFilterString(filterStr); - if (commonFilter) { - filters.push(commonFilter as Filter); - } - } - - // Then, add filter_properties if provided + // Add filter_properties if provided if ( config.filter_properties && layerDef && @@ -1533,7 +1695,7 @@ ${JSON.stringify(style, null, 2)} layerType: string, isHighlight: boolean = false ): unknown | null { - // Reasonable default line widths with zoom interpolation (25% thicker) + // Reasonable default line widths with zoom interpolation const roadWidths: Record = { roads: [ 'interpolate', @@ -1635,37 +1797,36 @@ ${JSON.stringify(style, null, 2)} 20, 4.0 ], - - // Administrative boundaries - thinner with zoom interpolation + // Administrative boundaries - thinner country_boundaries: [ 'interpolate', ['linear'], ['zoom'], 0, - 0.5, // Very thin at world view + 0.5, 4, - 0.8, // Slightly thicker at country level + 0.8, 8, - 1.2, // Moderate at region level + 1.2, 12, - 1.5, // Slightly thicker at city level + 1.5, 16, - 1.8 // Maximum thickness at street level + 1.8 ], state_boundaries: [ 'interpolate', ['linear'], ['zoom'], 2, - 0.3, // Almost invisible at world view + 0.3, 6, - 0.6, // Thin at country level + 0.6, 10, - 1.0, // Moderate at region level + 1.0, 14, - 1.3, // Slightly thicker at city level + 1.3, 18, - 1.5 // Maximum thickness at street level + 1.5 ] }; @@ -1686,134 +1847,63 @@ ${JSON.stringify(style, null, 2)} } private getDefaultOpacity(layerType: string, layerDefType: string): number { - // Sophisticated opacity values for different layer types + // Symbol layers should always be fully opaque for readability + if (layerDefType === 'symbol') { + return 1.0; + } + + // Layer-specific opacity for better visual hierarchy const opacityMap: Record = { - // Water features - slightly transparent for depth water: 0.85, waterway: 0.75, - - // Natural features - soft and subtle parks: 0.65, landuse: 0.45, - - // Roads - varied opacity for navigation clarity - motorways: 0.85, // High visibility for navigation - primary_roads: 0.75, // Clear but not dominant - secondary_roads: 0.65, // Visible but subdued - streets: 0.55, // Background context - paths: 0.45, // Subtle - railways: 0.7, // Clear but distinct - roads: 0.6, // General roads - - // Buildings - subtle presence + motorways: 0.85, + primary_roads: 0.75, + secondary_roads: 0.65, + streets: 0.55, + paths: 0.45, + railways: 0.7, + roads: 0.6, buildings: 0.6, building_3d: 0.7, - - // Administrative - very subtle, fading at higher zooms - country_boundaries: 0.5, // More subtle base opacity - state_boundaries: 0.4, // Even more subtle for states - - // Infrastructure + country_boundaries: 0.5, + state_boundaries: 0.4, airports: 0.7, transit: 0.75, - - // Labels should be fully opaque for readability place_labels: 1.0, road_labels: 1.0, poi_labels: 1.0 }; - // Symbol layers should always be fully opaque - if (layerDefType === 'symbol') { - return 1.0; - } - return opacityMap[layerType] || 0.7; } - private createDynamicLayerDefinition(layerType: string) { - // Check if this layer type exists as a source-layer - // No conversion needed - source-layer names already use underscores - const sourceLayer = layerType; - - // Check if this source-layer exists in STREETS_V8_FIELDS or our geometry mapping - const hasInStreetsV8 = sourceLayer in STREETS_V8_FIELDS; - const hasInGeometry = sourceLayer in SOURCE_LAYER_GEOMETRY; - - if (!hasInStreetsV8 && !hasInGeometry) { - return null; - } - - // Get geometry type from our hardcoded mapping - const geometry = SOURCE_LAYER_GEOMETRY[sourceLayer]; - if (!geometry) { - // Source-layer exists in STREETS_V8_FIELDS but not in our geometry mapping - // This shouldn't happen with Streets v8, but let's handle it gracefully - console.warn(`No geometry type found for source-layer: ${sourceLayer}`); - return null; - } - - // Determine layer type based on geometry - let type: 'fill' | 'line' | 'symbol' | 'circle' | 'fill-extrusion'; - let paintProperties: Array<{ - property: string; - description: string; - example: any; - }> = []; - - switch (geometry) { - case 'Polygon': - // Special case for buildings with 3D - if (sourceLayer === 'building' && layerType.includes('3d')) { - type = 'fill-extrusion'; - paintProperties = [ - { - property: 'fill-extrusion-color', - description: 'Building color', - example: '#AAAAAA' - }, - { - property: 'fill-extrusion-height', - description: 'Building height', - example: ['get', 'height'] - }, - { - property: 'fill-extrusion-base', - description: 'Building base height', - example: ['get', 'min_height'] - }, - { - property: 'fill-extrusion-opacity', - description: 'Building opacity', - example: 0.8 - } - ]; - } else { - type = 'fill'; - paintProperties = [ - { - property: 'fill-color', - description: 'Fill color', - example: '#000000' - }, - { - property: 'fill-opacity', - description: 'Fill opacity', - example: 0.5 - }, - { - property: 'fill-outline-color', - description: 'Outline color', - example: '#000000' - } - ]; - } - break; - - case 'LineString': - // Admin boundaries and natural features are often rendered as lines - type = 'line'; - paintProperties = [ + private getLayerTypeProperties( + layerType: + | 'fill' + | 'line' + | 'symbol' + | 'circle' + | 'fill-extrusion' + | 'heatmap' + ) { + const properties: { + paintProperties: Array<{ + property: string; + description: string; + example: any; + }>; + layoutProperties?: Array<{ + property: string; + description: string; + example: any; + }>; + } = { paintProperties: [] }; + + switch (layerType) { + case 'line': + properties.paintProperties = [ { property: 'line-color', description: 'Line color', @@ -1824,63 +1914,17 @@ ${JSON.stringify(style, null, 2)} property: 'line-opacity', description: 'Line opacity', example: 0.8 - } + }, + { + property: 'line-dasharray', + description: 'Dash pattern', + example: [2, 2] + }, + { property: 'line-gap-width', description: 'Gap width', example: 0 } ]; break; - - case 'Point': - // Points can be either circle or symbol layers - // Labels and text-based layers should be symbols - if ( - sourceLayer.includes('label') || - sourceLayer === 'motorway_junction' - ) { - type = 'symbol'; - paintProperties = [ - { - property: 'text-color', - description: 'Text color', - example: '#000000' - }, - { - property: 'text-halo-color', - description: 'Text halo color', - example: '#FFFFFF' - }, - { - property: 'text-halo-width', - description: 'Text halo width', - example: 1 - } - ]; - } else { - // Default to circle for point features without labels - type = 'circle'; - paintProperties = [ - { - property: 'circle-color', - description: 'Circle color', - example: '#000000' - }, - { - property: 'circle-radius', - description: 'Circle radius', - example: 5 - }, - { - property: 'circle-opacity', - description: 'Circle opacity', - example: 0.8 - } - ]; - } - break; - - default: - // This shouldn't happen with our hardcoded geometry mapping - console.warn(`Unknown geometry type: ${geometry}`); - type = 'fill'; - paintProperties = [ + case 'fill': + properties.paintProperties = [ { property: 'fill-color', description: 'Fill color', @@ -1890,292 +1934,291 @@ ${JSON.stringify(style, null, 2)} property: 'fill-opacity', description: 'Fill opacity', example: 0.5 + }, + { + property: 'fill-outline-color', + description: 'Outline color', + example: '#000000' + } + ]; + break; + case 'fill-extrusion': + properties.paintProperties = [ + { + property: 'fill-extrusion-color', + description: 'Extrusion color', + example: '#AAAAAA' + }, + { + property: 'fill-extrusion-height', + description: 'Extrusion height', + example: ['get', 'height'] + }, + { + property: 'fill-extrusion-base', + description: 'Extrusion base', + example: ['get', 'min_height'] + }, + { + property: 'fill-extrusion-opacity', + description: 'Extrusion opacity', + example: 0.8 + } + ]; + break; + case 'circle': + properties.paintProperties = [ + { + property: 'circle-radius', + description: 'Circle radius', + example: 5 + }, + { + property: 'circle-color', + description: 'Circle color', + example: '#007cbf' + }, + { + property: 'circle-opacity', + description: 'Circle opacity', + example: 0.8 + }, + { + property: 'circle-stroke-color', + description: 'Circle stroke color', + example: '#000000' + }, + { + property: 'circle-stroke-width', + description: 'Circle stroke width', + example: 1 } ]; + break; + case 'symbol': + properties.paintProperties = [ + { + property: 'text-color', + description: 'Text color', + example: '#000000' + }, + { + property: 'text-halo-color', + description: 'Text halo color', + example: '#FFFFFF' + }, + { + property: 'text-halo-width', + description: 'Text halo width', + example: 1 + }, + { property: 'icon-opacity', description: 'Icon opacity', example: 1 } + ]; + properties.layoutProperties = [ + { + property: 'text-field', + description: 'Text content', + example: ['get', 'name'] + }, + { + property: 'text-font', + description: 'Font stack', + example: ['DIN Pro Medium', 'Arial Unicode MS Regular'] + }, + { property: 'text-size', description: 'Text size', example: 14 }, + { + property: 'icon-image', + description: 'Icon sprite name', + example: 'marker-15' + } + ]; + break; + case 'heatmap': + properties.paintProperties = [ + { + property: 'heatmap-weight', + description: 'Point weight', + example: 1 + }, + { + property: 'heatmap-intensity', + description: 'Intensity', + example: 1 + }, + { + property: 'heatmap-radius', + description: 'Influence radius', + example: 30 + }, + { + property: 'heatmap-opacity', + description: 'Layer opacity', + example: 0.7 + } + ]; + break; } - return { - id: sourceLayer, // Use source-layer name as the id - type: type as any, - sourceLayer: sourceLayer, - description: `${sourceLayer} layer (${geometry} geometry)`, - paintProperties, - commonFilters: [], - examples: [] - }; + return properties; } - private findCorrectLayerForFilters( - config: StyleBuilderToolInput['layers'][0] - ): string | null { - // If no filter properties, we can't determine the correct layer - if (!config.filter_properties) { - return null; - } - - // Score each source layer based on how well it matches the filter properties - const layerScores: Record = {}; - const filterEntries = Object.entries(config.filter_properties); - - for (const [sourceLayer, fields] of Object.entries(STREETS_V8_FIELDS)) { - let score = 0; - const layerFields = fields as Record< - string, - { values?: readonly string[]; description?: string } - >; - - for (const [fieldName, fieldValue] of filterEntries) { - // Check if this field exists in this layer - if (fieldName in layerFields) { - score += 10; // Base score for having the field - - const fieldDef = layerFields[fieldName]; - if (fieldDef.values && Array.isArray(fieldDef.values)) { - // Check if the value exists in this field's allowed values - const valuesToCheck = Array.isArray(fieldValue) - ? fieldValue - : [fieldValue]; - - for (const val of valuesToCheck) { - const normalizedVal = String(val) - .toLowerCase() - .replace(/[_-\s]/g, ''); - - // Exact match (after normalization) - const exactMatch = fieldDef.values.some( - (v) => - String(v) - .toLowerCase() - .replace(/[_-\s]/g, '') === normalizedVal - ); - - if (exactMatch) { - score += 20; // High score for exact value match - } else { - // Partial match - const partialMatch = fieldDef.values.some( - (v) => - String(v) - .toLowerCase() - .replace(/[_-\s]/g, '') - .includes(normalizedVal) || - normalizedVal.includes( - String(v) - .toLowerCase() - .replace(/[_-\s]/g, '') - ) - ); - - if (partialMatch) { - score += 5; // Lower score for partial match - } - } - } - } else { - // Field exists but has no predefined values (like name fields) - // Still counts but with lower score - score += 5; - } - } - } - - if (score > 0) { - layerScores[sourceLayer] = score; - } - } + private createDynamicLayerDefinition( + layerType: string, + config?: StyleBuilderToolInput['layers'][0] + ) { + // Check if this layer type exists as a source-layer + // No conversion needed - source-layer names already use underscores + const sourceLayer = layerType; - // Find the layer with the highest score - let bestLayer: string | null = null; - let bestScore = 0; + // Check if this source-layer exists in STREETS_V8_FIELDS or our geometry mapping + const hasInStreetsV8 = sourceLayer in STREETS_V8_FIELDS; + const hasInGeometry = sourceLayer in SOURCE_LAYER_GEOMETRY; - for (const [layer, score] of Object.entries(layerScores)) { - if (score > bestScore) { - bestScore = score; - bestLayer = layer; - } + if (!hasInStreetsV8 && !hasInGeometry) { + return null; } - // If we found a match and it's different from what was requested, return it - if (bestLayer && bestLayer !== config.layer_type) { - return bestLayer; + // Get geometry type from our hardcoded mapping + const geometry = SOURCE_LAYER_GEOMETRY[sourceLayer]; + if (!geometry) { + // Source-layer exists in STREETS_V8_FIELDS but not in our geometry mapping + return null; } - return null; - } - - private getAvailableLayersInfo( - config: StyleBuilderToolInput['layers'][0] - ): string { - const messages: string[] = []; - - // First, list the valid layer types from MAPBOX_STYLE_LAYERS - const validPreDefinedLayers = Object.keys(MAPBOX_STYLE_LAYERS); - messages.push( - `Valid pre-defined layer types: ${validPreDefinedLayers.join(', ')}` - ); - - // List valid source layers from STREETS_V8_FIELDS - const validSourceLayers = Object.keys(STREETS_V8_FIELDS); - messages.push( - `Valid Streets v8 source layers: ${validSourceLayers.join(', ')}` - ); - - // If they specified filter properties, suggest layers that contain those fields - if (config.filter_properties) { - const suggestions: string[] = []; - - for (const [fieldName, fieldValue] of Object.entries( - config.filter_properties - )) { - // Find which layers contain this field - const layersWithField: string[] = []; - - for (const [sourceLayer, fields] of Object.entries(STREETS_V8_FIELDS)) { - const layerFields = fields as Record< - string, - { values?: readonly string[]; description?: string } - >; - if (fieldName in layerFields) { - const fieldDef = layerFields[fieldName]; - if (fieldDef.values && fieldDef.values.length > 0) { - // Show some example values for this field in this layer - const exampleValues = fieldDef.values.slice(0, 5).join(', '); - const moreValues = - fieldDef.values.length > 5 - ? `, ... (${fieldDef.values.length} total)` - : ''; - layersWithField.push( - `${sourceLayer} (${fieldName}: ${exampleValues}${moreValues})` - ); - } else { - layersWithField.push(`${sourceLayer} (has ${fieldName} field)`); - } + // Determine layer type based on render_type override or geometry + let type: + | 'fill' + | 'line' + | 'symbol' + | 'circle' + | 'fill-extrusion' + | 'heatmap'; + let paintProperties: Array<{ + property: string; + description: string; + example: any; + }> = []; + let layoutProperties: + | Array<{ + property: string; + description: string; + example: any; + }> + | undefined; + + // Check if render_type is explicitly specified and not 'auto' + if (config?.render_type && config.render_type !== 'auto') { + // Use the explicitly specified render type + type = config.render_type; + const properties = this.getLayerTypeProperties(type); + paintProperties = properties.paintProperties; + layoutProperties = properties.layoutProperties; + } else { + // Auto-detect based on geometry + switch (geometry) { + case 'Polygon': { + // Special case for buildings with 3D + if (sourceLayer === 'building' && layerType.includes('3d')) { + type = 'fill-extrusion'; + } else { + type = 'fill'; } + const polygonProps = this.getLayerTypeProperties(type); + paintProperties = polygonProps.paintProperties; + layoutProperties = polygonProps.layoutProperties; + break; } - if (layersWithField.length > 0) { - suggestions.push( - `Layers with field "${fieldName}": ${layersWithField.join(', ')}` - ); + case 'LineString': { + // Admin boundaries and natural features are often rendered as lines + type = 'line'; + const lineProps = this.getLayerTypeProperties(type); + paintProperties = lineProps.paintProperties; + layoutProperties = lineProps.layoutProperties; + break; } - // Also look for value matches - const layersWithValue: string[] = []; - const valueStr = String(fieldValue) - .toLowerCase() - .replace(/[_-\s]/g, ''); - - for (const [sourceLayer, fields] of Object.entries(STREETS_V8_FIELDS)) { - const layerFields = fields as Record< - string, - { values?: readonly string[]; description?: string } - >; - - for (const [layerFieldName, fieldDef] of Object.entries( - layerFields - )) { - if (fieldDef.values && Array.isArray(fieldDef.values)) { - const matchingValues = fieldDef.values.filter( - (v) => - String(v) - .toLowerCase() - .replace(/[_-\s]/g, '') - .includes(valueStr) || - valueStr.includes( - String(v) - .toLowerCase() - .replace(/[_-\s]/g, '') - ) - ); - - if (matchingValues.length > 0) { - layersWithValue.push( - `${sourceLayer}.${layerFieldName} has: ${matchingValues.slice(0, 3).join(', ')}` - ); - } - } + case 'Point': { + // Points can be either circle or symbol layers + // Labels and text-based layers should be symbols + if ( + sourceLayer.includes('label') || + sourceLayer === 'motorway_junction' + ) { + type = 'symbol'; + const symbolProps = this.getLayerTypeProperties(type); + paintProperties = symbolProps.paintProperties; + layoutProperties = symbolProps.layoutProperties; + } else { + // Default to circle for point features without labels + type = 'circle'; + const circleProps = this.getLayerTypeProperties(type); + paintProperties = circleProps.paintProperties; + layoutProperties = circleProps.layoutProperties; } + break; } - if (layersWithValue.length > 0) { - suggestions.push( - `Layers with value similar to "${fieldValue}": ${layersWithValue.slice(0, 5).join(', ')}` - ); + default: { + // Fallback to fill for unknown geometry + type = 'fill'; + const defaultProps = this.getLayerTypeProperties(type); + paintProperties = defaultProps.paintProperties; + layoutProperties = defaultProps.layoutProperties; } } - - if (suggestions.length > 0) { - messages.push( - `\nSuggestions based on your filters:\n${suggestions.join('\n')}` - ); - } } - return messages.join('\n'); + return { + id: sourceLayer, // Use source-layer name as the id + type: type, + sourceLayer: sourceLayer, + description: `${sourceLayer} layer (${geometry} geometry)`, + paintProperties, + layoutProperties, + commonFilters: [] + }; } private getHarmoniousColor(layerType: string, action: string): string { - // Define default colors for when user doesn't specify - const colorPalette = { - // Transportation colors - vibrant defaults - motorways: '#ff0000', // Pure red - primary_roads: '#ff6600', // Orange - secondary_roads: '#ffaa00', // Yellow-orange - streets: '#999999', // Medium gray - paths: '#666666', // Dark gray - railways: '#555555', // Very dark gray - roads: '#ff3333', // Generic road red - - // Water features (nice blues) - water: '#4A90E2', // Nice blue - waterway: '#5BA0F2', // Lighter blue - - // Natural features (greens) - parks: '#90C090', // Park green - landuse: '#A0D0A0', // Light green - - // Administrative (purples) - country_boundaries: '#9966CC', // Purple - state_boundaries: '#B399D4', // Light purple - - // Labels (dark tones for readability) - place_labels: '#333333', // Dark gray - road_labels: '#444444', // Medium dark gray - poi_labels: '#555555', // Gray - - // Infrastructure - buildings: '#D4C4B0', // Tan - building_3d: '#C4B4A0', // Darker tan - airports: '#CC99CC', // Light purple - transit: '#6699CC', // Blue-gray - - // Default/highlight colors - default: '#808080', // Neutral gray - highlight: '#FF0000' // Red for highlights + // Define sensible default colors for common layer types + const colorPalette: Record = { + motorways: '#ff6600', + primary_roads: '#ff9933', + secondary_roads: '#ffaa66', + streets: '#999999', + paths: '#666666', + railways: '#555555', + roads: '#888888', + water: '#4A90E2', + waterway: '#5BA0F2', + parks: '#90C090', + landuse: '#A0D0A0', + country_boundaries: '#9966CC', + state_boundaries: '#B399D4', + place_labels: '#333333', + road_labels: '#444444', + poi_labels: '#555555', + buildings: '#D4C4B0', + building_3d: '#C4B4A0', + airports: '#CC99CC', + transit: '#6699CC', + default: '#808080', + highlight: '#FF6B6B' }; - // Return color based on layer type and action if (action === 'highlight') { - // Use slightly more saturated versions for highlights + // Highlight colors are more saturated const highlightColors: Record = { - motorways: '#C0C0C0', // Medium gray for highlights - primary_roads: '#CCCCCC', // Slightly darker gray - secondary_roads: '#D0D0D0', // Light-medium gray - streets: '#D8D8D8', // Light gray - roads: '#CCCCCC', // Generic road highlight - water: '#7FA3CC', - parks: '#88B889', - country_boundaries: '#9B7FB8', - state_boundaries: '#B299CC', - transit: '#6B8FAA', - airports: '#D09099' + motorways: '#ff3300', + roads: '#ff6633', + water: '#2E7BC7', + parks: '#70A070', + buildings: '#B8A090' }; return highlightColors[layerType] || colorPalette.highlight; } - return ( - colorPalette[layerType as keyof typeof colorPalette] || - colorPalette.default - ); + return colorPalette[layerType] || colorPalette.default; } } diff --git a/test/resources/MapboxStyleLayersResource.test.ts b/test/resources/MapboxStyleLayersResource.test.ts index 6456976..cf03aeb 100644 --- a/test/resources/MapboxStyleLayersResource.test.ts +++ b/test/resources/MapboxStyleLayersResource.test.ts @@ -10,14 +10,14 @@ describe('MapboxStyleLayersResource', () => { describe('basic properties', () => { it('should have correct name and URI', () => { - expect(resource.name).toBe('Mapbox Style Layers Guide'); + expect(resource.name).toBe('Mapbox Style Specification Guide'); expect(resource.uri).toBe('resource://mapbox-style-layers'); expect(resource.mimeType).toBe('text/markdown'); }); it('should have a description', () => { expect(resource.description).toContain( - 'Comprehensive guide for Mapbox style layers' + 'Mapbox GL JS style specification reference' ); }); }); diff --git a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap index e64a816..ae7dcc4 100644 --- a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -70,8 +70,8 @@ The tool intelligently resolves layer types and filter properties using Streets You don't need exact layer names - the tool automatically finds the correct layer based on your filters. BASE STYLES: -• standard (DEFAULT): Modern Mapbox Standard with best performance -• streets/light/dark/satellite/outdoors: Classic styles +• standard (DEFAULT): Modern Mapbox Standard with best performance - ALWAYS USE THIS UNLESS SPECIFIED OTHERWISE +• streets-v11/light-v10/dark-v10/satellite-v9/outdoors-v11: Classic styles (only when explicitly requested) • blank: Empty canvas for full customization STANDARD STYLE CONFIG: @@ -81,6 +81,25 @@ Use standard_config to customize the basemap: • Show/hide: labels, roads, 3D buildings • Colors: water, roads, parks, etc. +LAYER ORDERING: +• Layers are rendered in order - last layer in the array appears visually on top +• The 'slot' parameter is OPTIONAL - by default, layer order in the array determines visibility +• For Standard style, you can optionally use 'slot' to control placement: + - No slot (default): Above all existing layers in the style + - 'top': Behind Place and Transit labels + - 'middle': Between basemap and labels + - 'bottom': Below most basemap features + +LAYER RENDERING: +• render_type controls HOW to visualize the layer (line, fill, symbol, etc.) +• Most important: Use render_type:"line" for outlines/borders even on polygon features +• Default "auto" picks based on geometry, but override for specific effects: + - Building outlines → render_type:"line" (not fill!) + - Solid buildings → render_type:"fill" or "fill-extrusion" (3D) + - Road lines → render_type:"line" (auto works too) + - POI dots → render_type:"circle" + - Labels → render_type:"symbol" + LAYER ACTIONS: • color: Apply a specific color • highlight: Make prominent @@ -94,10 +113,18 @@ Examples: • { type: 'wetland' } → finds 'landuse_overlay' layer • { maki: 'restaurant' } → finds 'poi_label' layer • { toll: true } → finds 'road' layer +• { admin_level: 0 } → finds 'admin' layer (for country boundaries) +• { admin_level: 1 } → finds 'admin' layer (for state/province boundaries) -Invalid layer names are auto-corrected based on the filter properties you provide. +IMPORTANT LAYER NAMES: +• Use "admin" for all boundaries (countries, states, etc.) +• Use "building" (singular, not "buildings") +• Use "road" for all streets, highways, paths -For detailed documentation: resource://mapbox-style-layers", +If a layer type is not recognized, the tool will provide helpful suggestions showing: +• All available source layers from Streets v8 +• Which fields are available in each layer +• Examples of how to properly specify layers and filters", "toolName": "style_builder_tool", }, { diff --git a/test/tools/style-builder-tool/StyleBuilderTool.test.ts b/test/tools/style-builder-tool/StyleBuilderTool.test.ts index eccac65..b405f62 100644 --- a/test/tools/style-builder-tool/StyleBuilderTool.test.ts +++ b/test/tools/style-builder-tool/StyleBuilderTool.test.ts @@ -66,9 +66,10 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'primary_roads', + layer_type: 'road', action: 'color', - color: '#ff0000' + color: '#ff0000', + filter_properties: { class: 'primary' } } ] }; @@ -86,10 +87,11 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'railways', + layer_type: 'road', action: 'highlight', color: '#ffff00', - width: 5 + width: 5, + filter_properties: { class: 'major_rail' } } ] }; @@ -108,7 +110,7 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'place_labels', + layer_type: 'place_label', action: 'hide' } ] @@ -127,7 +129,7 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'buildings', + layer_type: 'building', action: 'show' } ] @@ -148,10 +150,11 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'country_boundaries', + layer_type: 'admin', action: 'color', color: '#ff0000', - width: 3 + width: 3, + filter_properties: { admin_level: 0, maritime: 'false' } } ] }; @@ -169,7 +172,7 @@ describe('StyleBuilderTool', () => { // Find the country boundaries layer const countryLayer = style.layers.find( - (l: any) => l.id === 'admin-0-boundary-custom' + (l: any) => l.id.includes('admin') && l.id.includes('0') ); expect(countryLayer).toBeTruthy(); expect(countryLayer['source-layer']).toBe('admin'); @@ -188,10 +191,11 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'state_boundaries', + layer_type: 'admin', action: 'color', color: '#0000ff', - opacity: 0.5 + opacity: 0.5, + filter_properties: { admin_level: 1, maritime: 'false' } } ] }; @@ -205,7 +209,8 @@ describe('StyleBuilderTool', () => { const style = JSON.parse(jsonMatch![1]); const stateLayer = style.layers.find( - (l: any) => l.id === 'admin-1-boundary-custom' + (l: any) => + l['source-layer'] === 'admin' && l.id.includes('admin_level-1') ); expect(stateLayer).toBeTruthy(); expect(stateLayer['source-layer']).toBe('admin'); @@ -228,7 +233,8 @@ describe('StyleBuilderTool', () => { color: '#0099ff' }, { - layer_type: 'parks', + layer_type: 'landuse', + filter_properties: { class: 'park' }, action: 'color', color: '#00ff00' } @@ -301,10 +307,11 @@ describe('StyleBuilderTool', () => { const result = await tool.execute(input); - // Should not error, just skip unknown layer + // Should return help message, not error expect(result.isError).toBe(false); const text = result.content[0].text; - expect(text).toContain('Style Built Successfully'); + expect(text).toContain('not found'); + expect(text).toContain('Available source layers'); }); it('should handle custom filters', async () => { @@ -313,7 +320,7 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'motorways', + layer_type: 'road', action: 'color', color: '#ff0000', filter: ['==', ['get', 'class'], 'motorway'] @@ -330,7 +337,7 @@ describe('StyleBuilderTool', () => { const style = JSON.parse(jsonMatch![1]); const motorwayLayer = style.layers.find( - (l: any) => l.id && l.id.includes('motorway') + (l: any) => l['source-layer'] === 'road' ); expect(motorwayLayer).toBeTruthy(); expect(JSON.stringify(motorwayLayer.filter)).toContain('motorway'); @@ -344,7 +351,8 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'motorways', + layer_type: 'road', + filter_properties: { class: 'motorway' }, action: 'color', color: '#ff0000', width: 3, @@ -380,7 +388,8 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'primary_roads', + layer_type: 'road', + filter_properties: { class: 'primary' }, action: 'color', color: '#000000', property_based: 'class', @@ -418,7 +427,7 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'buildings', + layer_type: 'building', action: 'color', color: '#808080', expression: [ @@ -457,7 +466,7 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'buildings', + layer_type: 'building', action: 'show', opacity: 0.8, zoom_based: true, @@ -604,7 +613,7 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'motorways', + layer_type: 'road', action: 'color', color: '#ff0000', filter_properties: { @@ -635,7 +644,7 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'motorways', + layer_type: 'road', action: 'highlight', color: '#ff0000', filter_properties: { @@ -669,7 +678,7 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'country_boundaries', + layer_type: 'admin', action: 'color', color: '#0000ff', filter_properties: { @@ -731,11 +740,10 @@ describe('StyleBuilderTool', () => { expect(style.sources).toBeDefined(); expect(style.sources.composite).toBeDefined(); - // Check that layers have slot property for Standard style + // Check that layers don't have slot by default for Standard style + // No slot means the layer appears above all existing layers style.layers.forEach((layer: any) => { - expect(layer.slot).toBeDefined(); - // Water is not a road or label, so it should default to 'middle' - expect(layer.slot).toBe('middle'); + expect(layer.slot).toBeUndefined(); }); }); @@ -853,13 +861,14 @@ describe('StyleBuilderTool', () => { slot: 'bottom' }, { - layer_type: 'parks', + layer_type: 'landuse', + filter_properties: { class: 'park' }, action: 'color', color: '#00ff00', slot: 'middle' }, { - layer_type: 'poi_labels', + layer_type: 'poi_label', action: 'show', slot: 'top' } @@ -873,13 +882,15 @@ describe('StyleBuilderTool', () => { const style = JSON.parse(jsonMatch![1]); // Check that layers have correct custom slots - // Note: layer IDs include the original layer ID plus '-custom' suffix - const waterLayer = style.layers.find((l: any) => l.id === 'water-custom'); + const waterLayer = style.layers.find( + (l: any) => l['source-layer'] === 'water' + ); const parksLayer = style.layers.find( - (l: any) => l.id === 'landuse_park-custom' + (l: any) => + l['source-layer'] === 'landuse' && l.id.includes('class-park') ); const poiLayer = style.layers.find( - (l: any) => l.id === 'poi-label-custom' + (l: any) => l['source-layer'] === 'poi_label' ); expect(waterLayer).toBeTruthy(); @@ -891,10 +902,10 @@ describe('StyleBuilderTool', () => { expect(poiLayer.slot).toBe('top'); }); - it('should generate Blank style with sources but no imports', async () => { + it('should always default to Standard style when not specified', async () => { const input: StyleBuilderToolInput = { - style_name: 'Blank Style Test', - base_style: 'blank', + style_name: 'Default Style Test', + // Not specifying base_style - should default to standard layers: [ { layer_type: 'water', @@ -910,17 +921,18 @@ describe('StyleBuilderTool', () => { const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); - // Check that Blank style has sources but no imports - expect(style.sources).toBeTruthy(); - expect(style.sources.composite).toBeTruthy(); - expect(style.sprite).toContain('streets-v12'); - expect(style.glyphs).toContain('mapbox://fonts'); - expect(style.imports).toBeUndefined(); + // Check that it defaults to Standard style with imports + expect(style.imports).toBeDefined(); + expect(style.imports).toHaveLength(1); + expect(style.imports[0].id).toBe('basemap'); + expect(style.imports[0].url).toBe('mapbox://styles/mapbox/standard'); - // Blank styles should not have slot property - style.layers.forEach((layer: any) => { - expect(layer.slot).toBeUndefined(); - }); + // Standard style layers can have slot property, but we didn't specify one + const waterLayer = style.layers.find((layer: any) => + layer.id.includes('water') + ); + expect(waterLayer).toBeTruthy(); + expect(waterLayer.type).toBe('fill'); }); }); @@ -931,7 +943,7 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'landcover', // Wrong layer type + layer_type: 'landuse_overlay', action: 'color', color: '#00ff00', filter_properties: { @@ -945,8 +957,8 @@ describe('StyleBuilderTool', () => { expect(result.isError).toBe(false); const text = result.content[0].text; - expect(text).toContain('Auto-corrections Applied'); - expect(text).toContain('Using "landuse_overlay" instead'); + // No longer expecting auto-correction since we're using the correct layer + expect(text).toContain('Style Built Successfully'); // Check the generated style JSON const jsonMatch = text.match(/```json\n([\s\S]+?)\n```/); @@ -973,7 +985,7 @@ describe('StyleBuilderTool', () => { base_style: 'standard', layers: [ { - layer_type: 'unknown', // Completely unknown layer + layer_type: 'nonexistent', // Completely unknown layer action: 'color', color: '#ff0000', filter_properties: { @@ -987,8 +999,9 @@ describe('StyleBuilderTool', () => { expect(result.isError).toBe(false); const text = result.content[0].text; - expect(text).toContain('Auto-corrections Applied'); - expect(text).toContain('Using "poi_label" instead'); + expect(text).toContain( + 'Determined source layer "poi_label" from filter properties' + ); }); }); @@ -1004,16 +1017,17 @@ describe('StyleBuilderTool', () => { color: '#0066ff' }, { - layer_type: 'parks', + layer_type: 'landuse', + filter_properties: { class: 'park' }, action: 'highlight', color: '#00ff00' }, { - layer_type: 'place_labels', + layer_type: 'place_label', action: 'hide' }, { - layer_type: 'buildings', + layer_type: 'building', action: 'show' } ] From ce6d6191e8ae88e8c789659c80d1752f5b979210 Mon Sep 17 00:00:00 2001 From: jussi-sa Date: Fri, 19 Sep 2025 14:34:24 +0300 Subject: [PATCH 7/8] style building snapshot test --- .../tools/__snapshots__/tool-naming-convention.test.ts.snap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap index ae7dcc4..eb5dbaf 100644 --- a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -70,9 +70,9 @@ The tool intelligently resolves layer types and filter properties using Streets You don't need exact layer names - the tool automatically finds the correct layer based on your filters. BASE STYLES: -• standard (DEFAULT): Modern Mapbox Standard with best performance - ALWAYS USE THIS UNLESS SPECIFIED OTHERWISE -• streets-v11/light-v10/dark-v10/satellite-v9/outdoors-v11: Classic styles (only when explicitly requested) -• blank: Empty canvas for full customization +• standard: ALWAYS THE DEFAULT - Modern Mapbox Standard with best performance +• Classic styles: streets-v11/light-v10/dark-v10/satellite-v9/outdoors-v11 + Only use Classic when user explicitly says "create a classic style" or working with existing Classic style STANDARD STYLE CONFIG: Use standard_config to customize the basemap: From 1abb443cd88492f0a17f00d12e207a1fbc95cedf Mon Sep 17 00:00:00 2001 From: jussi-sa Date: Fri, 19 Sep 2025 15:28:42 +0300 Subject: [PATCH 8/8] review comments --- src/constants/mapboxStreetsV8Fields.ts | 1548 ++--------------- .../MapboxStyleLayersResource.ts | 705 +++----- .../StyleBuilderTool.schema.ts | 14 +- .../style-builder-tool/StyleBuilderTool.ts | 11 +- .../tool-naming-convention.test.ts.snap | 2 +- .../StyleComparisonTool.test.ts | 20 +- 6 files changed, 441 insertions(+), 1859 deletions(-) diff --git a/src/constants/mapboxStreetsV8Fields.ts b/src/constants/mapboxStreetsV8Fields.ts index 726cff3..745d30f 100644 --- a/src/constants/mapboxStreetsV8Fields.ts +++ b/src/constants/mapboxStreetsV8Fields.ts @@ -4,6 +4,116 @@ * AUTO-GENERATED - DO NOT EDIT MANUALLY */ +// Common field that appears identically in 13 layers +const ISO_3166_1_FIELD = { + values: [ + 'EG', + 'ET', + 'CD', + 'ZA', + 'TZ', + 'KE', + 'SD', + 'UG', + 'MA', + 'DZ', + 'GH', + 'CI', + 'CM', + 'MG', + 'MZ', + 'NG', + 'NE', + 'BF', + 'MW', + 'ML', + 'TD', + 'SN', + 'AO', + 'ZW', + 'CN', + 'IN', + 'ID', + 'PK', + 'BD', + 'RU', + 'JP', + 'PH', + 'VN', + 'TR', + 'IR', + 'TH', + 'MM', + 'KR', + 'IQ', + 'AF', + 'MY', + 'NP', + 'DE', + 'FR', + 'GB', + 'IT', + 'ES', + 'UA', + 'PL', + 'RO', + 'NL', + 'GR', + 'HR', + 'BE', + 'PT', + 'CZ', + 'HU', + 'BY', + 'SE', + 'AT', + 'CH', + 'BG', + 'RS', + 'DK', + 'FI', + 'US', + 'MX', + 'CA', + 'GT', + 'CU', + 'HT', + 'DO', + 'HN', + 'NI', + 'SV', + 'CR', + 'PR', + 'PA', + 'JM', + 'TT', + 'GP', + 'MQ', + 'AU', + 'PG', + 'NZ', + 'FJ', + 'MU', + 'RE', + 'MV', + 'SC', + 'BR', + 'CO', + 'AR', + 'PE', + 'VE', + 'CL', + 'EC', + 'BO', + 'PY', + 'UY' + ] as const +} as const; + +const ISO_3166_2_FIELD = { + values: [] as const // string +} as const; + export const STREETS_V8_FIELDS = { // ============ landuse ============ landuse: { @@ -141,115 +251,8 @@ export const STREETS_V8_FIELDS = { // ============ waterway ============ waterway: { - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, class: { description: 'class field', values: [ @@ -272,115 +275,8 @@ export const STREETS_V8_FIELDS = { // ============ aeroway ============ aeroway: { - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, type: { description: 'type field', values: ['runway', 'taxiway', 'apron', 'helipad'] as const @@ -393,115 +289,8 @@ export const STREETS_V8_FIELDS = { // ============ structure ============ structure: { - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, class: { description: 'class field', values: [ @@ -547,115 +336,8 @@ export const STREETS_V8_FIELDS = { // ============ building ============ building: { - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, extrude: { description: 'extrude field', values: ['true', 'false'] as const @@ -1172,115 +854,8 @@ export const STREETS_V8_FIELDS = { description: 'shield_text_color_beta field', values: ['black', 'blue', 'white', 'yellow', 'red', 'green'] as const }, - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, class: { description: 'class field', values: [ @@ -1421,111 +996,7 @@ export const STREETS_V8_FIELDS = { description: 'disputed field', values: ['true', 'false'] as const }, - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, + iso_3166_1: ISO_3166_1_FIELD, maritime: { description: 'maritime field', values: ['true', 'false'] as const @@ -1694,115 +1165,8 @@ export const STREETS_V8_FIELDS = { 'quarter' ] as const }, - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, worldview: { description: 'worldview field', values: ['JP', 'CN', 'IN', 'US', 'all'] as const @@ -1930,115 +1294,8 @@ export const STREETS_V8_FIELDS = { description: 'worldview field', values: ['JP', 'CN', 'IN', 'US', 'all'] as const }, - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - } + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD }, // ============ transit_stop_label ============ @@ -2251,115 +1508,8 @@ export const STREETS_V8_FIELDS = { 'jp-jr.osaka-subway' ] as const }, - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, filterrank: { description: 'filterrank field', values: [0, 1, 2, 3, 4, 5] as const @@ -2424,115 +1574,8 @@ export const STREETS_V8_FIELDS = { description: 'name_ko field', values: [] as const // string }, - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, name_script: { description: 'name_script field', values: [ @@ -3462,115 +2505,8 @@ export const STREETS_V8_FIELDS = { '\u7eff\u5730' ] as const }, - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, sizerank: { description: 'sizerank field', values: [ @@ -3581,115 +2517,8 @@ export const STREETS_V8_FIELDS = { // ============ motorway_junction ============ motorway_junction: { - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, ref: { description: 'ref field', values: [] as const // string @@ -3753,115 +2582,8 @@ export const STREETS_V8_FIELDS = { // ============ housenum_label ============ housenum_label: { - iso_3166_1: { - description: 'iso_3166_1 field', - values: [ - 'EG', - 'ET', - 'CD', - 'ZA', - 'TZ', - 'KE', - 'SD', - 'UG', - 'MA', - 'DZ', - 'GH', - 'CI', - 'CM', - 'MG', - 'MZ', - 'NG', - 'NE', - 'BF', - 'MW', - 'ML', - 'TD', - 'SN', - 'AO', - 'ZW', - 'CN', - 'IN', - 'ID', - 'PK', - 'BD', - 'RU', - 'JP', - 'PH', - 'VN', - 'TR', - 'IR', - 'TH', - 'MM', - 'KR', - 'IQ', - 'AF', - 'MY', - 'NP', - 'DE', - 'FR', - 'GB', - 'IT', - 'ES', - 'UA', - 'PL', - 'RO', - 'NL', - 'GR', - 'HR', - 'BE', - 'PT', - 'CZ', - 'HU', - 'BY', - 'SE', - 'AT', - 'CH', - 'BG', - 'RS', - 'DK', - 'FI', - 'US', - 'MX', - 'CA', - 'GT', - 'CU', - 'HT', - 'DO', - 'HN', - 'NI', - 'SV', - 'CR', - 'PR', - 'PA', - 'JM', - 'TT', - 'GP', - 'MQ', - 'AU', - 'PG', - 'NZ', - 'FJ', - 'MU', - 'RE', - 'MV', - 'SC', - 'BR', - 'CO', - 'AR', - 'PE', - 'VE', - 'CL', - 'EC', - 'BO', - 'PY', - 'UY' - ] as const - }, - iso_3166_2: { - description: 'iso_3166_2 field', - values: [] as const // string - }, + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, house_num: { description: 'house_num field', values: [] as const // string diff --git a/src/resources/mapbox-style-layers-resource/MapboxStyleLayersResource.ts b/src/resources/mapbox-style-layers-resource/MapboxStyleLayersResource.ts index 67fd994..048cd9d 100644 --- a/src/resources/mapbox-style-layers-resource/MapboxStyleLayersResource.ts +++ b/src/resources/mapbox-style-layers-resource/MapboxStyleLayersResource.ts @@ -30,445 +30,312 @@ export class MapboxStyleLayersResource extends BaseResource { const sections: string[] = []; // Header - sections.push('# Mapbox Style Specification Guide'); - sections.push(''); sections.push( - 'This guide provides the Mapbox GL JS style specification for creating custom map styles.' + [ + '# Mapbox Style Specification Guide', + '', + 'This guide provides the Mapbox GL JS style specification for creating custom map styles.', + '', + '## Streets v8 Source Layers', + '', + '### Source Layer → Geometry Type Mapping', + '', + '**Polygon layers:**', + '- `landuse` - Land use areas (parks, residential, industrial, etc.)', + '- `water` - Water bodies (oceans, lakes, rivers as polygons)', + '- `building` - Building footprints with height data', + '- `landuse_overlay` - Overlay features (wetlands, national parks)', + '', + '**LineString layers:**', + '- `road` - All roads, paths, railways', + '- `admin` - Administrative boundaries', + '- `waterway` - Rivers, streams, canals as lines', + '- `aeroway` - Airport runways and taxiways', + '- `structure` - Bridges, tunnels, fences', + '- `natural_label` - Natural feature label placement paths', + '', + '**Point layers:**', + '- `place_label` - City, state, country labels', + '- `poi_label` - Points of interest', + '- `airport_label` - Airport labels', + '- `transit_stop_label` - Transit stops', + '- `motorway_junction` - Highway exits', + '- `housenum_label` - House numbers', + '', + '## Layer Types and Properties', + '' + ].join('\n') ); - sections.push(''); - // Source layers and geometry types - sections.push('## Streets v8 Source Layers'); - sections.push(''); - sections.push('### Source Layer → Geometry Type Mapping'); - sections.push(''); - sections.push('**Polygon layers:**'); - sections.push( - '- `landuse` - Land use areas (parks, residential, industrial, etc.)' - ); - sections.push( - '- `water` - Water bodies (oceans, lakes, rivers as polygons)' + // Fill layer + sections.push( + [ + '### fill', + 'Used for: Polygon features (landuse, water, building, landuse_overlay)', + '', + '**Paint properties:**', + '- `fill-color` - The color of the filled area (default: `#000000`)', + '- `fill-opacity` - Opacity of the entire fill layer, 0-1 (default: `1`)', + '- `fill-outline-color` - Color of the outline (disabled if unset)', + '- `fill-pattern` - Name of image in sprite to use for fill pattern', + '- `fill-antialias` - Whether to antialias the fill (default: `true`)', + '- `fill-translate` - Geometry translation [x, y] in pixels (default: `[0, 0]`)', + '- `fill-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)', + '', + '**No layout properties for fill layers**', + '' + ].join('\n') ); - sections.push('- `building` - Building footprints with height data'); - sections.push( - '- `landuse_overlay` - Overlay features (wetlands, national parks)' - ); - sections.push(''); - sections.push('**LineString layers:**'); - sections.push('- `road` - All roads, paths, railways'); - sections.push('- `admin` - Administrative boundaries'); - sections.push('- `waterway` - Rivers, streams, canals as lines'); - sections.push('- `aeroway` - Airport runways and taxiways'); - sections.push('- `structure` - Bridges, tunnels, fences'); - sections.push('- `natural_label` - Natural feature label placement paths'); - sections.push(''); - sections.push('**Point layers:**'); - sections.push('- `place_label` - City, state, country labels'); - sections.push('- `poi_label` - Points of interest'); - sections.push('- `airport_label` - Airport labels'); - sections.push('- `transit_stop_label` - Transit stops'); - sections.push('- `motorway_junction` - Highway exits'); - sections.push('- `housenum_label` - House numbers'); - sections.push(''); - sections.push('## Layer Types and Properties'); - sections.push(''); - sections.push('### fill'); - sections.push( - 'Used for: Polygon features (landuse, water, building, landuse_overlay)' - ); - sections.push(''); - sections.push('**Paint properties:**'); - sections.push( - '- `fill-color` - The color of the filled area (default: `#000000`)' - ); - sections.push( - '- `fill-opacity` - Opacity of the entire fill layer, 0-1 (default: `1`)' - ); - sections.push( - '- `fill-outline-color` - Color of the outline (disabled if unset)' - ); - sections.push( - '- `fill-pattern` - Name of image in sprite to use for fill pattern' - ); - sections.push( - '- `fill-antialias` - Whether to antialias the fill (default: `true`)' - ); - sections.push( - '- `fill-translate` - Geometry translation [x, y] in pixels (default: `[0, 0]`)' - ); - sections.push( - '- `fill-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)' - ); - sections.push(''); - sections.push('**No layout properties for fill layers**'); - sections.push(''); - - sections.push('### line'); - sections.push( - 'Used for: LineString features (road, admin, waterway, aeroway, structure, natural_label)' - ); - sections.push(''); - sections.push('**Paint properties:**'); - sections.push( - '- `line-color` - The color of the line (default: `#000000`)' - ); - sections.push( - '- `line-width` - Width of the line in pixels (default: `1`)' - ); - sections.push('- `line-opacity` - Opacity of the line, 0-1 (default: `1`)'); - sections.push( - '- `line-blur` - Blur applied to the line in pixels (default: `0`)' - ); - sections.push( - '- `line-dasharray` - Dash pattern [dash, gap, dash, gap...] (solid if unset)' - ); - sections.push( - '- `line-gap-width` - Width of inner gap in line (default: `0`)' - ); - sections.push( - '- `line-offset` - Line offset perpendicular to direction (default: `0`)' - ); - sections.push( - '- `line-pattern` - Name of image in sprite for line pattern' - ); - sections.push( - '- `line-gradient` - Gradient along the line (requires `lineMetrics: true` in source)' - ); - sections.push( - '- `line-translate` - Geometry translation [x, y] in pixels (default: `[0, 0]`)' + // Line layer + sections.push( + [ + '### line', + 'Used for: LineString features (road, admin, waterway, aeroway, structure, natural_label)', + '', + '**Paint properties:**', + '- `line-color` - The color of the line (default: `#000000`)', + '- `line-width` - Width of the line in pixels (default: `1`)', + '- `line-opacity` - Opacity of the line, 0-1 (default: `1`)', + '- `line-blur` - Blur applied to the line in pixels (default: `0`)', + '- `line-dasharray` - Dash pattern [dash, gap, dash, gap...] (solid if unset)', + '- `line-gap-width` - Width of inner gap in line (default: `0`)', + '- `line-offset` - Line offset perpendicular to direction (default: `0`)', + '- `line-pattern` - Name of image in sprite for line pattern', + '- `line-gradient` - Gradient along the line (requires `lineMetrics: true` in source)', + '- `line-translate` - Geometry translation [x, y] in pixels (default: `[0, 0]`)', + '- `line-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)', + '', + '**Layout properties:**', + '- `line-cap` - Display of line ends: `butt`, `round`, `square` (default: `butt`)', + '- `line-join` - Display of line joins: `bevel`, `round`, `miter` (default: `miter`)', + '- `line-miter-limit` - Maximum miter length (default: `2`)', + '- `line-round-limit` - Maximum round join radius (default: `1.05`)', + '- `line-sort-key` - Sort key for layer ordering', + '' + ].join('\n') ); - sections.push( - '- `line-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)' - ); - sections.push(''); - sections.push('**Layout properties:**'); - sections.push( - '- `line-cap` - Display of line ends: `butt`, `round`, `square` (default: `butt`)' - ); - sections.push( - '- `line-join` - Display of line joins: `bevel`, `round`, `miter` (default: `miter`)' - ); - sections.push('- `line-miter-limit` - Maximum miter length (default: `2`)'); - sections.push( - '- `line-round-limit` - Maximum round join radius (default: `1.05`)' - ); - sections.push('- `line-sort-key` - Sort key for layer ordering'); - sections.push(''); - sections.push('### symbol'); - sections.push( - 'Used for: Point and LineString labels (all *_label layers, natural_label, motorway_junction)' - ); - sections.push(''); - sections.push('**Layout properties (text):**'); - sections.push('- `text-field` - Text to display, e.g., `["get", "name"]`'); - sections.push( - '- `text-font` - Font stack, e.g., `["DIN Pro Regular", "Arial Unicode MS Regular"]`' - ); - sections.push('- `text-size` - Font size in pixels (default: `16`)'); - sections.push( - '- `text-max-width` - Maximum text width in ems (default: `10`)' - ); - sections.push( - '- `text-line-height` - Text line height in ems (default: `1.2`)' - ); - sections.push( - '- `text-letter-spacing` - Letter spacing in ems (default: `0`)' - ); - sections.push( - '- `text-justify` - Text justification: `auto`, `left`, `center`, `right` (default: `center`)' - ); - sections.push( - '- `text-anchor` - Text anchor: `center`, `left`, `right`, `top`, `bottom`, `top-left`, etc.' - ); - sections.push( - '- `text-max-angle` - Maximum angle for curved text (default: `45`)' - ); - sections.push('- `text-rotate` - Text rotation in degrees (default: `0`)'); - sections.push( - '- `text-padding` - Padding around text for collision (default: `2`)' - ); - sections.push( - '- `text-keep-upright` - Keep text upright when map rotates (default: `true`)' - ); - sections.push( - '- `text-transform` - Text case: `none`, `uppercase`, `lowercase` (default: `none`)' - ); - sections.push( - '- `text-offset` - Text offset [x, y] in ems (default: `[0, 0]`)' - ); - sections.push( - '- `text-allow-overlap` - Allow text to overlap (default: `false`)' - ); - sections.push( - '- `text-ignore-placement` - Ignore placement collisions (default: `false`)' - ); - sections.push( - '- `text-optional` - Hide text if icon collides (default: `false`)' - ); - sections.push(''); - sections.push('**Layout properties (icon):**'); - sections.push( - '- `icon-image` - Name of icon in sprite, e.g., `["get", "maki"]`' - ); - sections.push('- `icon-size` - Scale factor for icon (default: `1`)'); - sections.push('- `icon-rotate` - Icon rotation in degrees (default: `0`)'); - sections.push( - '- `icon-padding` - Padding around icon for collision (default: `2`)' + // Symbol layer + sections.push( + [ + '### symbol', + 'Used for: Point and LineString labels (all *_label layers, natural_label, motorway_junction)', + '', + '**Layout properties (text):**', + '- `text-field` - Text to display, e.g., `["get", "name"]`', + '- `text-font` - Font stack, e.g., `["DIN Pro Regular", "Arial Unicode MS Regular"]`', + '- `text-size` - Font size in pixels (default: `16`)', + '- `text-max-width` - Maximum text width in ems (default: `10`)', + '- `text-line-height` - Text line height in ems (default: `1.2`)', + '- `text-letter-spacing` - Letter spacing in ems (default: `0`)', + '- `text-justify` - Text justification: `auto`, `left`, `center`, `right` (default: `center`)', + '- `text-anchor` - Text anchor: `center`, `left`, `right`, `top`, `bottom`, `top-left`, etc.', + '- `text-max-angle` - Maximum angle for curved text (default: `45`)', + '- `text-rotate` - Text rotation in degrees (default: `0`)', + '- `text-padding` - Padding around text for collision (default: `2`)', + '- `text-keep-upright` - Keep text upright when map rotates (default: `true`)', + '- `text-transform` - Text case: `none`, `uppercase`, `lowercase` (default: `none`)', + '- `text-offset` - Text offset [x, y] in ems (default: `[0, 0]`)', + '- `text-allow-overlap` - Allow text to overlap (default: `false`)', + '- `text-ignore-placement` - Ignore placement collisions (default: `false`)', + '- `text-optional` - Hide text if icon collides (default: `false`)', + '', + '**Layout properties (icon):**', + '- `icon-image` - Name of icon in sprite, e.g., `["get", "maki"]`', + '- `icon-size` - Scale factor for icon (default: `1`)', + '- `icon-rotate` - Icon rotation in degrees (default: `0`)', + '- `icon-padding` - Padding around icon for collision (default: `2`)', + '- `icon-keep-upright` - Keep icon upright (default: `false`)', + '- `icon-offset` - Icon offset [x, y] in ems (default: `[0, 0]`)', + '- `icon-anchor` - Icon anchor: `center`, `left`, `right`, `top`, `bottom`, etc.', + '- `icon-pitch-alignment` - Icon alignment: `map`, `viewport`, `auto` (default: `auto`)', + '- `icon-text-fit` - Scale icon to text: `none`, `width`, `height`, `both` (default: `none`)', + '- `icon-text-fit-padding` - Padding for icon-text-fit [top, right, bottom, left]', + '- `icon-allow-overlap` - Allow icon to overlap (default: `false`)', + '- `icon-ignore-placement` - Ignore icon collisions (default: `false`)', + '- `icon-optional` - Hide icon if text collides (default: `false`)', + '', + '**Layout properties (symbol):**', + '- `symbol-placement` - Symbol placement: `point`, `line`, `line-center` (default: `point`)', + '- `symbol-spacing` - Distance between symbols on line (default: `250`)', + '- `symbol-avoid-edges` - Avoid symbols at tile edges (default: `false`)', + '- `symbol-sort-key` - Sort key for symbol ordering', + '- `symbol-z-order` - Z-order: `auto`, `viewport-y`, `source` (default: `auto`)', + '', + '**Paint properties (text):**', + '- `text-color` - Color of the text (default: `#000000`)', + '- `text-halo-color` - Color of the halo around text (default: `rgba(0, 0, 0, 0)`)', + '- `text-halo-width` - Width of the halo (default: `0`)', + '- `text-halo-blur` - Blur of the halo (default: `0`)', + '- `text-opacity` - Opacity of the text, 0-1 (default: `1`)', + '- `text-translate` - Text translation [x, y] in pixels (default: `[0, 0]`)', + '- `text-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)', + '', + '**Paint properties (icon):**', + '- `icon-color` - Tint color for SDF icons', + '- `icon-halo-color` - Color of icon halo for SDF icons', + '- `icon-halo-width` - Width of icon halo (default: `0`)', + '- `icon-halo-blur` - Blur of icon halo (default: `0`)', + '- `icon-opacity` - Opacity of the icon, 0-1 (default: `1`)', + '- `icon-translate` - Icon translation [x, y] in pixels (default: `[0, 0]`)', + '- `icon-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)', + '' + ].join('\n') ); - sections.push( - '- `icon-keep-upright` - Keep icon upright (default: `false`)' - ); - sections.push( - '- `icon-offset` - Icon offset [x, y] in ems (default: `[0, 0]`)' - ); - sections.push( - '- `icon-anchor` - Icon anchor: `center`, `left`, `right`, `top`, `bottom`, etc.' - ); - sections.push( - '- `icon-pitch-alignment` - Icon alignment: `map`, `viewport`, `auto` (default: `auto`)' - ); - sections.push( - '- `icon-text-fit` - Scale icon to text: `none`, `width`, `height`, `both` (default: `none`)' - ); - sections.push( - '- `icon-text-fit-padding` - Padding for icon-text-fit [top, right, bottom, left]' - ); - sections.push( - '- `icon-allow-overlap` - Allow icon to overlap (default: `false`)' - ); - sections.push( - '- `icon-ignore-placement` - Ignore icon collisions (default: `false`)' - ); - sections.push( - '- `icon-optional` - Hide icon if text collides (default: `false`)' - ); - sections.push(''); - sections.push('**Layout properties (symbol):**'); - sections.push( - '- `symbol-placement` - Symbol placement: `point`, `line`, `line-center` (default: `point`)' - ); - sections.push( - '- `symbol-spacing` - Distance between symbols on line (default: `250`)' - ); - sections.push( - '- `symbol-avoid-edges` - Avoid symbols at tile edges (default: `false`)' - ); - sections.push('- `symbol-sort-key` - Sort key for symbol ordering'); - sections.push( - '- `symbol-z-order` - Z-order: `auto`, `viewport-y`, `source` (default: `auto`)' - ); - sections.push(''); - sections.push('**Paint properties (text):**'); - sections.push('- `text-color` - Color of the text (default: `#000000`)'); - sections.push( - '- `text-halo-color` - Color of the halo around text (default: `rgba(0, 0, 0, 0)`)' - ); - sections.push('- `text-halo-width` - Width of the halo (default: `0`)'); - sections.push('- `text-halo-blur` - Blur of the halo (default: `0`)'); - sections.push('- `text-opacity` - Opacity of the text, 0-1 (default: `1`)'); - sections.push( - '- `text-translate` - Text translation [x, y] in pixels (default: `[0, 0]`)' - ); - sections.push( - '- `text-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)' - ); - sections.push(''); - sections.push('**Paint properties (icon):**'); - sections.push('- `icon-color` - Tint color for SDF icons'); - sections.push('- `icon-halo-color` - Color of icon halo for SDF icons'); - sections.push('- `icon-halo-width` - Width of icon halo (default: `0`)'); - sections.push('- `icon-halo-blur` - Blur of icon halo (default: `0`)'); - sections.push('- `icon-opacity` - Opacity of the icon, 0-1 (default: `1`)'); - sections.push( - '- `icon-translate` - Icon translation [x, y] in pixels (default: `[0, 0]`)' - ); - sections.push( - '- `icon-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)' - ); - sections.push(''); - sections.push('### circle'); - sections.push( - 'Used for: Point features (can be used with POI or custom point data)' - ); - sections.push(''); - sections.push('**Paint properties:**'); - sections.push( - '- `circle-color` - The color of the circle (default: `#000000`)' - ); - sections.push('- `circle-radius` - Circle radius in pixels (default: `5`)'); - sections.push( - '- `circle-opacity` - Opacity of the circle, 0-1 (default: `1`)' + // Circle layer + sections.push( + [ + '### circle', + 'Used for: Point features (can be used with POI or custom point data)', + '', + '**Paint properties:**', + '- `circle-color` - The color of the circle (default: `#000000`)', + '- `circle-radius` - Circle radius in pixels (default: `5`)', + '- `circle-opacity` - Opacity of the circle, 0-1 (default: `1`)', + '- `circle-blur` - Amount to blur the circle (default: `0`)', + '- `circle-stroke-color` - Color of the circle stroke', + '- `circle-stroke-width` - Width of the circle stroke (default: `0`)', + '- `circle-stroke-opacity` - Opacity of the circle stroke, 0-1 (default: `1`)', + '- `circle-translate` - Circle translation [x, y] in pixels (default: `[0, 0]`)', + '- `circle-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)', + '- `circle-pitch-scale` - Circle scaling: `map` or `viewport` (default: `map`)', + '- `circle-pitch-alignment` - Circle alignment: `map` or `viewport` (default: `viewport`)', + '', + '**Layout properties:**', + '- `circle-sort-key` - Sort key for circle ordering', + '' + ].join('\n') ); - sections.push('- `circle-blur` - Amount to blur the circle (default: `0`)'); - sections.push('- `circle-stroke-color` - Color of the circle stroke'); - sections.push( - '- `circle-stroke-width` - Width of the circle stroke (default: `0`)' - ); - sections.push( - '- `circle-stroke-opacity` - Opacity of the circle stroke, 0-1 (default: `1`)' - ); - sections.push( - '- `circle-translate` - Circle translation [x, y] in pixels (default: `[0, 0]`)' - ); - sections.push( - '- `circle-translate-anchor` - Reference for translate: `map` or `viewport` (default: `map`)' - ); - sections.push( - '- `circle-pitch-scale` - Circle scaling: `map` or `viewport` (default: `map`)' - ); - sections.push( - '- `circle-pitch-alignment` - Circle alignment: `map` or `viewport` (default: `viewport`)' - ); - sections.push(''); - sections.push('**Layout properties:**'); - sections.push('- `circle-sort-key` - Sort key for circle ordering'); - sections.push(''); - sections.push('### fill-extrusion'); - sections.push( - 'Used for: 3D buildings (building layer with height/min_height attributes)' - ); - sections.push(''); - sections.push('**Paint properties:**'); - sections.push( - '- `fill-extrusion-color` - Base color of the extrusion (default: `#000000`)' - ); - sections.push( - '- `fill-extrusion-height` - Height in meters, e.g., `["get", "height"]` (default: `0`)' - ); - sections.push( - '- `fill-extrusion-base` - Base height in meters, e.g., `["get", "min_height"]` (default: `0`)' - ); - sections.push( - '- `fill-extrusion-opacity` - Opacity of the extrusion, 0-1 (default: `1`)' + // Fill-extrusion layer + sections.push( + [ + '### fill-extrusion', + 'Used for: 3D buildings (building layer with height/min_height attributes)', + '', + '**Paint properties:**', + '- `fill-extrusion-color` - Base color of the extrusion (default: `#000000`)', + '- `fill-extrusion-height` - Height in meters, e.g., `["get", "height"]` (default: `0`)', + '- `fill-extrusion-base` - Base height in meters, e.g., `["get", "min_height"]` (default: `0`)', + '- `fill-extrusion-opacity` - Opacity of the extrusion, 0-1 (default: `1`)', + '- `fill-extrusion-pattern` - Name of image in sprite for pattern', + '- `fill-extrusion-translate` - Geometry translation [x, y] in pixels (default: `[0, 0]`)', + '- `fill-extrusion-translate-anchor` - Reference: `map` or `viewport` (default: `map`)', + '- `fill-extrusion-vertical-gradient` - Use vertical gradient (default: `true`)', + '', + '**No layout properties for fill-extrusion layers**', + '' + ].join('\n') ); - sections.push( - '- `fill-extrusion-pattern` - Name of image in sprite for pattern' - ); - sections.push( - '- `fill-extrusion-translate` - Geometry translation [x, y] in pixels (default: `[0, 0]`)' - ); - sections.push( - '- `fill-extrusion-translate-anchor` - Reference: `map` or `viewport` (default: `map`)' - ); - sections.push( - '- `fill-extrusion-vertical-gradient` - Use vertical gradient (default: `true`)' - ); - sections.push(''); - sections.push('**No layout properties for fill-extrusion layers**'); - sections.push(''); - sections.push('## Common Patterns'); - sections.push(''); - sections.push('### Filtering Examples'); - sections.push(''); - sections.push('**Parks only (not cemeteries or golf courses):**'); - sections.push('```json'); - sections.push('{'); - sections.push(' "layer_type": "landuse",'); - sections.push(' "filter_properties": { "class": "park" }'); - sections.push('}'); - sections.push('```'); - sections.push(''); - sections.push('**Major roads:**'); - sections.push('```json'); - sections.push('{'); - sections.push(' "layer_type": "road",'); - sections.push( - ' "filter_properties": { "class": ["motorway", "trunk", "primary"] }' - ); - sections.push('}'); - sections.push('```'); - sections.push(''); - sections.push('**Country boundaries:**'); - sections.push('```json'); - sections.push('{'); - sections.push(' "layer_type": "admin",'); - sections.push( - ' "filter_properties": { "admin_level": 0, "maritime": "false" }' + // Common patterns and examples + sections.push( + [ + '## Common Patterns', + '', + '### Filtering Examples', + '', + '**Parks only (not cemeteries or golf courses):**', + '```json', + '{', + ' "layer_type": "landuse",', + ' "filter_properties": { "class": "park" }', + '}', + '```', + '', + '**Major roads:**', + '```json', + '{', + ' "layer_type": "road",', + ' "filter_properties": { "class": ["motorway", "trunk", "primary"] }', + '}', + '```', + '', + '**Country boundaries:**', + '```json', + '{', + ' "layer_type": "admin",', + ' "filter_properties": { "admin_level": 0, "maritime": "false" }', + '}', + '```', + '', + '**3D Buildings:**', + '```json', + '{', + ' "layer_type": "building",', + ' "filter_properties": { "extrude": "true" }', + '}', + '```', + '' + ].join('\n') ); - sections.push('}'); - sections.push('```'); - sections.push(''); - sections.push('**3D Buildings:**'); - sections.push('```json'); - sections.push('{'); - sections.push(' "layer_type": "building",'); - sections.push(' "filter_properties": { "extrude": "true" }'); - sections.push('}'); - sections.push('```'); - sections.push(''); // Available fields reference - sections.push('## Available Filter Fields'); - sections.push(''); - sections.push( - 'For detailed field values in each source layer, use the style_builder_tool.' - ); sections.push( - 'The tool will provide specific guidance when a layer is not recognized.' + [ + '## Available Filter Fields', + '', + 'For detailed field values in each source layer, use the style_builder_tool.', + 'The tool will provide specific guidance when a layer is not recognized.', + '', + '### Key Fields by Layer:', + '', + '**landuse:** class, type', + '**road:** class, type, structure, toll, oneway', + '**admin:** admin_level, disputed, maritime', + '**building:** type, height, min_height, extrude', + '**water:** (no filter fields - all water features)', + '**waterway:** class, type', + '**place_label:** class, type, capital', + '**poi_label:** maki, class, filterrank', + '**transit_stop_label:** mode, stop_type, network', + '' + ].join('\n') ); - sections.push(''); - sections.push('### Key Fields by Layer:'); - sections.push(''); - sections.push('**landuse:** class, type'); - sections.push('**road:** class, type, structure, toll, oneway'); - sections.push('**admin:** admin_level, disputed, maritime'); - sections.push('**building:** type, height, min_height, extrude'); - sections.push('**water:** (no filter fields - all water features)'); - sections.push('**waterway:** class, type'); - sections.push('**place_label:** class, type, capital'); - sections.push('**poi_label:** maki, class, filterrank'); - sections.push('**transit_stop_label:** mode, stop_type, network'); - sections.push(''); - sections.push('## Working with Styles'); - sections.push(''); - sections.push('### Using style_builder_tool'); - sections.push(''); - sections.push( - 'The style_builder_tool is the primary way to create Mapbox styles. It:' - ); - sections.push( - '- Automatically determines the correct geometry type for each source layer' - ); - sections.push( - '- Applies appropriate paint properties based on the action (color, highlight, hide, show)' - ); - sections.push('- Generates proper filters from filter_properties'); - sections.push( - '- Provides helpful suggestions when layers are not recognized' + // Working with styles + sections.push( + [ + '## Working with Styles', + '', + '### Using style_builder_tool', + '', + 'The style_builder_tool is the primary way to create Mapbox styles. It:', + '- Automatically determines the correct geometry type for each source layer', + '- Applies appropriate paint properties based on the action (color, highlight, hide, show)', + '- Generates proper filters from filter_properties', + '- Provides helpful suggestions when layers are not recognized', + '', + '### Example Usage', + '', + '```', + 'style_builder_tool({', + ' style_name: "Custom Style",', + ' base_style: "standard",', + ' layers: [', + ' {', + ' layer_type: "water",', + ' action: "color",', + ' color: "#0099ff"', + ' },', + ' {', + ' layer_type: "landuse",', + ' filter_properties: { class: "park" },', + ' action: "color",', + ' color: "#00ff00"', + ' },', + ' {', + ' layer_type: "road",', + ' filter_properties: { class: ["motorway", "trunk"] },', + ' action: "highlight"', + ' }', + ' ]', + '})', + '```' + ].join('\n') ); - sections.push(''); - sections.push('### Example Usage'); - sections.push(''); - sections.push('```'); - sections.push('style_builder_tool({'); - sections.push(' style_name: "Custom Style",'); - sections.push(' base_style: "standard",'); - sections.push(' layers: ['); - sections.push(' {'); - sections.push(' layer_type: "water",'); - sections.push(' action: "color",'); - sections.push(' color: "#0099ff"'); - sections.push(' },'); - sections.push(' {'); - sections.push(' layer_type: "landuse",'); - sections.push(' filter_properties: { class: "park" },'); - sections.push(' action: "color",'); - sections.push(' color: "#00ff00"'); - sections.push(' },'); - sections.push(' {'); - sections.push(' layer_type: "road",'); - sections.push(' filter_properties: { class: ["motorway", "trunk"] },'); - sections.push(' action: "highlight"'); - sections.push(' }'); - sections.push(' ]'); - sections.push('})'); - sections.push('```'); return sections.join('\n'); } diff --git a/src/tools/style-builder-tool/StyleBuilderTool.schema.ts b/src/tools/style-builder-tool/StyleBuilderTool.schema.ts index 8730516..256f4f6 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.schema.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.schema.ts @@ -127,18 +127,20 @@ export const StyleBuilderToolSchema = z.object({ base_style: z .enum([ 'standard', - 'streets-v11', - 'light-v10', - 'dark-v10', + 'streets-v12', + 'light-v11', + 'dark-v11', 'satellite-v9', - 'satellite-streets-v11', - 'outdoors-v11' + 'satellite-streets-v12', + 'outdoors-v12', + 'navigation-day-v1', + 'navigation-night-v1' ]) .default('standard') .describe( 'Base style template. ALWAYS use "standard" as the default for all new styles. ' + 'Standard style provides the best performance and modern features. ' + - 'Only use Classic styles (streets/light/dark/satellite/outdoors) when explicitly requested with "create a classic style" or when working with an existing Classic style.' + 'Only use Classic styles (streets/light/dark/satellite/outdoors/navigation) when explicitly requested with "create a classic style" or when working with an existing Classic style.' ), layers: z diff --git a/src/tools/style-builder-tool/StyleBuilderTool.ts b/src/tools/style-builder-tool/StyleBuilderTool.ts index 0d6e8af..43e89a7 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.ts @@ -60,7 +60,7 @@ You don't need exact layer names - the tool automatically finds the correct laye BASE STYLES: • standard: ALWAYS THE DEFAULT - Modern Mapbox Standard with best performance -• Classic styles: streets-v11/light-v10/dark-v10/satellite-v9/outdoors-v11 +• Classic styles: streets-v12/light-v11/dark-v11/satellite-v9/outdoors-v12/satellite-streets-v12/navigation-day-v1/navigation-night-v1 Only use Classic when user explicitly says "create a classic style" or working with existing Classic style STANDARD STYLE CONFIG: @@ -324,15 +324,6 @@ ${JSON.stringify(style, null, 2)} name: input.style_name } as MapboxStyle; - // Determine which base style to use - // const isClassicStyle = [ - // 'streets', - // 'light', - // 'dark', - // 'satellite', - // 'outdoors' - // ].includes(baseStyle); - // For standard style, use imports to inherit from Mapbox Standard if (baseStyle === 'standard') { // Follow the exact order from the working Mapbox Studio example diff --git a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap index eb5dbaf..bbcac06 100644 --- a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -71,7 +71,7 @@ You don't need exact layer names - the tool automatically finds the correct laye BASE STYLES: • standard: ALWAYS THE DEFAULT - Modern Mapbox Standard with best performance -• Classic styles: streets-v11/light-v10/dark-v10/satellite-v9/outdoors-v11 +• Classic styles: streets-v12/light-v11/dark-v11/satellite-v9/outdoors-v12/satellite-streets-v12/navigation-day-v1/navigation-night-v1 Only use Classic when user explicitly says "create a classic style" or working with existing Classic style STANDARD STYLE CONFIG: diff --git a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts index 4c07f21..df8189f 100644 --- a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts +++ b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts @@ -16,7 +16,7 @@ describe('StyleComparisonTool', () => { describe('run', () => { it('should generate comparison URL with provided access token', async () => { const input = { - before: 'mapbox/streets-v11', + before: 'mapbox/streets-v12', after: 'mapbox/outdoors-v12', accessToken: 'pk.test.token' }; @@ -28,13 +28,13 @@ describe('StyleComparisonTool', () => { const url = (result.content[0] as { type: 'text'; text: string }).text; expect(url).toContain('https://agent.mapbox.com/tools/style-compare'); expect(url).toContain('access_token=pk.test.token'); - expect(url).toContain('before=mapbox%2Fstreets-v11'); + expect(url).toContain('before=mapbox%2Fstreets-v12'); expect(url).toContain('after=mapbox%2Foutdoors-v12'); }); it('should require access token', async () => { const input = { - before: 'mapbox/streets-v11', + before: 'mapbox/streets-v12', after: 'mapbox/satellite-v9' // Missing accessToken }; @@ -49,7 +49,7 @@ describe('StyleComparisonTool', () => { it('should handle full style URLs', async () => { const input = { - before: 'mapbox://styles/mapbox/streets-v11', + before: 'mapbox://styles/mapbox/streets-v12', after: 'mapbox://styles/mapbox/outdoors-v12', accessToken: 'pk.test.token' }; @@ -58,7 +58,7 @@ describe('StyleComparisonTool', () => { expect(result.isError).toBe(false); const url = (result.content[0] as { type: 'text'; text: string }).text; - expect(url).toContain('before=mapbox%2Fstreets-v11'); + expect(url).toContain('before=mapbox%2Fstreets-v12'); expect(url).toContain('after=mapbox%2Foutdoors-v12'); }); @@ -84,7 +84,7 @@ describe('StyleComparisonTool', () => { it('should reject secret tokens', async () => { const input = { - before: 'mapbox/streets-v11', + before: 'mapbox/streets-v12', after: 'mapbox/outdoors-v12', accessToken: 'sk.secret.token' }; @@ -102,7 +102,7 @@ describe('StyleComparisonTool', () => { it('should reject invalid token formats', async () => { const input = { - before: 'mapbox/streets-v11', + before: 'mapbox/streets-v12', after: 'mapbox/outdoors-v12', accessToken: 'invalid.token' }; @@ -157,7 +157,7 @@ describe('StyleComparisonTool', () => { it('should include hash fragment with map position when coordinates are provided', async () => { const input = { - before: 'mapbox/streets-v11', + before: 'mapbox/streets-v12', after: 'mapbox/outdoors-v12', accessToken: 'pk.test.token', zoom: 5.72, @@ -175,7 +175,7 @@ describe('StyleComparisonTool', () => { it('should not include hash fragment when coordinates are incomplete', async () => { // Only zoom provided const input1 = { - before: 'mapbox/streets-v11', + before: 'mapbox/streets-v12', after: 'mapbox/outdoors-v12', accessToken: 'pk.test.token', zoom: 10 @@ -188,7 +188,7 @@ describe('StyleComparisonTool', () => { // Only latitude and longitude, no zoom const input2 = { - before: 'mapbox/streets-v11', + before: 'mapbox/streets-v12', after: 'mapbox/outdoors-v12', accessToken: 'pk.test.token', latitude: 40.7128,