ZoomLevelSet and ZoomLevel Guide¶
Namespace: ThinkGeo.Core
Applies to: All ThinkGeo products (WPF, WinForms, Blazor, MAUI, GIS Server, etc.)
Overview¶
ZoomLevelSet and ZoomLevel are the two classes that control when and how a layer's features are drawn at different map scales.
Every FeatureLayer has a ZoomLevelSet property. That set contains twenty named zoom levels — ZoomLevel01 through ZoomLevel20 — each of which holds styles and a cascade directive. When the map renders, it looks at the current scale, finds the active zoom level for that scale, and applies the styles attached to that level.
ZoomLevelSet— the container. Owns the twenty levels and defines the scale at which each one activates.ZoomLevel— a single scale band. Owns the styles (DefaultPointStyle,DefaultLineStyle,DefaultAreaStyle,DefaultTextStyle,CustomStyles) and theApplyUntilZoomLevelcascade.
This two-class design means you configure a layer's visual appearance entirely through layer.ZoomLevelSet.ZoomLevelNN.* — no subclassing or override needed.
How Scale Bands Work¶
Each ZoomLevel activates at a specific scale and stays active until the next defined level (or until overridden by ApplyUntilZoomLevel). The levels are numbered from largest scale (most zoomed out, ZoomLevel01) to smallest scale (most zoomed in, ZoomLevel20).
When the map renders a layer, it finds the highest-numbered level whose scale is greater than or equal to the current map scale. If that level has styles assigned, it uses them. If it does not, the renderer walks back down to find the nearest level that does — but only within the range allowed by ApplyUntilZoomLevel.
This means:
- A style set on
ZoomLevel01withApplyUntilZoomLevel = Level20is visible at every zoom level. - A style set on
ZoomLevel12withApplyUntilZoomLevel = Level13is visible only when the map is zoomed to levels 12 or 13. - A level with no
ApplyUntilZoomLevelset applies only at that exact level.
The Universal "Show at All Scales" Pattern¶
The single most common pattern in the codebase — used for InMemoryFeatureLayer, result layers, validation layers, and simple overlays — is to attach styles to ZoomLevel01 and cascade them all the way to ZoomLevel20:
layer.ZoomLevelSet.ZoomLevel01.DefaultPointStyle =
PointStyle.CreateSimpleCircleStyle(GeoColors.Blue, 8, GeoColors.Black);
layer.ZoomLevelSet.ZoomLevel01.DefaultLineStyle =
LineStyle.CreateSimpleLineStyle(GeoColors.Blue, 4, true);
layer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle =
AreaStyle.CreateSimpleAreaStyle(GeoColor.FromArgb(50, GeoColors.Blue), GeoColors.Blue);
layer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
You can assign any combination of the four default style slots on the same ZoomLevel. Only the style types that match the layer's geometry will be drawn.
Scale-Banded Styling¶
For data that should look different at different zoom levels — becoming more detailed as the user zooms in — assign styles to multiple zoom levels. The RenderBasedOnScales sample demonstrates this comprehensively across three different layer types.
Points: Progressive Size + Label¶
Hotels start as tiny dots at mid zoom and grow with labels as the user zooms closer:
// ZoomLevel 12-13 — small dot, no label
hotelsLayer.ZoomLevelSet.ZoomLevel12.DefaultPointStyle =
new PointStyle(PointSymbolType.Circle, 4, GeoBrushes.DarkRed, new GeoPen(GeoBrushes.White, 2));
hotelsLayer.ZoomLevelSet.ZoomLevel12.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level13;
// ZoomLevel 14-15 — medium dot, no label
hotelsLayer.ZoomLevelSet.ZoomLevel14.DefaultPointStyle =
new PointStyle(PointSymbolType.Circle, 8, GeoBrushes.DarkRed, new GeoPen(GeoBrushes.White, 2));
hotelsLayer.ZoomLevelSet.ZoomLevel14.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level15;
// ZoomLevel 16-20 — large dot + name label
hotelsLayer.ZoomLevelSet.ZoomLevel16.DefaultPointStyle =
new PointStyle(PointSymbolType.Circle, 12, GeoBrushes.DarkRed, new GeoPen(GeoBrushes.White, 2));
hotelsLayer.ZoomLevelSet.ZoomLevel16.DefaultTextStyle = new TextStyle(
"NAME", new GeoFont("Segoe UI", 12, DrawingFontStyles.Bold), GeoBrushes.DarkRed)
{
TextPlacement = TextPlacement.Lower,
YOffsetInPixel = 4,
HaloPen = new GeoPen(GeoBrushes.White, 2),
DrawingLevel = DrawingLevel.LabelLevel,
AllowLineCarriage = true
};
hotelsLayer.ZoomLevelSet.ZoomLevel16.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
Lines: Progressive Width + Labels¶
Streets use a different ZoomLevel entry for almost every level, with no ApplyUntilZoomLevel on single-level entries:
// ZoomLevel 10-11 — hairline
streetsLayer.ZoomLevelSet.ZoomLevel10.DefaultLineStyle =
new LineStyle(new GeoPen(GeoBrushes.LightGray, 1));
streetsLayer.ZoomLevelSet.ZoomLevel10.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level11;
// ZoomLevel 12 — slightly thicker (no ApplyUntilZoomLevel — active only at level 12)
streetsLayer.ZoomLevelSet.ZoomLevel12.DefaultLineStyle =
new LineStyle(new GeoPen(GeoBrushes.LightGray, 2), new GeoPen(GeoBrushes.White, 1));
// ZoomLevel 13 — thicker + label appears
streetsLayer.ZoomLevelSet.ZoomLevel13.DefaultLineStyle =
new LineStyle(new GeoPen(GeoBrushes.LightGray, 4), new GeoPen(GeoBrushes.White, 2));
streetsLayer.ZoomLevelSet.ZoomLevel13.DefaultTextStyle = new TextStyle(
"FULL_NAME", new GeoFont("Segoe UI", 6, DrawingFontStyles.Bold), GeoBrushes.Black)
{
SplineType = SplineType.StandardSplining,
HaloPen = new GeoPen(GeoBrushes.White, 2),
DrawingLevel = DrawingLevel.LabelLevel,
GridSize = 20
};
// ZoomLevel 17-18 — wide road casing
streetsLayer.ZoomLevelSet.ZoomLevel17.DefaultLineStyle =
new LineStyle(new GeoPen(GeoBrushes.Gray, 13), new GeoPen(GeoBrushes.White, 12));
streetsLayer.ZoomLevelSet.ZoomLevel17.DefaultTextStyle = new TextStyle(
"FULL_NAME", new GeoFont("Segoe UI", 10, DrawingFontStyles.Bold), GeoBrushes.Black)
{
SplineType = SplineType.StandardSplining,
HaloPen = new GeoPen(GeoBrushes.White, 2),
DrawingLevel = DrawingLevel.LabelLevel,
GridSize = 20
};
streetsLayer.ZoomLevelSet.ZoomLevel17.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level18;
Areas: Fill + Label¶
Parks show fill at all scales but only add a label at closer zoom levels:
// ZoomLevel 10-14 — fill only
parksLayer.ZoomLevelSet.ZoomLevel10.DefaultAreaStyle =
new AreaStyle(GeoBrushes.PastelGreen);
parksLayer.ZoomLevelSet.ZoomLevel10.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level14;
// ZoomLevel 15-20 — fill + park name label
parksLayer.ZoomLevelSet.ZoomLevel15.DefaultAreaStyle =
new AreaStyle(GeoBrushes.PastelGreen);
parksLayer.ZoomLevelSet.ZoomLevel15.DefaultTextStyle = new TextStyle(
"NAME", new GeoFont("Segoe UI", 12, DrawingFontStyles.Bold), GeoBrushes.DarkGreen)
{
FittingPolygon = false,
HaloPen = new GeoPen(GeoBrushes.White, 2),
DrawingLevel = DrawingLevel.LabelLevel,
AllowLineCarriage = true,
FittingPolygonFactor = 1
};
parksLayer.ZoomLevelSet.ZoomLevel15.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
ZoomLevel.DefaultTextStyle Properties¶
When attaching a TextStyle to a ZoomLevel, several properties control label rendering quality. All are confirmed in the RenderBasedOnScales sample:
| Property | Type | Description |
|---|---|---|
SplineType | SplineType | How the label follows a curved line. StandardSplining bends labels along roads. |
HaloPen | GeoPen | A contrasting halo/outline drawn behind the text for legibility. |
DrawingLevel | DrawingLevel | Render pass. Use DrawingLevel.LabelLevel so labels draw on top of geometry. |
GridSize | int | Minimum pixel distance between repeated labels on the same feature. |
TextPlacement | TextPlacement | Where to position a label relative to a point (Lower, Upper, etc.). |
YOffsetInPixel | float | Vertical nudge in pixels from the anchor point. |
AllowLineCarriage | bool | Whether to wrap long labels onto multiple lines. |
FittingPolygon | bool | Whether to constrain the label inside the polygon boundary. |
FittingPolygonFactor | float | Scale factor for the fitting polygon region. |
CustomStyles¶
DefaultPointStyle, DefaultLineStyle, DefaultAreaStyle, and DefaultTextStyle are convenience slots for the most common case of one style per geometry type. When you need multiple styles on the same layer (e.g., a circle plus a halo, or a casing plus a fill), use CustomStyles instead.
var pointStyle = new PointStyle(PointSymbolType.Circle, 12, GeoBrushes.Blue,
new GeoPen(GeoBrushes.White, 2));
// Always clear before adding to avoid accumulating styles on re-runs
hotelsLayer.ZoomLevelSet.ZoomLevel01.CustomStyles.Clear();
hotelsLayer.ZoomLevelSet.ZoomLevel01.CustomStyles.Add(pointStyle);
hotelsLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
CustomStyles is a collection, so you can add as many styles as needed. They render in order — first added, first drawn. DefaultPointStyle and CustomStyles are independent; you can use both on the same level and they will both render.
Always call
CustomStyles.Clear()beforeCustomStyles.Add()when styles are applied in a method that might be called more than once (e.g., a button click handler or theme switcher). Otherwise styles accumulate with each call.
DrawingLevel¶
DrawingLevel controls which rendering pass a style draws in. It is available on both geometry styles and text styles, and is set directly on the style object, not on the ZoomLevel:
// Geometry drawn in a high pass — renders on top of lower-level geometry
cityLimits.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle.DrawingLevel = DrawingLevel.LevelFour;
// Labels always use LabelLevel to ensure they draw above all geometry
streetsLayer.ZoomLevelSet.ZoomLevel13.DefaultTextStyle.DrawingLevel = DrawingLevel.LabelLevel;
The draw passes in ascending order are LevelOne, LevelTwo, LevelThree, LevelFour, then LabelLevel on top. Use LevelFour for boundary or overlay geometry that should appear above normal fill layers. Use LabelLevel for all text.
Using ZoomLevelSet as a Scale Reference¶
ZoomLevelSet is not only used on layers — it can be instantiated standalone as a reference for named scale values. This is the canonical way to snap the map to a well-known scale by name rather than by raw number:
// Snap to a specific named zoom level after a query
var standardZoomLevelSet = new ZoomLevelSet();
// Prevent zooming in closer than ZoomLevel18 on a point result
if (MapView.CurrentScale < standardZoomLevelSet.ZoomLevel18.Scale)
await MapView.ZoomToAsync(standardZoomLevelSet.ZoomLevel18.Scale);
// Jump to a fixed zoom level to show a route result
await MapView.ZoomToAsync(standardZoomLevelSet.ZoomLevel13.Scale);
new ZoomLevelSet() constructs the default set of 20 standard web-map zoom levels. Each level's .Scale property gives you the scale denominator for that level in the standard tile grid. This lets you write readable, maintainable navigation code without hard-coding raw scale numbers.
ApplyUntilZoomLevel Reference¶
ApplyUntilZoomLevel is an enum. Its values map directly to the named zoom levels:
ApplyUntilZoomLevel.Level01 // apply only at the level it is set on (or cascade up to Level01)
ApplyUntilZoomLevel.Level13 // active from the set level up through Level13
ApplyUntilZoomLevel.Level20 // active from the set level all the way to the most zoomed-in level
Setting ApplyUntilZoomLevel on a level means: "use this level's styles for all zoom levels from here up to (and including) the specified level." If ApplyUntilZoomLevel is not set on a level, that level's styles are active only at the exact scale that level represents.
Common Pitfalls¶
1. Not Setting ApplyUntilZoomLevel — Layer Disappears When Zooming¶
The most common beginner mistake. If you set styles on ZoomLevel01 but do not set ApplyUntilZoomLevel, your layer will only be visible when the map is at the exact scale of ZoomLevel01 (fully zoomed out). At any other scale, nothing renders.
Always pair a style assignment with ApplyUntilZoomLevel:
// WRONG — only visible at one specific scale
layer.ZoomLevelSet.ZoomLevel01.DefaultPointStyle = PointStyle.CreateSimpleCircleStyle(GeoColors.Red, 8);
// CORRECT — visible at all scales
layer.ZoomLevelSet.ZoomLevel01.DefaultPointStyle = PointStyle.CreateSimpleCircleStyle(GeoColors.Red, 8);
layer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
2. Gaps Between Scale Bands¶
When defining multiple scale bands, make sure each band ends where the next begins. A gap between ApplyUntilZoomLevel.Level13 on ZoomLevel12 and ZoomLevel14 means the layer is invisible between levels 13 and 14. Check that your bands tile contiguously:
// Band 1: levels 12-13
layer.ZoomLevelSet.ZoomLevel12.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level13;
// Band 2: levels 14-15 (starts immediately where band 1 ends)
layer.ZoomLevelSet.ZoomLevel14.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level15;
// Band 3: levels 16-20 (starts immediately where band 2 ends)
layer.ZoomLevelSet.ZoomLevel16.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
3. Accumulating CustomStyles¶
If your style-setup code runs more than once (mode toggles, theme changes, re-initialization), CustomStyles.Add() will stack styles on top of each other rather than replacing them. Always call CustomStyles.Clear() immediately before Add().
4. Setting DrawingLevel on the ZoomLevel Instead of the Style¶
DrawingLevel is a property of the style object itself (DefaultAreaStyle.DrawingLevel, DefaultTextStyle.DrawingLevel), not of the ZoomLevel. There is no ZoomLevel.DrawingLevel property.
Reference Table¶
ZoomLevel Properties¶
| Property | Type | Description |
|---|---|---|
DefaultPointStyle | PointStyle | Style applied to point geometry at this level. |
DefaultLineStyle | LineStyle | Style applied to line geometry at this level. |
DefaultAreaStyle | AreaStyle | Style applied to area/polygon geometry at this level. |
DefaultTextStyle | TextStyle | Label style applied at this level. |
CustomStyles | Collection<Style> | Additional styles drawn in order; use when more than one style is needed per geometry type. |
ApplyUntilZoomLevel | ApplyUntilZoomLevel | Cascades this level's styles up through the specified level number. |
Scale | double | The scale denominator at which this zoom level activates. Read-only. |
ZoomLevelSet Named Levels¶
| Property | Typical Scale (web Mercator) |
|---|---|
ZoomLevel01 | ~591,657,550 (world view) |
ZoomLevel05 | ~36,978,595 |
ZoomLevel10 | ~1,155,580 |
ZoomLevel13 | ~144,448 |
ZoomLevel15 | ~36,112 |
ZoomLevel16 | ~18,056 |
ZoomLevel18 | ~4,514 |
ZoomLevel20 | ~1,128 (street level) |
Exact scale values depend on the tile grid in use. Use
new ZoomLevelSet().ZoomLevelNN.Scaleto read the precise value at runtime rather than hard-coding numbers.