Skip to content

Commit e52b701

Browse files
committed
Fix issue #141: Add Google Styling Wizard JSON support to StaticMapRequest
This PR adds comprehensive support for Google Maps Static API styling, addressing GitHub issue #141 where users wanted to use Google Styling Wizard JSON output with StaticMapRequest. ## Key Features ### 🎨 Google Styling Wizard JSON Support - **MapStyleHelper.FromJson()** - Parse JSON directly from Google Styling Wizard - **Comprehensive JSON parsing** - Supports all styler properties (color, visibility, lightness, saturation, gamma, hue, weight) - **Error handling** - Proper validation and exception handling for malformed JSON ### 🛠️ Fluent Builder API - **MapStyleBuilder** - Type-safe, IntelliSense-enabled fluent API for creating styles - **Method chaining** - Clean, readable syntax: `.AddStyle().WithColor().WithVisibility().Build()` - **Multiple creation methods** - Support for feature-only, element-only, and combined styling ### 📊 Complete Feature Coverage - **All Google Maps feature types** - roads, water, poi, administrative, transit, and **new landscape types** - **All element types** - geometry, labels, with sub-types (fill, stroke, text, icon) - **All styler properties** - Complete implementation matching Google's Static Maps API ### 🔧 Enhanced StaticMapRequest - **New Styles property** - `IList<MapStyleRule> Styles` for multiple style rules - **Backward compatible** - Existing Style property still works - **Correct URL generation** - Proper pipe-separated format as required by Google Maps API ## Usage Examples ### JSON Approach (from GitHub issue #141) ```csharp string googleJson = @"[{""elementType"": ""geometry"", ""stylers"": [{""color"": ""#f5f5f5""}]}]"; var request = new StaticMapRequest(location, zoom, size) { Styles = MapStyleHelper.FromJson(googleJson) }; ``` ### Builder Approach (recommended) ```csharp var styles = MapStyleBuilder.Create() .AddElementStyle(MapElementType.Geometry).WithColor("#f5f5f5").And() .AddElementStyle(MapElementType.LabelsIcon).WithVisibility(MapVisibility.Off) .Build(); var request = new StaticMapRequest(location, zoom, size) { Styles = styles }; ``` ## Testing - **11 comprehensive test cases** covering JSON parsing, builder API, URL generation, and edge cases - **All tests passing** (92/92 total test suite) - **Color format support** - Both `#` (Google Styling Wizard) and `0x` (Google documented) formats ## Implementation Details - **Proper URL encoding** - Generates correct `style=feature:type|element:type|property:value` format - **Multiple style support** - Each rule generates separate `style=` parameter as required - **Landscape feature types added** - Missing landscape, landscape.man_made, landscape.natural, etc. - **Complete documentation** - XML docs, examples, and comprehensive test coverage Fixes #141
1 parent 9ea17f4 commit e52b701

13 files changed

+1620
-8
lines changed
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using NUnit.Framework;
4+
using GoogleMapsApi.StaticMaps;
5+
using GoogleMapsApi.StaticMaps.Entities;
6+
using GoogleMapsApi.StaticMaps.Enums;
7+
using GoogleMapsApi.Entities.Common;
8+
9+
namespace GoogleMapsApi.Test
10+
{
11+
/// <summary>
12+
/// Tests for Google Styling Wizard functionality (GitHub issue #141)
13+
/// </summary>
14+
[TestFixture]
15+
public class StaticMapGoogleStylingTests
16+
{
17+
[Test]
18+
public void TestGoogleStylingJsonParsing()
19+
{
20+
// Test JSON from GitHub issue #141
21+
string testJson = @"[
22+
{
23+
""elementType"": ""geometry"",
24+
""stylers"": [
25+
{
26+
""color"": ""#f5f5f5""
27+
}
28+
]
29+
},
30+
{
31+
""elementType"": ""labels.icon"",
32+
""stylers"": [
33+
{
34+
""visibility"": ""off""
35+
}
36+
]
37+
}
38+
]";
39+
40+
// Test JSON parsing
41+
var styles = MapStyleHelper.FromJson(testJson);
42+
43+
Assert.That(styles.Count, Is.EqualTo(2), "Should parse 2 style rules");
44+
45+
// Verify first rule
46+
var firstRule = styles[0];
47+
Assert.That(firstRule.ElementType, Is.EqualTo("geometry"));
48+
Assert.That(firstRule.Stylers.Count, Is.EqualTo(1));
49+
Assert.That(firstRule.Stylers[0].Color, Is.EqualTo("#f5f5f5"));
50+
51+
// Verify second rule
52+
var secondRule = styles[1];
53+
Assert.That(secondRule.ElementType, Is.EqualTo("labels.icon"));
54+
Assert.That(secondRule.Stylers.Count, Is.EqualTo(1));
55+
Assert.That(secondRule.Stylers[0].Visibility, Is.EqualTo("off"));
56+
}
57+
58+
[Test]
59+
public void TestStaticMapRequestWithGoogleStyling()
60+
{
61+
// Test JSON from GitHub issue #141
62+
string testJson = @"[
63+
{
64+
""elementType"": ""geometry"",
65+
""stylers"": [
66+
{
67+
""color"": ""#f5f5f5""
68+
}
69+
]
70+
},
71+
{
72+
""elementType"": ""labels.icon"",
73+
""stylers"": [
74+
{
75+
""visibility"": ""off""
76+
}
77+
]
78+
}
79+
]";
80+
81+
var styles = MapStyleHelper.FromJson(testJson);
82+
83+
var request = new StaticMapRequest(new Location(40.714728, -73.998672), 12, new ImageSize(800, 600))
84+
{
85+
ApiKey = "test_key",
86+
Styles = styles
87+
};
88+
89+
var staticMapsEngine = new StaticMapsEngine();
90+
string mapUrl = staticMapsEngine.GenerateStaticMapURL(request);
91+
92+
// Debug output
93+
Console.WriteLine($"Generated URL: {mapUrl}");
94+
95+
// Verify the URL contains the expected style parameters (URL-encoded)
96+
Assert.That(mapUrl.Contains("style=element%3Ageometry%7Ccolor%3A%23f5f5f5"), Is.True,
97+
"URL should contain geometry style");
98+
Assert.That(mapUrl.Contains("style=element%3Alabels.icon%7Cvisibility%3Aoff"), Is.True,
99+
"URL should contain labels.icon style");
100+
Assert.That(mapUrl.Contains("key=test_key"), Is.True,
101+
"URL should contain API key");
102+
}
103+
104+
[Test]
105+
public void TestBuilderStyleCreation()
106+
{
107+
// Create styles using the fluent API
108+
var styles = MapStyleBuilder.Create()
109+
.AddElementStyle(MapElementType.Geometry)
110+
.WithColor("#f5f5f5")
111+
.And()
112+
.AddElementStyle(MapElementType.LabelsIcon)
113+
.WithVisibility(MapVisibility.Off)
114+
.Build();
115+
116+
var request = new StaticMapRequest(new Location(40.714728, -73.998672), 12, new ImageSize(800, 600))
117+
{
118+
ApiKey = "test_key",
119+
Styles = styles
120+
};
121+
122+
var staticMapsEngine = new StaticMapsEngine();
123+
string mapUrl = staticMapsEngine.GenerateStaticMapURL(request);
124+
125+
// Debug output
126+
Console.WriteLine($"Generated URL: {mapUrl}");
127+
128+
// Verify the URL contains the expected style parameters (URL-encoded)
129+
Assert.That(mapUrl.Contains("style=element%3Ageometry%7Ccolor%3A%23f5f5f5"), Is.True,
130+
"URL should contain geometry style");
131+
Assert.That(mapUrl.Contains("style=element%3Alabels.icon%7Cvisibility%3Aoff"), Is.True,
132+
"URL should contain labels.icon style");
133+
}
134+
135+
[Test]
136+
public void TestInvalidJsonThrowsException()
137+
{
138+
string invalidJson = "invalid json";
139+
140+
Assert.Throws<ArgumentException>(() => MapStyleHelper.FromJson(invalidJson));
141+
}
142+
143+
[Test]
144+
public void TestEmptyJsonThrowsException()
145+
{
146+
Assert.Throws<ArgumentException>(() => MapStyleHelper.FromJson(""));
147+
Assert.Throws<ArgumentException>(() => MapStyleHelper.FromJson(null));
148+
}
149+
150+
[Test]
151+
public void TestColorFormatHandling()
152+
{
153+
// Test both # and 0x color formats
154+
var stylesWithHashColor = new List<MapStyleRule>
155+
{
156+
new MapStyleRule
157+
{
158+
ElementType = "geometry",
159+
Stylers = new List<MapStyleStyler>
160+
{
161+
new MapStyleStyler { Color = "#f5f5f5" }
162+
}
163+
}
164+
};
165+
166+
var stylesWithHexColor = new List<MapStyleRule>
167+
{
168+
new MapStyleRule
169+
{
170+
ElementType = "geometry",
171+
Stylers = new List<MapStyleStyler>
172+
{
173+
new MapStyleStyler { Color = "0xf5f5f5" }
174+
}
175+
}
176+
};
177+
178+
var requestWithHash = new StaticMapRequest(new Location(40.714728, -73.998672), 12, new ImageSize(800, 600))
179+
{
180+
ApiKey = "test_key",
181+
Styles = stylesWithHashColor
182+
};
183+
184+
var requestWithHex = new StaticMapRequest(new Location(40.714728, -73.998672), 12, new ImageSize(800, 600))
185+
{
186+
ApiKey = "test_key",
187+
Styles = stylesWithHexColor
188+
};
189+
190+
var staticMapsEngine = new StaticMapsEngine();
191+
string hashUrl = staticMapsEngine.GenerateStaticMapURL(requestWithHash);
192+
string hexUrl = staticMapsEngine.GenerateStaticMapURL(requestWithHex);
193+
194+
Console.WriteLine($"Hash color URL: {hashUrl}");
195+
Console.WriteLine($"Hex color URL: {hexUrl}");
196+
197+
// Both should work - verify URL contains color parameter
198+
Assert.That(hashUrl.Contains("color%3A%23f5f5f5") || hashUrl.Contains("color:#f5f5f5"), Is.True,
199+
"Hash format color should be present in URL");
200+
Assert.That(hexUrl.Contains("color%3A0xf5f5f5") || hexUrl.Contains("color:0xf5f5f5"), Is.True,
201+
"Hex format color should be present in URL");
202+
}
203+
204+
[Test]
205+
public void TestFluentApiStyleCreation()
206+
{
207+
// Test the new fluent API
208+
var styles = MapStyleBuilder.Create()
209+
.AddStyle(MapFeatureType.Water, MapElementType.Geometry)
210+
.WithColor("#e9e9e9")
211+
.WithLightness(17)
212+
.And()
213+
.AddStyle(MapFeatureType.Road, MapElementType.Geometry)
214+
.WithColor("#ffffff")
215+
.WithLightness(16)
216+
.And()
217+
.AddElementStyle(MapElementType.LabelsIcon)
218+
.WithVisibility(MapVisibility.Off)
219+
.Build();
220+
221+
Assert.That(styles.Count, Is.EqualTo(3), "Should create 3 style rules");
222+
223+
// Verify first rule (water)
224+
var waterRule = styles[0];
225+
Assert.That(waterRule.FeatureType, Is.EqualTo("water"));
226+
Assert.That(waterRule.ElementType, Is.EqualTo("geometry"));
227+
Assert.That(waterRule.Stylers.Count, Is.EqualTo(2));
228+
Assert.That(waterRule.Stylers[0].Color, Is.EqualTo("#e9e9e9"));
229+
Assert.That(waterRule.Stylers[1].Lightness, Is.EqualTo(17));
230+
231+
// Verify second rule (road)
232+
var roadRule = styles[1];
233+
Assert.That(roadRule.FeatureType, Is.EqualTo("road"));
234+
Assert.That(roadRule.ElementType, Is.EqualTo("geometry"));
235+
Assert.That(roadRule.Stylers.Count, Is.EqualTo(2));
236+
Assert.That(roadRule.Stylers[0].Color, Is.EqualTo("#ffffff"));
237+
Assert.That(roadRule.Stylers[1].Lightness, Is.EqualTo(16));
238+
239+
// Verify third rule (labels.icon)
240+
var labelsRule = styles[2];
241+
Assert.That(labelsRule.FeatureType, Is.Null);
242+
Assert.That(labelsRule.ElementType, Is.EqualTo("labels.icon"));
243+
Assert.That(labelsRule.Stylers.Count, Is.EqualTo(1));
244+
Assert.That(labelsRule.Stylers[0].Visibility, Is.EqualTo("off"));
245+
}
246+
247+
[Test]
248+
public void TestLandscapeFeatureTypes()
249+
{
250+
// Test the new landscape feature types
251+
var styles = MapStyleBuilder.Create()
252+
.AddStyle(MapFeatureType.Landscape, MapElementType.Geometry)
253+
.WithColor("#f0f0f0")
254+
.And()
255+
.AddStyle(MapFeatureType.LandscapeManMade, MapElementType.Geometry)
256+
.WithColor("#e0e0e0")
257+
.And()
258+
.AddStyle(MapFeatureType.LandscapeNatural, MapElementType.Geometry)
259+
.WithColor("#d0d0d0")
260+
.And()
261+
.AddStyle(MapFeatureType.LandscapeNaturalLandcover, MapElementType.GeometryFill)
262+
.WithColor("#c0c0c0")
263+
.And()
264+
.AddStyle(MapFeatureType.LandscapeNaturalTerrain, MapElementType.GeometryStroke)
265+
.WithColor("#b0b0b0")
266+
.Build();
267+
268+
Assert.That(styles.Count, Is.EqualTo(5), "Should create 5 landscape style rules");
269+
270+
// Verify landscape feature types are correctly mapped
271+
Assert.That(styles[0].FeatureType, Is.EqualTo("landscape"));
272+
Assert.That(styles[1].FeatureType, Is.EqualTo("landscape.man_made"));
273+
Assert.That(styles[2].FeatureType, Is.EqualTo("landscape.natural"));
274+
Assert.That(styles[3].FeatureType, Is.EqualTo("landscape.natural.landcover"));
275+
Assert.That(styles[4].FeatureType, Is.EqualTo("landscape.natural.terrain"));
276+
}
277+
278+
[Test]
279+
public void TestBuilderWithMultipleStylers()
280+
{
281+
// Test adding multiple stylers to a single rule
282+
var styles = MapStyleBuilder.Create()
283+
.AddStyle(MapFeatureType.Water, MapElementType.Geometry)
284+
.WithColor("#1976d2")
285+
.WithLightness(25)
286+
.WithSaturation(30)
287+
.WithGamma(1.2f)
288+
.Build();
289+
290+
Assert.That(styles.Count, Is.EqualTo(1));
291+
292+
var rule = styles[0];
293+
Assert.That(rule.Stylers.Count, Is.EqualTo(4), "Should have 4 stylers");
294+
Assert.That(rule.Stylers[0].Color, Is.EqualTo("#1976d2"));
295+
Assert.That(rule.Stylers[1].Lightness, Is.EqualTo(25));
296+
Assert.That(rule.Stylers[2].Saturation, Is.EqualTo(30));
297+
Assert.That(rule.Stylers[3].Gamma, Is.EqualTo(1.2f));
298+
}
299+
300+
[Test]
301+
public void TestBuilderComparedToJson()
302+
{
303+
// GitHub issue #141 JSON example
304+
string testJson = @"[
305+
{
306+
""elementType"": ""geometry"",
307+
""stylers"": [
308+
{
309+
""color"": ""#f5f5f5""
310+
}
311+
]
312+
},
313+
{
314+
""elementType"": ""labels.icon"",
315+
""stylers"": [
316+
{
317+
""visibility"": ""off""
318+
}
319+
]
320+
}
321+
]";
322+
323+
// Parse JSON styles
324+
var jsonStyles = MapStyleHelper.FromJson(testJson);
325+
326+
// Create equivalent styles using builder
327+
var builderStyles = MapStyleBuilder.Create()
328+
.AddElementStyle(MapElementType.Geometry)
329+
.WithColor("#f5f5f5")
330+
.And()
331+
.AddElementStyle(MapElementType.LabelsIcon)
332+
.WithVisibility(MapVisibility.Off)
333+
.Build();
334+
335+
// Compare results
336+
Assert.That(builderStyles.Count, Is.EqualTo(jsonStyles.Count));
337+
338+
for (int i = 0; i < jsonStyles.Count; i++)
339+
{
340+
Assert.That(builderStyles[i].ElementType, Is.EqualTo(jsonStyles[i].ElementType));
341+
Assert.That(builderStyles[i].FeatureType, Is.EqualTo(jsonStyles[i].FeatureType));
342+
Assert.That(builderStyles[i].Stylers.Count, Is.EqualTo(jsonStyles[i].Stylers.Count));
343+
344+
// Compare first styler (both should have only one styler each)
345+
if (builderStyles[i].Stylers.Count > 0 && jsonStyles[i].Stylers.Count > 0)
346+
{
347+
Assert.That(builderStyles[i].Stylers[0].Color, Is.EqualTo(jsonStyles[i].Stylers[0].Color));
348+
Assert.That(builderStyles[i].Stylers[0].Visibility, Is.EqualTo(jsonStyles[i].Stylers[0].Visibility));
349+
}
350+
}
351+
352+
Console.WriteLine("Builder and JSON approaches produce equivalent results");
353+
}
354+
}
355+
}

0 commit comments

Comments
 (0)