Manual/Reference

Meratyn Project Format

Projects are saved as .meratyn.json files.

Meratyn writes project JSON compactly. This keeps embedded GeoJSON practical for large local datasets while preserving the same JSON schema and load compatibility; use a JSON formatter when reviewing a project file by hand.

When a project needs an explicit local-save choice (such as embedding local vector data or keeping credentials), Meratyn remembers that choice only for repeat saves of the current open project. It is never serialized, shared, or reused after opening a different project. Turning off optional data embedding still preserves canonical GeoJSON already stored on a layer and authored camera formulas. A local save acknowledges its captured authoring revision; edits made while it is running remain unsaved.

Before an upload, the Share dialog warns when a project still points to local files, private-network hosts, or credential-bearing URLs. The warning does not block sharing; it helps authors identify data a recipient may be unable to load.

The file is a recursive override of Meratyn’s canonical default project, not a full dump of runtime state. An untouched project therefore exports as {}. Objects contain only changed keys; ordered project content such as layers, dashboard widgets, and story chapters remains as complete arrays when present. Opening a file overlays its values on the same defaults before the project is loaded into the app.

Layer styles omit properties equal to the built-in style defaults, including empty expressions and disabled options. Changed nested style objects and rule arrays retain their replacement semantics. Layer visibility and opacity, group settings, secondary cameras, story chapters, and print settings also omit their known defaults. Array order and item identities remain intact. Feature data and extension settings keep meaningful nulls, empty strings, and empty arrays; these values are not removed indiscriminately.

Extrusion height fields store feature thickness in meters above the configured base. The renderer places the roof at base + height; changing the base moves the feature without changing its thickness. This applies to both property-based heights and height expressions.

Override schema

Every field below is optional in the file. An omitted field keeps its canonical default.

FieldTypeDescription
versionstringFormat version (0.2.0)
namestringProject display name
mapViewobjectcenter, zoom, bearing, pitch, optional bbox
mapControlsobjectOptional visibility and corner-position overrides for project-owned built-in map controls
basemapStyleUrlstringMapLibre style JSON URL, or an empty string for a blank background
basemapVisiblebooleanWhether the Background layer is visible
basemapOpacitynumberBackground layer opacity from 0 to 1
layersarrayLayer definitions (see below)
stylesobjectMap of layer id → LayerStyle
pluginsobjectOptional external plugin manifest URLs, active plugin IDs, plugin map-control positions, and plugin settings
legendobjectOptional Print Layout legend customizations (title, grouping, ordering, per-item rename/hide)
printLayoutobjectOptional normalized Print Layout composer settings, including atlas and data-block selection
storymapobjectOptional story chapters and presentation settings; omitted only when there is no authored content
widgetsarrayOptional Dashboard panel chart widgets (see below); omitted when there are none
dashboardColumnsnumberOptional Dashboard widget-grid column count (1-6, default 2); omitted when default
styleLibraryarrayOptional project-scoped Style Manager entries (name, tags, kind, LayerStyle subset); omitted when empty
commentsarrayOptional map review threads anchored to a point or feature (see below); omitted when there are none
metadataobjectFree-form project metadata

Public showcase workspace

Managed public examples can opt into a focused, project-scoped workbench. This metadata opens one relevant feature surface and allowlists only the menus, commands, data sources, and plugins used by the scenario:

{
  "metadata": {
    "publicShowcase": true,
    "showcase": {
      "surface": "processing-history",
      "workspace": {
        "allowedMenus": ["project", "view", "processing", "controls"],
        "allowedMenuItems": [
          "project.share",
          "view.zoomIn",
          "processing.whitebox",
          "processing.history"
        ],
        "allowedDataSources": [],
        "allowedPlugins": ["maplibre-gl-components", "maplibre-gl-swipe"],
        "allowedComponentControls": ["terrain"]
      }
    }
  }
}

The runtime honors showcase.workspace only when publicShowcase is exactly true. It decodes the allowlists into disposable project-session state and intersects them with the user’s local UI profile. The workspace can therefore hide additional discovery surfaces but cannot reveal an item hidden by the user or overwrite their preferences. Settings → Interface remains reachable. Ordinary projects ignore this presentation metadata.

Map preferences may include coordinateFormat (dd, dms, ddm, or utm), which controls the live pointer readout. The field is optional for backwards compatibility and defaults to dd when omitted or invalid. The selected feature set remains runtime capability state rather than authored project data; selected feature statistics therefore follow the current session selection and are not serialized into the project.

allowedComponentControls is an optional complete allowlist for the MapLibre Components control grid. An empty list hides the grid while preserving the plugin runtime needed by restored COG, Zarr, and other plugin-owned layers; omitting the field preserves the full grid for ordinary projects.

showcase.guide is the bilingual project guide opened from the map. It holds a title, introduction, ordered steps (title and body), requirements, and an optional glossary of { term, definition } entries, each an { en, es } pair. Glossary terms that appear in the introduction, step bodies, or requirements become inline explanations, matched case-insensitively as whole words so that buyers without GIS training can follow the guide.

showcase.translations shows the rest of the project in the interface language. It maps a language code to a table keyed by the project’s English display text, for example { "es": { "Fatal crashes": "Siniestros fatales" } }. When a project opens, the app replaces display text (layer, group, and model node names; widget, legend, and story titles; field, class, and legend labels; descriptions; and provenance methods) with the table’s entry for the current language. Ids, URLs, field names that styles and filters read, and data values never change, so a translated project renders and queries exactly like the original. A widget category may map to a field holding the same categories in the target language, such as "pattern_en": "pattern_es". Text already in another language maps back to English first, so a project saved while translated reopens correctly; a language without a table falls back to English. Explore showcases keep their tables in scripts/assets/showcases/translations/<slug>.es.json, and bun scripts/assets/explore-catalog.ts --check fails when a display string has no translation or an entry is no longer used.

Plugin state

{
  "manifestUrls": ["https://example.com/plugins/example-plugin/plugin.json"],
  "activePluginIds": ["maplibre-layer-control", "maplibre-gl-swipe"],
  "mapControlPositions": {
    "maplibre-layer-control": "top-right",
    "maplibre-gl-swipe": "top-left"
  },
  "settings": {
    "maplibre-gl-swipe": {
      "orientation": "vertical",
      "position": 50,
      "collapsed": false,
      "active": true,
      "leftLayers": ["layer-a"],
      "rightLayers": ["layer-b"]
    }
  }
}

Projects without a plugins section open with the built-in default plugin state.

Built-in map controls

mapControls stores only values that differ from the built-in defaults for Navigation, Fullscreen, Compass, Geolocate, Globe, Terrain, Scale, Attribution, Logo, and Maptoolkit Logo. The section is omitted when every control is at its default. On open, these sparse overrides are merged onto the canonical defaults, so the Map menu, saved JSON, and restored map all read one contract.

{
  "visibility": { "geolocate": true },
  "positions": {
    "geolocate": "bottom-left"
  }
}

Plugin-owned controls remain under plugins; they are not duplicated in mapControls.

Credentials at external egress

Project files can contain service URLs and plugin configuration. Before a project is shared, exported as HTML, sent to an external embed host, or saved into the saved layer library (My Data), Meratyn creates a detached copy that removes credential fields, signed URL parameters and userinfo. Runtime environment variables and geocoder keys are cleared, and opaque plugins.settings is cleared wholesale. Inline GeoJSON remains data and is copied unchanged.

An ordinary local Save offers an explicit choice to keep credentials in that local project file; the external egress paths never offer that override.

The existing co-located Python widget may request full project fidelity through trustedWidget: true on its accepted host message. This narrow widget bridge is not a general trust grant: ordinary external embeds remain credential-redacted.

Standalone HTML exports additionally sandbox their viewer iframe. Only the capabilities needed for script execution, same-origin storage/handshake, form submits, and anchor downloads are enabled; popups and top-level navigation are withheld.

Comments

comments stores bounded review threads. Every comment has an id, an author (name and hex color), non-empty body, valid timestamp createdAt, resolved flag, and replies; each reply has its own id, author, body, and createdAt. Point anchors store lngLat; feature anchors store layerId and string or numeric featureId, with optional lngLat for a fallback map position.

{
  "comments": [
    {
      "id": "comment-1",
      "anchor": { "type": "point", "lngLat": [-75.5636, 6.2442] },
      "author": { "name": "Ada", "color": "#123abc" },
      "body": "Verify this boundary.",
      "createdAt": "2026-08-04T05:00:00.000Z",
      "resolved": false,
      "replies": [
        {
          "id": "reply-1",
          "author": { "name": "Lin", "color": "#0f766e" },
          "body": "Confirmed.",
          "createdAt": "2026-08-04T05:05:00.000Z"
        }
      ]
    },
    {
      "id": "comment-2",
      "anchor": {
        "type": "feature",
        "layerId": "parcels",
        "featureId": "42",
        "lngLat": [-74.071, 4.712]
      },
      "author": { "name": "Ada", "color": "#123abc" },
      "body": "Check the owner attribute.",
      "createdAt": "2026-08-04T05:10:00.000Z",
      "resolved": true,
      "replies": []
    }
  ]
}

Legend

The map and Print Layout legends derive their classes from visible layers’ symbology. The legend object stores presentation edits and class identity, so customizations survive layer additions and removals.

{
  "title": "Legend",
  "groupByLayer": true,
  "order": ["layer-b", "layer-a"],
  "overrides": {
    "layer-a": { "label": "Roads" },
    "layer-b::0": { "label": "Low" },
    "layer-b::1": { "hidden": true }
  }
}
  • title — heading drawn above the legend entries.
  • groupByLayer — when true, graduated/categorized classes are grouped under a per-layer heading; when false, classes are listed flat.
  • order — top-level entry order by layer id (top-first); layers not listed keep their default order after the listed ones.
  • overrides stores per-item label and hidden edits. A layer id names a whole entry. Historical ${layerId}::${index} keys name displayed classes; ${layerId}::stop:${sourceIndex} keys retain generated classes by authored stop position when sampling changes.

Reserved ${layerId}::identity:stop:${sourceIndex} entries contain { "classIdentity": true }. They retain generated-class ownership after its last label or visibility edit is cleared. These entries are metadata, never visible labels or translation inputs. Keep them when hand-editing a saved project. Removing the layer’s legend customizations removes its identity entries too.

Projects without a legend section open with the default legend (auto-generated from the layers, titled “Legend”).

Story map

A story map turns the project into a scroll-driven narrative. Each chapter captures a camera view plus text, and can fade project layers in or out on enter/exit. Settings entered before the first chapter are saved and restored. The section is omitted only when it has no chapters and all settings are at their defaults.

{
  "title": "A Tour of Three Cities",
  "subtitle": "Built with Meratyn",
  "byline": "By the Meratyn team",
  "footer": "Source: OpenStreetMap",
  "theme": "dark",
  "showMarkers": true,
  "markerColor": "#3fb1ce",
  "inset": false,
  "insetPosition": "bottom-right",
  "chapters": [
    {
      "id": "intro",
      "title": "Cartagena",
      "description": "A walled city on the Caribbean coast. <em>HTML allowed.</em>",
      "image": "https://example.com/cartagena.jpg",
      "alignment": "left",
      "hidden": false,
      "location": {
        "center": [-75.5144, 10.391],
        "zoom": 11,
        "pitch": 45,
        "bearing": 0
      },
      "mapAnimation": "flyTo",
      "rotateAnimation": false,
      "onChapterEnter": [{ "layerId": "layer-a", "opacity": 1, "duration": 2000 }],
      "onChapterExit": [{ "layerId": "layer-a", "opacity": 0 }]
    }
  ]
}

alignment is one of left, center, right, full; mapAnimation is flyTo, easeTo, or jumpTo. Layer opacity changes reference project layer ids. Build and present story maps from Project → Story Map, or export a self-contained HTML page for static hosting.

Relations

relations links features of two layers one-to-many, such as the constructions registered on a cadastral lot. Nothing is copied between layers; a feature’s related records are resolved when they are shown. Keys match as persistent joins do: empty values never match, and non-empty values compare as strings. Relations to a layer that no longer exists, and malformed entries, are dropped when the project is read. The section is omitted when the project has none.

{
  "relations": [
    {
      "id": "lot-constructions",
      "name": "Constructions on this lot",
      "parent": { "layerId": "igac-lots", "field": "CODIGO" },
      "child": { "layerId": "igac-constructions", "field": "TERRENO_CO" }
    }
  ]
}

A feature on the parent layer lists its children under the relation’s name; a feature on the child layer shows its parent.

World

world gives a project a walkable 3D world. It opens inside the map area of the same shell, so the layer list, docks, dashboard, and selection keep working on the same project: visibility, styles, and filters set there show in the world, and a feature selected in the world is selected everywhere. A 3D world button on the map opens it and Map returns; a showcase whose surface is world opens with it. The world draws the project’s own visible layers with their own styles: vector layers become geometry (extruded polygons use the layer’s extrusion height, base, and color, and the 3D wall and roof colors), web tile layers are draped on the ground, and each feature’s record comes from its layer’s featurePresentation and the project’s relations.

{
  "world": {
    "origin": { "lng": -75.7917, "lat": 8.8846 },
    "radius": 350,
    "place": { "name": "Cereté", "region": "Córdoba" },
    "start": {
      "lng": -75.7915,
      "lat": 8.884672,
      "heading": 345,
      "home": { "layerId": "lots", "featureId": "231620101000001960008000000000" }
    },
    "streetLayer": { "layerId": "streets", "nameField": "name", "surfaceField": "surface" },
    "toggles": ["imagery", "not-in-cadastre"],
    "viewpointFields": { "heading": "view_heading", "lat": "view_lat", "lng": "view_lng" },
    "model": {
      "url": "https://storage.example.org/worlds/cerete/cerete-world.glb",
      "featureLayerId": "buildings",
      "idField": "bid",
      "roofGroupField": "roof_group",
      "frame": {
        "origin": { "lng": -75.7917, "lat": 8.8846 },
        "metersPerDegree": { "lat": 111320, "lng": 109985 }
      },
      "drape": ["Ground", "Lots"],
      "surfaces": [{ "layerId": "lots", "height": 0.18 }]
    }
  }
}
  • origin anchors the world’s local meter frame; radius is the half-width of the square world in meters (50 to 2,000, or up to 8,000 with a tiled model).
  • place names the world in its header; region stands in for the street name while no street is near.
  • start is where a visitor first stands and Home returns to, with heading in degrees clockwise from north. home is the feature Home selects.
  • streetLayer names the line layer whose nameField says which street the visitor is on; surfaceField (concrete, asphalt, or unpaved) paves the draped ground.
  • toggles lists layers offered as quick switches over the world. They switch the same visibility as the Layers panel.
  • viewpointFields names feature properties holding an authored street-level viewpoint, used by Go there and Street View instead of one derived from the feature’s extent.
  • model is a glTF neighbourhood (optionally Draco-compressed) drawn instead of the extrusions of featureLayerId. Meshes whose node has extras.barrio = "building" carry each feature’s idField value in their second UV set, so picking a mesh selects the feature, and hover, selection, and tints of other visible layers containing the same ids paint the model. frame states the model’s own equirectangular frame so it lines up with the layers; nodes in drape take the ground imagery; surfaces are walkable heights above the ground; roofGroupField groups records under one roof so the roof takes one color from the imagery. The map draws the same model in place of that layer’s extrusions: the layer stays as invisible footprints for clicks and selection, the model’s flat ground surfaces give way to the map’s own imagery and basemap, and features another visible layer lists (by idField) are tinted as in the world.
  • model.format is gltf (the default: url is one file loaded whole) or 3d-tiles: url is a 3D Tiles tileset.json whose glTF tiles follow the same conventions, in the model’s Z-up metric frame (x east, y north). Only the tiles around the viewer load at full detail and distant ground loads as coarser levels, so a model may cover a whole town. A tiled model brings its own ground. model.collision names its collision manifest (collision.json: a cellSize grid of glTF collision cells); the walker loads the cells around the visitor, follows their floors, and is stopped by their walls instead of using footprints and surfaces. The map keeps drawing featureLayerId as its own extrusions for a tiled model, since a town-wide model stands on terrain heights the flat map cannot seat. Both files are produced by scripts/assets/worlds/blender_cells.py and scripts/assets/worlds/tileset.ts.

With relations, a feature on a relation’s parent layer is a place (a lot): the selection outlines and tags its place, marks everything on it as related, and choosing a place with a single feature on it selects that feature. A malformed world is dropped and the project opens as a map only.

Dashboard widgets

{
  "widgets": [
    {
      "id": "w1",
      "layerId": "layer-a",
      "type": "histogram",
      "field": "pop",
      "bins": 12
    },
    {
      "id": "w2",
      "layerId": "layer-a",
      "type": "bar",
      "category": "kind",
      "aggregation": "sum",
      "valueField": "pop",
      "title": "Population by kind"
    },
    {
      "id": "w3",
      "layerId": "layer-a",
      "type": "selector",
      "category": "kind",
      "multiple": true,
      "selection": ["residential", "commercial"]
    }
  ]
}

Each widget may set placement to dashboard, centerDock, leftDock, rightDock, or headerDock. Omitting it keeps the widget in the standalone Dashboard below the workspace (ShellSurfaces), just like dashboard. centerDock belongs to the Dashboard tab of the central attribute workspace; its widgets and opening state are independent from the standalone Dashboard. The editor and Map Tools → Dashboard offer all five locations, and each dock displays only its assigned widgets. The header dashboard uses the shared HeaderDock host, including its panel switcher, collapse control, and adjustable height.

Widgets can move between visible dashboards by dragging or through the widget’s Move to menu. Closed destinations have temporary drag targets, except for centerDock, which accepts drops only when its dashboard is visible.

Each widget binds a chart to a layer’s attributes. type is one of histogram, scatter, bar, line, box, pie, indicator, or selector. Which other keys apply depends on the type: field (histogram/line/box), xField/yField (scatter), category + aggregation + valueField (bar/pie), bins (histogram). Bar aggregation is count/sum/mean; pie is count/sum only. title is an optional label and color an optional hex (#rgb/#rrggbb) for the chart’s marks (the series color for single-series charts; the base of a monochromatic ramp for bar/pie). An indicator can use indicatorAggregation (count, sum, mean, min, max, or median) and reads field for every aggregation except count, plus optional prefix and suffix. A selector requires category; its optional multiple flag permits more than one value, and its persisted selection is normalized to unique non-empty strings (or one value when multiple is omitted or false). Unused keys are ignored. The Dashboard panel (Tools → Dashboard, or the Dashboard button in the attribute table) also stores dashboardColumns, the widget-grid column count (1-6, default 2), at the top level of the project. Charts read from GeoJSON-backed vector layers and DuckDB query layers; widgets bound to a missing or non-attribute layer are shown as empty.

Layer object

{
  "id": "uuid",
  "name": "My Layer",
  "type": "geojson",
  "source": { "type": "geojson" },
  "visible": true,
  "opacity": 1,
  "style": {
    "minZoom": 0,
    "maxZoom": 24,
    "fillColor": "#3b82f6",
    "strokeColor": "#1e40af",
    "strokeWidth": 2,
    "strokeWidthUnit": "pixels",
    "strokeOpacity": 1,
    "fillOpacity": 0.6,
    "circleRadius": 6,
    "rasterBrightnessMin": 0,
    "rasterBrightnessMax": 1,
    "rasterSaturation": 0,
    "rasterContrast": 0,
    "rasterHueRotate": 0
  },
  "metadata": {},
  "geojson": { "type": "FeatureCollection", "features": [] },
  "editorTracking": {
    "enabled": true,
    "createdByField": "created_by",
    "createdAtField": "created_at",
    "editedByField": "edited_by",
    "editedAtField": "edited_at"
  },
  "sourcePath": "/path/to/file.geojson"
}

Vector styles may also store typed MapLibre expression strings in fillOpacityExpression, strokeColorExpression, strokeOpacityExpression, strokeWidthExpression, and circleRadiusExpression. Each expression overrides its corresponding literal value while it is valid. The project decoder validates these fields independently with Effect Schema, so one invalid hand-edited expression is dropped without discarding the rest of the layer style.

A custom point marker (markerShape: "custom") stores its source in markerSvg: inline SVG markup, or a data: or http(s): URL of an SVG, PNG, JPEG, or GIF image. Any other URL scheme is ignored. A graduated style whose breaks were edited by hand records vectorStyleClassificationScheme: "manual"; the stored breaks are used as they are. vectorStyleColorRampReversed: true runs vectorStyleColorRamp from its last color to its first whenever classes are regenerated; it defaults to false. A categorized style with vectorStyleAllCategories: true classifies every distinct value, so its class count follows the field’s complete category count; it defaults to false. Labels that format a numeric field store labels.numberFormatEnabled, labels.numberDecimals (a whole number from 0 to 10; any other value restores the default), and labels.numberLocale (a BCP 47 tag, or empty to follow the app language).

Editor tracking

An in-memory GeoJSON layer may include editorTracking to maintain author and timestamp fields automatically. enabled controls whether the policy is active; the four optional field names default to created_by, created_at, edited_by, and edited_at. Enabled configurations must use four distinct, non-empty names. Malformed or colliding names are treated as tracking-off by read-only queries so an imported project remains usable; write paths refuse to stamp until the policy is corrected. Tracking columns are read-only in the Attribute Table and are not valid targets for rename, delete, or field-calculator operations.

Layer operation capabilities

A layer may include a capabilities object with optional boolean query, create, update, delete, and export fields. Missing fields use the source-kind default: ordinary in-project GeoJSON is queryable and editable; derived queries, tiles, and raster layers are read-only; export remains enabled unless explicitly disabled. Invalid or empty objects are discarded while loading.

These flags are product policy enforced by the Layer panel, Attribute Table, GeoEditor, sharing/export preparation, and PostGIS write-back. export: false removes embedded GeoJSON snapshots from a shared/exported project but deliberately leaves a live source descriptor intact. The PostGIS sidecar’s echoed check is consistency protection for the project policy, not authorization; database grants must enforce access against untrusted callers.

Quick filters

quickFilters stores editable control state on a vector layer. It never stores the compiled MapLibre expression. Each entry has a stable id, a field, a kind (categorical, range, date, or text), and an optional enabled flag. Its value fields depend on the kind:

{
  "quickFilters": [
    {
      "id": "status",
      "field": "status",
      "kind": "categorical",
      "values": ["open", "planned"]
    },
    {
      "id": "height",
      "field": "height_m",
      "kind": "range",
      "min": 10,
      "max": 80
    },
    {
      "id": "survey",
      "field": "surveyed_at",
      "kind": "date",
      "dateKind": "iso",
      "start": "2026-08-01",
      "end": "2026-08-26"
    },
    {
      "id": "name",
      "field": "name",
      "kind": "text",
      "operator": "contains",
      "text": "port"
    }
  ]
}

Categorical values retain string, finite-number, and Boolean types. Range bounds are inclusive and may be null. Date bounds are inclusive ISO calendar days; dateKind is iso, epochMs, or epochS. Text operator is contains, startsWith, or equals. Empty or disabled controls compile to no constraint. At layer sync, active quick filters are combined with native, time, and rule-based filters under a single conjunction.

Attribute-table export exclusions

For a vector layer, metadata.columnSettings.excluded may contain property names that must be omitted from external data egress. This is distinct from hidden, which controls only the local attribute-table view and never removes data. Local project saves retain the full property set. Meratyn applies excluded only to detached external copies for vector exports, standalone HTML, Share uploads, Python to_project(), and other external egress; the in-memory layer and its geometry, feature IDs, null properties, and unrelated metadata remain unchanged.

{
  "metadata": {
    "columnSettings": {
      "hidden": ["internal_note"],
      "excluded": ["personal_email"]
    }
  }
}

Online layer references

Layers added through the built-in Catalog plugin keep their current MapLibre source plus a stable Share reference in metadata:

{
  "metadata": {
    "hostedSourceKind": "mappview-hosted",
    "hostedLayerId": "8ab66014-6c72-42b5-b2f5-c1aa7909c77b",
    "hostedServiceId": "wms",
    "hostedServiceType": "wms",
    "hostedStableUrl": "https://share.example/api/layers/8ab66014-6c72-42b5-b2f5-c1aa7909c77b/gateway/wms",
    "shareBaseUrl": "https://share.example"
  }
}

The app may temporarily use a short-lived access URL or in-memory authorization while a private layer is open. Project serialization removes those runtime credentials and restores hostedStableUrl and hostedStableSource. A consumer therefore resolves access with their own Share session; a project file never grants access by carrying its author’s token.

The stable layer ID survives data replacement, metadata changes, style changes, and provider migrations. Do not persist an implementation server’s resource ID or internal service URL in a project.

For WFS GetFeature and GeoJSON URL layers, metadata.refresh can persist an optional auto-refresh interval. intervalMs can be any positive interval in milliseconds:

{
  "metadata": {
    "refresh": { "enabled": true, "intervalMs": 60000 }
  }
}

Manual refresh uses the same saved source URL without requiring this metadata.

GeoJSON URL and ArcGIS feature-query layers load after the project opens: the map and every other layer appear at once, and each service layer shows as loading until its rows arrive. A layer can therefore omit geojson and ship only its source URL. When it does embed rows, they display immediately and are replaced by the service’s rows if its refresh is enabled; with refresh disabled they are used as-is. A service that fails reports an issue on its layer and never stops the project from opening.

For local-file vector layers on the desktop app, metadata.watch can persist a “watch this file for changes” toggle. When enabled, the desktop app registers a filesystem watcher that reloads the layer’s features from sourcePath whenever the file changes on disk:

{
  "metadata": {
    "watch": { "enabled": true }
  }
}

The key is omitted when watching is off, and it has no effect off the desktop host (the browser cannot watch a local filesystem path).

A layer may carry persistent attribute joins (Layer properties → Joins): live left joins that materialize columns from another layer’s attribute table — typically a geometry-less table added via Delimited Text with no coordinate fields — into this layer’s feature properties, matched on a key field:

{
  "joins": [
    {
      "id": "uuid",
      "joinLayerId": "other-layer-uuid",
      "targetField": "name",
      "joinField": "state_name",
      "fields": ["pop_2025", "median_income"],
      "prefix": "census_",
      "enabled": true,
      "addedFields": ["census_pop_2025", "census_median_income"],
      "stats": {
        "matchedCount": 50,
        "unmatchedTargetCount": 2,
        "unmatchedJoinCount": 2
      }
    }
  ]
}

fields (subset to bring over; omitted = every field except the key), prefix, and enabled are optional. addedFields and stats are engine bookkeeping, rewritten on every apply: addedFields lists the output columns the join added (so re-applying can strip them first, keeping the operation idempotent), and stats records the last match counts shown in the Joins UI. Joins re-resolve against the loaded layer set on project open, so a saved copy of the joined output self-heals if the join table changed.

When a geojson layer enables style.simpleStyleEnabled, individual features may override the layer style with simplestyle-spec properties (stroke, fill, stroke-width, fill-opacity, …). Meratyn also honors a per-feature text-color on text-marker points (used by the Annotations layer), falling back to style.textColor when a feature does not set it.

Layer types

Typev1.0 status
geojsonSupported for imported files and GeoJSON URLs
xyzSupported for raster tile templates
wmsSupported as tiled WMS GetMap layers
rasterSupported for raster tile templates
vector-tilesSupported for MapLibre vector tile sources
mbtilesSupported in the desktop app through a local MapLibre protocol
arcgisSupported for ArcGIS FeatureServer and VectorTileServer layers; rendered MapServer/ImageServer services are persisted as raster layers with ArcGIS metadata
pmtilesSupported through the Components plugin
cogSupported for COG and GeoTIFF raster layers
flatgeobufSupported through the Components plugin and imported as GeoJSON when loaded as a local vector file
zarrSupported through the Components plugin
lidarSupported through the Components plugin
gaussian-splatSupported through the Components plugin
geoparquetImported as GeoJSON via DuckDB-WASM
duckdb-querySupported for SQL query-result layers
3d-tilesSupported through the maplibre-gl-3d-tiles plugin

A geojson layer loaded page by page from an ArcGIS FeatureServer or MapServer query stores source.arcgisQueryUrl, source.pageSize, and source.maxFeatures, plus an optional source.arcgisWhere SQL filter. The filter is applied to every page and to the feature count, so a layer such as “fatal crashes only” stays filtered when it is refreshed or reopened.

API

import { createEmptyProject, parseProject, serializeProject } from "@mappview/core/project";

Feature presentation

Vector layers may carry an optional featurePresentation policy. Its clickEnabled and hoverEnabled flags control authored interaction surfaces; titleField selects a title; and ordered fields provide field, optional label, visible, and one of text, number, date, link, or image formats. Omit the object to preserve legacy identify presentation. Links and images are restricted to credential-free HTTP(S) URLs; internal, object, and unsafe values are not rendered.

Blank background appearance

When basemapStyleUrl is the Blank basemap, blankBackgroundColor may contain a CSS hex color. Omit it or set it to null to use the active theme default; theme colors are not copied into a project.