DHI.Services.MIKECore for Rasters – Internal Guide (DFS2)¶
This guide explains the Rasters Providers module with emphasis on the MIKECore (
Dfs2TimeStepServer) provider. The MIKECore provider implements a time-step server over DFS2 files (MIKE Zero 2D time-varying rasters), turning them into in-memoryRasterobjects with geospatial metadata and optional bitmap rendering.
Key types (mental model)¶
-
Dfs2TimeStepServerThe main entry point. Opens a DFS2 file, lists available items (variables/layers), lists datetimes, and materializes a raster for a specific(itemId, dateTime). -
BaseTimeStepServer<TKey, TResult>Contract implemented by servers; hereTKey = string(item id),TResult = object(a boxedRaster). -
IFileSource/FileSourceAbstraction over storage. Default implementation reads from local disk but you can provide your own (e.g., network share, object storage). The server never assumes local I/O—it always goes throughIFileSource. -
Item<string>Describes a DFS2 variable (sometimes called “item” in MIKE). The provider enrichesItem.Metadatawith spatial extent, grid size, units, and projection. -
Maybe<T>A tiny optional container.Get(...)returnsMaybe<object>: it’s empty when the item/date is not found; value when the raster exists. -
Raster(provider’s nested class) A concreteBaseRasterwith pixel values (IList<float>), grid size, pixel size, WKT projection, and helpers (min/max,ToBitmap(), etc.). -
MIKE Zero / DFS dependencies Uses
Generic.MikeZero.DFS(dfs123,Dfs2File,IDfsAxisEqD2, etc.) to read files and metadata.
What the provider does (and how)¶
1) File opening & validation¶
- Constructor validates the DFS2 file exists in the
IFileSourceroot. - All reads go through
IFileSource.ReadDfs2(path, func)which safely opens/dispensesDfs2Fileto your lambda.
2) Time axis handling¶
- If the DFS2 time axis is equidistant, it enumerates via
TimeAxis.GetDateTimes(). - If non-equidistant, it reads each timestep to compute UTC datetimes from
StartDateTime + secondsFromAxis.
3) Spatial axis & extents¶
- Only equidistant 2D axis (EqD2) is fully supported.
For EqD2:
- Reads cell size (
Dx,Dy) and counts (XCount,YCount). - Computes the geographic bbox:
- Lower-left geo anchor:
(Longitude, Latitude)from the DFS2 projection header. - Projects that to the projected space, offsets by grid span
|Dx| * (XCount-1)and|Dy| * (YCount-1), then transforms back to geo for the upper-right corner. - Expands bbox by half a pixel on all sides so the reported
x0,y0,x1,y1describe outer cell edges, not centers.
- Lower-left geo anchor:
- Reads cell size (
- For non-EqD2 axes: the provider returns local coordinates (no geodetic transform). A TODO is left in code to implement full handling.
4) Delete values & missing data¶
- DFS2
DeleteValueFloatcells are replaced with 0 (conservative default). If you need “null semantics”, see “Customization” below.
5) Item metadata surfaced¶
For each DFS2 item, you get:
dataType,valueType(DFS metadata)quantity,unit(human-readable descriptions)x0,y0,x1,y1(bbox in degrees for EqD2; local units otherwise)dx,dy(cell size; units follow the DFS projection)nx,ny(grid size)projection(full WKT)
6) Bitmap rendering¶
Raster.ToBitmap()generates anSKBitmapusing a simple linear gradient from LightSkyBlue → DarkBlue across[min, max]. This is intentionally neutral; render styling is typically owned by the caller/UI. You can override this (see “Customization”).
Public API (you can call this as-is)¶
Construction¶
// simplest: local file
var server = new Dfs2TimeStepServer(@"C:\data\mike\rain.dfs2");
// advanced: custom file source root + relative file path
IFileSource fs = new FileSource(@"\\nas\dfs2"); // or your own implementation
var server = new Dfs2TimeStepServer(fs, "rain_202501.dfs2");
Exceptions
ArgumentNullExceptionif args are null.ArgumentExceptionif the DFS2 file doesn’t exist in the givenIFileSource.
GetDateTimes(ClaimsPrincipal user = null) : IList<DateTime>¶
Returns all available timesteps (UTC DateTime).
IList<DateTime> stamps = server.GetDateTimes();
GetItems(ClaimsPrincipal user = null) : IEnumerable<Item<string>>¶
Lists item descriptors (id = name). Check item.Metadata for spatial/units info (see above).
foreach (var it in server.GetItems())
{
Console.WriteLine($"{it.Id} [{it.Metadata["unit"]}] {it.Metadata["nx"]}x{it.Metadata["ny"]}");
}
Get(string itemId, DateTime dateTime, ClaimsPrincipal user = null) : Maybe<object>¶
Returns a boxed Raster for the item/time or empty if not found.
var itemId = server.GetItems().First().Id;
var t = server.GetDateTimes().Last();
var maybe = server.Get(itemId, t);
if (!maybe.HasValue) { /* not found */ }
var raster = (Dfs2TimeStepServer.Raster)maybe.Value;
Console.WriteLine($"{raster.Name} @ {raster.DateTime:o}");
Console.WriteLine($"{raster.Size.Width}x{raster.Size.Height} px");
Console.WriteLine($"Center={raster.GeoCenter.X},{raster.GeoCenter.Y} PixelSize={raster.PixelSize.Width}x{raster.PixelSize.Height}");
Console.WriteLine($"Unit={raster.PixelValueUnit} Min={raster.MinValue} Max={raster.MaxValue}");
You can then draw a bitmap:
using var bmp = raster.ToBitmap();
using var stream = File.OpenWrite(@"C:\out\raster.png");
bmp.Encode(stream, SkiaSharp.SKEncodedImageFormat.Png, quality: 95);
Data contract of Raster (what you get back)¶
DateTime— timestepName— DFS2 item nameValues—IList<float>length =nx * ny(row-major, index =y * nx + x)Size—SKSizeI(nx,ny)PixelSize—SKSize(dx,dy)PixelValueUnit— e.g.,mm/h,m, etc.GeoCenter—SKPointin degrees for EqD2; local otherwiseGeoProjectionString— WKT textMetadata— dictionary containing the extras listed earlierToBitmap()—SKBitmapcolorized image for visualization
Indexing:
(x=0, y=0)is top-left;xincreases to the right,yincreases downward.
Security model (what’s enforced?)¶
The provider surface carries an optional ClaimsPrincipal user but does not enforce per-call authorization on its own. It’s there to compose with an upstream authorization component (e.g., an API layer that filters accessible items or masks values). Pass your user through if you intend to add such checks later.
Integration patterns¶
1) Use from a console/batch job¶
var server = new Dfs2TimeStepServer(@"C:\dfs2\precip.dfs2");
// pick an item & time
var item = server.GetItems().First(i => i.Id.Contains("Precipitation"));
var when = server.GetDateTimes().First(dt => dt.Hour == 0);
// get raster
var raster = (Dfs2TimeStepServer.Raster)server.Get(item.Id, when).Value;
// simple zonal statistic over a 3x3 around (x=100,y=200)
int nx = raster.Size.Width;
float Sum(int x0, int y0) {
float s = 0; int n = 0;
for (int y=y0; y<y0+3; y++)
for (int x=x0; x<x0+3; x++) { s += raster.Values[y*nx+x]; n++; }
return s / n;
}
Console.WriteLine($"3x3 mean @ (100,200) = {Sum(100,200)} {raster.PixelValueUnit}");
2) Serve through a Web API¶
You can wrap Dfs2TimeStepServer in a controller method:
[HttpGet("dfs2/{item}/{time}/png")]
public IActionResult Render(string item, DateTime time)
{
var server = _servers.Resolve("rain2025"); // your DI/factory
var m = server.Get(item, time);
if (!m.HasValue) return NotFound();
using var bmp = ((Dfs2TimeStepServer.Raster)m.Value).ToBitmap();
using var ms = new MemoryStream();
bmp.Encode(ms, SKEncodedImageFormat.Png, 95);
ms.Position = 0;
return File(ms, "image/png");
}
3) Plug a custom storage¶
Implement IFileSource and provide it to the constructor:
public sealed class S3FileSource : IFileSource { /* Exists, ReadDfs2, ... */ }
var fs = new S3FileSource("s3://bucket/prefix");
var server = new Dfs2TimeStepServer(fs, "rain.dfs2");
Performance notes & best practices¶
- Re-use the server instance for multiple queries against the same file; it avoids repeated validation and allows your
IFileSourceto pool file handles. - Avoid loading everything: call
GetDateTimes()andGetItems()first to narrow your request;Get(...)loads only one item/time slice. - Memory footprint:
Valuesis a flatfloat[]of sizenx*ny. Large grids (e.g., 10k × 10k) can consume gigabytes—plan accordingly. - Threading: API is synchronous; safe to call concurrently if your
IFileSourceis thread-safe and you open independentDfs2Fileinstances per call (the default pattern withReadDfs2is safe). - Delete values: currently coerced to 0. If you need “no-data”, either post-process or customize (see below).
Customization points¶
-
Missing values policy Change the line that maps
DeleteValueFloatto0to your preferred behavior (e.g.,float.NaN). Downstream stats/bitmaps should then handle NaNs as you see fit. -
Color mapping Override or replace
ToBitmap()to apply your corporate color ramp or log scaling. E.g., implement quantized color bins with legend ticks. -
Non-EqD2 axis support The code currently returns local coordinates for non-equidistant axes. To add full support:
- Sample the axis coordinates per row/column.
- Build a warped transform for the bbox (or report per-cell geolocations).
- Extend
Metadatato include per-axis arrays if needed.
-
Alternate projections The extents are computed by projecting the lower-left point to the native projected space, offsetting by span, and projecting back. If your DFS2 has unusual orientations, verify
projection.Orientationhandling and adjust in theCartographycalls.
Error handling & return semantics¶
- Construction: throws early if the file can’t be found.
Get(itemId, dateTime): returns empty when:- the timestamp is not present (no index match)
- the item id isn’t found in the DFS2 items
- Metadata presence: All keys shown are present for EqD2. For other axes,
projectionand localx0…y1are present but in local units.
Field reference (quick)¶
GetItems() → Item<string>.Metadata keys¶
| Key | Type | Meaning |
|---|---|---|
dataType |
enum str | DFS item data type |
valueType |
enum str | DFS item value type |
quantity |
string | Human-readable quantity |
unit |
string | Unit name (e.g., mm/h) |
x0,y0,x1,y1 |
double | Geo bbox (degrees) or local coords |
dx,dy |
double | Pixel size |
nx,ny |
int | Grid size |
projection |
string | WKT |
Raster properties¶
Values(float[]) — lengthnx*nySize(SKSizeI) —(nx, ny)PixelSize(SKSize) —(dx, dy)GeoCenter(SKPoint) —(lon, lat)(EqD2)PixelValueUnit(string)GeoProjectionString(stringWKT)Metadata— same keys as above + min/max computed lazily inBaseRaster
Worked end-to-end example¶
Goal: Export all hourly precipitation rasters for 2025-02-01 to PNG for a dashboard.
var server = new Dfs2TimeStepServer(@"D:\dfs2\precip_2025.dfs2");
var item = server.GetItems().First(i => i.Id.Contains("Precipitation"));
// choose the day
var stamps = server.GetDateTimes().Where(dt => dt.Date == new DateTime(2025,2,1)).ToList();
Directory.CreateDirectory(@"D:\png\2025-02-01");
foreach (var dt in stamps)
{
var m = server.Get(item.Id, dt);
if (!m.HasValue) continue;
var raster = (Dfs2TimeStepServer.Raster)m.Value;
using var bmp = raster.ToBitmap();
var outPath = $@"D:\png\2025-02-01\{dt:HHmm}.png";
using var fs = File.Create(outPath);
bmp.Encode(fs, SKEncodedImageFormat.Png, 95);
Console.WriteLine($"Wrote {outPath} [{raster.Size.Width}x{raster.Size.Height}] Min={raster.MinValue:F2} Max={raster.MaxValue:F2} {raster.PixelValueUnit}");
}
Extending the provider family¶
Although this guide focuses on DFS2, the same pattern applies to other raster sources:
- Implement a
*TimeStepServerover your format. - Keep the file/source abstraction (
IFileSource) to support alternative storage. - Populate the same item metadata and raster fields so downstream consumers remain format-agnostic.
- (Optional) Provide a bitmap method for quick visualization.