diff --git a/src/constants/mapboxStreetsV8Fields.ts b/src/constants/mapboxStreetsV8Fields.ts index 16201a8..745d30f 100644 --- a/src/constants/mapboxStreetsV8Fields.ts +++ b/src/constants/mapboxStreetsV8Fields.ts @@ -1,118 +1,124 @@ /** - * 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 - } - }, +// 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; - 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 - } - }, +const ISO_3166_2_FIELD = { + values: [] as const // string +} as const; +export const STREETS_V8_FIELDS = { + // ============ landuse ============ landuse: { class: { - description: 'Landuse classification', + description: 'class field', values: [ 'aboriginal_lands', 'agriculture', @@ -135,42 +141,120 @@ 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: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, class: { - description: 'Waterway classification', + description: 'class field', values: [ 'river', 'canal', @@ -181,132 +265,2330 @@ export const STREETS_V8_FIELDS = { ] as const }, type: { - description: 'Waterway type', + description: 'type field', values: ['river', 'canal', 'stream', 'ditch', 'drain'] as const } }, + // ============ water ============ + water: {}, + + // ============ aeroway ============ aeroway: { + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, type: { - description: 'Aeroway type', + description: 'type field', values: ['runway', 'taxiway', 'apron', 'helipad'] as const + }, + ref: { + description: 'ref field', + values: [] as const // string } }, - place_label: { + // ============ structure ============ + structure: { + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, class: { - description: 'Place classification', + description: 'class field', values: [ - 'country', - 'state', - 'settlement', - 'settlement_subdivision' + 'cliff', + 'crosswalk', + 'entrance', + 'fence', + 'gate', + 'hedge', + 'land' ] as const }, - capital: { - description: 'Capital admin level', - values: [2, 3, 4, 5, 6] as const - }, - filterrank: { - description: 'Priority for label density', - type: 'number' as const // 0-5 - }, - symbolrank: { - description: 'Symbol ranking', - 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 } }, - poi_label: { - class: { - description: 'POI thematic grouping', - type: 'string' as const + // ============ building ============ + building: { + iso_3166_1: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, + extrude: { + description: 'extrude field', + values: ['true', 'false'] as const }, - filterrank: { - description: 'Priority for label density', - type: 'number' as const // 0-5 + building_id: { + description: 'building_id field', + values: [] as const // number - Range: 0 to 100000000000 }, - maki: { - description: 'Icon to use (e.g., airport, hospital, restaurant, park)', - type: 'string' as const + 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: [ + '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 } }, - natural_label: { + // ============ landuse_overlay ============ + landuse_overlay: { class: { - description: 'Natural feature classification', + description: 'class field', + values: ['national_park', 'wetland', 'wetland_noveg'] as const + }, + type: { + description: 'type field', values: [ - 'glacier', - 'landform', - 'water_feature', 'wetland', - 'ocean', - 'sea', - 'river', - 'water', - 'reservoir', - 'dock', - 'canal', - 'drain', - 'ditch', - 'stream', - 'continent' + '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 }, - elevation_m: { - description: 'Elevation in meters', - type: 'number' 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 } }, - transit_stop_label: { - mode: { - description: 'Transit mode', + // ============ 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: [ - 'rail', - 'metro_rail', - 'light_rail', - 'tram', - 'bus', - 'monorail', - 'funicular', - 'bicycle', - 'ferry', - 'narrow_gauge', - 'preserved', - 'miniature' + '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: 'Icon type (visual representation of transit type)', + 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: [ - 'rail', - 'rail-metro', - 'rail-light', - 'entrance', - 'bus', - 'bicycle-share', - 'ferry' + '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 - } - }, - - airport_label: { - class: { - description: 'Airport classification', - values: ['military', 'civil'] as const }, - maki: { - description: 'Icon type (visual representation)', - values: ['airport', 'heliport', 'rocket'] 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: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, + 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: ISO_3166_1_FIELD, + 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: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, + 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: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD + }, + + // ============ 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: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, + 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: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, + 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: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, + 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: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, + 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: ISO_3166_1_FIELD, + iso_3166_2: ISO_3166_2_FIELD, + 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 deleted file mode 100644 index 655adb7..0000000 --- a/src/constants/mapboxStyleLayers.ts +++ /dev/null @@ -1,784 +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' - ] - }, - - 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: ['class: park, hospital, school, museum, etc.'], - 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..048cd9d 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,313 @@ export class MapboxStyleLayersResource extends BaseResource { const sections: string[] = []; // Header - sections.push('# Mapbox Style Creation Guide'); - sections.push(''); - sections.push('## How to Create a Custom Mapbox Style'); - sections.push(''); - sections.push('### Step-by-Step Process:'); sections.push( - '1. **Understand the request** - What layers should be visible? What colors/styling?' + [ + '# 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( - '2. **Use style_builder_tool** - This tool generates the style JSON configuration' - ); - sections.push( - '3. **Apply the style** - Use create_style_tool to create a new style or update_style_tool to modify existing' - ); - sections.push(''); - sections.push('### Example Workflow:'); - sections.push('```'); - sections.push( - 'User: "Create a dark mode style with blue water and hidden labels"' - ); - sections.push('Assistant: '); - sections.push('1. Uses style_builder_tool with:'); - sections.push(' - global_settings: { mode: "dark" }'); - sections.push(' - layers: ['); - sections.push( - ' { layer_type: "water", action: "color", color: "#0066ff" },' - ); - 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(''); - sections.push('### Common User Requests → Layer Mappings'); - sections.push(''); - sections.push('- **"change water color"** → `water`, `waterway`'); - sections.push( - '- **"highlight parks"** → `parks` (landuse with class=park)' - ); - sections.push( - '- **"show railways"** → `railways` (road with class=major_rail)' - ); - sections.push( - '- **"color roads"** → `motorways`, `primary_roads`, `secondary_roads`, `streets`' - ); - sections.push('- **"3D buildings"** → `building_3d` (fill-extrusion)'); - sections.push( - '- **"hide labels"** → `place_labels`, `road_labels`, `poi_labels`' - ); - sections.push( - '- **"show borders"** → `country_boundaries`, `state_boundaries`' - ); - sections.push('- **"transit/subway"** → `transit`, `railways`'); - sections.push( - '- **"country boundaries"** → `country_boundaries` (admin layer, admin_level=0)' - ); - sections.push( - '- **"state boundaries"** → `state_boundaries` (admin layer, admin_level=1)' - ); - sections.push(''); - - // Layer categories - sections.push('## Layer Categories'); - 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(''); - - 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(''); - }); - - // 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",'); + // Fill layer sections.push( - ' filter: ["match", ["get", "class"], ["major_rail", "minor_rail"], true, false],' + [ + '### 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(' paint: {'); - sections.push(' "line-color": "#ff0000", // Red'); + + // Line layer sections.push( - ' "line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 14, 2, 20, 8]' + [ + '### 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(' }'); - sections.push(' }'); - sections.push(']'); - sections.push('```'); - 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('```'); - 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('```'); - 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('```'); - sections.push(''); - - // Tips - sections.push('## Tips for LLM Usage'); - sections.push(''); + // Symbol layer sections.push( - '1. **Layer Order Matters**: Layers are drawn in the order they appear (first = bottom)' + [ + '### 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') ); + + // Circle layer sections.push( - '2. **Use Filters**: Filter by `class`, `type`, or other properties to target specific features' + [ + '### 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') ); + + // Fill-extrusion layer sections.push( - '3. **Zoom Levels**: Use interpolation for smooth transitions across zoom levels' + [ + '### 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') ); + + // Common patterns and examples sections.push( - '4. **Source Layers**: Most features come from `composite` source with specific `source-layer`' + [ + '## 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') ); + + // Available fields reference sections.push( - '5. **Color Formats**: Use hex colors (#rrggbb), rgb(), hsl(), or named colors' + [ + '## 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') ); + + // Working with styles sections.push( - '6. **Opacity**: Use opacity properties for transparency (0 = transparent, 1 = opaque)' + [ + '## 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(''); 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/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..256f4f6 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(), @@ -81,16 +109,39 @@ 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-v12', + 'light-v11', + 'dark-v11', + 'satellite-v9', + '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/navigation) when explicitly requested with "create a classic style" or when working with an existing Classic style.' + ), layers: z .array(LayerConfigSchema) @@ -103,7 +154,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 e6430c5..43e89a7 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.ts @@ -3,66 +3,117 @@ 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 { 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' | 'heatmap'; + sourceLayer: string; + description: string; + paintProperties: Array<{ + property: string; + description: string; + example: unknown; + }>; + layoutProperties?: Array<{ + property: string; + description: string; + example: unknown; + }>; + commonFilters: 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 = `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 - -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) -• 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) - -ACTIONS YOU CAN APPLY: -• color - Set the layer's color -• highlight - Make layer prominent with 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" -• "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)" - -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)" - -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'] }`; + 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: ALWAYS THE DEFAULT - Modern Mapbox Standard with best performance +• 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: +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 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 +• 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 +• { admin_level: 0 } → finds 'admin' layer (for country boundaries) +• { admin_level: 1 } → finds 'admin' layer (for state/province boundaries) + +IMPORTANT LAYER NAMES: +• Use "admin" for all boundaries (countries, states, etc.) +• Use "building" (singular, not "buildings") +• Use "road" for all streets, highways, paths + +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 }); @@ -70,7 +121,42 @@ 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, 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 = + corrections.length > 0 + ? `\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: [ @@ -79,9 +165,11 @@ To show multiple transit types: filter_properties: { maki: ['bus', 'entrance', ' 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:** @@ -110,172 +198,660 @@ ${JSON.stringify(style, null, 2)} } } - private buildStyle(input: StyleBuilderToolInput): MapboxStyle { + private buildStyle(input: StyleBuilderToolInput): { + style: MapboxStyle; + corrections: string[]; + layerHelp?: string; + availableProperties?: Record; + } { const layers: Layer[] = []; + const allCorrections: string[] = []; + 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 + 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) { if (config.action === 'hide') continue; - const layerDef = MAPBOX_STYLE_LAYERS[config.layer_type]; - if (!layerDef) { - console.warn(`Unknown layer type: ${config.layer_type}`); - continue; + // Determine the source layer for this config + let sourceLayer = config.layer_type; + let layerDef: DynamicLayerDefinition | null = null; + + // 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); + } } - const layer = this.createLayer(layerDef, config, input.global_settings); - if (layer) { - layers.push(layer); + // If still no match, return helpful information + if (!layerDef) { + const helpMessage = this.generateLayerHelp(config); + return { + style: {} as MapboxStyle, + corrections: [], + layerHelp: helpMessage, + availableProperties: {} + }; } - } - // 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); - } + const result = this.createLayer( + layerDef, + config, + input.global_settings, + isUsingStandard + ); + 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); + } } - return { + // Note: We no longer automatically add layers that weren't explicitly requested + // The user should specify all desired layers in the input + + // 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; + + // For standard style, use imports to inherit from Mapbox Standard + if (baseStyle === '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; + + // 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', + 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 { + // Classic styles - 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; + } + + 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], + layerDef: DynamicLayerDefinition, config: StyleBuilderToolInput['layers'][0], - globalSettings?: StyleBuilderToolInput['global_settings'] - ): Layer | null { + globalSettings?: StyleBuilderToolInput['global_settings'], + isUsingStandard?: boolean + ): { layer: Layer | null; corrections: string[] } { + // Generate a unique ID for the layer based on its properties + let layerId = `${layerDef.id || config.layer_type}-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 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) { layer.source = 'composite'; 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 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) { - paint[opacityProp] = this.generateExpression( - config.opacity, - config, - 'opacity' - ); + // Apply opacity - use specified value or smart defaults + const opacityProp = this.getOpacityProperty(layerDef.type); + if (opacityProp) { + // 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); + + // 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' + ); + } } } - // 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, + config.action === 'highlight' + ); + + 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,53 +864,66 @@ ${JSON.stringify(style, null, 2)} layer.paint = paint; } - // Add layout properties if needed - if (layerDef.layoutProperties && layerDef.layoutProperties.length > 0) { + // Add layout properties with better defaults for specific layer types + if ( + 'layoutProperties' in layerDef && + layerDef.layoutProperties && + Array.isArray(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 if ( + 'layoutProperties' in layerDef && + Array.isArray(layerDef.layoutProperties) + ) { + // 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; } } - return layer; - } - - private parseFilterString(filterStr: string): unknown | null { - // Parse filter strings like "class: park, cemetery" 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(); - // Handle boolean strings - 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 { - filters.push(['match', ['get', property], valueList, true, false]); - } - } - } - - if (filters.length === 0) return null; - if (filters.length === 1) return filters[0]; - return ['all', ...filters]; + return { layer, corrections: filterResult.corrections }; } private getColorProperty(layerType: string): string | null { @@ -367,7 +956,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]; + const layerDef = this.createDynamicLayerDefinition( + config.layer_type, + config + ); const description = layerDef?.description || config.layer_type; switch (config.action) { @@ -392,6 +984,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'); } @@ -471,41 +1134,289 @@ ${JSON.stringify(style, null, 2)} return value; } - private generateDataDrivenExpression( - property: string, - valueMap: Record, - defaultValue: unknown - ): unknown { - const expression: unknown[] = ['match', ['get', property]]; + /** + * 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; + } - for (const [key, value] of Object.entries(valueMap)) { - expression.push(key); - expression.push(value); + // Calculate common characters + let common = 0; + for (let i = 0; i < Math.min(s1.length, s2.length); i++) { + if (s1[i] === s2[i]) common++; } - expression.push(defaultValue); - return expression; + return common / Math.max(s1.length, s2.length); } - private generateZoomInterpolation( - minZoom: number, - maxZoom: number, - minValue: number, - maxValue: number, - interpolationType: 'linear' | 'exponential' = 'linear' - ): unknown { - const interpolation = - interpolationType === 'exponential' ? ['exponential', 1.5] : ['linear']; + /** + * Find the closest matching value for a field using intelligent matching + */ + private findClosestFieldValue( + fieldName: string, + inputValue: string | number | boolean, + 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') { + 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 ? '...' : ''}` + }; + } - return [ - 'interpolate', - interpolation, - ['zoom'], - minZoom, - minValue, - maxZoom, - maxValue + // 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 - 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, + 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, + sourceLayer + ); + 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( @@ -514,63 +1425,791 @@ ${JSON.stringify(style, null, 2)} string, string | number | boolean | (string | number | boolean)[] > - ): Filter | null { + ): { filter: Filter | null; corrections: string[] } { 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]; - 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 fieldDef = layerFields[property as keyof typeof layerFields]; - if (!fieldDef) continue; + const resolved = this.resolveFilterProperty(sourceLayer, property, value); + + if (resolved.correction) { + corrections.push(resolved.correction); + } - // 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]); + // 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 + // 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; } - // Handle single value - else { - filters.push(['==', ['get', property], value]); + + 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 = + 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); + } + } + } + + // Validate and auto-correct values against defined values + if ( + fieldDef && + 'values' in fieldDef && + Array.isArray(fieldDef.values) && + fieldDef.values.length > 0 + ) { + const validValues = fieldDef.values; + + 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, + 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); + } + processedValue = correctedValues; + } else { + // For single values, validate and correct + const result = this.findClosestFieldValue( + property, + processedValue, + 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; + } + } + + // 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 + ]); } } - 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: 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: [] }; } - // If filter_properties is provided, build from that - if (config.filter_properties && layerDef.sourceLayer) { - return this.buildAdvancedFilter( + const filters: Filter[] = []; + const allCorrections: string[] = []; + + // Add filter_properties if provided + if ( + config.filter_properties && + layerDef && + 'sourceLayer' in layerDef && + layerDef.sourceLayer + ) { + const result = this.buildAdvancedFilter( layerDef.sourceLayer, config.filter_properties ); + if (result.filter) { + filters.push(result.filter); + } + if (result.corrections.length > 0) { + allCorrections.push(...result.corrections); + } + } + + // Combine filters if there are multiple + const filter = + filters.length === 0 + ? null + : filters.length === 1 + ? filters[0] + : (['all', ...filters] as Filter); + + return { filter, corrections: allCorrections }; + } + + private isRoadLayer(layerType: string): boolean { + return [ + 'roads', + 'motorways', + 'primary_roads', + 'secondary_roads', + 'streets', + 'paths', + 'railways' + ].includes(layerType); + } + + private getDefaultLineWidth( + layerType: string, + isHighlight: boolean = false + ): unknown | null { + // Reasonable default line widths with zoom interpolation + 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 - thinner + country_boundaries: [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + 0.5, + 4, + 0.8, + 8, + 1.2, + 12, + 1.5, + 16, + 1.8 + ], + state_boundaries: [ + 'interpolate', + ['linear'], + ['zoom'], + 2, + 0.3, + 6, + 0.6, + 10, + 1.0, + 14, + 1.3, + 18, + 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 { + // Symbol layers should always be fully opaque for readability + if (layerDefType === 'symbol') { + return 1.0; } - // 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; + // Layer-specific opacity for better visual hierarchy + const opacityMap: Record = { + water: 0.85, + waterway: 0.75, + parks: 0.65, + landuse: 0.45, + 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, + country_boundaries: 0.5, + state_boundaries: 0.4, + airports: 0.7, + transit: 0.75, + place_labels: 1.0, + road_labels: 1.0, + poi_labels: 1.0 + }; + + return opacityMap[layerType] || 0.7; + } + + 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', + example: '#000000' + }, + { property: 'line-width', description: 'Line width', example: 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 'fill': + properties.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 '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 properties; + } + + 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; + + // 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 + return null; + } + + // 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; + } + + 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; + } + + 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; + } + + default: { + // Fallback to fill for unknown geometry + type = 'fill'; + const defaultProps = this.getLayerTypeProperties(type); + paintProperties = defaultProps.paintProperties; + layoutProperties = defaultProps.layoutProperties; + } + } + } + + 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 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' + }; + + if (action === 'highlight') { + // Highlight colors are more saturated + const highlightColors: Record = { + motorways: '#ff3300', + roads: '#ff6633', + water: '#2E7BC7', + parks: '#70A070', + buildings: '#B8A090' + }; + return highlightColors[layerType] || colorPalette.highlight; } - return null; + return colorPalette[layerType] || 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..2a8cd40 --- /dev/null +++ b/src/utils/styleUtils.ts @@ -0,0 +1,55 @@ +/** + * Filters out expanded Mapbox styles from imports to reduce response size. + * This preserves the reference to the style (e.g., mapbox://styles/mapbox/standard) + * 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<{ + id?: unknown; + url: string; + data?: unknown; + config?: Record; + [key: string]: 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/') + ) { + // 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; + }); + } + + return filtered as T; +} 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 ac7e99f..bbcac06 100644 --- a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -64,60 +64,67 @@ 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. -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 +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. -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) -• 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) +BASE STYLES: +• standard: ALWAYS THE DEFAULT - Modern Mapbox Standard with best performance +• 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 -ACTIONS YOU CAN APPLY: -• color - Set the layer's color -• highlight - Make layer prominent with color/width -• hide - Remove layer from view -• show - Display layer with default styling +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. -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" +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 -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)" +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" -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)" +LAYER ACTIONS: +• color: Apply a specific color +• highlight: Make prominent +• hide: Remove from view +• show: Display with defaults -For detailed layer properties and filters, check resource://mapbox-style-layers +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 +• { admin_level: 0 } → finds 'admin' layer (for country boundaries) +• { admin_level: 1 } → finds 'admin' layer (for state/province boundaries) -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'] }", +IMPORTANT LAYER NAMES: +• Use "admin" for all boundaries (countries, states, etc.) +• Use "building" (singular, not "buildings") +• Use "road" for all streets, highways, paths + +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 c838360..b405f62 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,12 +63,13 @@ 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', + layer_type: 'road', action: 'color', - color: '#ff0000' + color: '#ff0000', + filter_properties: { class: 'primary' } } ] }; @@ -83,13 +84,14 @@ 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', + layer_type: 'road', action: 'highlight', color: '#ffff00', - width: 5 + width: 5, + filter_properties: { class: 'major_rail' } } ] }; @@ -105,10 +107,10 @@ 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', + layer_type: 'place_label', action: 'hide' } ] @@ -124,10 +126,10 @@ 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', + layer_type: 'building', action: 'show' } ] @@ -145,13 +147,14 @@ 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', + 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'); @@ -185,13 +188,14 @@ 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', + 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'); @@ -220,7 +225,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', @@ -228,7 +233,8 @@ describe('StyleBuilderTool', () => { color: '#0099ff' }, { - layer_type: 'parks', + layer_type: 'landuse', + filter_properties: { class: 'park' }, action: 'color', color: '#00ff00' } @@ -246,24 +252,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 +283,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 +295,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, @@ -300,19 +307,20 @@ 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 () => { const input: StyleBuilderToolInput = { style_name: 'Custom Filter Test', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { - layer_type: 'motorways', + layer_type: 'road', action: 'color', color: '#ff0000', filter: ['==', ['get', 'class'], 'motorway'] @@ -329,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'); @@ -340,10 +348,11 @@ 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', + layer_type: 'road', + filter_properties: { class: 'motorway' }, action: 'color', color: '#ff0000', width: 3, @@ -376,10 +385,11 @@ 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', + layer_type: 'road', + filter_properties: { class: 'primary' }, action: 'color', color: '#000000', property_based: 'class', @@ -414,10 +424,10 @@ 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', + layer_type: 'building', action: 'color', color: '#808080', expression: [ @@ -453,10 +463,10 @@ 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', + layer_type: 'building', action: 'show', opacity: 0.8, zoom_based: true, @@ -517,7 +527,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 () => { @@ -558,13 +574,46 @@ 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', - base_style: 'streets-v12', + base_style: 'standard', layers: [ { - layer_type: 'motorways', + layer_type: 'road', action: 'color', color: '#ff0000', filter_properties: { @@ -592,10 +641,10 @@ 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', + layer_type: 'road', action: 'highlight', color: '#ff0000', filter_properties: { @@ -626,10 +675,10 @@ 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', + layer_type: 'admin', action: 'color', color: '#0000ff', filter_properties: { @@ -659,11 +708,308 @@ 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 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).toBeUndefined(); + }); + }); + + 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', + 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: 'landuse', + filter_properties: { class: 'park' }, + action: 'color', + color: '#00ff00', + slot: 'middle' + }, + { + layer_type: 'poi_label', + 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 + const waterLayer = style.layers.find( + (l: any) => l['source-layer'] === 'water' + ); + const parksLayer = style.layers.find( + (l: any) => + l['source-layer'] === 'landuse' && l.id.includes('class-park') + ); + const poiLayer = style.layers.find( + (l: any) => l['source-layer'] === 'poi_label' + ); + + 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 always default to Standard style when not specified', async () => { + const input: StyleBuilderToolInput = { + style_name: 'Default Style Test', + // Not specifying base_style - should default to 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 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'); + + // 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'); + }); + }); + + 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: 'landuse_overlay', + 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; + // 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```/); + 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: 'nonexistent', // 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( + 'Determined source layer "poi_label" from filter properties' + ); + }); + }); + 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', @@ -671,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' } ] 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,