Skip to content

Commit f4f5e64

Browse files
committed
style helper tool
1 parent b150fe3 commit f4f5e64

4 files changed

Lines changed: 380 additions & 0 deletions

File tree

src/tools/__snapshots__/tool-naming-convention.test.ts.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ exports[`Tool Naming Convention should maintain consistent tool list (snapshot t
6262
"description": "Generate a comparison URL for comparing two Mapbox styles side-by-side",
6363
"toolName": "style_comparison_tool",
6464
},
65+
{
66+
"className": "StyleHelperTool",
67+
"description": "Interactive helper for creating custom Mapbox styles with specific features and colors",
68+
"toolName": "style_helper_tool",
69+
},
6570
{
6671
"className": "TilequeryTool",
6772
"description": "Query vector and raster data from Mapbox tilesets at geographic coordinates",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { z } from 'zod';
2+
3+
export const StyleHelperToolSchema = z.object({
4+
step: z
5+
.enum(['start', 'features', 'colors', 'generate'])
6+
.optional()
7+
.describe('Current step in the wizard'),
8+
name: z.string().optional().describe('Name for the style'),
9+
// Feature toggles
10+
show_pois: z.boolean().optional().describe('Show POI labels'),
11+
show_road_labels: z.boolean().optional().describe('Show road labels'),
12+
show_place_labels: z.boolean().optional().describe('Show city/town labels'),
13+
show_transit: z.boolean().optional().describe('Show transit features'),
14+
show_buildings: z.boolean().optional().describe('Show buildings'),
15+
show_parks: z.boolean().optional().describe('Show parks and green spaces'),
16+
// Colors
17+
road_color: z.string().optional().describe('Road color (hex)'),
18+
water_color: z.string().optional().describe('Water color (hex)'),
19+
building_color: z.string().optional().describe('Building color (hex)'),
20+
land_color: z.string().optional().describe('Land/background color (hex)'),
21+
park_color: z.string().optional().describe('Park color (hex)'),
22+
label_color: z.string().optional().describe('Label text color (hex)')
23+
});
24+
25+
export type StyleHelperToolInput = z.infer<typeof StyleHelperToolSchema>;
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
import { BaseTool } from '../BaseTool.js';
2+
import {
3+
StyleHelperToolSchema,
4+
type StyleHelperToolInput
5+
} from './StyleHelperTool.schema.js';
6+
7+
export class StyleHelperTool extends BaseTool<typeof StyleHelperToolSchema> {
8+
name = 'style_helper_tool';
9+
description =
10+
'Interactive helper for creating custom Mapbox styles with specific features and colors';
11+
12+
constructor() {
13+
super({ inputSchema: StyleHelperToolSchema });
14+
}
15+
16+
protected async execute(input: StyleHelperToolInput) {
17+
const step = input.step || 'start';
18+
19+
switch (step) {
20+
case 'start':
21+
return this.handleStart();
22+
case 'features':
23+
return this.handleFeatures(input);
24+
case 'colors':
25+
return this.handleColors(input);
26+
case 'generate':
27+
return this.handleGenerate(input);
28+
default:
29+
return this.handleStart();
30+
}
31+
}
32+
33+
private handleStart() {
34+
return {
35+
content: [
36+
{
37+
type: 'text' as const,
38+
text: `**Mapbox Style Helper - Initialized**
39+
40+
**Current step: 1 of 4**
41+
42+
**Waiting for:** Style name
43+
44+
---
45+
**Status: REQUIRES USER INPUT FOR NAME**`
46+
}
47+
],
48+
isError: false
49+
};
50+
}
51+
52+
private handleFeatures(input: StyleHelperToolInput) {
53+
if (!input.name) {
54+
return this.handleStart();
55+
}
56+
57+
return {
58+
content: [
59+
{
60+
type: 'text' as const,
61+
text: `**Style:** ${input.name}
62+
63+
**Current step: 2 of 4**
64+
65+
**Waiting for:** Feature toggles
66+
67+
**Available options:**
68+
• show_place_labels (true/false)
69+
• show_road_labels (true/false)
70+
• show_pois (true/false)
71+
• show_buildings (true/false)
72+
• show_parks (true/false)
73+
• show_transit (true/false)
74+
75+
---
76+
**Status: REQUIRES USER FEATURE SELECTION**`
77+
}
78+
],
79+
isError: false
80+
};
81+
}
82+
83+
private handleColors(input: StyleHelperToolInput) {
84+
if (!input.name) {
85+
return this.handleStart();
86+
}
87+
88+
const features = this.getFeatureSummary(input);
89+
90+
return {
91+
content: [
92+
{
93+
type: 'text' as const,
94+
text: `**Style:** ${input.name}
95+
**Features:** ${features}
96+
97+
**Current step: 3 of 4**
98+
99+
**Waiting for:** Color values
100+
101+
**Required:**
102+
• road_color (hex)
103+
• water_color (hex)
104+
• land_color (hex)
105+
• label_color (hex)
106+
107+
**Optional:**
108+
• building_color (hex)
109+
• park_color (hex)
110+
111+
---
112+
**Status: REQUIRES USER COLOR SELECTION**`
113+
}
114+
],
115+
isError: false
116+
};
117+
}
118+
119+
private handleGenerate(input: StyleHelperToolInput) {
120+
if (
121+
!input.name ||
122+
!input.road_color ||
123+
!input.water_color ||
124+
!input.land_color ||
125+
!input.label_color
126+
) {
127+
return {
128+
content: [
129+
{
130+
type: 'text' as const,
131+
text: 'Missing required colors. Please complete all color steps.'
132+
}
133+
],
134+
isError: true
135+
};
136+
}
137+
138+
const style = this.generateStyle(input);
139+
140+
return {
141+
content: [
142+
{
143+
type: 'text' as const,
144+
text: `**COMPLETED: Style Generated**
145+
146+
**Name:** ${input.name}
147+
148+
**Final Configuration:**
149+
• POIs: ${input.show_pois ? 'shown' : 'hidden'}
150+
• Road Labels: ${input.show_road_labels ? 'shown' : 'hidden'}
151+
• Place Labels: ${input.show_place_labels ? 'shown' : 'hidden'}
152+
• Transit: ${input.show_transit ? 'shown' : 'hidden'}
153+
• Buildings: ${input.show_buildings ? 'shown' : 'hidden'}
154+
• Parks: ${input.show_parks ? 'shown' : 'hidden'}
155+
156+
**Colors:**
157+
• Roads: ${input.road_color}
158+
• Water: ${input.water_color}
159+
• Buildings: ${input.building_color || '#e0e0e0'}
160+
• Land: ${input.land_color}
161+
• Parks: ${input.park_color || '#d0e5d0'}
162+
• Labels: ${input.label_color}
163+
164+
**Generated Style JSON:**
165+
\`\`\`json
166+
${JSON.stringify(style, null, 2)}
167+
\`\`\`
168+
169+
---
170+
**Status: STYLE GENERATION COMPLETE**`
171+
}
172+
],
173+
isError: false
174+
};
175+
}
176+
177+
private generateStyle(input: StyleHelperToolInput) {
178+
const layers: Record<string, unknown>[] = [
179+
// Background
180+
{
181+
id: 'land',
182+
type: 'background',
183+
paint: {
184+
'background-color': input.land_color
185+
}
186+
},
187+
// Water
188+
{
189+
id: 'water',
190+
type: 'fill',
191+
source: 'composite',
192+
'source-layer': 'water',
193+
paint: {
194+
'fill-color': input.water_color
195+
}
196+
}
197+
];
198+
199+
// Parks (if enabled)
200+
if (input.show_parks) {
201+
layers.push({
202+
id: 'landuse_park',
203+
type: 'fill',
204+
source: 'composite',
205+
'source-layer': 'landuse',
206+
filter: ['==', ['get', 'class'], 'park'],
207+
paint: {
208+
'fill-color': input.park_color || '#d0e5d0',
209+
'fill-opacity': 0.8
210+
}
211+
});
212+
}
213+
214+
// Roads - simplified with just two layers
215+
layers.push({
216+
id: 'road',
217+
type: 'line',
218+
source: 'composite',
219+
'source-layer': 'road',
220+
layout: {
221+
'line-cap': 'round',
222+
'line-join': 'round'
223+
},
224+
paint: {
225+
'line-color': input.road_color,
226+
'line-width': [
227+
'interpolate',
228+
['exponential', 1.5],
229+
['zoom'],
230+
5,
231+
0.5,
232+
18,
233+
20
234+
]
235+
}
236+
});
237+
238+
// Buildings (if enabled)
239+
if (input.show_buildings) {
240+
layers.push({
241+
id: 'building',
242+
type: 'fill',
243+
source: 'composite',
244+
'source-layer': 'building',
245+
minzoom: 14,
246+
paint: {
247+
'fill-color': input.building_color || '#e0e0e0',
248+
'fill-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 15, 1]
249+
}
250+
});
251+
}
252+
253+
// Place labels (if enabled)
254+
if (input.show_place_labels !== false) {
255+
// Default to true
256+
layers.push({
257+
id: 'place_label',
258+
type: 'symbol',
259+
source: 'composite',
260+
'source-layer': 'place_label',
261+
layout: {
262+
'text-field': ['get', 'name'],
263+
'text-font': ['DIN Pro Medium', 'Arial Unicode MS Regular'],
264+
'text-size': ['interpolate', ['linear'], ['zoom'], 8, 12, 16, 20]
265+
},
266+
paint: {
267+
'text-color': input.label_color,
268+
'text-halo-color': input.land_color,
269+
'text-halo-width': 1.5
270+
}
271+
});
272+
}
273+
274+
// Road labels (if enabled)
275+
if (input.show_road_labels) {
276+
const roadLabel: Record<string, unknown> = {
277+
id: 'road_label',
278+
type: 'symbol',
279+
source: 'composite',
280+
'source-layer': 'road',
281+
minzoom: 13,
282+
layout: {
283+
'symbol-placement': 'line',
284+
'text-field': ['get', 'name'],
285+
'text-font': ['DIN Pro Regular', 'Arial Unicode MS Regular'],
286+
'text-size': 12
287+
},
288+
paint: {
289+
'text-color': input.label_color,
290+
'text-halo-color': input.land_color,
291+
'text-halo-width': 1
292+
}
293+
};
294+
layers.push(roadLabel);
295+
}
296+
297+
// POI labels (if enabled)
298+
if (input.show_pois) {
299+
const poiLabel: Record<string, unknown> = {
300+
id: 'poi_label',
301+
type: 'symbol',
302+
source: 'composite',
303+
'source-layer': 'poi_label',
304+
minzoom: 13,
305+
layout: {
306+
'text-field': ['get', 'name'],
307+
'text-font': ['DIN Pro Regular', 'Arial Unicode MS Regular'],
308+
'text-size': 11
309+
},
310+
paint: {
311+
'text-color': input.label_color,
312+
'text-halo-color': input.land_color,
313+
'text-halo-width': 1
314+
}
315+
};
316+
layers.push(poiLabel);
317+
}
318+
319+
return {
320+
version: 8,
321+
name: input.name,
322+
metadata: {
323+
'mapbox:autocomposite': true
324+
},
325+
sources: {
326+
composite: {
327+
type: 'vector',
328+
url: 'mapbox://mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v8'
329+
}
330+
},
331+
sprite: 'mapbox://sprites/mapbox/streets-v12',
332+
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
333+
layers: layers
334+
};
335+
}
336+
337+
private getFeatureSummary(input: StyleHelperToolInput): string {
338+
const features = [];
339+
if (input.show_pois) features.push('POIs');
340+
if (input.show_road_labels) features.push('road labels');
341+
if (input.show_place_labels) features.push('place labels');
342+
if (input.show_transit) features.push('transit');
343+
if (input.show_buildings) features.push('buildings');
344+
if (input.show_parks) features.push('parks');
345+
346+
return features.length > 0 ? features.join(', ') : 'none selected yet';
347+
}
348+
}

0 commit comments

Comments
 (0)