We’re migrating an existing WPF desktop GIS (ThinkGeo.UI.Wpf, 15.0.0-beta136) to optional GPU
rendering via GpuBasemap + MapStyle + FeatureSourceVectorTileSource, following
HowDoISample.v15. The app’s cartography is stored as classic ThinkGeo styles
(AreaStyle, LineStyle, PointStyle, TextStyle) on ZoomLevelSet.ZoomLevel01, and we
translate those into code-built style layers (StyleLayer.Create*) at scene-build time.
That works well — polygons, lines, circular points, labels and GeoTIFFs all render. Thank you
for ClassicRasterTileSource in particular; serving an existing GdalRasterSource as tiles was
a one-liner.
Below are the places where we had to write adapter code or fall back to classic rendering, and
where a small addition on your side would remove that. Roughly in priority order for us.
1. Code-built fill layers can’t use fill-pattern
FillStyleLayer exposes HasFillPattern and FillPatternPixelRatio, and StyleImages
documents fill-pattern as reading from the registered images. But StyleLayer.CreateFill(...)
has no pattern parameter, and the ctor that takes the evaluators is internal. As far as we can
tell, a pattern fill is reachable only through a parsed style document, not through a code
layer.
This is what blocks polygon hatching for us: AreaStyle.CreateHatchStyle(GeoHatchStyle, ...)
layers fall back to classic for the whole scene. We can rasterize the hatch ourselves and
register it via MapStyle.Images — we just have no way to point a code-built fill layer at it.
Ask: a fillPattern (sprite name, or Func<Feature, double, string>) parameter on
StyleLayer.CreateFill.
2. Code-built symbol layers can’t use icon-image
Same shape. SymbolStyleLayer's internal constructor takes symbolIconIdEvaluator and
symbolIconEvaluator, and SymbolLayout carries the full icon half of the spec
(IconAnchor, IconOffsetX/Y, IconSize, IconAllowOverlap, IconTextFit, …). But
StyleLayer.CreateSymbol only takes text, paint, layout, filter, zoom bounds and
sortKey — no icon name.
We have nine non-circular PointSymbolType values (Square, Triangle, Cross, Diamond, Star, …).
Each is a small sprite we’d happily generate and register; without an icon selector on the code
path, every layer using one stays on the classic renderer.
Ask: expose the icon evaluator on StyleLayer.CreateSymbol, or make the
SymbolStyleLayer constructor public.
3. No bridge from classic styles to GPU paints
You own both sides of this conversion — AreaStyle/LineStyle/PointStyle/TextStyle and
FillPaint/LinePaint/CirclePaint/SymbolPaint/SymbolLayout — and every application
migrating an existing map will write the same translation we did. Ours is ~120 lines and it
already has judgement calls in it that we’d rather inherit than invent (see 4 and 5).
Ask, in rough order of usefulness:
- A
FeatureLayer-aware tile source, or aMapStyle.AddLayer(FeatureLayer), that reads the
layer’s ownZoomLevelSetand emits the equivalent style layers. - Failing that, converters:
FillPaint.FromAreaStyle(AreaStyle)and friends.
4. GeoPen.DashStyle → line-dasharray
LinePaint.Dash and DashArray are exactly what we need, but the mapping from
LineDashStyle.Dash / Dot / DashDot / DashDotDot to dash/gap run lengths is ours to
guess. We currently hardcode the GDI+ pen patterns (3,1 / 1,1 / 3,1,1,1 / 3,1,1,1,1,1), since
DashArray takes the same line-width units GDI+ does. That matches classic closely but it’s
an assumption about your classic renderer, not a documented equivalence.
Ask: a DashArray.FromLineDashStyle(LineDashStyle), or just documenting the intended
mapping. LineDashStyle.Custom has no GPU equivalent at all as far as we can see.
5. SymbolLayout takes a font family string, not a GeoFont
Our labels are stored as GeoFont(family, size, FontStyle) where the style carries Bold /
Italic / Underline / Strikeout. SymbolLayout takes fontFamily and size only, so weight and
slant are dropped on the GPU path. We didn’t want to guess whether the local rasterizer resolves
a font-stack name like "Arial Bold" the way a fontnik stack would.
Ask: a SymbolLayout overload taking GeoFont, or documentation of how FontFamily
is resolved when no glyph server is configured (SetGlyphs unset) and which stack names the
embedded Noto Sans + LocalFontsDirectory path understands.
6. AddStyleLayers stamps SourceId onto the tile source object
MapStyle.AddStyleLayers(IVectorTileSource, sourceId, ...) assigns
FeatureSourceVectorTileSource.SourceId = sourceId. So calling it twice with the same source
object and different ids re-points the earlier registration.
We hit this interleaving rasters with vectors while preserving layer order: a raster in the
middle of the stack splits the vector layers into runs, and each run therefore needs its own
FeatureSourceVectorTileSource rather than one shared source keyed by source-layer.
Ask: treat the source id as per-registration rather than as state on the source object —
or confirm that one source per run is the intended usage, in which case we’ll stop worrying
about it.
7. Can SetLayerVisibility address a code-built layer?
MapStyle.SetLayerVisibility(layerId, visible) scans the composed document for a layer with
that id. Code layers are inserted as placeholder code-slot-N entries, so the id we passed to
StyleLayer.CreateFill doesn’t appear there — but the method still raises
LayerVisibilityChanged with our id unconditionally, which suggests an attached map might apply
it anyway.
Question: is toggling a code-built layer’s visibility supported? Right now we rebuild the
whole scene for a visibility change, which costs a re-encode and a visible blink as new tiles
arrive. The same question applies to changing a code layer’s paint — is there a live restyle
path, or is scene replacement the intended model?
8. Scale ↔ zoom
Our layers carry min/max visibility as classic scale denominators; StyleLayer.MinZoom/MaxZoom
are MapLibre zooms. We avoided the conversion entirely (we rebuild the scene when a layer
crosses its threshold, reusing our own scale rule) because we couldn’t find a published
constant for the zoom↔scale relation this renderer uses, and a near-miss would show up as
layers appearing one zoom early or late versus the classic map.
Ask: a documented ZoomFromScale / ScaleFromZoom on the GPU side, or scale-based
overloads for the layer zoom bounds.
9. ClassicRasterTileSource and non-GDAL sources
The docs are clear that the source must answer in Web Mercator, and GdalRasterSource.WarpToWebMercator
handles that for GDAL-backed files. For other RasterSource implementations there’s no
equivalent switch. Setting RasterSource.ProjectionConverter converts the requested extent but
doesn’t resample, so anything but an axis-aligned match comes back skewed.
Question: is warping inside ClassicRasterTileSource (using the source’s own Projection)
something you’d consider, so that any classic raster source can be served? Today our
non-GDAL image layers (PNG/JPG/BMP through WpfRasterLayer) stay on the classic renderer.
10. Overlays above the GPU basemap
Selection, highlighting and edit previews are classic overlays in our app, and GpuBasemap sits
below the overlay stack, so those still work. But it does mean we can’t mix a GPU-rendered layer
above a classic overlay.
Question: is a GPU-rendered overlay (or a supported way to interleave GPU content with
LayerOverlays) on the roadmap?







