Showing posts with label Matrikkelen. Show all posts
Showing posts with label Matrikkelen. Show all posts

Saturday, 1 August 2026

Kartverket Matrikkel Eiendoms API with MAUI

This article will show how looking up data from Eiendoms API (v1) from Kartverket. The data is retrieved from Matrikkelen, the official registry of properties and land parcels in Norway. The demo will show using a MAUI app and the source code with the demo is available in my Github repository to be cloned from here :


Screenshots

Clicking on a property in the map will show the matrikkelen information for the first hit of the properties around the clicked point.

Using Kartverket's Eiendom API v1

The Eiendom API offers basic Eiendomsinformasjon, property information. A Swagger page and the endpoint of the API is available here:

The service, that is a client to contact and retrieved the Eiendomsinformation for a given point that uses the /punkt API method is shown below. Please note that the json naming dictates we must set the Property naming policy to camelCase, as we can see from the JsonProperty naming convention used further down in the Json POCO classes shown in KartverketResponse.cs code snippet.

KartverketService.cs


using System.Globalization;
using System.Net.Http.Json;
using System.Text.Json;

namespace MauiMapAppDemo.Services
{

    public class KartverketService
    {

        private const string _apiKartverketEiendomV1BaseUrl = "https://api.kartverket.no/eiendom/v1/";

        private readonly HttpClient _httpClient = new HttpClient
        {
            BaseAddress = new Uri(_apiKartverketEiendomV1BaseUrl)
        };

        /// <summary>
        /// Retrieves matrikkel informasjon from given point (Punkt) from Kartverket's Eiendom API v1
        /// </summary>
        /// <param name="latitude">Latitude</param>
        /// <param name="longitude">Longitude</param>
        /// <param name="koordSys">Defaulting here to EUREF89 = 4258 as the coordinate system id, which Kartverket uses and is also what Google maps coords are using</param>
        /// <returns></returns>
        public async Task<KartverketPunktResponse?> GetMatrikkelInformationFromLocationAsync(double latitude, double longitude, int koordSys = 4258)
        {
            string url = $"punkt?ost={longitude.ToString(CultureInfo.InvariantCulture)}&nord={latitude.ToString(CultureInfo.InvariantCulture)}&koordsys={koordSys}&radius=10&utkoordsys={koordSys}&treffPerSide=1&side=1";

            var kartverketResponseForLocation = await _httpClient.GetFromJsonAsync<KartverketPunktResponse>(url, options: new System.Text.Json.JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            });
            return kartverketResponseForLocation;
        }

    }
}

About the choice of EUREF 89 = 4258 as the default coordinate system in the call above. Google maps uses actually WGS 84 which is EPSG coordinate system id 4326, but the difference is less than a meter. Since a 10 meter radius of the clicked point is used, we pass in the clicked Location point's latitude and longitude to the north and east parameters in the url call above in the KartverketService.cs sample code shown, with a given radius of 10 and although we get a result of multiple hits within the 10 meter radius, the first if any of properties or parcels found is returned from the service, by setting the side and treffPerSide parameters.

🌍 What are EUREF89 and EPSG?

🗺️ EUREF89 is a geographic coordinate reference system used across Europe, including by Kartverket in Norway. It defines how latitude and longitude positions are represented on the Earth's surface.

🔢 EPSG:4258 is the unique identifier for the EUREF89 coordinate system. EPSG codes are standardized reference numbers that mapping software, APIs, and GIS tools use to identify coordinate systems.

📍 In this project, coordinates are obtained from Google Maps and sent to Kartverket's Eiendom API v1 using EUREF89 (EPSG:4258). Using the same coordinate reference system helps ensure accurate property lookups in the Norwegian Matrikkel.

🏠 The result is that a location selected on the map can be translated directly into property and cadastral information from Kartverket.

The following Json classes will map the returned Json data from the service to objects. To create these classes, I just copied example output response from the API method /punkt and used Paste Special => Paste Json as classes in VS 2026.

KartverketPunktResponse.cs


using System.Text;
using System.Text.Json.Serialization;

namespace MauiMapAppDemo.Services
{
    public class KartverketPunktResponse
    {
        [JsonPropertyName("eiendom")]
        public List<Punkt>? Eiendom { get; set; }

        [JsonPropertyName("metadata")]
        public Metadata? Metadata { get; set; }
    }

    public class Metadata
    {
        [JsonPropertyName("side")]
        public int Side { get; set; }

        [JsonPropertyName("sokeStreng")]
        public string? SokeStreng { get; set; }

        [JsonPropertyName("totaltAntallTreff")]
        public int TotaltAntallTreff { get; set; }

        [JsonPropertyName("treffPerSide")]
        public int TreffPerSide { get; set; }

        [JsonPropertyName("viserFra")]
        public int ViserFra { get; set; }

        [JsonPropertyName("viserTil")]
        public int ViserTil { get; set; }
    }

    public class Punkt
    {
        [JsonPropertyName("bruksnummer")]
        public int Bruksnummer { get; set; }

        [JsonPropertyName("festenummer")]
        public int Festenummer { get; set; }

        [JsonPropertyName("gardsnummer")]
        public int Gardsnummer { get; set; }

        [JsonPropertyName("hovedområde")]
        public bool HovedOmrade { get; set; }

        [JsonPropertyName("kommunenummer")]
        public string? Kommunenummer { get; set; }

        [JsonPropertyName("lokalid")]
        public int Lokalid { get; set; }

        [JsonPropertyName("matrikkelnummertekst")]
        public string? Matrikkelnummertekst { get; set; }

        [JsonPropertyName("meterFraPunkt")]
        public int MeterFraPunkt { get; set; }

        [JsonPropertyName("nøyaktighetsklasseteig")]
        public string? Noyaktighetsklasseteig { get; set; }

        [JsonPropertyName("objekttype")]
        public string? Objekttype { get; set; }

        [JsonPropertyName("oppdateringsdato")]
        public DateTime? Oppdateringsdato { get; set; }

        [JsonPropertyName("representasjonspunkt")]
        public Representasjonspunkt? Representasjonspunkt { get; set; }

        [JsonPropertyName("seksjonsnummer")]
        public int Seksjonsnummer { get; set; }

        [JsonPropertyName("teigmedflerematrikkelenheter")]
        public bool Teigmedflerematrikkelenheter { get; set; }

        [JsonPropertyName("uregistrertjordsameie")]
        public bool Uregistrertjordsameie { get; set; }

        public string FullstendigMatrikkelNummer
        {
            get
            {
                var sb = new StringBuilder();
                if (!string.IsNullOrWhiteSpace(Kommunenummer))
                {
                    sb.Append(Kommunenummer?.ToString().PadLeft(4, '0'));
                }
                if (Gardsnummer > 0)
                {
                    sb.Append("/" + Gardsnummer);
                }
                if (Bruksnummer > 0)
                {
                    sb.Append("/" + Bruksnummer);
                }
                if (Festenummer > 0)
                {
                    sb.Append("/" + Festenummer);
                }
                if (Seksjonsnummer > 0)
                {
                    sb.Append("/" + Seksjonsnummer);
                }

                return sb.ToString();
            }
        }
    }

    public class Representasjonspunkt
    {
        [JsonPropertyName("koordsys")]
        public int Koordsys { get; set; }

        [JsonPropertyName("nord")]
        public double Nord { get; set; }

        [JsonPropertyName("øst")]
        public double Ost { get; set; }
    }

}



Groundwork on the parts of property information for Fullstendig matrikkelnummer

🏠 Components of a Full Matrikkel Number

Icon Component Abbreviation Example Description
🏛️ Municipality Number Knr. 5001 Identifies the municipality where the property is located. An overview of Kommunenummer can be seen here : https://www.kartverket.no/til-lands/fakta-om-norge/norske-fylke-og-kommunar
🌾 Farm Number - Gårdnummer / Gardsnummer Gnr. 138 Identifies the original farm or cadastral area within the municipality.
🏠 Usage Number / Bruksnummer Bnr. 4515 Identifies a specific property or parcel within a farm number.
📜 Leasehold Number / Festenummer Fnr. 7 Used when the property is a leased parcel of land (festetomt). The festetomt / lease agreements typically lasts very long, in the area of 80-100 years typically and can be renewed.
🏢 Section Number Snr. 12 Identifies a specific section or apartment within a sectioned property.

📋 Example

5001 / 138 / 4515 / 7 / 12

Note that the first three numbers are ALWAYS set for all properties in Norway. That is Kommunenummer, Gårdsnummer (Gardsnummer) and Bruksnummer. And some properties also are leased, so they got a lease number or sectioned too. The numbers are read left to right in descending order of the overview shown above.
Value Meaning
🏛️ 5001 Municipality Number
🌾 138 Farm Number (Gårdsnummer)
🏠 4515 Usage Number (Bruksnummer)
📜 7 Leasehold Number (Festenummer)
🏢 12 Section Number (Seksjonsnummer)

💡 A full matrikkel number is built in the following order:

🏛️ Municipality Number → 🌾 Farm Number → 🏠 Usage Number → (📜 Leasehold Number → 🏢 Section Number )



📐 Getting Property Boundary Area Information (Grenseinformasjon)

You can retrieve boundary information for a selected property using the /punkt/omrader endpoint in Eiendoms API (v1).

The API returns a GeoJSON geometry describing the property's boundaries. The coordinates are provided as latitude and longitude values, so they must be projected into a metric coordinate system before calculating the property's area.

🗺️ In a GeoJSON polygon, the coordinates are returned as a multi-dimensional array:

  • ✅ The first array represents the property's outer boundary.
  • ➖ Any additional arrays represent holes or excluded areas within the property.

📏 To calculate the total area:

  1. Calculate the area of the outer boundary polygon.
  2. Subtract the area of each interior polygon (hole).

💡 This follows the standard GeoJSON polygon specification and ensures that any gaps within the property are excluded from the final area calculation.

The following screenshots shows selecting a property for testing out the precision of the area calculations using of a polygon of property's boundary using Eiendoms API v1.
As the screenshots show, the demo shows an agreement with Trondheimskart solution, which is the property map that Trondheim kommune is using : The calculated area in Trondheimskartet is 1125 square metres and the app calculate it to 1126 square metres.

🗺️ Area and polygon overview

This note explains how the app turns Kartverket omrade data into a visible polygon and a simple area summary. The goal is to keep the UI readable while still showing the geometry work behind the scenes.

The flow is: tap a point, fetch Kartverket data, build a closed polygon from the returned coordinates, and show the area in square metres. The main page binds the polygon path and the area label separately so the overlay and the text can update independently.

📐 Area calculation

The shared geometry helper uses the shoelace formula. If the incoming ring is still in WGS84 degrees, it first transforms the coordinates to UTM Zone 33N so the result comes out in square metres. The shoelace formulae is also known as Gauss area formula and is visually described with an example here from University of Waterloo in Canada great article:

Note that the code below uses Nuget package ProjNet
using ProjNet.CoordinateSystems;
using ProjNet.CoordinateSystems.Transformations;

namespace MauiMapAppDemo.Services
{
    public static class GeometryUtils
    {
        public static double CalculatePolygonArea(double[][] ring, bool transformToMetricCoordinateSystem = true)
        {
            if (ring == null || ring.Length < 3)
            {
                throw new ArgumentException("Polygon must contain at least three points.", nameof(ring));
            }

            var projectedRing = new double[ring.Length][];

            if (transformToMetricCoordinateSystem)
            {
                var ctFactory = new CoordinateTransformationFactory();
                var wgs84 = GeographicCoordinateSystem.WGS84;
                var utm33 = ProjectedCoordinateSystem.WGS84_UTM(33, true);
                var transform = ctFactory.CreateFromCoordinateSystems(wgs84, utm33);

                for (int i = 0; i < ring.Length; i++)
                {
                    var projected = transform.MathTransform.Transform(ring[i][0], ring[i][1]);
                    projectedRing[i] = new[] { projected.x, projected.y };
                }
            }

            double area = 0;

            for (int i = 0; i < projectedRing.Length; i++)
            {
                int next = (i + 1) % projectedRing.Length;
                area += projectedRing[i][0] * projectedRing[next][1];
                area -= projectedRing[next][0] * projectedRing[i][1];
            }

            return Math.Abs(area) / 2.0;
        }

        public static double CalculateTotalArea(double[][][] coordinates, bool transformToMetricCoordinateSystem = true)
        {
            if (coordinates == null || coordinates.Length == 0)
            {
                return 0;
            }

            double totalArea = 0;

            for (int ringIndex = 0; ringIndex < coordinates.Length; ringIndex++)
            {
                var ringArea = CalculatePolygonArea(coordinates[ringIndex], transformToMetricCoordinateSystem);

                if (ringIndex == 0)
                {
                    totalArea += ringArea;
                }
                else
                {
                    totalArea -= ringArea;
                }
            }

            return totalArea;
        }
    }
}

For GeoJSON, the first ring is the outer boundary and later rings are holes. That matches how Kartverket polygons are usually interpreted when the app calculates total area from an omrade response.

🛰️ Kartverket omrade fetch

The service builds the omrader URL from the clicked latitude and longitude, then deserializes the response with a shared camel-case serializer option. The resulting geometry is what the app later converts into a polygon overlay.

public async Task<KartverketOmraadeResponse?> GetGeoJsonFromLocationAsync(double latitude, double longitude, int koordSys = 4258)
{
    string url = $"punkt/omrader?ost={longitude.ToString(CultureInfo.InvariantCulture)}&nord={latitude.ToString(CultureInfo.InvariantCulture)}&koordsys={koordSys}&radius=10&utkoordsys={koordSys}&treffPerSide=1&side=1";

    var kartverketResponseForLocation = await _httpClient.GetFromJsonAsync<KartverketOmraadeResponse>(url, options: s_camelCaseJsonOptions);
    return kartverketResponseForLocation;
}

🧾 Kartverket omrade response model

The response model is where the geometry turns into a calculated area value. The top-level object exposes features, and the area is derived from the nested polygon coordinates through a helper property.

public class KartverketOmraadeResponse
{
    [JsonPropertyName("features")]
    public Feature[] Features { get; set; }

    [JsonPropertyName("type")]
    public string Type { get; set; }

    public double? TotalAreaOfAllAreas
    {
        get
        {
            try
            {
                double totalArea = Features.Sum(f => GeometryUtils.CalculateTotalArea(f.Geometry.Coordinates));
                return totalArea;
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}

public class Geometry
{
    [JsonPropertyName("coordinates")]
    public double[][][] Coordinates { get; set; }

    [JsonPropertyName("type")]
    public string Type { get; set; }

    public double? TotalArea
    {
        get
        {
            try
            {
                double totalArea = GeometryUtils.CalculateTotalArea(Coordinates);
                return totalArea;
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}

public class Feature
{
    [JsonPropertyName("geometry")]
    public Geometry Geometry { get; set; }

    [JsonPropertyName("properties")]
    public Properties Properties { get; set; }

    [JsonPropertyName("type")]
    public string Type { get; set; }
}

The nice part here is that the area calculation is pushed into the model layer through read-only properties. That makes the popup logic simpler because it can ask for the total area instead of re-running geometry math in the UI layer. 📦

🧩 Polygon refresh behavior

The map behavior listens to the bound polygon path and rebuilds the overlay whenever the path changes. It clears the old polygon first, then adds the latest path back to the map.

private void RefreshMatrikkelPolygon()
{
    if (_map?.MapElements == null)
    {
        return;
    }

    ClearMatrikkelPolygon();

    if (!IsMatrikkelMode || !MatrikkelPolygonPath.Any())
    {
        return;
    }

    var polygon = new Polygon
    {
        StrokeColor = Colors.Red,
        StrokeWidth = 5,
        FillColor = Color.FromArgb("#22FF0000")
    };

    foreach (var location in MatrikkelPolygonPath)
    {
        polygon.Geopath.Add(location);
    }

    if (polygon.Geopath.Count < 3)
    {
        return;
    }

    var firstLocation = polygon.Geopath.First();
    var lastLocation = polygon.Geopath.Last();

    if (firstLocation.Latitude != lastLocation.Latitude || firstLocation.Longitude != lastLocation.Longitude)
    {
        polygon.Geopath.Add(firstLocation);
    }

    _matrikkelPolygon = polygon;
    _map.MapElements.Add(polygon);
}

That final closure step matters. If the last point does not match the first point, the behavior appends the first point again so the polygon is visually closed and the shape is easier to reason about. 🔁

📊 Area presentation

The viewmodel keeps the user-facing area text separate from the map geometry. When Kartverket returns a usable polygon, the app formats the result as square metres and also shows the same value in mål.

if (omraadeResponse?.TotalAreaOfAllAreas is double totalArea && totalArea > 0)
{
    MatrikkelAreaText = FormatMatrikkelAreaText(totalArea);
}
else
{
    MatrikkelAreaText = "Areal: ukjent";
}

private static string FormatMatrikkelAreaText(double areaSquareMetres)
{
    var maal = areaSquareMetres / 1000d;
    return $"Areal: {areaSquareMetres:N0} m² ({maal:N2} mål)";
}

The bound properties behind that flow are the polygon path and the area label. One drives the overlay, the other is what the user sees on the page.

public IEnumerable<Location> MatrikkelPolygonPath
{
    get => (IEnumerable<Location>)GetValue(MatrikkelPolygonPathProperty);
    set => SetValue(MatrikkelPolygonPathProperty, value);
}

public static readonly BindableProperty MatrikkelPolygonPathProperty =
    BindableProperty.Create(
        nameof(MatrikkelPolygonPath),
        typeof(Location[]),
        typeof(MapPinsBehavior),
        defaultValue: Array.Empty<Location>(),
        propertyChanged: OnMatrikkelPolygonPathChanged);

public static readonly BindableProperty IsMatrikkelModeProperty =
    BindableProperty.Create(
        nameof(IsMatrikkelMode),
        typeof(bool),
        typeof(MapPinsBehavior),
        false);

📱 Map page wiring

The map page binds the behavior directly. In this project the page is MapsDemo.xaml, and the snippet below shows the same behavior wiring that drives the map, measurement mode, and matrikkel polygon overlay.

<maps:Map Grid.Row="3"
          Margin="12"
          x:Name="MapCtrl"
          MapType="Street">
    <maps:Map.Behaviors>
        <behaviors:MapPinsBehavior
            BindingContext="{Binding Source={x:Reference MapCtrl}, Path=BindingContext}"
            Center="{Binding MapCenter}"
            PinItems="{Binding CabinPins}"
            IsMeasuringMode="{Binding IsMeasuringMode}"
            IsMatrikkelMode="{Binding IsMatrikkelMode}"
            MeasureStart="{Binding FirstLocationMeasureMode}"
            MatrikkelPolygonPath="{Binding MatrikkelPolygonPath}"
            MeasureEnd="{Binding SecondLocationMeasureMode}"
            MapClickedCommand="{Binding MapClickedCommand}"
            PinClickedCommand="{Binding PinClickedCommand}" />
    </maps:Map.Behaviors>
</maps:Map>

✨ Short takeaway

The app gets geometry from Kartverket, calculates area in a metric-friendly way, closes the polygon before drawing it, and keeps the user-facing square-metre text separate from the map overlay. That makes the map interaction clearer and easier to explain in a blog post.