Skip to content

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 the ApplyUntilZoomLevel cascade.

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 ZoomLevel01 with ApplyUntilZoomLevel = Level20 is visible at every zoom level.
  • A style set on ZoomLevel12 with ApplyUntilZoomLevel = Level13 is visible only when the map is zoomed to levels 12 or 13.
  • A level with no ApplyUntilZoomLevel set 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() before CustomStyles.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.Scale to read the precise value at runtime rather than hard-coding numbers.