DHI.Services.GIS (Maps) — Internal Developer Guide¶
Package(s):
DHI.Services.GIS(core services, sources, caching, styles)- Optional: your provider assemblies that implement
IMapSource/IGroupedMapSource - Optional:
SkiaSharpconsumers (for post-processing, saving PNGs/JPEGs, etc.)
This doc is for internal DHI developers consuming the NuGet packages. It explains what the module does, how to wire it in your host, how to call it, and how to implement your own map sources.
1) What this module gives you¶
- A rendering service (
MapService) that turns a data source (IMapSource) + style into raster maps (SKBitmap). - A group-aware service (
GroupedMapService) overLayerentities grouped by name, with the same map rendering capabilities + aGetStreampassthrough. - Temporal support: ask a source for the available
DateTimesteps; render individual frames or a stack keyed by time. - Styling via
MapStyle:- Styles can be provided from a repository (
MapStyleService+IMapStyleRepository) or directly as a style code string (no repo required).
- Styles can be provided from a repository (
- Caching decorators ready to use:
CachedMapSource(in-memory) andFileCachedMapSource(file-backed cache), pluggable through connection objects.
- Connection objects to instantiate services by type name + connection string from configuration files (no compile-time coupling).
Rendering uses SkiaSharp. Returned bitmaps (
SKBitmap) are IDisposable — your code must dispose them when done.
2) Core concepts¶
2.1 IMapSource (what renders pixels)¶
You provide or configure a concrete IMapSource that knows how to paint a bitmap for a requested:
- style (as a resolved
MapStyle, not a string), - CRS,
BoundingBox(map extent),- pixel size,
sourceId(e.g., path to a dataset, dataset id),- optional
DateTime(for time-varying datasets), - optional
item(e.g., variable/layer/code), Parameters(free-form key/value hints).
public interface IMapSource
{
SKBitmap GetMap(MapStyle style, string crs, BoundingBox bbox, int width, int height,
string sourceId, DateTime? dateTime, string item, Parameters parameters);
SortedDictionary<DateTime, SKBitmap> GetMaps(MapStyle style, BoundingBox bbox, SKSizeI size,
Dictionary<DateTime, string> timeSteps,
string item, Parameters parameters);
SortedSet<DateTime> GetDateTimes(string id);
SortedSet<DateTime> GetDateTimes(string id, DateRange range);
}
The base class BaseMapSource already implements GetMaps(id→bitmap) with a parallel fan-out and then sorts the frames.
CRS: the caching decorators support EPSG:3857 only (Web Mercator). If you pass other CRSs into the cached sources they will throw. For non-cached pipelines, your
IMapSourcedecides what to support.
2.2 MapService (what you call)¶
A thin facade that:
- Resolves the style string to a
MapStyle(either viaMapStyleServicerepo, or treats the input string as a StyleCode if no repo is configured). - Delegates to your configured
IMapSource.
var map = mapService.GetMap(
style: "rain: #0000FF,#00FFFF,#00FF00,#FFFF00,#FF0000", // repo id or style-code
crs: "EPSG:3857",
boundingBox: bbox, width: 1024, height: 768,
sourceId: "...", dateTime: null, item: "hs", parameters: Parameters.Empty);
2.3 GroupedMapService (layers + groups)¶
Wraps a IGroupedMapSource (repository of Layer entities with .Group) + a MapService instance:
- CRUD/lookup through
BaseGroupedDiscreteService<Layer, string>, - Same rendering calls as
MapService, GetStream(id)for streamable artifacts (e.g., export file of a layer). Throws 404 (KeyNotFound) if the stream is missing.
2.4 Styles (MapStyle, Palette)¶
- A
MapStyleis either a StyleCode (StyleCodestring) or a file (StyleFile) defining bands with colors and labels. - The
Paletteclass parses the StyleCode at runtime. It supports:- Single threshold:
10:#00FF00 - Stepped with ^ (start^step):
0^2:#0000FF,#00FFFF,#00FF00,#FFFF00,#FF0000- thresholds: 0,2,4,6,8 for 5 colors
- Range with \~ (start\~end):
0~10:#0000FF,#00FFFF,#00FF00,#FFFF00,#FF0000- thresholds linearly spaced across [0..10]
- Single threshold:
- You can chain bands with
|:0~10:#0000FF,#00FFFF|10~20:#00FF00,#FFFF00,#FF0000
- Legend helpers (
ToBitmapHorizontal/Vertical) let you render a legend bitmap.
3) Wiring it up¶
You can register services manually or via connection objects.
3.1 Manual registration¶
using DHI.Services;
using DHI.Services.GIS.Maps;
// Set [AppData] for [AppData] path resolution if you use it elsewhere
AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(env.ContentRootPath, "App_Data"));
// Example: provider-specific grouped source (you ship this in another package)
var sqliteConn = $"database={Path.Combine(appData, "MCSQLiteTest.sqlite")};dbflavour=SQLite";
ServiceLocator.Register(
new GroupedMapService(
groupedMapSource: new GroupedMapSource(sqliteConn), // your IGroupedMapSource
mapStyleService: new MapStyleService(
new MapStyleRepository(Path.Combine(appData, "styles.json"),
/* serializer options */ SerializerOptionsDefault.Options))),
"mc-groupedmap");
A
MapStyleServiceis optional. If omitted, thestyleargument you pass toGetMap(...)is interpreted as a StyleCode.
3.2 Connection-based registration (config-driven)¶
"mc-groupedmap": {
"$type": "DHI.Services.GIS.WebApi.GroupedMapServiceConnection, DHI.Services.GIS.WebApi",
"MapSourceConnectionString": "database=[AppData]MCSQLiteTest.sqlite;dbflavour=SQLite",
"MapSourceType": "DHI.Services.Provider.MCLite.GroupedMapSource, DHI.Services.Provider.MCLite",
"Name": "Local MCLite gis connection to workspace1",
"Id": "mc-groupedmap"
}
At runtime:
// e.g., read the JSON above, build the connection & create:
var conn = /* hydrate GroupedMapServiceConnection from config */;
var groupedMapService = (GroupedMapService)conn.Create();
ServiceLocator.Register(groupedMapService, conn.Id);
Available connection flavors:
MapServiceConnection(no groups)CachedMapServiceConnection(in-memory cache)FileCachedMapServiceConnection(file cache)GroupedMapServiceConnection(for grouped layers)
4) Using the services¶
4.1 Render a map¶
var bbox = new BoundingBox(
xmin: 1.110e6, ymin: 6.40e6,
xmax: 1.130e6, ymax: 6.42e6); // EPSG:3857 meters
using var img = groupedMapService.GetMap(
style: "0~10:#0000FF,#00FFFF,#00FF00,#FFFF00,#FF0000", // direct StyleCode
crs: "EPSG:3857",
boundingBox: bbox,
width: 1024, height: 768,
sourceId: "dataset-01",
dateTime: null,
item: "waterlevel",
parameters: Parameters.Empty);
// Save it with SkiaSharp
using var fs = File.OpenWrite("map.png");
img.Encode(SKEncodedImageFormat.Png, 95).SaveTo(fs);
Dispose the returned
SKBitmap. If you create multiple images, dispose them promptly to avoid leaking GPU/CPU memory.
4.2 Render a time stack¶
var times = new Dictionary<DateTime, string>
{
[new DateTime(2024, 04, 01, 0, 0, 0, DateTimeKind.Utc)] = "dataset-01@T0",
[new DateTime(2024, 04, 01, 1, 0, 0, DateTimeKind.Utc)] = "dataset-01@T1",
};
var size = new SKSizeI(800, 600);
var frames = groupedMapService.GetMaps(
style: "rain", // will be resolved via MapStyleService; else treated as StyleCode
boundingBox: bbox,
size: size,
timeSteps: times,
item: "precip",
parameters: Parameters.Empty);
foreach (var (t, bmp) in frames)
{
using (bmp) using var fs = File.OpenWrite($"frame-{t:yyyyMMdd-HHmm}.png");
bmp.Encode(SKEncodedImageFormat.Png, 95).SaveTo(fs);
}
4.3 Discover available times¶
var timesAll = groupedMapService.GetDateTimes("dataset-01"); // SortedSet<DateTime>
var timesSpan = groupedMapService.GetDateTimes("dataset-01", new DateRange(t0, t1));
4.4 Work with layers (groups)¶
// list layers
var layers = groupedMapService.GetAll(); // IEnumerable<Layer>
foreach (var layer in layers)
{
Console.WriteLine($"{layer.Group}/{layer.Name} bbox:{layer.BoundingBox} crs:{layer.CoordinateSystem}");
}
// list a group's full names
var ids = groupedMapService.GetFullNames("hydro-layers");
4.5 Stream an artifact¶
If your IGroupedMapSource supports file streaming (e.g., export of a feature collection):
var (stream, fileType, fileName) = groupedMapService.GetStream("layer-001");
using var _ = stream; // use it / return it to a client
The service throws KeyNotFoundException if the stream is not available.
5) Styling, legends & colors¶
5.1 StyleCode quick reference¶
- Single:
"5:#00FF00" - Start^Step:
"0^2:#0000FF,#00FFFF,#00FF00"→ thresholds: 0,2,4 - Start\~End:
"0~10:#0000FF,#00FFFF,#00FF00,#FFFF00,#FF0000"(equally spaced) - Multiple bands:
"0~5:#00FFFF,#00FF00|5~15:#FFFF00,#FF0000"
Default PaletteType is LowerThresholdValues (i.e., band color applies when value >= band and < next upper). UpperThresholdValues is also supported by Palette but not selectable from StyleCode directly; use it only if you construct your own Palette.
5.2 Legend bitmaps¶
var ms = new MapStyle("rain", "Rain", "0~10:#0000FF,#00FFFF,#00FF00,#FFFF00,#FF0000");
using var horiz = ms.ToBitmapHorizontal(400, 32);
using var vert = ms.ToBitmapVertical(80, 200);
6) Caching strategies¶
You can wrap your IMapSource with:
6.1 In-memory (CachedMapSource / CachedMapServiceConnection)¶
Configurable via Parameters:
CachedImageWidth(default 1024) – tiles are rendered at this width, height is scaled to aspect.NumberOfCachedZoomLevels(default 5) – number of zoom grids to pre-compute.CacheExpirationInMinutes(default 20) – sliding expiration per tile.
Invalidation: the cache auto-clears tiles if the sourceId you pass is a file path and the file LastWriteTime changes. If your sourceId is not a file path, cached tiles won’t be invalidated automatically (use file-cached variant or a custom keying strategy in your IMapSource).
CRS: requires "EPSG:3857"; otherwise throws.
6.2 File-backed (FileCachedMapSource / FileCachedMapServiceConnection)¶
Same knobs as memory, plus:
CacheRoot(optional) – cache directory; if omitted, a default location is used.
Expiration: default is int.MaxValue minutes (i.e., near-permanent) unless you set CacheExpirationInMinutes.
Both caching decorators tile the requested view (
ZoomLevels) and stitch them. They use parallelism to fill tiles, then crop the stitched image to your exact bounding box.
7) Implementing your own map source¶
Ship this in a provider package and reference it from the host.
public sealed class MyGridMapSource : BaseMapSource
{
public MyGridMapSource(string conn, Parameters props) { /* open dataset, etc. */ }
public override SKBitmap GetMap(MapStyle style, string crs, BoundingBox bbox,
int width, int height, string sourceId,
DateTime? dateTime, string item, Parameters p)
{
if (!crs.IsGoogle()) throw new NotSupportedException("EPSG:3857 only.");
// 1) Fetch/read your data window for bbox+dateTime+item
// 2) Rasterize using SkiaSharp:
var bmp = new SKBitmap(width, height);
using var g = new SKCanvas(bmp);
// Use style.GetPalette()/GetColor(value) to colorize
// Or use MapGraphic.PaintMap(...) helpers for band fills and contours
return bmp; // DO NOT dispose bmp here, caller owns it
}
public override SortedSet<DateTime> GetDateTimes(string id)
=> new(new[] { DateTime.UtcNow }); // or probe your dataset
}
Tips
- Threading:
BaseMapSource.GetMapsmay callGetMapconcurrently; ensure your data access is safe (clone readers, avoid shared mutable state). - Performance: prefer drawing into
SKBitmap/SKCanvasand reuseSKPaintobjects inside loops where possible. - Memory: never hold on to the final
SKBitmapreturned to customers; the caller will dispose it. For intermediate tiles, the caching decorators hold them until eviction.
8) Known behaviors¶
- CRS: the caching decorators only accept
"EPSG:3857"(Web Mercator). If you need other CRSs, use a custom, non-cachedIMapSourceor reproject upstream. - Disposal:
SKBitmapimplementsIDisposable. Always wrap returned images inusingor dispose explicitly. - Parallel GetMaps:
MapService→IMapSource.GetMapsthroughBaseMapSource.GetMapsuses a thread-safe aggregation.BaseGroupedMapSource.GetMaps(grouped variant) usesSortedDictionary.Addinside a parallel loop in this version; treat it as non-thread-safe if you ever call it directly from a subclass. Prefer the non-groupedMapService.GetMapspath or override with your own thread-safe aggregation.
- Cache invalidation for non-file sources: the cached decorators only clear tiles when
sourceIdlooks like a file path whose timestamp changes. Otherwise, tiles persist until expiration. - Style resolution: If you pass
styleand you did not inject aMapStyleService, the string is assumed to be a StyleCode, not an id. If you did injectMapStyleService, the string must be a known style id or you’ll get a 404-likeKeyNotFoundException. - Legend contrast: legend text color is auto-picked (black/white) based on band color brightness.
9) Map styles repository¶
If you want to manage styles centrally:
var repo = new MapStyleRepository(Path.Combine(appData, "styles.json"));
var styleSvc = new MapStyleService(repo);
// Then inject styleSvc into MapService / GroupedMapService
styles.json is a repository of MapStyle entities with at least:
[
{ "Id": "rain", "Name": "Rain (0-10)", "StyleCode": "0~10:#0000FF,#00FFFF,#00FF00,#FFFF00,#FF0000" },
{ "Id": "wl", "Name": "Water Level", "StyleFile": "[AppData]wl-style.csv" }
]
If you use StyleFile, the CSV is parsed as:
Lower,Upper,ColorHex,Label
0,1,#E0F3F8,0–1
1,2,#ABD9E9,1–2
...
10) API reference (public surface you’ll likely touch)¶
-
Services
MapServiceSKBitmap GetMap(string style, string crs, BoundingBox bbox, int width, int height, string sourceId, DateTime? dateTime, string item, Parameters parameters)SortedDictionary<DateTime, SKBitmap> GetMaps(string style, BoundingBox bbox, SKSizeI size, Dictionary<DateTime, string> timeSteps, string item, Parameters parameters)SortedSet<DateTime> GetDateTimes(string id)(+ range overload)
GroupedMapService:IGroupedMapServiceInherits all fromMapService(via_mapService) + CRUD from a grouped repository +(Stream stream, string fileType, string fileName) GetStream(string id, ClaimsPrincipal user = null)
-
Connections
MapServiceConnection,CachedMapServiceConnection,FileCachedMapServiceConnection,GroupedMapServiceConnection- All support
MapSourceType(assembly-qualified),MapSourceConnectionString,MapSourceProperties(Parameters) - Optionally:
MapStyleRepositoryType,MapStyleConnectionString
- All support
-
Entities
Layer(Id,Name,Group,BoundingBox,CoordinateSystem)MapStyle(Id,Name,StyleCodeorStyleFile) + legend helpersPalette(parsed fromStyleCode),MapStyleBandTile,TileImage,ZoomLevels
-
Interfaces
IMapSource,IMapServiceIGroupedMapSource,IGroupedMapServiceIMapStyleRepository