Skip to content

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: SkiaSharp consumers (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) over Layer entities grouped by name, with the same map rendering capabilities + a GetStream passthrough.
  • Temporal support: ask a source for the available DateTime steps; 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).
  • Caching decorators ready to use:
    • CachedMapSource (in-memory) and FileCachedMapSource (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 IMapSource decides what to support.

2.2 MapService (what you call)

A thin facade that:

  • Resolves the style string to a MapStyle (either via MapStyleService repo, 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 MapStyle is either a StyleCode (StyleCode string) or a file (StyleFile) defining bands with colors and labels.
  • The Palette class 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]
  • 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 MapStyleService is optional. If omitted, the style argument you pass to GetMap(...) 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.GetMaps may call GetMap concurrently; ensure your data access is safe (clone readers, avoid shared mutable state).
  • Performance: prefer drawing into SKBitmap/SKCanvas and reuse SKPaint objects inside loops where possible.
  • Memory: never hold on to the final SKBitmap returned 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-cached IMapSource or reproject upstream.
  • Disposal: SKBitmap implements IDisposable. Always wrap returned images in using or dispose explicitly.
  • Parallel GetMaps:
    • MapServiceIMapSource.GetMaps through BaseMapSource.GetMaps uses a thread-safe aggregation.
    • BaseGroupedMapSource.GetMaps (grouped variant) uses SortedDictionary.Add inside a parallel loop in this version; treat it as non-thread-safe if you ever call it directly from a subclass. Prefer the non-grouped MapService.GetMaps path or override with your own thread-safe aggregation.
  • Cache invalidation for non-file sources: the cached decorators only clear tiles when sourceId looks like a file path whose timestamp changes. Otherwise, tiles persist until expiration.
  • Style resolution: If you pass style and you did not inject a MapStyleService, the string is assumed to be a StyleCode, not an id. If you did inject MapStyleService, the string must be a known style id or you’ll get a 404-like KeyNotFoundException.
  • 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

    • MapService
      • SKBitmap 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 : IGroupedMapService Inherits all from MapService (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
  • Entities

    • Layer (Id, Name, Group, BoundingBox, CoordinateSystem)
    • MapStyle (Id, Name, StyleCode or StyleFile) + legend helpers
    • Palette (parsed from StyleCode), MapStyleBand
    • Tile, TileImage, ZoomLevels
  • Interfaces

    • IMapSource, IMapService
    • IGroupedMapSource, IGroupedMapService
    • IMapStyleRepository