diff --git a/docs/design/visibility.md b/docs/design/visibility.md
index 05f7e80..576d448 100644
--- a/docs/design/visibility.md
+++ b/docs/design/visibility.md
@@ -138,6 +138,10 @@ Each decision lives at the cheapest layer that can express it correctly:
The tile-build numbers are set generously (the cap well above any budget, floors below any style threshold that might move) precisely so the style side can be retuned without touching them.
+## Standards mode is the off-switch for every deviation on this page
+
+The `standards` style option renders strict S-52 portrayal instead of the chart's own: every mechanism above — budgets, `topOfCell`, decoration legibility floors, size tokens and ramps — is a deliberate departure from the standards, and standards mode neutralizes them all in one place ([`visibility.ts`](../../style/layers/visibility.ts), `visibility(standards)`). Symbols draw at their fixed S-52 size, decorations draw whenever their symbol does, visibility comes from the SCAMIN-derived `std_minzoom` the tiles carry per feature ([zoom.md](zoom.md)), and sectors switch to display-fixed 20 mm arcs drawn as rotated sprites ([`sectors.ts`](../../style/layers/sectors.ts)) — the portrayal this page argues against, kept available because seeing the standard is how the departures stay honest. Two ceilings are accepted rather than papered over: the destructive tile cap still bounds what dense cells carry below z13, and an all-scales class exists only from its tile floor.
+
## Safety guarantees
- A hazard inside any supported safety depth is always in the tiles, and exempt from its budget when it breaches the mariner's setting.
diff --git a/docs/design/zoom.md b/docs/design/zoom.md
index 4575102..613aede 100644
--- a/docs/design/zoom.md
+++ b/docs/design/zoom.md
@@ -43,6 +43,8 @@ The tiles carry this decision's inputs in S-52's own vocabulary, both DEM-derive
“Derived” combines the home-band floor with the class's SCAMIN steps: the first zoom at which the standards would show the feature, assuming coverage in every band. Each departure includes its rationale so a later audit does not have to reconstruct or relitigate the decision.
+The derived floor is not documentation-only: `SeamarkZoomRules.getStandardMinZoom` bakes it into every `seamark` and `light` feature as `std_minzoom` (0 = SCAMIN NOT SET), and the style's standards mode thresholds on it at runtime ([`index.ts`](../../style/layers/index.ts)). The tile floor stays the Minzoom column — everywhere at or below the derived value, so the strict filter always has the feature to work with. The one exception runs the other way: an all-scales class (`std_minzoom` 0) still only exists from its tile floor, so standards mode shows TSS linework from z2 and conspicuous landmarks from z6, not from z0.
+
| Type(s) | Minzoom | Derived | Note |
| --------------------------------------------- | -------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| TSS lanes / lines / boundaries | 2 | any (NOT SET) | ✓ the linework is the passage-planning signal and NOT SET licenses any floor; z2 matches OpenSeaMap-vector |
diff --git a/src/main/java/Lights.java b/src/main/java/Lights.java
index 1715837..d1d3733 100644
--- a/src/main/java/Lights.java
+++ b/src/main/java/Lights.java
@@ -4,7 +4,8 @@
import org.locationtech.jts.geom.*;
/**
- * Turns a light's OSM sector tags into arc and leg lines on the ground.
+ * Turns a light's OSM sector tags into arc and leg lines on the ground, and into a point per sector
+ * and per sector limit carrying the angles those figures span.
*
*
S-52 fixes the arc to the display — 20 mm radius whatever the scale (PresLib 4.0.4,
* `LIGHTS06`) — but on a chart the reader zooms, a display-fixed figure is the one thing moving
@@ -12,6 +13,9 @@
* the chart like its neighbours, and the style stops drawing it at the zoom where the radius
* outgrows the screen. The radius is a drawing size, not the light's range: nobody navigates by
* being inside the arc, and the range is stated in the characteristic.
+ *
+ *
A line cannot hold a constant screen size, so the points are what a display-fixed portrayal
+ * draws from: the light's position plus the bearings to rotate a sprite to.
*/
public class Lights {
@@ -41,13 +45,34 @@ private LightGeometry(Geometry geometry, Map attrs) {
/** A coloured sector, drawn as an arc at the light's nominal radius. */
static LightGeometry sector(
Geometry arc, String color, String visibility, String range, boolean extended) {
+ return new LightGeometry(arc, sectorAttrs("sector", color, visibility, range, extended));
+ }
+
+ /** The same sector as a point at the light, for a portrayal fixed to the display. */
+ static LightGeometry sectorPoint(
+ Point at,
+ String color,
+ String visibility,
+ String range,
+ double start,
+ double end,
+ boolean extended) {
+ Map attrs = sectorAttrs("sector_point", color, visibility, range, extended);
+ attrs.put("sector_start", start);
+ attrs.put("sector_end", end);
+ attrs.put("sector_width", width(start, end));
+ return new LightGeometry(at, attrs);
+ }
+
+ private static Map sectorAttrs(
+ String subtype, String color, String visibility, String range, boolean extended) {
Map attrs = new HashMap<>();
- attrs.put("subtype", "sector");
+ attrs.put("subtype", subtype);
if (extended) attrs.put("extended", true);
if (color != null) attrs.put("color", color);
if (visibility != null) attrs.put("visibility", visibility);
if (range != null) attrs.put("range", range);
- return new LightGeometry(arc, attrs);
+ return attrs;
}
/** One radial leg, at a bearing where some sector begins or ends. */
@@ -57,6 +82,15 @@ static LightGeometry leg(Geometry line, String range) {
if (range != null) attrs.put("range", range);
return new LightGeometry(line, attrs);
}
+
+ /** The same limit as a point at the light, for a portrayal fixed to the display. */
+ static LightGeometry limit(Point at, double bearing, String range) {
+ Map attrs = new HashMap<>();
+ attrs.put("subtype", "limit");
+ attrs.put("bearing", bearing);
+ if (range != null) attrs.put("range", range);
+ return new LightGeometry(at, attrs);
+ }
}
/** Sector width in degrees, going clockwise from start to end. */
@@ -115,13 +149,13 @@ public static List extractLightGeometries(SourceFeature sf, Strin
if (from.equals(to) || (from == 0 && to == 360)) continue;
boolean extended = overlappedByWider(from, to, limits);
double radius = (extended ? EXTENDED_SCALE : 1) * arcRadius / metersPerWorldUnit;
+ String color = Seamark.resolveLightColor(segment.get("colour"));
+ String visibility = segment.get("visibility");
+ String range = segment.get("range");
results.add(
LightGeometry.sector(
- createArc(center, from, to, radius),
- Seamark.resolveLightColor(segment.get("colour")),
- segment.get("visibility"),
- segment.get("range"),
- extended));
+ createArc(center, from, to, radius), color, visibility, range, extended));
+ results.add(LightGeometry.sectorPoint(center, color, visibility, range, from, to, extended));
}
// Adjacent sectors share a limit, and one leg per bearing is enough. Bearings normalize
@@ -145,6 +179,7 @@ public static List extractLightGeometries(SourceFeature sf, Strin
double legRadius = EXTENDED_SCALE * arcRadius / metersPerWorldUnit;
for (Map.Entry leg : legs.entrySet()) {
results.add(LightGeometry.leg(createLeg(center, leg.getKey(), legRadius), leg.getValue()));
+ results.add(LightGeometry.limit(center, leg.getKey(), leg.getValue()));
}
return results;
diff --git a/src/main/java/Seamap.java b/src/main/java/Seamap.java
index 4cb7ef6..5733e32 100644
--- a/src/main/java/Seamap.java
+++ b/src/main/java/Seamap.java
@@ -214,6 +214,8 @@ private void doProcessFeature(SourceFeature sf, FeatureCollector features) {
if (type != null) {
// add seamark to vector tile
attrs.put("osm_id", sf.id());
+ // the floor a strict S-52 portrayal would use, for a style that offers one
+ attrs.put("std_minzoom", SeamarkZoomRules.getStandardMinZoom(attrs));
// anyGeometry() makes a polygon of any closed way, which is wrong for the types that are
// linear however they're drawn — a TSS lane or a cable loop is never an area.
FeatureCollector.Feature feature =
@@ -258,6 +260,8 @@ private void doProcessFeature(SourceFeature sf, FeatureCollector features) {
lightFeature.setId(featureId(sf));
lightFeature.setAttr("osm_id", sf.id());
lightFeature.setAttr("type", type);
+ // sector geometry appears with the light it belongs to, never on a floor of its own
+ lightFeature.setAttr("std_minzoom", attrs.get("std_minzoom"));
lightGeom.attrs.forEach((k, v) -> lightFeature.setAttr(k, v));
lightFeature.setMinZoom(SeamarkZoomRules.getLightMinZoom(attrs));
}
diff --git a/src/main/java/SeamarkZoomRules.java b/src/main/java/SeamarkZoomRules.java
index 2b40384..50ae2bf 100644
--- a/src/main/java/SeamarkZoomRules.java
+++ b/src/main/java/SeamarkZoomRules.java
@@ -32,6 +32,66 @@ public class SeamarkZoomRules {
Map.entry("fog_signal", 10),
Map.entry("anchorage", 9));
+ /**
+ * The same floors as the standards put them: the home band's scale plus the class's SCAMIN steps
+ * (S-57 UOC Table 2.5), which is where the chart's own floors deliberately depart. The derivation
+ * and every departure are tabulated in docs/design/zoom.md.
+ */
+ private static final Map STANDARD_FLOORS =
+ Map.ofEntries(
+ Map.entry("light_major", 5),
+ Map.entry("light_minor", 11),
+ Map.entry("fog_signal", 12),
+ Map.entry("platform", 5),
+ Map.entry("anchorage", 12),
+ Map.entry("cable_area", 10),
+ Map.entry("pipeline_area", 10),
+ Map.entry("marine_farm", 10),
+ Map.entry("buoy_lateral", 12),
+ Map.entry("beacon_lateral", 12),
+ Map.entry("buoy_special_purpose", 12),
+ Map.entry("beacon_special_purpose", 12),
+ Map.entry("mooring", 14),
+ Map.entry("harbour", 11),
+ Map.entry("small_craft_facility", 15));
+
+ /**
+ * The strict floor: the zoom at which the S-57/S-52 derivation in docs/design/zoom.md would first
+ * show the feature, band compilation and SCAMIN together. 0 is SCAMIN NOT SET — all scales, which
+ * in practice means the feature's own {@link #getMinZoom}, since nothing exists below it.
+ * Wherever SCAMIN does set a floor this is the later of the two, so a style thresholding on it is
+ * always asking for a feature the tile carries.
+ *
+ * @param attrs Map containing seamark attributes (type, category, etc.)
+ * @return the standards-derived minimum zoom
+ */
+ public static int getStandardMinZoom(Map attrs) {
+ String type = (String) attrs.get("type");
+ String category = (String) attrs.get("category");
+
+ int base;
+ if (type == null) {
+ base = chartFloor(type, category, attrs);
+ } else if (isHazard(type)) {
+ // the contextual derivation is itself the standard — UDWHAZ and S-4 B-404, not a departure
+ base = hazardMinZoom(type, category, attrs);
+ } else if (type.startsWith("separation_")) {
+ base = 0; // the whole TSS carries SCAMIN NOT SET
+ } else if ("landmark".equals(type)) {
+ // CONVIS promotes to STANDARD and a lit LNDMRK takes no SCAMIN; a plain one is Coastal detail
+ base = isSteeredBy(attrs) ? 0 : 11;
+ } else if (STANDARD_FLOORS.containsKey(type)) {
+ base = STANDARD_FLOORS.get(type);
+ } else if (isRestrictedArea(type)) {
+ base = 6;
+ } else if (isMediumHighPriorityType(type)) {
+ base = 9;
+ } else {
+ base = chartFloor(type, category, attrs);
+ }
+ return promoteByRange(base, attrs, 5, 6);
+ }
+
/**
* Get the minimum zoom level for a given seamark based on its type and attributes.
*
@@ -39,9 +99,12 @@ public class SeamarkZoomRules {
* @return minimum zoom level (0-14)
*/
public static int getMinZoom(Map attrs) {
- String type = (String) attrs.get("type");
- String category = (String) attrs.get("category");
+ int base = chartFloor((String) attrs.get("type"), (String) attrs.get("category"), attrs);
+ return promoteByRange(base, attrs, 4, 6);
+ }
+ /** The chart's own floor for a type, before a light's reach promotes it. */
+ private static int chartFloor(String type, String category, Map attrs) {
int base;
if (type == null) {
base = 8; // default
@@ -64,29 +127,39 @@ public static int getMinZoom(Map attrs) {
base = 8;
}
- // A conspicuous landmark is part of what a mariner steers by (CONVIS promotes to the
- // STANDARD display category in S-52), a lit one is an aid in its own right (LNDMRK carrying
- // a light takes no SCAMIN), and wind turbines are conspicuous by what they are — the tag is
- // rarely present, and without this a wind farm is empty sea below z10.
+ // Wind turbines are conspicuous by what they are — the conspicuity tag is rarely present, and
+ // without this a wind farm is empty sea below z10.
if ("landmark".equals(type)
- && ("conspicuous".equals(attrs.get("seamark:landmark:conspicuity"))
- || attrs.get("light") != null
- || "windmotor".equals(category)
- || "windmill".equals(category))) {
+ && (isSteeredBy(attrs) || "windmotor".equals(category) || "windmill".equals(category))) {
base = Math.min(base, 6);
}
- // A light's reach outranks how its host happens to be typed: plenty of real lighthouses are
- // tagged light_minor or sit on plain beacons. The S-52 major-light test is 10 M nominal
- // range (LIGHTS06); 15 M is landfall class. Range only ever promotes, never demotes.
- Object range = attrs.get("light_range");
- if (range instanceof Number n) {
- if (n.doubleValue() >= 15) return Math.min(base, 4);
- if (n.doubleValue() >= 10) return Math.min(base, 6);
+ return base;
+ }
+
+ /**
+ * A light's reach outranks how its host happens to be typed: plenty of real lighthouses are
+ * tagged light_minor or sit on plain beacons. The S-52 major-light test is 10 M nominal range
+ * (LIGHTS06); 15 M is landfall class. Range only ever promotes, never demotes.
+ */
+ private static int promoteByRange(
+ int base, Map attrs, int landfallZoom, int majorZoom) {
+ if (attrs.get("light_range") instanceof Number n) {
+ if (n.doubleValue() >= 15) return Math.min(base, landfallZoom);
+ if (n.doubleValue() >= 10) return Math.min(base, majorZoom);
}
return base;
}
+ /**
+ * A landmark a mariner steers by: conspicuous (CONVIS promotes to the STANDARD display category
+ * in S-52) or carrying a light, which makes it an aid in its own right and takes its SCAMIN away.
+ */
+ private static boolean isSteeredBy(Map attrs) {
+ return "conspicuous".equals(attrs.get("seamark:landmark:conspicuity"))
+ || attrs.get("light") != null;
+ }
+
/**
* Floor for a light's sector arcs and legs: z8 for style headroom (the zoom a sector actually
* draws at is a style threshold, kept above this so it can move without a planet build), but
diff --git a/src/test/java/LightsTest.java b/src/test/java/LightsTest.java
index ad87f13..48b6038 100644
--- a/src/test/java/LightsTest.java
+++ b/src/test/java/LightsTest.java
@@ -1,4 +1,5 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.onthegomap.planetiler.reader.SimpleFeature;
@@ -145,6 +146,75 @@ void northLegIsNotPaintedTwice() {
assertEquals(3, of(all, "leg").size(), "270, north and 90 — never north twice");
}
+ /** Every sector is also a point at the light, carrying the angles it spans. */
+ @Test
+ void emitsOnePointPerSector() {
+ var all =
+ sectorsOf(
+ light(
+ "seamark:light:1:colour", "red",
+ "seamark:light:1:sector_start", "10",
+ "seamark:light:1:sector_end", "70",
+ "seamark:light:2:colour", "green",
+ "seamark:light:2:sector_start", "70",
+ "seamark:light:2:sector_end", "180"));
+ var points = of(all, "sector_point");
+ assertEquals(2, points.size());
+ for (var point : points) {
+ assertEquals("Point", point.geometry.getGeometryType());
+ }
+ var red = points.stream().filter(g -> "red".equals(g.attrs.get("color"))).findFirst().get();
+ assertEquals(10.0, red.attrs.get("sector_start"));
+ assertEquals(70.0, red.attrs.get("sector_end"));
+ assertEquals(60.0, red.attrs.get("sector_width"));
+ }
+
+ /** A point's width wraps through north rather than going negative. */
+ @Test
+ void pointWidthWrapsThroughNorth() {
+ var all =
+ sectorsOf(light("seamark:light:1:sector_start", "350", "seamark:light:1:sector_end", "20"));
+ assertEquals(30.0, of(all, "sector_point").get(0).attrs.get("sector_width"));
+ }
+
+ /** The smaller of an overlapping pair is marked on the point too. */
+ @Test
+ void marksTheSmallerOfTwoOverlappingSectorPoints() {
+ var points =
+ of(
+ sectorsOf(
+ light(
+ "seamark:light:1:sector_start", "0",
+ "seamark:light:1:sector_end", "180",
+ "seamark:light:2:sector_start", "80",
+ "seamark:light:2:sector_end", "100")),
+ "sector_point");
+ var wide = points.stream().filter(g -> g.attrs.get("sector_width").equals(180.0)).findFirst();
+ var narrow = points.stream().filter(g -> g.attrs.get("sector_width").equals(20.0)).findFirst();
+ assertFalse(wide.get().attrs.containsKey("extended"));
+ assertTrue((Boolean) narrow.get().attrs.get("extended"));
+ }
+
+ /** One point per deduped limit, at a bearing normalized into [0, 360). */
+ @Test
+ void emitsOnePointPerLimit() {
+ var all =
+ sectorsOf(
+ light(
+ "seamark:light:1:sector_start", "270",
+ "seamark:light:1:sector_end", "360",
+ "seamark:light:2:sector_start", "0",
+ "seamark:light:2:sector_end", "90"));
+ var limits = of(all, "limit");
+ assertEquals(3, limits.size(), "270, north and 90 — never north twice");
+ assertEquals(
+ List.of(0.0, 90.0, 270.0),
+ limits.stream().map(g -> (Double) g.attrs.get("bearing")).sorted().toList());
+ for (var limit : limits) {
+ assertEquals("Point", limit.geometry.getGeometryType());
+ }
+ }
+
/** An all-round light gets a flare, never an arc. */
@Test
void skipsLightsWithNoRealSector() {
diff --git a/src/test/java/SeamapTest.java b/src/test/java/SeamapTest.java
index 0d32326..bf7ba51 100644
--- a/src/test/java/SeamapTest.java
+++ b/src/test/java/SeamapTest.java
@@ -22,17 +22,21 @@ class SeamapTest {
private static final GeometryFactory GF = new GeometryFactory();
- private static List seamarkFeatures(SimpleFeature sf) {
+ private static List layerFeatures(SimpleFeature sf, String layer) {
FeatureCollector collector =
new FeatureCollector.Factory(PlanetilerConfig.defaults(), Stats.inMemory()).get(sf);
new Seamap().processFeature(sf, collector);
List out = new ArrayList<>();
for (FeatureCollector.Feature f : collector) {
- if ("seamark".equals(f.getLayer())) out.add(f);
+ if (layer.equals(f.getLayer())) out.add(f);
}
return out;
}
+ private static List seamarkFeatures(SimpleFeature sf) {
+ return layerFeatures(sf, "seamark");
+ }
+
/** A point source is already its own label; a second centroid would draw the name twice. */
@Test
void pointLandmarkEmitsOneFeature() {
@@ -60,6 +64,32 @@ void polygonLandmarkEmitsLabelPoint() {
assertEquals(2, seamarkFeatures(sf).size());
}
+ /**
+ * A sector light writes both portrayals — arcs and legs on the ground, points at the light — and
+ * all of them appear with the mark, on its standard floor.
+ */
+ @Test
+ void sectorLightsEmitBothPortrayalsUnderTheHostsStandardFloor() {
+ Map tags = new HashMap<>();
+ tags.put("seamark:type", "light_minor");
+ tags.put("seamark:light:1:colour", "red");
+ tags.put("seamark:light:1:sector_start", "10");
+ tags.put("seamark:light:1:sector_end", "70");
+ SimpleFeature sf =
+ SimpleFeature.create(GF.createPoint(new Coordinate(11.0, 55.0)), tags, "osm", null, 1);
+
+ var lights = layerFeatures(sf, "light");
+ var subtypes = lights.stream().map(f -> f.getAttrsAtZoom(14).get("subtype")).toList();
+ assertTrue(subtypes.contains("sector"), subtypes.toString());
+ assertTrue(subtypes.contains("leg"), subtypes.toString());
+ assertTrue(subtypes.contains("sector_point"), subtypes.toString());
+ assertTrue(subtypes.contains("limit"), subtypes.toString());
+ for (FeatureCollector.Feature light : lights) {
+ assertEquals(11, light.getAttrsAtZoom(14).get("std_minzoom"));
+ }
+ assertEquals(11, seamarkFeatures(sf).get(0).getAttrsAtZoom(14).get("std_minzoom"));
+ }
+
/** A seamark point at tile-pixel x/y, classified the way processFeature classifies it. */
private static VectorTile.Feature seamark(String name, double x, double y, Object... keyValues) {
Map attrs = new HashMap<>();
diff --git a/src/test/java/SeamarkZoomRulesTest.java b/src/test/java/SeamarkZoomRulesTest.java
index 08a457c..a13e273 100644
--- a/src/test/java/SeamarkZoomRulesTest.java
+++ b/src/test/java/SeamarkZoomRulesTest.java
@@ -1,6 +1,8 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -85,6 +87,111 @@ void lightGeometryWaitsForItsHost() {
assertEquals(13, SeamarkZoomRules.getLightMinZoom(attrs("type", "rock", "near_shore", true)));
}
+ /** Band plus SCAMIN, with the classes the chart draws early held to their derived floor. */
+ @Test
+ void standardFloorsFollowTheDerivation() {
+ assertEquals(11, SeamarkZoomRules.getStandardMinZoom(attrs("type", "light_minor")));
+ assertEquals(12, SeamarkZoomRules.getStandardMinZoom(attrs("type", "buoy_lateral")));
+ assertEquals(12, SeamarkZoomRules.getStandardMinZoom(attrs("type", "beacon_special_purpose")));
+ assertEquals(12, SeamarkZoomRules.getStandardMinZoom(attrs("type", "fog_signal")));
+ assertEquals(12, SeamarkZoomRules.getStandardMinZoom(attrs("type", "anchorage")));
+ assertEquals(14, SeamarkZoomRules.getStandardMinZoom(attrs("type", "mooring")));
+ assertEquals(15, SeamarkZoomRules.getStandardMinZoom(attrs("type", "small_craft_facility")));
+ assertEquals(11, SeamarkZoomRules.getStandardMinZoom(attrs("type", "harbour")));
+ assertEquals(9, SeamarkZoomRules.getStandardMinZoom(attrs("type", "buoy_cardinal")));
+ assertEquals(9, SeamarkZoomRules.getStandardMinZoom(attrs("type", "beacon_isolated_danger")));
+ assertEquals(5, SeamarkZoomRules.getStandardMinZoom(attrs("type", "platform")));
+ assertEquals(6, SeamarkZoomRules.getStandardMinZoom(attrs("type", "restricted_area")));
+ // the area classes the restricted-area list would otherwise sweep up carry two SCAMIN steps
+ assertEquals(10, SeamarkZoomRules.getStandardMinZoom(attrs("type", "cable_area")));
+ assertEquals(10, SeamarkZoomRules.getStandardMinZoom(attrs("type", "marine_farm")));
+ }
+
+ /** SCAMIN NOT SET is every scale, linework and fills alike. */
+ @Test
+ void tssCarriesNoScamin() {
+ assertEquals(0, SeamarkZoomRules.getStandardMinZoom(attrs("type", "separation_lane")));
+ assertEquals(0, SeamarkZoomRules.getStandardMinZoom(attrs("type", "separation_boundary")));
+ assertEquals(0, SeamarkZoomRules.getStandardMinZoom(attrs("type", "separation_zone")));
+ assertEquals(0, SeamarkZoomRules.getStandardMinZoom(attrs("type", "separation_roundabout")));
+ }
+
+ /** A light's reach promotes it under the standards too, one band later than the chart's. */
+ @Test
+ void standardFloorsPromoteByRange() {
+ assertEquals(5, SeamarkZoomRules.getStandardMinZoom(attrs("type", "light_major")));
+ assertEquals(
+ 5, SeamarkZoomRules.getStandardMinZoom(attrs("type", "light_minor", "light_range", 15)));
+ assertEquals(
+ 6, SeamarkZoomRules.getStandardMinZoom(attrs("type", "light_minor", "light_range", 10)));
+ assertEquals(
+ 11, SeamarkZoomRules.getStandardMinZoom(attrs("type", "light_minor", "light_range", 5)));
+ }
+
+ /** The contextual hazard derivation is the standard, so both floors agree. */
+ @Test
+ void hazardFloorsAreAlreadyTheStandard() {
+ assertEquals(8, SeamarkZoomRules.getStandardMinZoom(attrs("type", "rock")));
+ assertEquals(
+ 13, SeamarkZoomRules.getStandardMinZoom(attrs("type", "rock", "near_shore", true)));
+ assertEquals(11, SeamarkZoomRules.getStandardMinZoom(attrs("type", "wreck", "depth", 42.0)));
+ assertEquals(
+ 6, SeamarkZoomRules.getStandardMinZoom(attrs("type", "wreck", "category", "dangerous")));
+ }
+
+ /** Conspicuity or a light takes a landmark's SCAMIN away; being a turbine does not. */
+ @Test
+ void standardLandmarksSplitByConspicuityAlone() {
+ assertEquals(
+ 11, SeamarkZoomRules.getStandardMinZoom(attrs("type", "landmark", "category", "tower")));
+ assertEquals(
+ 0,
+ SeamarkZoomRules.getStandardMinZoom(
+ attrs("type", "landmark", "seamark:landmark:conspicuity", "conspicuous")));
+ assertEquals(
+ 0,
+ SeamarkZoomRules.getStandardMinZoom(
+ attrs("type", "landmark", "category", "tower", "light", "Fl.5s")));
+ assertEquals(
+ 11,
+ SeamarkZoomRules.getStandardMinZoom(attrs("type", "landmark", "category", "windmotor")));
+ }
+
+ /**
+ * Wherever SCAMIN sets a floor at all, the feature reaches the tile by then — a style filtering
+ * on the standard floor never asks for something the tile doesn't carry. The all-scales classes
+ * are the exception by construction: they arrive at their own floor, since no tile exists below
+ * z0.
+ */
+ @Test
+ void theChartFloorNeverTrailsTheStandardFloor() {
+ for (String type :
+ List.of(
+ "light_major",
+ "light_minor",
+ "fog_signal",
+ "platform",
+ "anchorage",
+ "cable_area",
+ "restricted_area",
+ "buoy_lateral",
+ "buoy_cardinal",
+ "mooring",
+ "harbour",
+ "landmark",
+ "small_craft_facility",
+ "separation_lane",
+ "rock",
+ "wreck")) {
+ Map attrs = attrs("type", type);
+ int standard = SeamarkZoomRules.getStandardMinZoom(attrs);
+ if (standard == 0) continue;
+ assertTrue(
+ SeamarkZoomRules.getMinZoom(attrs) <= standard,
+ type + " arrives after the standards would draw it");
+ }
+ }
+
/** A plain tower is Coastal detail; what a mariner steers by keeps the early floor. */
@Test
void landmarksSplitByConspicuity() {
diff --git a/style/README.md b/style/README.md
index 9e62f13..94dd894 100644
--- a/style/README.md
+++ b/style/README.md
@@ -15,7 +15,7 @@ const map = new maplibregl.Map({
});
```
-`style()` is async: land hillshading comes from the VersaTiles elevation tiles, and the builder fetches their TileJSON. Options: `tiles` (seamark TileJSON URL), `seascape`, `versatiles`, `language`, `spriteBase`, `hillshade` (on by default; `false` to skip, or an object to tune the shading), and the seascape passthroughs — `flavor` (overrides merged over its `day`), `unit`, `safety`, `shading`, and the `dem`/`vector`/`coverage` source id overrides.
+`style()` is async: land hillshading comes from the VersaTiles elevation tiles, and the builder fetches their TileJSON. Options: `tiles` (seamark TileJSON URL), `seascape`, `versatiles`, `language`, `spriteBase`, `hillshade` (on by default; `false` to skip, or an object to tune the shading), `standards` (off by default; strict S-52 portrayal — fixed symbol sizes, SCAMIN-derived visibility, display-fixed sector arcs), and the seascape passthroughs — `flavor` (overrides merged over its `day`), `unit`, `safety`, `shading`, and the `dem`/`vector`/`coverage` source id overrides.
## Composed
@@ -37,7 +37,7 @@ const map = new maplibregl.Map({ style /* ... */ });
```
- `sources({ url? })` — the `seamap` vector source (defaults to `https://tiles.openwaters.io/seamap/tiles.json`).
-- `layers({ font? })` — the chart layers, split into `areas` and `symbols` to preserve draw order around your land layers. `font` renames glyph fontstacks (`"Noto Sans Regular"`) to match your glyph server.
+- `layers({ font?, safety?, unit?, standards? })` — the chart layers, split into `areas` and `symbols` to preserve draw order around your land layers. `font` renames glyph fontstacks (`"Noto Sans Regular"`) to match your glyph server; `safety`, `unit`, and `standards` mean the same as `style()`'s options of those names.
- `sprite(base)` — the `style.sprite` entry pointing at wherever you serve the sheet.
When tags compose a colour combination the sheet doesn't carry, the layers fall back to the shape's `generic` icon in the style itself (a `coalesce` of `image` expressions), so unusual marks never render as nothing.
diff --git a/style/__snapshots__/index.test.ts.snap b/style/__snapshots__/index.test.ts.snap
new file mode 100644
index 0000000..1319cbc
--- /dev/null
+++ b/style/__snapshots__/index.test.ts.snap
@@ -0,0 +1,8105 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`keeps the chart portrayal byte for byte 1`] = `
+{
+ "areas": [
+ {
+ "filter": [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "anchorage",
+ "anchor_berth",
+ "mooring",
+ "dredged_area",
+ "marine_farm",
+ "harbour",
+ ],
+ ],
+ ],
+ "id": "allowed-areas",
+ "paint": {
+ "line-color": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "anchorage",
+ "anchor_berth",
+ "mooring",
+ ],
+ ],
+ ],
+ "#ec008c",
+ "black",
+ ],
+ "line-dasharray": [
+ 4,
+ 4,
+ ],
+ "line-opacity": 0.8,
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 6,
+ 0.5,
+ 10,
+ 1.5,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "anchorage",
+ "anchor_berth",
+ "mooring",
+ "marine_farm",
+ ],
+ ],
+ ],
+ "id": "allowed-areas-labels",
+ "layout": {
+ "icon-image": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "anchorage",
+ "anchor_berth",
+ "mooring",
+ ],
+ ],
+ ],
+ "freenauticalchart:anchor",
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "crustaceans",
+ "oysters_mussels",
+ "pearl_culture",
+ ],
+ ],
+ ],
+ "freenauticalchart:shellfish",
+ "freenauticalchart:marine-farm",
+ ],
+ "icon-overlap": "always",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ 0.2,
+ 12,
+ 0.8,
+ ],
+ "symbol-placement": "line",
+ "symbol-spacing": 90,
+ },
+ "paint": {
+ "icon-opacity": 0.8,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "restricted_area",
+ "military_area",
+ "production_area",
+ "cable_area",
+ "dumping_ground",
+ "pipeline_area",
+ ],
+ ],
+ ],
+ "id": "restricted-areas",
+ "paint": {
+ "line-color": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "nature_reserve",
+ "bird_sanctuary",
+ "game_reserve",
+ "seal_sanctuary",
+ "fish_sanctuary",
+ "ecological_reserve",
+ "essa",
+ "pssa",
+ ],
+ ],
+ ],
+ "green",
+ "#ec008c",
+ ],
+ "line-dasharray": [
+ 4,
+ 2,
+ ],
+ "line-opacity": 0.8,
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 6,
+ 0.5,
+ 10,
+ 1.5,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "restricted_area",
+ "military_area",
+ "production_area",
+ ],
+ ],
+ ],
+ "id": "restricted-areas-label",
+ "layout": {
+ "symbol-placement": "line",
+ "text-field": [
+ "get",
+ "name",
+ ],
+ "text-font": [
+ "Noto Sans Regular",
+ ],
+ "text-size": 11,
+ },
+ "paint": {
+ "text-color": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "nature_reserve",
+ "bird_sanctuary",
+ "game_reserve",
+ "seal_sanctuary",
+ "fish_sanctuary",
+ "ecological_reserve",
+ "essa",
+ "pssa",
+ ],
+ ],
+ ],
+ "green",
+ "#ec008c",
+ ],
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "restricted_area",
+ "military_area",
+ "production_area",
+ "cable_area",
+ "dumping_ground",
+ "pipeline_area",
+ ],
+ ],
+ ],
+ "id": "restricted-areas-fill",
+ "paint": {
+ "fill-color": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "nature_reserve",
+ "bird_sanctuary",
+ "game_reserve",
+ "seal_sanctuary",
+ "fish_sanctuary",
+ "ecological_reserve",
+ "essa",
+ "pssa",
+ ],
+ ],
+ ],
+ "green",
+ "#ec008c",
+ ],
+ "fill-opacity": 0.04,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "fill",
+ },
+ {
+ "filter": [
+ "let",
+ "restriction",
+ [
+ "coalesce",
+ [
+ "get",
+ [
+ "concat",
+ "seamark:",
+ [
+ "get",
+ "type",
+ ],
+ ":restriction",
+ ],
+ ],
+ [
+ "get",
+ "seamark:restriction",
+ ],
+ [
+ "get",
+ "restriction",
+ ],
+ "",
+ ],
+ [
+ "any",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "military_area",
+ ],
+ [
+ "==",
+ [
+ "get",
+ "category",
+ ],
+ "military",
+ ],
+ [
+ "in",
+ "no_entry",
+ [
+ "var",
+ "restriction",
+ ],
+ ],
+ [
+ "in",
+ "restricted_entry",
+ [
+ "var",
+ "restriction",
+ ],
+ ],
+ [
+ "in",
+ "no_anchoring",
+ [
+ "var",
+ "restriction",
+ ],
+ ],
+ ],
+ ],
+ "id": "restricted-areas-fill-pattern",
+ "paint": {
+ "fill-opacity": [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "military_area",
+ ],
+ 0.1,
+ [
+ "==",
+ [
+ "get",
+ "category",
+ ],
+ "military",
+ ],
+ 0.1,
+ 0.4,
+ ],
+ "fill-pattern": [
+ "let",
+ "restriction",
+ [
+ "coalesce",
+ [
+ "get",
+ [
+ "concat",
+ "seamark:",
+ [
+ "get",
+ "type",
+ ],
+ ":restriction",
+ ],
+ ],
+ [
+ "get",
+ "seamark:restriction",
+ ],
+ [
+ "get",
+ "restriction",
+ ],
+ "",
+ ],
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "military_area",
+ ],
+ "freenauticalchart:military",
+ [
+ "==",
+ [
+ "get",
+ "category",
+ ],
+ "military",
+ ],
+ "freenauticalchart:military",
+ [
+ "in",
+ "no_entry",
+ [
+ "var",
+ "restriction",
+ ],
+ ],
+ "freenauticalchart:no-entry",
+ [
+ "in",
+ "restricted_entry",
+ [
+ "var",
+ "restriction",
+ ],
+ ],
+ "freenauticalchart:no-entry",
+ [
+ "in",
+ "no_anchoring",
+ [
+ "var",
+ "restriction",
+ ],
+ ],
+ "freenauticalchart:no-anchor",
+ "",
+ ],
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "fill",
+ },
+ ],
+ "symbols": [
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "geometry-type",
+ ],
+ "Polygon",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "rock",
+ "wreck",
+ "obstruction",
+ ],
+ ],
+ ],
+ [
+ "!",
+ [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "obstruction",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "boom",
+ "shark_net",
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ "id": "hazard-areas-fill",
+ "minzoom": 8,
+ "paint": {
+ "fill-color": "#61b7ff",
+ "fill-opacity": 0.2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "fill",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "geometry-type",
+ ],
+ "Polygon",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "rock",
+ "wreck",
+ "obstruction",
+ ],
+ ],
+ ],
+ [
+ "!",
+ [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "obstruction",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "boom",
+ "shark_net",
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ "id": "hazard-areas",
+ "minzoom": 8,
+ "paint": {
+ "line-color": [
+ "case",
+ [
+ "<=",
+ [
+ "coalesce",
+ [
+ "get",
+ "depth",
+ ],
+ [
+ "get",
+ "seabed_depth",
+ ],
+ -9999,
+ ],
+ 2,
+ ],
+ "#333",
+ "#768c97",
+ ],
+ "line-dasharray": [
+ "case",
+ [
+ "<=",
+ [
+ "coalesce",
+ [
+ "get",
+ "depth",
+ ],
+ [
+ "get",
+ "seabed_depth",
+ ],
+ -9999,
+ ],
+ 2,
+ ],
+ [
+ "literal",
+ [
+ 1,
+ 1.5,
+ ],
+ ],
+ [
+ "literal",
+ [
+ 4,
+ 2,
+ ],
+ ],
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ 0.6,
+ 14,
+ 1.4,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "geometry-type",
+ ],
+ "Point",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "rock",
+ "wreck",
+ "obstruction",
+ ],
+ ],
+ ],
+ [
+ "<=",
+ [
+ "coalesce",
+ [
+ "get",
+ "depth",
+ ],
+ [
+ "get",
+ "seabed_depth",
+ ],
+ 9999,
+ ],
+ 2,
+ ],
+ [
+ ">=",
+ [
+ "coalesce",
+ [
+ "get",
+ "surrounding_depth",
+ ],
+ -9999,
+ ],
+ 2,
+ ],
+ [
+ "!",
+ [
+ "==",
+ [
+ "coalesce",
+ [
+ "get",
+ "near_shore",
+ ],
+ false,
+ ],
+ true,
+ ],
+ ],
+ ],
+ "id": "isolated-dangers",
+ "paint": {
+ "circle-opacity": 0,
+ "circle-radius": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ 6,
+ 12,
+ 10,
+ ],
+ "circle-stroke-color": "#ec008c",
+ "circle-stroke-opacity": 0.85,
+ "circle-stroke-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ 1,
+ 12,
+ 1.4,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "circle",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "rock",
+ ],
+ [
+ "any",
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ [
+ "<=",
+ [
+ "coalesce",
+ [
+ "get",
+ "depth",
+ ],
+ [
+ "get",
+ "seabed_depth",
+ ],
+ 9999,
+ ],
+ 2,
+ ],
+ ],
+ [
+ "!",
+ [
+ "in",
+ [
+ "coalesce",
+ [
+ "get",
+ "seamark:rock:water_level",
+ ],
+ [
+ "get",
+ "seamark:water_level",
+ ],
+ [
+ "get",
+ "water_level",
+ ],
+ "",
+ ],
+ [
+ "literal",
+ [
+ "dry",
+ "part-submerged",
+ "part_submerged",
+ ],
+ ],
+ ],
+ ],
+ ],
+ "id": "rocks_outline",
+ "layout": {
+ "icon-image": "freenauticalchart:obstruction",
+ "icon-overlap": "always",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ 0.33,
+ 11,
+ 0.8,
+ ],
+ },
+ "minzoom": 8,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "rock",
+ ],
+ [
+ "any",
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ [
+ "<=",
+ [
+ "coalesce",
+ [
+ "get",
+ "depth",
+ ],
+ [
+ "get",
+ "seabed_depth",
+ ],
+ 9999,
+ ],
+ 2,
+ ],
+ ],
+ ],
+ "id": "rocks",
+ "layout": {
+ "icon-image": [
+ "match",
+ [
+ "coalesce",
+ [
+ "get",
+ "seamark:rock:water_level",
+ ],
+ [
+ "get",
+ "seamark:water_level",
+ ],
+ [
+ "get",
+ "water_level",
+ ],
+ "",
+ ],
+ "covers",
+ "freenauticalchart:rock-covers",
+ "awash",
+ "freenauticalchart:rock-awash",
+ [
+ "dry",
+ "part-submerged",
+ "part_submerged",
+ ],
+ "freenauticalchart:rock-dry",
+ "freenauticalchart:rock-submerged",
+ ],
+ "icon-overlap": "always",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ 0.33,
+ 11,
+ 1,
+ ],
+ },
+ "minzoom": 8,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "obstruction",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "boom",
+ "shark_net",
+ ],
+ ],
+ ],
+ ],
+ "id": "floating-barriers",
+ "paint": {
+ "line-color": "#3d3d3d",
+ "line-dasharray": [
+ 4,
+ 2,
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ 0.6,
+ 14,
+ 1.6,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "obstruction",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "boom",
+ "shark_net",
+ ],
+ ],
+ ],
+ ],
+ "id": "floating-barriers-label",
+ "layout": {
+ "symbol-placement": "line",
+ "symbol-spacing": 250,
+ "text-field": [
+ "case",
+ [
+ "has",
+ "name",
+ ],
+ [
+ "get",
+ "name",
+ ],
+ [
+ "==",
+ [
+ "get",
+ "category",
+ ],
+ "shark_net",
+ ],
+ "Shark Net",
+ "Floating Barrier",
+ ],
+ "text-font": [
+ "Noto Sans Regular",
+ ],
+ "text-offset": [
+ 0,
+ -0.8,
+ ],
+ "text-optional": true,
+ "text-size": 10,
+ },
+ "minzoom": 12,
+ "paint": {
+ "text-color": "#333",
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 1.5,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "wreck",
+ "obstruction",
+ ],
+ ],
+ ],
+ [
+ "any",
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ [
+ "<=",
+ [
+ "coalesce",
+ [
+ "get",
+ "depth",
+ ],
+ [
+ "get",
+ "seabed_depth",
+ ],
+ 9999,
+ ],
+ 2,
+ ],
+ ],
+ [
+ "any",
+ [
+ "==",
+ [
+ "geometry-type",
+ ],
+ "Point",
+ ],
+ [
+ "!",
+ [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "obstruction",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "boom",
+ "shark_net",
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ "id": "obstructions",
+ "layout": {
+ "icon-image": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "foul_ground",
+ "distributed_remains",
+ ],
+ ],
+ ],
+ "freenauticalchart:foul",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "obstruction",
+ ],
+ "freenauticalchart:obstruction",
+ [
+ "==",
+ [
+ "get",
+ "category",
+ ],
+ "non-dangerous",
+ ],
+ "freenauticalchart:wreck-non-dangerous",
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "hull_showing",
+ "mast_showing",
+ ],
+ ],
+ ],
+ "freenauticalchart:wreck-hull_showing",
+ "freenauticalchart:wreck-dangerous",
+ ],
+ "icon-overlap": "always",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ 0.33,
+ 11,
+ 1,
+ ],
+ },
+ "minzoom": 6,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "rock",
+ "wreck",
+ "obstruction",
+ ],
+ ],
+ ],
+ [
+ "has",
+ "depth",
+ ],
+ [
+ ">=",
+ [
+ "get",
+ "depth",
+ ],
+ 0,
+ ],
+ ],
+ "id": "hazard-depths",
+ "layout": {
+ "text-field": [
+ "to-string",
+ [
+ "get",
+ "depth",
+ ],
+ ],
+ "text-font": [
+ "Noto Sans Italic",
+ ],
+ "text-justify": "auto",
+ "text-optional": true,
+ "text-radial-offset": 1.7,
+ "text-size": 10,
+ "text-variable-anchor": [
+ "left",
+ "right",
+ "bottom",
+ "top",
+ ],
+ },
+ "minzoom": 12,
+ "paint": {
+ "text-color": [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "depth",
+ ],
+ 2,
+ ],
+ "#000",
+ "#768c97",
+ ],
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 1.5,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "has",
+ "seamark:seabed_area:surface",
+ ],
+ "id": "seabed",
+ "layout": {
+ "text-field": [
+ "get",
+ "seamark:seabed_area:surface",
+ ],
+ "text-font": [
+ "Noto Sans Italic",
+ ],
+ "text-letter-spacing": 0.1,
+ "text-max-width": 5,
+ "text-padding": 10,
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ 9,
+ 13,
+ 11,
+ ],
+ },
+ "paint": {
+ "text-color": "#333",
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "cable_submarine",
+ "pipeline_submarine",
+ ],
+ ],
+ ],
+ "id": "cables-pipes",
+ "paint": {
+ "line-opacity": 0.5,
+ "line-pattern": [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "pipeline_submarine",
+ ],
+ "freenauticalchart:pipeline",
+ "freenauticalchart:cable",
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ 4,
+ 12,
+ 8,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "separation_zone",
+ ],
+ "id": "TSS-separation-zone",
+ "paint": {
+ "fill-color": "#ec008c",
+ "fill-opacity": 0.15,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "fill",
+ },
+ {
+ "filter": [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "separation_crossing",
+ ],
+ "id": "TSS-crossing-zone",
+ "paint": {
+ "fill-color": "#ec008c",
+ "fill-opacity": 0.1,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "fill",
+ },
+ {
+ "filter": [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "separation_lane",
+ ],
+ "id": "TSS-separation-lane-arrows",
+ "layout": {
+ "icon-image": "freenauticalchart:TSS-arrow",
+ "icon-overlap": "always",
+ "icon-padding": 0,
+ "icon-rotate": 90,
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 2,
+ 0.15,
+ 6,
+ 0.15,
+ 14,
+ 0.6,
+ ],
+ "symbol-placement": "line",
+ "symbol-spacing": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 6,
+ 40,
+ 14,
+ 180,
+ ],
+ },
+ "paint": {
+ "icon-opacity": 0.4,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "separation_boundary",
+ ],
+ "id": "TSS-separation-boundary",
+ "paint": {
+ "line-color": "#ec008c",
+ "line-dasharray": [
+ 4,
+ 4,
+ ],
+ "line-opacity": 0.8,
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 2,
+ 1,
+ 6,
+ 1,
+ 10,
+ 2,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "separation_line",
+ ],
+ "id": "TSS-separation-line",
+ "paint": {
+ "line-color": "#ec008c",
+ "line-opacity": 0.4,
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 2,
+ 1,
+ 6,
+ 1,
+ 10,
+ 2,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "ferry_route",
+ ],
+ "id": "ferry",
+ "layout": {
+ "line-join": "round",
+ },
+ "paint": {
+ "line-color": "#7c8af6",
+ "line-dasharray": [
+ 2,
+ 2,
+ ],
+ "line-opacity": 0.6,
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ 0.7,
+ 14,
+ 2,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "navigation_line",
+ ],
+ "id": "navigation-lines",
+ "paint": {
+ "line-color": "black",
+ "line-dasharray": [
+ 4,
+ 2,
+ ],
+ "line-width": 0.8,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "recommended_track",
+ ],
+ "id": "navigation-tracks",
+ "paint": {
+ "line-color": "gray",
+ "line-dasharray": [
+ 4,
+ 2,
+ ],
+ "line-width": 1,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "shoreline_construction",
+ ],
+ "id": "shoreline-constructions",
+ "minzoom": 12,
+ "paint": {
+ "line-color": "#3d3d3d",
+ "line-dasharray": [
+ "match",
+ [
+ "coalesce",
+ [
+ "get",
+ "seamark:shoreline_construction:water_level",
+ ],
+ [
+ "get",
+ "seamark:water_level",
+ ],
+ [
+ "get",
+ "water_level",
+ ],
+ "",
+ ],
+ [
+ "covers",
+ "flooding",
+ "awash",
+ ],
+ [
+ "literal",
+ [
+ 2,
+ 2,
+ ],
+ ],
+ [
+ "literal",
+ [
+ 1,
+ 0,
+ ],
+ ],
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 12,
+ 0.6,
+ 16,
+ 1.8,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "pontoon",
+ ],
+ "id": "pontoons",
+ "minzoom": 12,
+ "paint": {
+ "line-color": "#3d3d3d",
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 12,
+ 0.6,
+ 16,
+ 1.8,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "pile",
+ "mooring",
+ ],
+ ],
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "piles",
+ "minzoom": 11,
+ "paint": {
+ "circle-radius": 3,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "circle",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "platform",
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "platforms",
+ "layout": {
+ "icon-image": "freenauticalchart:platform",
+ "icon-overlap": "always",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ 0.4,
+ 10,
+ 1,
+ ],
+ "text-field": [
+ "step",
+ [
+ "zoom",
+ ],
+ "",
+ 12,
+ [
+ "get",
+ "name",
+ ],
+ ],
+ "text-font": [
+ "Noto Sans Regular",
+ ],
+ "text-justify": "auto",
+ "text-optional": true,
+ "text-radial-offset": 1,
+ "text-size": 11,
+ "text-variable-anchor": [
+ "left",
+ "right",
+ "bottom",
+ "top",
+ ],
+ },
+ "minzoom": 8,
+ "paint": {
+ "text-color": "#333",
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "crane",
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "cranes",
+ "layout": {
+ "icon-image": "freenauticalchart:crane",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 14,
+ 0.6,
+ 16,
+ 1,
+ ],
+ "symbol-sort-key": [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ },
+ "minzoom": 14,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "rescue_station",
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "rescue-stations",
+ "layout": {
+ "icon-image": "freenauticalchart:rescue",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 10,
+ 0.5,
+ 13,
+ 1,
+ ],
+ "symbol-sort-key": [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ },
+ "minzoom": 10,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "radar_station",
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "radar-stations",
+ "layout": {
+ "icon-image": "freenauticalchart:radar_scanner",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 11,
+ 0.6,
+ 14,
+ 1,
+ ],
+ "symbol-sort-key": [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ },
+ "minzoom": 11,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "has",
+ "radio_station",
+ ],
+ "id": "radio_station",
+ "minzoom": 8,
+ "paint": {
+ "circle-opacity": 0,
+ "circle-radius": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ 5,
+ 12,
+ 40,
+ ],
+ "circle-stroke-color": "#ec008c",
+ "circle-stroke-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 10,
+ 0.5,
+ 12,
+ 1.5,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "circle",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "small_craft_facility",
+ ],
+ [
+ "!",
+ [
+ "in",
+ [
+ "get",
+ "access",
+ ],
+ [
+ "literal",
+ [
+ "no",
+ "private",
+ "permit",
+ "customers",
+ ],
+ ],
+ ],
+ ],
+ ],
+ "id": "small-craft-facilities",
+ "layout": {
+ "icon-image": [
+ "match",
+ [
+ "get",
+ "category",
+ ],
+ "slipway",
+ "freenauticalchart:poi-slipway",
+ "fuel_station",
+ "freenauticalchart:poi-fuel",
+ [
+ "water_tap",
+ "drinking_water",
+ ],
+ "freenauticalchart:poi-water",
+ "pump-out",
+ "freenauticalchart:poi-pumpout",
+ [
+ "visitor_berth",
+ "visitors_mooring",
+ "berth",
+ ],
+ "freenauticalchart:poi-berth",
+ "nautical_club",
+ "freenauticalchart:poi-club",
+ "boat_hoist",
+ "freenauticalchart:poi-hoist",
+ "boatyard",
+ "freenauticalchart:poi-boatyard",
+ "boat_storage",
+ "freenauticalchart:poi-boat-storage",
+ "toilets",
+ "freenauticalchart:poi-toilets",
+ "showers",
+ "freenauticalchart:poi-showers",
+ "laundrette",
+ "freenauticalchart:poi-laundry",
+ "provisions",
+ "freenauticalchart:poi-provisions",
+ "restaurant",
+ "freenauticalchart:poi-restaurant",
+ "chandler",
+ "freenauticalchart:poi-chandler",
+ "electricity",
+ "freenauticalchart:poi-electricity",
+ "access_point",
+ "freenauticalchart:poi-kayak",
+ "fish_cleaning",
+ "freenauticalchart:poi-fish-cleaning",
+ "fishing",
+ "freenauticalchart:poi-fishing",
+ "beach",
+ "freenauticalchart:poi-beach",
+ "freenauticalchart:poi-generic",
+ ],
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 14,
+ 0.8,
+ 17,
+ 1,
+ ],
+ "text-field": [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "category",
+ ],
+ "fuel_station",
+ ],
+ [
+ "coalesce",
+ [
+ "get",
+ "fuel",
+ ],
+ "",
+ ],
+ "",
+ ],
+ "text-font": [
+ "Noto Sans Regular",
+ ],
+ "text-justify": "auto",
+ "text-optional": true,
+ "text-size": 10,
+ "text-variable-anchor-offset": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 14,
+ [
+ "literal",
+ [
+ "left",
+ [
+ 2.03,
+ -0,
+ ],
+ "right",
+ [
+ -2.03,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -2.03,
+ ],
+ "top",
+ [
+ 0,
+ 2.03,
+ ],
+ ],
+ ],
+ 17,
+ [
+ "literal",
+ [
+ "left",
+ [
+ 2.35,
+ -0,
+ ],
+ "right",
+ [
+ -2.35,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -2.35,
+ ],
+ "top",
+ [
+ 0,
+ 2.35,
+ ],
+ ],
+ ],
+ ],
+ },
+ "minzoom": 14,
+ "paint": {
+ "text-color": "#333",
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "geometry-type",
+ ],
+ "Point",
+ ],
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "harbour",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "category",
+ ],
+ [
+ "literal",
+ [
+ "marina",
+ "fishing",
+ ],
+ ],
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "harhours",
+ "layout": {
+ "icon-image": [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "category",
+ ],
+ "marina",
+ ],
+ "freenauticalchart:poi-marina",
+ [
+ "==",
+ [
+ "get",
+ "category",
+ ],
+ "fishing",
+ ],
+ "freenauticalchart:poi-fishing-harbour",
+ "",
+ ],
+ "icon-overlap": "always",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ 0.1,
+ 12,
+ 1,
+ ],
+ "text-field": [
+ "get",
+ "name",
+ ],
+ "text-font": [
+ "Noto Sans Regular",
+ ],
+ "text-justify": "auto",
+ "text-optional": true,
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ 9,
+ 12,
+ 12,
+ ],
+ "text-variable-anchor-offset": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 8,
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.72,
+ -0,
+ ],
+ "right",
+ [
+ -1.72,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -1.72,
+ ],
+ "top",
+ [
+ 0,
+ 1.72,
+ ],
+ ],
+ ],
+ 12,
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.96,
+ -0,
+ ],
+ "right",
+ [
+ -1.96,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -1.96,
+ ],
+ "top",
+ [
+ 0,
+ 1.96,
+ ],
+ ],
+ ],
+ ],
+ },
+ "minzoom": 8,
+ "paint": {
+ "icon-opacity": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "access",
+ ],
+ [
+ "literal",
+ [
+ "no",
+ "private",
+ "permit",
+ "customers",
+ ],
+ ],
+ ],
+ 0.5,
+ 1,
+ ],
+ "text-color": "#333",
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "access",
+ ],
+ [
+ "literal",
+ [
+ "no",
+ "private",
+ "permit",
+ "customers",
+ ],
+ ],
+ ],
+ 0.5,
+ 1,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "any",
+ [
+ "has",
+ "seamark:light:colour",
+ ],
+ [
+ "has",
+ "seamark:light:1:colour",
+ ],
+ ],
+ [
+ ">=",
+ [
+ "zoom",
+ ],
+ 10,
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "lights",
+ "layout": {
+ "icon-anchor": "top",
+ "icon-image": [
+ "coalesce",
+ [
+ "image",
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "case",
+ [
+ "==",
+ [
+ "coalesce",
+ [
+ "get",
+ "seamark:light:category",
+ ],
+ [
+ "get",
+ "seamark:light:1:category",
+ ],
+ "",
+ ],
+ "floodlight",
+ ],
+ "floodlight/",
+ "light/",
+ ],
+ [
+ "get",
+ "light_color",
+ ],
+ ],
+ ],
+ [
+ "image",
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "case",
+ [
+ "==",
+ [
+ "coalesce",
+ [
+ "get",
+ "seamark:light:category",
+ ],
+ [
+ "get",
+ "seamark:light:1:category",
+ ],
+ "",
+ ],
+ "floodlight",
+ ],
+ "floodlight/",
+ "light/",
+ ],
+ [
+ "coalesce",
+ [
+ "get",
+ "seamark:light:colour",
+ ],
+ [
+ "get",
+ "seamark:light:1:colour",
+ ],
+ ],
+ ],
+ ],
+ [
+ "image",
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "case",
+ [
+ "==",
+ [
+ "coalesce",
+ [
+ "get",
+ "seamark:light:category",
+ ],
+ [
+ "get",
+ "seamark:light:1:category",
+ ],
+ "",
+ ],
+ "floodlight",
+ ],
+ "floodlight/",
+ "light/",
+ ],
+ "generic",
+ ],
+ ],
+ ],
+ "icon-offset": [
+ 0,
+ 2,
+ ],
+ "icon-overlap": "always",
+ "icon-rotate": -45,
+ "icon-rotation-alignment": "viewport",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 12,
+ 0.7,
+ 14,
+ 1,
+ ],
+ },
+ "minzoom": 10,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "light_minor",
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "light-minor",
+ "layout": {
+ "icon-image": "freenauticalchart:light-minor",
+ "icon-overlap": "always",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ 0.275,
+ 12,
+ 1,
+ ],
+ },
+ "minzoom": 8,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "light_major",
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "light-major",
+ "layout": {
+ "icon-image": "freenauticalchart:light-major",
+ "icon-overlap": "always",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ 0.275,
+ 11,
+ 1,
+ ],
+ },
+ "minzoom": 6,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "has",
+ "seamark:fog_signal:category",
+ ],
+ [
+ ">=",
+ [
+ "zoom",
+ ],
+ 11,
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "fogsignals",
+ "layout": {
+ "icon-image": "freenauticalchart:fogsignal",
+ "icon-overlap": "always",
+ "icon-rotate": 90,
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 12,
+ 0.7,
+ 14,
+ 1,
+ ],
+ },
+ "minzoom": 10,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "subtype",
+ ],
+ "leg",
+ ],
+ [
+ ">=",
+ [
+ "zoom",
+ ],
+ 10,
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "sector-legs",
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ },
+ "maxzoom": 17,
+ "minzoom": 10,
+ "paint": {
+ "line-color": "#666",
+ "line-dasharray": [
+ 2,
+ 3,
+ ],
+ "line-opacity": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 15,
+ 0.8,
+ 17,
+ 0,
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 10,
+ 0.8,
+ 14,
+ 1.2,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "light",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "subtype",
+ ],
+ "sector",
+ ],
+ [
+ "!",
+ [
+ "in",
+ [
+ "coalesce",
+ [
+ "get",
+ "visibility",
+ ],
+ "",
+ ],
+ [
+ "literal",
+ [
+ "obscured",
+ "part_obscured",
+ "partially_obscured",
+ "faint",
+ ],
+ ],
+ ],
+ ],
+ [
+ ">=",
+ [
+ "zoom",
+ ],
+ 10,
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "sector-arc-casing",
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ },
+ "maxzoom": 17,
+ "minzoom": 10,
+ "paint": {
+ "line-color": "#333",
+ "line-opacity": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 15,
+ 1,
+ 17,
+ 0,
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 10,
+ 3,
+ 14,
+ 5,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "light",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "subtype",
+ ],
+ "sector",
+ ],
+ [
+ "!",
+ [
+ "in",
+ [
+ "coalesce",
+ [
+ "get",
+ "visibility",
+ ],
+ "",
+ ],
+ [
+ "literal",
+ [
+ "obscured",
+ "part_obscured",
+ "partially_obscured",
+ "faint",
+ ],
+ ],
+ ],
+ ],
+ [
+ ">=",
+ [
+ "zoom",
+ ],
+ 10,
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "sector-arc",
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ },
+ "maxzoom": 17,
+ "minzoom": 10,
+ "paint": {
+ "line-color": [
+ "match",
+ [
+ "get",
+ "color",
+ ],
+ "green",
+ "#00a650",
+ "red",
+ "#ed1c24",
+ [
+ "white",
+ "yellow",
+ "orange",
+ "amber",
+ ],
+ "#fab20b",
+ "#ec008c",
+ ],
+ "line-opacity": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 15,
+ 1,
+ 17,
+ 0,
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 10,
+ 1.5,
+ 14,
+ 3,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "light",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "subtype",
+ ],
+ "sector",
+ ],
+ [
+ "!",
+ [
+ "!",
+ [
+ "in",
+ [
+ "coalesce",
+ [
+ "get",
+ "visibility",
+ ],
+ "",
+ ],
+ [
+ "literal",
+ [
+ "obscured",
+ "part_obscured",
+ "partially_obscured",
+ "faint",
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ ">=",
+ [
+ "zoom",
+ ],
+ 10,
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "sector-arc-obscured",
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ },
+ "maxzoom": 17,
+ "minzoom": 10,
+ "paint": {
+ "line-color": "#666",
+ "line-dasharray": [
+ 2,
+ 2,
+ ],
+ "line-opacity": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 15,
+ 1,
+ 17,
+ 0,
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 10,
+ 1,
+ 14,
+ 2,
+ ],
+ },
+ "source": "seamap",
+ "source-layer": "light",
+ "type": "line",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "has",
+ "topmark_shape",
+ ],
+ [
+ ">=",
+ [
+ "zoom",
+ ],
+ 10,
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "topmarks",
+ "layout": {
+ "icon-anchor": "bottom",
+ "icon-image": [
+ "coalesce",
+ [
+ "image",
+ [
+ "case",
+ [
+ "has",
+ "topmark_color_pattern",
+ ],
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "get",
+ "topmark_shape",
+ ],
+ "/",
+ [
+ "get",
+ "topmark_color_pattern",
+ ],
+ "/",
+ [
+ "get",
+ "topmark_color",
+ ],
+ ],
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "get",
+ "topmark_shape",
+ ],
+ "/",
+ [
+ "get",
+ "topmark_color",
+ ],
+ ],
+ ],
+ ],
+ [
+ "image",
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "get",
+ "topmark_shape",
+ ],
+ "/generic",
+ ],
+ ],
+ [
+ "image",
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "get",
+ "topmark_shape",
+ ],
+ ],
+ ],
+ ],
+ "icon-offset": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -19.2,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "pillar",
+ "spar",
+ "stake",
+ "pole",
+ "perch",
+ "post",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -22,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "buoyant",
+ "lattice",
+ "pile",
+ "tower",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -20,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "cairn",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -22,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "conical",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -12,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "can",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -8,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "spherical",
+ "barrel",
+ "super-buoy",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -9,
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -26,
+ ],
+ ],
+ ],
+ "icon-overlap": "always",
+ "icon-rotate": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "pillar",
+ "barrel",
+ "conical",
+ "spar",
+ "can",
+ ],
+ ],
+ ],
+ 15,
+ 0,
+ ],
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ 0.25,
+ 12,
+ 1,
+ ],
+ },
+ "minzoom": 6,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "any",
+ [
+ "in",
+ "buoy",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ "beacon",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "buoys",
+ "layout": {
+ "icon-anchor": "bottom",
+ "icon-image": [
+ "coalesce",
+ [
+ "image",
+ [
+ "case",
+ [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "shape",
+ ],
+ "withy",
+ ],
+ [
+ "==",
+ [
+ "get",
+ "category",
+ ],
+ "port",
+ ],
+ ],
+ "freenauticalchart:withy-port",
+ [
+ "==",
+ [
+ "get",
+ "shape",
+ ],
+ "withy",
+ ],
+ "freenauticalchart:withy-starboard",
+ "",
+ ],
+ ],
+ [
+ "image",
+ [
+ "case",
+ [
+ "all",
+ [
+ "has",
+ "color_pattern",
+ ],
+ [
+ "!",
+ [
+ "==",
+ [
+ "get",
+ "color_pattern",
+ ],
+ "no",
+ ],
+ ],
+ ],
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "shape",
+ ],
+ "lattice",
+ ],
+ "pile",
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "buoyant",
+ ],
+ ],
+ ],
+ "pile",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "pole",
+ "perch",
+ "post",
+ ],
+ ],
+ ],
+ "stake",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "ice-buoy",
+ "ice_buoy",
+ ],
+ ],
+ ],
+ "super-buoy",
+ [
+ "all",
+ [
+ "!",
+ [
+ "has",
+ "shape",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "light_float",
+ ],
+ "light_float",
+ "super-buoy",
+ ],
+ [
+ "get",
+ "shape",
+ ],
+ ],
+ ],
+ "/",
+ [
+ "get",
+ "color_pattern",
+ ],
+ "/",
+ [
+ "get",
+ "color",
+ ],
+ ],
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "buoyant",
+ ],
+ ],
+ ],
+ "pile",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "pole",
+ "perch",
+ "post",
+ ],
+ ],
+ ],
+ "stake",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "ice-buoy",
+ "ice_buoy",
+ ],
+ ],
+ ],
+ "super-buoy",
+ [
+ "all",
+ [
+ "!",
+ [
+ "has",
+ "shape",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "light_float",
+ ],
+ "light_float",
+ "super-buoy",
+ ],
+ [
+ "get",
+ "shape",
+ ],
+ ],
+ "/",
+ [
+ "coalesce",
+ [
+ "get",
+ "color",
+ ],
+ "generic",
+ ],
+ ],
+ ],
+ ],
+ [
+ "image",
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "shape",
+ ],
+ "lattice",
+ ],
+ "pile",
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "buoyant",
+ ],
+ ],
+ ],
+ "pile",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "pole",
+ "perch",
+ "post",
+ ],
+ ],
+ ],
+ "stake",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "ice-buoy",
+ "ice_buoy",
+ ],
+ ],
+ ],
+ "super-buoy",
+ [
+ "all",
+ [
+ "!",
+ [
+ "has",
+ "shape",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "light_float",
+ ],
+ "light_float",
+ "super-buoy",
+ ],
+ [
+ "get",
+ "shape",
+ ],
+ ],
+ ],
+ "/",
+ [
+ "get",
+ "color",
+ ],
+ ],
+ ],
+ [
+ "image",
+ [
+ "concat",
+ "freenauticalchart:",
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "buoyant",
+ ],
+ ],
+ ],
+ "pile",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "pole",
+ "perch",
+ "post",
+ ],
+ ],
+ ],
+ "stake",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "ice-buoy",
+ "ice_buoy",
+ ],
+ ],
+ ],
+ "super-buoy",
+ [
+ "all",
+ [
+ "!",
+ [
+ "has",
+ "shape",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "light_float",
+ ],
+ "light_float",
+ "super-buoy",
+ ],
+ [
+ "get",
+ "shape",
+ ],
+ ],
+ "/generic",
+ ],
+ ],
+ [
+ "image",
+ [
+ "case",
+ [
+ "in",
+ "beacon",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ "freenauticalchart:stake/generic",
+ "freenauticalchart:pillar/generic",
+ ],
+ ],
+ ],
+ "icon-offset": [
+ 0,
+ 4,
+ ],
+ "icon-overlap": "always",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ 0.25,
+ 12,
+ 1,
+ ],
+ },
+ "minzoom": 6,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "has",
+ "radar_reflector",
+ ],
+ [
+ "in",
+ "beacon",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "!=",
+ [
+ "get",
+ "shape",
+ ],
+ "buoyant",
+ ],
+ [
+ ">=",
+ [
+ "zoom",
+ ],
+ 11,
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "radar-reflectors",
+ "layout": {
+ "icon-anchor": "bottom",
+ "icon-image": "freenauticalchart:radar-reflector",
+ "icon-offset": [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -16,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "pillar",
+ "spar",
+ "stake",
+ "pole",
+ "perch",
+ "post",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -10,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "buoyant",
+ "lattice",
+ "pile",
+ "tower",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -10,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "cairn",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -22,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "conical",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -12,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "can",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -8,
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "spherical",
+ "barrel",
+ "super-buoy",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -9,
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ -26,
+ ],
+ ],
+ ],
+ "icon-overlap": "always",
+ "icon-rotate": -60,
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ 0.25,
+ 12,
+ 1,
+ ],
+ },
+ "minzoom": 10,
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "landmark",
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ [
+ "any",
+ [
+ "!=",
+ [
+ "get",
+ "category",
+ ],
+ "tower",
+ ],
+ [
+ "has",
+ "color",
+ ],
+ [
+ "has",
+ "name",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "function",
+ ],
+ [
+ "literal",
+ [
+ "radio",
+ "radar",
+ "television",
+ "light_support",
+ "leading",
+ ],
+ ],
+ ],
+ ],
+ ],
+ "id": "landmarks",
+ "layout": {
+ "icon-anchor": "bottom",
+ "icon-image": [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "seamark:landmark:conspicuity",
+ ],
+ "conspicuous",
+ ],
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "function",
+ ],
+ [
+ "literal",
+ [
+ "church",
+ "chapel",
+ ],
+ ],
+ ],
+ "freenauticalchart:convis/church",
+ [
+ "match",
+ [
+ "get",
+ "category",
+ ],
+ "cairn",
+ "freenauticalchart:convis/cairn-lmk",
+ "chimney",
+ "freenauticalchart:convis/chimney",
+ "column",
+ "freenauticalchart:convis/column",
+ "cross",
+ "freenauticalchart:convis/cross-lmk",
+ "dish_aerial",
+ "freenauticalchart:convis/dish_aerial",
+ "dome",
+ "freenauticalchart:convis/dome",
+ "flagstaff",
+ "freenauticalchart:convis/flagstaff",
+ "flare_stack",
+ "freenauticalchart:convis/flare-stack",
+ "mast",
+ "freenauticalchart:convis/mast",
+ "monument",
+ "freenauticalchart:convis/monument",
+ "obelisk",
+ "freenauticalchart:convis/obelisk",
+ "radar_scanner",
+ "freenauticalchart:convis/radar_scanner",
+ "spire",
+ "freenauticalchart:convis/spire",
+ "statue",
+ "freenauticalchart:convis/statue",
+ "tower",
+ "freenauticalchart:convis/tower-lmk",
+ "windmill",
+ "freenauticalchart:convis/windmill",
+ "windmotor",
+ "freenauticalchart:convis/windmotor",
+ "windsock",
+ "freenauticalchart:convis/windsock",
+ "freenauticalchart:convis/monument",
+ ],
+ ],
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "function",
+ ],
+ [
+ "literal",
+ [
+ "church",
+ "chapel",
+ ],
+ ],
+ ],
+ "freenauticalchart:church",
+ [
+ "match",
+ [
+ "get",
+ "category",
+ ],
+ "cairn",
+ "freenauticalchart:cairn-lmk",
+ "chimney",
+ "freenauticalchart:chimney",
+ "column",
+ "freenauticalchart:column",
+ "cross",
+ "freenauticalchart:cross-lmk",
+ "dish_aerial",
+ "freenauticalchart:dish_aerial",
+ "dome",
+ "freenauticalchart:dome",
+ "flagstaff",
+ "freenauticalchart:flagstaff",
+ "flare_stack",
+ "freenauticalchart:flare-stack",
+ "mast",
+ "freenauticalchart:mast",
+ "monument",
+ "freenauticalchart:monument",
+ "obelisk",
+ "freenauticalchart:obelisk",
+ "radar_scanner",
+ "freenauticalchart:radar_scanner",
+ "spire",
+ "freenauticalchart:spire",
+ "statue",
+ "freenauticalchart:statue",
+ "tower",
+ "freenauticalchart:tower-lmk",
+ "windmill",
+ "freenauticalchart:windmill",
+ "windmotor",
+ "freenauticalchart:windmotor",
+ "windsock",
+ "freenauticalchart:windsock",
+ "freenauticalchart:monument",
+ ],
+ ],
+ ],
+ "icon-offset": [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "seamark:landmark:conspicuity",
+ ],
+ "conspicuous",
+ ],
+ [
+ "literal",
+ [
+ 0,
+ 6.4,
+ ],
+ ],
+ [
+ "literal",
+ [
+ 0,
+ 5.2,
+ ],
+ ],
+ ],
+ "icon-overlap": "always",
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "seamark:landmark:conspicuity",
+ ],
+ "conspicuous",
+ ],
+ 0.25,
+ 0.3076923076923077,
+ ],
+ 12,
+ 1,
+ ],
+ "text-field": [
+ "step",
+ [
+ "zoom",
+ ],
+ "",
+ 12,
+ [
+ "case",
+ [
+ "any",
+ [
+ "has",
+ "seamark:light:colour",
+ ],
+ [
+ "has",
+ "seamark:light:1:colour",
+ ],
+ ],
+ "",
+ [
+ "!",
+ [
+ "==",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ 0,
+ ],
+ ],
+ "",
+ [
+ "get",
+ "name",
+ ],
+ ],
+ ],
+ "text-font": [
+ "Noto Sans Regular",
+ ],
+ "text-justify": "auto",
+ "text-optional": true,
+ "text-padding": 8,
+ "text-size": 12,
+ "text-variable-anchor-offset": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ [
+ "literal",
+ [
+ "left",
+ [
+ 0.85,
+ -0,
+ ],
+ "right",
+ [
+ -0.85,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -1.3,
+ ],
+ "top",
+ [
+ 0,
+ 0.6,
+ ],
+ ],
+ ],
+ 16,
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.82,
+ -0,
+ ],
+ "right",
+ [
+ -1.82,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -3.3,
+ ],
+ "top",
+ [
+ 0,
+ 1.15,
+ ],
+ ],
+ ],
+ ],
+ },
+ "minzoom": 6,
+ "paint": {
+ "text-color": "#333",
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "has",
+ "name",
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "ferry_route",
+ ],
+ ],
+ ],
+ ],
+ "id": "line_symbols",
+ "layout": {
+ "symbol-placement": "line",
+ "text-field": [
+ "get",
+ "name",
+ ],
+ "text-font": [
+ "Noto Sans Regular",
+ ],
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 10,
+ 10,
+ 16,
+ 12,
+ ],
+ },
+ "minzoom": 10,
+ "paint": {
+ "text-color": [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "ferry_route",
+ ],
+ "#7c8af6",
+ "#333",
+ ],
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "geometry-type",
+ ],
+ "LineString",
+ ],
+ [
+ "has",
+ "name",
+ ],
+ [
+ "!",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "landmark",
+ "harbour",
+ "ferry_route",
+ ],
+ ],
+ ],
+ ],
+ ],
+ "id": "seamark-line-label",
+ "layout": {
+ "symbol-placement": "line-center",
+ "text-field": [
+ "get",
+ "name",
+ ],
+ "text-font": [
+ "Noto Sans Regular",
+ ],
+ "text-letter-spacing": 0.1,
+ "text-line-height": 1.6,
+ "text-max-width": 5,
+ "text-offset": [
+ 0,
+ -0.65,
+ ],
+ "text-pitch-alignment": "viewport",
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 13,
+ 10,
+ 16,
+ 13,
+ ],
+ },
+ "minzoom": 13,
+ "paint": {
+ "text-color": "#333",
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "==",
+ [
+ "geometry-type",
+ ],
+ "Point",
+ ],
+ [
+ "has",
+ "name",
+ ],
+ [
+ "!",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "landmark",
+ "harbour",
+ ],
+ ],
+ ],
+ ],
+ [
+ "!",
+ [
+ "any",
+ [
+ "has",
+ "seamark:light:colour",
+ ],
+ [
+ "has",
+ "seamark:light:1:colour",
+ ],
+ ],
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ [
+ "==",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ 0,
+ ],
+ ],
+ "id": "seamark-label",
+ "layout": {
+ "text-field": [
+ "case",
+ [
+ "any",
+ [
+ "in",
+ "buoy",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "format",
+ [
+ "get",
+ "name",
+ ],
+ {
+ "text-font": [
+ "literal",
+ [
+ "Noto Sans Italic",
+ ],
+ ],
+ },
+ ],
+ [
+ "get",
+ "name",
+ ],
+ ],
+ "text-font": [
+ "Noto Sans Regular",
+ ],
+ "text-justify": "auto",
+ "text-padding": 8,
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 12,
+ 10,
+ 16,
+ 13,
+ ],
+ "text-variable-anchor-offset": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 12,
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "rock",
+ "wreck",
+ "obstruction",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.7,
+ -0,
+ ],
+ "right",
+ [
+ -1.7,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -1.7,
+ ],
+ "top",
+ [
+ 0,
+ 1.7,
+ ],
+ ],
+ ],
+ [
+ "any",
+ [
+ "in",
+ "buoy",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ "beacon",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "can",
+ "conical",
+ "spherical",
+ "barrel",
+ "super-buoy",
+ "ice-buoy",
+ "ice_buoy",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.5,
+ -0.6,
+ ],
+ "right",
+ [
+ -1.5,
+ -0.6,
+ ],
+ "bottom",
+ [
+ 0,
+ -3.2,
+ ],
+ "top",
+ [
+ 0,
+ 1.2,
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.5,
+ -1.2,
+ ],
+ "right",
+ [
+ -1.5,
+ -1.2,
+ ],
+ "bottom",
+ [
+ 0,
+ -3.2,
+ ],
+ "top",
+ [
+ 0,
+ 1.2,
+ ],
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.5,
+ -0,
+ ],
+ "right",
+ [
+ -1.5,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -3.2,
+ ],
+ "top",
+ [
+ 0,
+ 1.2,
+ ],
+ ],
+ ],
+ ],
+ 16,
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "rock",
+ "wreck",
+ "obstruction",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.4,
+ -0,
+ ],
+ "right",
+ [
+ -1.4,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -1.4,
+ ],
+ "top",
+ [
+ 0,
+ 1.4,
+ ],
+ ],
+ ],
+ [
+ "any",
+ [
+ "in",
+ "buoy",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ "beacon",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "can",
+ "conical",
+ "spherical",
+ "barrel",
+ "super-buoy",
+ "ice-buoy",
+ "ice_buoy",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.1538461538461537,
+ -0.46153846153846156,
+ ],
+ "right",
+ [
+ -1.1538461538461537,
+ -0.46153846153846156,
+ ],
+ "bottom",
+ [
+ 0,
+ -2.4615384615384617,
+ ],
+ "top",
+ [
+ 0,
+ 0.9230769230769231,
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.1538461538461537,
+ -0.9230769230769231,
+ ],
+ "right",
+ [
+ -1.1538461538461537,
+ -0.9230769230769231,
+ ],
+ "bottom",
+ [
+ 0,
+ -2.4615384615384617,
+ ],
+ "top",
+ [
+ 0,
+ 0.9230769230769231,
+ ],
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.1538461538461537,
+ -0,
+ ],
+ "right",
+ [
+ -1.1538461538461537,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -2.4615384615384617,
+ ],
+ "top",
+ [
+ 0,
+ 0.9230769230769231,
+ ],
+ ],
+ ],
+ ],
+ ],
+ },
+ "minzoom": 12,
+ "paint": {
+ "text-color": "#333",
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "has",
+ "radar_transponder",
+ ],
+ [
+ ">=",
+ [
+ "zoom",
+ ],
+ 11,
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "racon-labels",
+ "layout": {
+ "text-anchor": "top",
+ "text-field": [
+ "case",
+ [
+ "has",
+ "radar_transponder_group",
+ ],
+ [
+ "concat",
+ "Racon(",
+ [
+ "get",
+ "radar_transponder_group",
+ ],
+ ")",
+ ],
+ "Racon",
+ ],
+ "text-font": [
+ "Noto Sans Italic",
+ ],
+ "text-offset": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ [
+ "literal",
+ [
+ 0,
+ 0.85,
+ ],
+ ],
+ 16,
+ [
+ "literal",
+ [
+ 0,
+ 1.4,
+ ],
+ ],
+ ],
+ "text-optional": true,
+ "text-size": 10,
+ },
+ "minzoom": 11,
+ "paint": {
+ "text-color": "#ec008c",
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ {
+ "filter": [
+ "all",
+ [
+ "any",
+ [
+ "has",
+ "seamark:light:colour",
+ ],
+ [
+ "has",
+ "seamark:light:1:colour",
+ ],
+ ],
+ [
+ ">=",
+ [
+ "zoom",
+ ],
+ 11,
+ ],
+ [
+ "<",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 2,
+ "major_aid",
+ 1,
+ "minor_aid",
+ 1,
+ "harbour",
+ 1,
+ "structure",
+ 1,
+ 999,
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 4,
+ "major_aid",
+ 3,
+ "minor_aid",
+ 2,
+ "harbour",
+ 1,
+ "structure",
+ 2,
+ 999,
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 8,
+ "major_aid",
+ 6,
+ "minor_aid",
+ 4,
+ "harbour",
+ 3,
+ "structure",
+ 4,
+ 999,
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "family",
+ ],
+ "hazard",
+ 999,
+ "major_aid",
+ 999,
+ "minor_aid",
+ 999,
+ "harbour",
+ 999,
+ "structure",
+ 999,
+ 999,
+ ],
+ ],
+ ],
+ ],
+ "id": "lights-label",
+ "layout": {
+ "text-field": [
+ "step",
+ [
+ "zoom",
+ ],
+ [
+ "case",
+ [
+ "any",
+ [
+ "in",
+ "buoy",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "format",
+ [
+ "get",
+ "light",
+ ],
+ {
+ "text-font": [
+ "literal",
+ [
+ "Noto Sans Italic",
+ ],
+ ],
+ },
+ ],
+ [
+ "format",
+ [
+ "get",
+ "light",
+ ],
+ {},
+ ],
+ ],
+ 12,
+ [
+ "case",
+ [
+ "!",
+ [
+ "all",
+ [
+ "has",
+ "name",
+ ],
+ [
+ "==",
+ [
+ "coalesce",
+ [
+ "get",
+ "cell_rank",
+ ],
+ 0,
+ ],
+ 0,
+ ],
+ ],
+ ],
+ [
+ "case",
+ [
+ "any",
+ [
+ "in",
+ "buoy",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "format",
+ [
+ "get",
+ "light",
+ ],
+ {
+ "text-font": [
+ "literal",
+ [
+ "Noto Sans Italic",
+ ],
+ ],
+ },
+ ],
+ [
+ "format",
+ [
+ "get",
+ "light",
+ ],
+ {},
+ ],
+ ],
+ [
+ "any",
+ [
+ "in",
+ "buoy",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "format",
+ [
+ "get",
+ "name",
+ ],
+ {
+ "font-scale": 1.15,
+ "text-font": [
+ "literal",
+ [
+ "Noto Sans Italic",
+ ],
+ ],
+ },
+ "
+",
+ {},
+ [
+ "get",
+ "light",
+ ],
+ {
+ "text-font": [
+ "literal",
+ [
+ "Noto Sans Italic",
+ ],
+ ],
+ },
+ ],
+ [
+ "format",
+ [
+ "get",
+ "name",
+ ],
+ {
+ "font-scale": 1.15,
+ },
+ "
+",
+ {},
+ [
+ "get",
+ "light",
+ ],
+ {},
+ ],
+ ],
+ ],
+ "text-font": [
+ "Noto Sans Regular",
+ ],
+ "text-justify": "auto",
+ "text-padding": 4,
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 12,
+ 10,
+ 16,
+ 14,
+ ],
+ "text-variable-anchor-offset": [
+ "interpolate",
+ [
+ "linear",
+ ],
+ [
+ "zoom",
+ ],
+ 9,
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "landmark",
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1,
+ -0,
+ ],
+ "right",
+ [
+ -1,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -1.55,
+ ],
+ "top",
+ [
+ 0,
+ 0.7,
+ ],
+ ],
+ ],
+ [
+ "any",
+ [
+ "in",
+ "buoy",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ "beacon",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 0.3,
+ -0.3,
+ ],
+ "right",
+ [
+ -0.3,
+ -0.3,
+ ],
+ "bottom",
+ [
+ 0,
+ -1.15,
+ ],
+ "top",
+ [
+ 0,
+ 1.15,
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 0.3,
+ -0,
+ ],
+ "right",
+ [
+ -0.3,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -1.15,
+ ],
+ "top",
+ [
+ 0,
+ 1.15,
+ ],
+ ],
+ ],
+ ],
+ 12,
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "landmark",
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.24,
+ -0,
+ ],
+ "right",
+ [
+ -1.24,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -2.1,
+ ],
+ "top",
+ [
+ 0,
+ 0.83,
+ ],
+ ],
+ ],
+ [
+ "any",
+ [
+ "in",
+ "buoy",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ "beacon",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "can",
+ "conical",
+ "spherical",
+ "barrel",
+ "super-buoy",
+ "ice-buoy",
+ "ice_buoy",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.5,
+ -0.6,
+ ],
+ "right",
+ [
+ -1.5,
+ -0.6,
+ ],
+ "bottom",
+ [
+ 0,
+ -3.2,
+ ],
+ "top",
+ [
+ 0,
+ 1.2,
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.5,
+ -1.2,
+ ],
+ "right",
+ [
+ -1.5,
+ -1.2,
+ ],
+ "bottom",
+ [
+ 0,
+ -3.2,
+ ],
+ "top",
+ [
+ 0,
+ 1.2,
+ ],
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.5,
+ -0,
+ ],
+ "right",
+ [
+ -1.5,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -3.2,
+ ],
+ "top",
+ [
+ 0,
+ 1.2,
+ ],
+ ],
+ ],
+ ],
+ 16,
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "type",
+ ],
+ "landmark",
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.56,
+ -0,
+ ],
+ "right",
+ [
+ -1.56,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -2.83,
+ ],
+ "top",
+ [
+ 0,
+ 1,
+ ],
+ ],
+ ],
+ [
+ "any",
+ [
+ "in",
+ "buoy",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ "beacon",
+ [
+ "get",
+ "type",
+ ],
+ ],
+ [
+ "in",
+ [
+ "get",
+ "type",
+ ],
+ [
+ "literal",
+ [
+ "light_float",
+ "light_vessel",
+ ],
+ ],
+ ],
+ ],
+ [
+ "case",
+ [
+ "in",
+ [
+ "get",
+ "shape",
+ ],
+ [
+ "literal",
+ [
+ "can",
+ "conical",
+ "spherical",
+ "barrel",
+ "super-buoy",
+ "ice-buoy",
+ "ice_buoy",
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.0714285714285714,
+ -0.42857142857142855,
+ ],
+ "right",
+ [
+ -1.0714285714285714,
+ -0.42857142857142855,
+ ],
+ "bottom",
+ [
+ 0,
+ -2.2857142857142856,
+ ],
+ "top",
+ [
+ 0,
+ 0.8571428571428571,
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.0714285714285714,
+ -0.8571428571428571,
+ ],
+ "right",
+ [
+ -1.0714285714285714,
+ -0.8571428571428571,
+ ],
+ "bottom",
+ [
+ 0,
+ -2.2857142857142856,
+ ],
+ "top",
+ [
+ 0,
+ 0.8571428571428571,
+ ],
+ ],
+ ],
+ ],
+ [
+ "literal",
+ [
+ "left",
+ [
+ 1.0714285714285714,
+ -0,
+ ],
+ "right",
+ [
+ -1.0714285714285714,
+ -0,
+ ],
+ "bottom",
+ [
+ 0,
+ -2.2857142857142856,
+ ],
+ "top",
+ [
+ 0,
+ 0.8571428571428571,
+ ],
+ ],
+ ],
+ ],
+ ],
+ },
+ "minzoom": 11,
+ "paint": {
+ "text-halo-blur": 1,
+ "text-halo-color": "rgba(255,255,255,0.8)",
+ "text-halo-width": 2,
+ },
+ "source": "seamap",
+ "source-layer": "seamark",
+ "type": "symbol",
+ },
+ ],
+}
+`;
diff --git a/style/index.test.ts b/style/index.test.ts
index c3172e3..b67ac20 100644
--- a/style/index.test.ts
+++ b/style/index.test.ts
@@ -12,6 +12,9 @@ import { chartLayers } from "./layers/index.ts";
const { areas, symbols } = layers();
const all = [...areas, ...symbols];
+const strict = chartLayers({ standards: true });
+const strictLayers = [...strict.areas, ...strict.symbols];
+
it("produces a valid style", () => {
const style: StyleSpecification = {
version: 8,
@@ -140,6 +143,11 @@ it("keeps layer ids and order stable", () => {
]);
});
+// The chart's own portrayal is what ships; a second mode alongside it must not move it at all.
+it("keeps the chart portrayal byte for byte", () => {
+ expect(chartLayers()).toMatchSnapshot();
+});
+
describe("isolated dangers", () => {
const layer = chartLayers({ safety: 2 }).symbols.find((l) => l.id === "isolated-dangers") as {
filter: never;
@@ -290,6 +298,129 @@ describe("thinning and decoration", () => {
});
});
+// The strict S-52 portrayal: the standard's own answers instead of the chart's, so every
+// deviation the chart makes deliberately has to be absent here.
+describe("standards mode", () => {
+ const point = (properties: Record) => ({ type: 1, properties }) as never;
+ const draws = (id: string, zoom: number, properties: Record) => {
+ const layer = strictLayers.find((l) => l.id === id) as { filter: never };
+ return featureFilter(layer.filter).filter({ zoom }, point(properties), undefined as never);
+ };
+
+ it("produces a valid style", () => {
+ const style: StyleSpecification = {
+ version: 8,
+ glyphs: "https://example.com/glyphs/{fontstack}/{range}.pbf",
+ sprite: [sprite("https://example.com/sprites")],
+ sources: sources(),
+ layers: strictLayers,
+ };
+ expect(validateStyleMin(style)).toEqual([]);
+ });
+
+ // SCAMIN is the whole of S-52's scale rule, and the tiles carry the derived floor per feature
+ it("gates every seamap layer on the tiles' strict scale floor", () => {
+ const clause = JSON.stringify([">=", ["zoom"], ["coalesce", ["get", "std_minzoom"], 0]]);
+ for (const layer of strictLayers) {
+ expect(JSON.stringify((layer as { filter?: unknown }).filter), layer.id).toContain(clause);
+ }
+ expect(JSON.stringify(chartLayers())).not.toContain("std_minzoom");
+ });
+
+ it("holds a feature back until its own SCAMIN floor, where the chart never does", () => {
+ const buoy = { type: "buoy_lateral", family: "minor_aid", std_minzoom: 14 };
+ expect(draws("buoys", 12, buoy)).toBe(false);
+ expect(draws("buoys", 14, buoy)).toBe(true);
+ const chart = chartLayers().symbols.find((l) => l.id === "buoys") as { filter: never };
+ expect(featureFilter(chart.filter).filter({ zoom: 12 }, point(buoy), undefined as never)).toBe(
+ true,
+ );
+ });
+
+ it("draws light sectors as display-fixed arcs rather than ground geometry", () => {
+ const ids = strict.symbols.map((l) => l.id);
+ for (const id of ["sector-arc-start", "sector-arc-middle-casing", "sector-arc-end-obscured"]) {
+ expect(ids).toContain(id);
+ }
+ for (const id of ["sector-arc", "sector-arc-casing", "sector-arc-obscured"]) {
+ expect(ids).not.toContain(id);
+ }
+ // the arcs ride the sector points and their limits, not the tiles' arc and leg lines
+ const sector = { subtype: "sector_point", sector_start: 10, sector_end: 40, sector_width: 30 };
+ expect(draws("sector-arc-start", 12, sector)).toBe(true);
+ expect(draws("sector-arc-start", 12, { ...sector, subtype: "sector" })).toBe(false);
+ expect(draws("sector-legs", 12, { subtype: "limit", bearing: 40 })).toBe(true);
+ expect(draws("sector-legs", 12, { subtype: "leg" })).toBe(false);
+ });
+
+ /**
+ * One sprite covers its own span and no more, so a sector wider than that needs all three
+ * copies — obscured sectors included, or a 100° obscured sector shows only its middle 40°.
+ */
+ it("covers an obscured sector with the same three copies as a coloured one", () => {
+ const obscured = {
+ subtype: "sector_point",
+ visibility: "obscured",
+ sector_start: 10,
+ sector_end: 110,
+ sector_width: 100,
+ };
+ for (const part of ["start", "middle", "end"]) {
+ expect(draws(`sector-arc-${part}-obscured`, 12, obscured), part).toBe(true);
+ expect(draws(`sector-arc-${part}`, 12, obscured), part).toBe(false);
+ }
+ });
+
+ // the sprite index picks the largest arc whose three copies fit, so the two must agree
+ it("keeps the sprite spans in step with the artwork", () => {
+ // the end copy rotates back by the sprite's span: ["-", sector_end, ["step", width, span0, …]]
+ const endRotate = (strictLayers.find((l) => l.id === "sector-arc-end") as { layout: never })
+ .layout["icon-rotate"] as [string, unknown, [string, unknown, ...number[]]];
+ // in the step, every other entry from the first output is a span
+ const spans = endRotate[2].slice(2).filter((_, i) => i % 2 === 0) as number[];
+ expect(spans).toHaveLength(5);
+ spans.forEach((span, i) => {
+ const svg = readFileSync(`sprites/icons/arc-${i}-G.svg`, "utf8");
+ const radians = Number(/sodipodi:end="([\d.]+)"/.exec(svg)?.[1]);
+ expect(Number(((radians * 180) / Math.PI).toFixed(2)), `arc-${i}`).toBe(span);
+ });
+ });
+
+ // S-52 sizes every symbol in millimetres on the display; a zoom ramp is the chart's own idea
+ it("holds every symbol at a fixed size", () => {
+ const variants = [{}, { "seamark:landmark:conspicuity": "conspicuous" }];
+ let checked = 0;
+ for (const layer of strictLayers) {
+ const layout = (layer as { layout?: Record }).layout;
+ for (const key of ["icon-size", "text-size"] as const) {
+ const size = layout?.[key];
+ if (size === undefined) continue;
+ const compiled = createExpression(size);
+ if (compiled.result !== "success") throw new Error(`${layer.id}: ${key} failed`);
+ for (const properties of variants) {
+ const at = (zoom: number) =>
+ compiled.value.evaluate({ zoom }, { type: 1, properties } as never) as number;
+ for (let zoom = 0; zoom <= 22; zoom += 0.25) {
+ expect(at(zoom), `${layer.id} ${key} at z${zoom}`).toBe(at(0));
+ }
+ // the sheet is rasterized at display resolution, so an icon above 1 upsamples and blurs
+ if (key === "icon-size") expect(at(0), `${layer.id} icon-size`).toBeLessThanOrEqual(1);
+ }
+ checked++;
+ }
+ }
+ expect(checked).toBeGreaterThan(0);
+ });
+
+ // no density budget, no legibility floors: a symbol that passes SCAMIN draws, dressed
+ it("draws every mark and its decorations whatever the crowd", () => {
+ const buoy = { type: "buoy_lateral", family: "minor_aid", topmark_shape: "cone" };
+ expect(draws("buoys", 8, { ...buoy, cell_rank: 40 })).toBe(true);
+ expect(draws("topmarks", 8, { ...buoy, cell_rank: 40 })).toBe(true);
+ expect(draws("seamark-label", 14, { ...buoy, name: "Nyhavn", cell_rank: 3 })).toBe(true);
+ });
+});
+
const PRIVATE = ["no", "private", "permit", "customers"];
describe("restricted access", () => {
@@ -417,7 +548,8 @@ describe.skipIf(!existsSync(spriteIndex))("sprite sheet", () => {
node.forEach(walk);
}
};
- for (const layer of all) {
+ // both portrayals: the sector arcs only draw in standards mode, and their sprites still ship
+ for (const layer of [...all, ...strictLayers]) {
walk((layer as { layout?: { "icon-image"?: unknown } }).layout?.["icon-image"]);
walk((layer as { paint?: { "fill-pattern"?: unknown } }).paint?.["fill-pattern"]);
}
@@ -427,6 +559,15 @@ describe.skipIf(!existsSync(spriteIndex))("sprite sheet", () => {
expect([...literals].filter((name) => !icons.has(name))).toEqual([]);
});
+ // The sector layers compose an arc name from a sprite index and a colour token, which the walk
+ // above cannot see, and there is no /generic fallback behind them — so spell the grid out.
+ it("contains every sector arc the standards portrayal can compose", () => {
+ const arcs = [0, 1, 2, 3, 4].flatMap((index) =>
+ ["G", "R", "Y", "M", "casing", "obscured"].map((suffix) => `arc-${index}-${suffix}`),
+ );
+ expect([...arcs, "sector-leg"].filter((name) => !icons.has(name))).toEqual([]);
+ });
+
// style() adds fill patterns the layers() walk above never sees (the unsurveyed
// water stipple); check every chart-sheet pattern in the whole style resolves
it("contains every fill pattern the whole style references", async () => {
diff --git a/style/index.ts b/style/index.ts
index 6dbf135..2ae6f3c 100644
--- a/style/index.ts
+++ b/style/index.ts
@@ -65,6 +65,12 @@ export interface LayersOptions {
safety?: number;
/** Depth unit for hazard depth numerals, matching seascape's soundings. */
unit?: "m" | "ft" | "fm";
+ /**
+ * Strict S-52 portrayal, off by default: fixed symbol sizes, SCAMIN-derived visibility, and
+ * light sectors as display-fixed arcs. The chart's own answers — density budgets, decoration
+ * legibility floors, size ramps, ground-geometry sectors — are what it ships with.
+ */
+ standards?: boolean;
}
/**
@@ -73,11 +79,11 @@ export interface LayersOptions {
* fills) belong below land fills, `symbols` (hazards, buoys, lights, topmarks,
* landmarks, labels) on top of everything.
*/
-export function layers({ font, safety, unit }: LayersOptions = {}): {
+export function layers({ font, safety, unit, standards }: LayersOptions = {}): {
areas: LayerSpecification[];
symbols: LayerSpecification[];
} {
- const { areas, symbols } = chartLayers({ safety, unit });
+ const { areas, symbols } = chartLayers({ safety, unit, standards });
if (font) {
for (const layer of [...areas, ...symbols]) {
const layout = "layout" in layer ? (layer.layout as { "text-font"?: string[] }) : undefined;
@@ -162,6 +168,11 @@ export interface StyleOptions {
unit?: Unit;
/** Safety contour depth in metres, 0 = off (seascape). */
safety?: number;
+ /**
+ * Strict S-52 portrayal of the chart symbology, off by default: fixed symbol sizes,
+ * SCAMIN-derived visibility, and display-fixed light sector arcs.
+ */
+ standards?: boolean;
/**
* Water shading: ENC-style vector depth bands by default — their edges coincide with the
* contour lines by construction, and they stay crisp under overzoom. "relief" selects the
@@ -192,6 +203,7 @@ export async function style({
flavor,
unit,
safety,
+ standards,
shading = "bands",
dem,
vector,
@@ -261,7 +273,7 @@ export async function style({
paint: { "fill-pattern": "freenauticalchart:unsurveyed" },
});
- const { areas, symbols } = layers({ font: versatilesFont, safety, unit });
+ const { areas, symbols } = layers({ font: versatilesFont, safety, unit, standards });
// draw sea: replace versatiles' first two layers (background, water) with the
// chart's own background, bathymetry, sea areas, and seamap land
diff --git a/style/layers/areas.ts b/style/layers/areas.ts
index 9b4c7e1..a99a1f4 100644
--- a/style/layers/areas.ts
+++ b/style/layers/areas.ts
@@ -1,5 +1,6 @@
import type { ExpressionSpecification, LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
import { colors } from "./palette.js";
+import type { Visibility } from "./visibility.js";
/** Restricted-area colouring: conservation green, everything else chart magenta (RESARE04). */
const restrictionColor: ExpressionSpecification = [
@@ -39,7 +40,7 @@ const restriction: ExpressionSpecification = [
* across a coastline stops at the shore. Allowed areas (anchorages, moorings) draw before
* restricted areas — RESARE outranks ACHARE (S-52 priority 5 vs 3) where they overlap.
*/
-export function areas(): LayerSpecification[] {
+export function areas({ symbolSize }: Visibility): LayerSpecification[] {
return [
{
id: "allowed-areas",
@@ -92,7 +93,7 @@ export function areas(): LayerSpecification[] {
"icon-overlap": "always",
"symbol-placement": "line",
"symbol-spacing": 90,
- "icon-size": ["interpolate", ["linear"], ["zoom"], 8, 0.2, 12, 0.8],
+ "icon-size": symbolSize(8, 0.2, 12, 0.8),
},
paint: { "icon-opacity": 0.8 },
},
diff --git a/style/layers/hazards.ts b/style/layers/hazards.ts
index 4080cc5..c38ac25 100644
--- a/style/layers/hazards.ts
+++ b/style/layers/hazards.ts
@@ -1,6 +1,6 @@
import type { ExpressionSpecification, LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
import { colors } from "./palette.js";
-import { TOKEN, sizeRamp, withinBudget } from "./visibility.js";
+import { TOKEN, type Visibility } from "./visibility.js";
const HAZARD_TYPES = ["rock", "wreck", "obstruction"];
@@ -56,7 +56,11 @@ const navigableSurround = (safety: number): ExpressionSpecification => [
* `safety` is the safety depth in metres, defaulting to seascape's 2 m small-craft default; it
* drives the isolated-danger highlight and the hazard-area boundary style.
*/
-export function hazards(safety = 2, unit: "m" | "ft" | "fm" = "m"): LayerSpecification[] {
+export function hazards(
+ { sizeRamp, symbolSize, withinBudget }: Visibility,
+ safety = 2,
+ unit: "m" | "ft" | "fm" = "m",
+): LayerSpecification[] {
// the surveyed depth in the mariner's unit, floored toward shallower — the safe direction
// (S-4 B-412); metres print as tagged, often with a decimal
const depthText: ExpressionSpecification =
@@ -334,7 +338,7 @@ export function hazards(safety = 2, unit: "m" | "ft" | "fm" = "m"): LayerSpecifi
"text-letter-spacing": 0.1,
"text-max-width": 5,
"text-padding": 10,
- "text-size": ["interpolate", ["linear"], ["zoom"], 8, 9, 13, 11],
+ "text-size": symbolSize(8, 9, 13, 11),
},
paint: {
"text-color": colors.label,
diff --git a/style/layers/index.ts b/style/layers/index.ts
index 07ebc52..2671c7d 100644
--- a/style/layers/index.ts
+++ b/style/layers/index.ts
@@ -1,4 +1,8 @@
-import type { LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
+import type {
+ ExpressionSpecification,
+ FilterSpecification,
+ LayerSpecification,
+} from "@maplibre/maplibre-gl-style-spec";
import { areas } from "./areas.js";
import { hazards } from "./hazards.js";
import { routes } from "./routes.js";
@@ -7,7 +11,24 @@ import { lights } from "./lights.js";
import { sectors } from "./sectors.js";
import { marks } from "./marks.js";
import { labels } from "./labels.js";
-import { MAX_SAFETY_DEPTH } from "./visibility.js";
+import { MAX_SAFETY_DEPTH, visibility } from "./visibility.js";
+
+/**
+ * The strict scale floor S-52 reads off SCAMIN, carried per feature by the tiles
+ * (`SeamarkZoomRules.java`). Coalesced to 0 so a chart built against older tiles still draws.
+ */
+const withinScale: ExpressionSpecification = [
+ ">=",
+ ["zoom"],
+ ["coalesce", ["get", "std_minzoom"], 0],
+];
+
+function atStandardScale(layer: LayerSpecification): LayerSpecification {
+ if (!("source" in layer) || layer.source !== "seamap") return layer;
+ const own = "filter" in layer ? layer.filter : undefined;
+ const filter = (own ? ["all", own, withinScale] : withinScale) as FilterSpecification;
+ return { ...layer, filter } as LayerSpecification;
+}
/**
* The chart symbology, split around the consumer's land fills: `areas` are sea areas that a
@@ -17,8 +38,15 @@ import { MAX_SAFETY_DEPTH } from "./visibility.js";
* half. The other is symbol collision: MapLibre places symbols in *reverse* draw order, so the
* later a layer appears here the more likely its label survives a crowded harbour. Labels last is
* deliberate.
+ *
+ * `standards` swaps the chart's own portrayal for a strict S-52 one: fixed symbol sizes,
+ * SCAMIN-derived visibility, and light sectors as display-fixed arcs.
*/
-export function chartLayers({ safety, unit }: { safety?: number; unit?: "m" | "ft" | "fm" } = {}): {
+export function chartLayers({
+ safety,
+ unit,
+ standards = false,
+}: { safety?: number; unit?: "m" | "ft" | "fm"; standards?: boolean } = {}): {
areas: LayerSpecification[];
symbols: LayerSpecification[];
} {
@@ -26,16 +54,23 @@ export function chartLayers({ safety, unit }: { safety?: number; unit?: "m" | "f
// have thinned or excluded the hazard entirely, so a deeper setting would silently miss
// hazards it appears to highlight. Clamp rather than honour a promise the data cannot keep.
const clamped = Math.min(safety ?? 2, MAX_SAFETY_DEPTH);
- return {
- areas: areas(),
+ const v = visibility(standards);
+ const built = {
+ areas: areas(v),
symbols: [
- ...hazards(clamped, unit),
- ...routes(),
- ...structures(),
- ...lights(),
- ...sectors(),
- ...marks(),
- ...labels(),
+ ...hazards(v, clamped, unit),
+ ...routes(v),
+ ...structures(v),
+ ...lights(v),
+ ...sectors(v),
+ ...marks(v),
+ ...labels(v),
],
};
+ if (!standards) return built;
+ // one pass rather than a clause in every module: SCAMIN governs every seamark the same way
+ return {
+ areas: built.areas.map(atStandardScale),
+ symbols: built.symbols.map(atStandardScale),
+ };
}
diff --git a/style/layers/labels.ts b/style/layers/labels.ts
index 4b2d500..7214256 100644
--- a/style/layers/labels.ts
+++ b/style/layers/labels.ts
@@ -1,7 +1,7 @@
import type { ExpressionSpecification, LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
import { colors } from "./palette.js";
import { MARK, anchorOffsets, markOffsets } from "./placement.js";
-import { RAMP_FROM, TOKEN, decoration, topOfCell, withinBudget } from "./visibility.js";
+import { RAMP_FROM, TOKEN, type Visibility } from "./visibility.js";
const halo = {
"text-halo-color": colors.halo,
@@ -137,7 +137,12 @@ function landmarkIcon(folder: "" | "convis/"): ExpressionSpecification {
* name entirely. Instead `landmarks` blanks its own name for lit features once `lights-label` is
* drawing, and `lights-label` stacks name over characteristic. Keep the two together.
*/
-export function labels(): LayerSpecification[] {
+export function labels({
+ decoration,
+ symbolSize,
+ topOfCell,
+ withinBudget,
+}: Visibility): LayerSpecification[] {
return [
{
id: "landmarks",
@@ -210,15 +215,12 @@ export function labels(): LayerSpecification[] {
// bolder in the convis/ art rather than an upscale. The artwork is the same artwork
// though, so what it can survive shrinking to is the same on-screen floor for both —
// hence the larger variant's smaller floor fraction.
- "icon-size": [
- "interpolate",
- ["linear"],
- ["zoom"],
+ "icon-size": symbolSize(
RAMP_FROM,
["case", convis, TOKEN.detail / LMK_SCALE_CONVIS, TOKEN.detail / LMK_SCALE],
12,
1,
- ],
+ ),
},
paint: { "text-color": colors.label, ...halo },
},
@@ -232,7 +234,7 @@ export function labels(): LayerSpecification[] {
layout: {
"symbol-placement": "line",
"text-field": ["get", "name"],
- "text-size": ["interpolate", ["linear"], ["zoom"], 10, 10, 16, 12],
+ "text-size": symbolSize(10, 10, 16, 12),
"text-font": ["Noto Sans Regular"],
},
paint: {
@@ -262,7 +264,7 @@ export function labels(): LayerSpecification[] {
"text-max-width": 5,
"text-offset": [0, -0.65],
"text-pitch-alignment": "viewport",
- "text-size": ["interpolate", ["linear"], ["zoom"], 13, 10, 16, 13],
+ "text-size": symbolSize(13, 10, 16, 13),
},
paint: { "text-color": colors.label, ...halo },
},
@@ -292,7 +294,7 @@ export function labels(): LayerSpecification[] {
] as ExpressionSpecification,
"text-font": ["Noto Sans Regular"],
"text-justify": "auto",
- "text-size": ["interpolate", ["linear"], ["zoom"], 12, 10, 16, 13],
+ "text-size": symbolSize(12, 10, 16, 13),
"text-padding": 8,
// MARK's pixel clearances in this layer's ems per stop. Hazards are centred symbols
// (and can wear the wide isolated-danger octagon), so they take even margins.
@@ -380,7 +382,7 @@ export function labels(): LayerSpecification[] {
],
"text-font": ["Noto Sans Regular"],
"text-justify": "auto",
- "text-size": ["interpolate", ["linear"], ["zoom"], 12, 10, 16, 14],
+ "text-size": symbolSize(12, 10, 16, 14),
// tighter than the name layers: the block carries the characteristic, and padding that
// makes it fail to place hides information a mariner steers by
"text-padding": 4,
diff --git a/style/layers/lights.ts b/style/layers/lights.ts
index 8ca0232..d740978 100644
--- a/style/layers/lights.ts
+++ b/style/layers/lights.ts
@@ -1,5 +1,5 @@
import type { ExpressionSpecification, LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
-import { TOKEN, decoration, sizeRamp, withinBudget } from "./visibility.js";
+import { TOKEN, type Visibility } from "./visibility.js";
/** Sprite folder for the flare icon: floodlights have their own artwork. */
const lightPrefix: ExpressionSpecification = [
@@ -18,7 +18,12 @@ const lightPrefix: ExpressionSpecification = [
* standalone light symbols, and fog signals. The light *characteristic* label lives in
* labels.ts, where it shares an anchor with the name labels it has to cooperate with.
*/
-export function lights(): LayerSpecification[] {
+export function lights({
+ decoration,
+ sizeRamp,
+ symbolSize,
+ withinBudget,
+}: Visibility): LayerSpecification[] {
return [
{
id: "lights",
@@ -56,7 +61,7 @@ export function lights(): LayerSpecification[] {
// guaranteed placement; the budget and legibility filters above do the thinning
"icon-overlap": "always",
"icon-rotation-alignment": "viewport",
- "icon-size": ["interpolate", ["linear"], ["zoom"], 12, 0.7, 14, 1],
+ "icon-size": symbolSize(12, 0.7, 14, 1),
},
},
{
@@ -103,7 +108,7 @@ export function lights(): LayerSpecification[] {
"icon-image": "freenauticalchart:fogsignal",
"icon-overlap": "always",
"icon-rotate": 90,
- "icon-size": ["interpolate", ["linear"], ["zoom"], 12, 0.7, 14, 1],
+ "icon-size": symbolSize(12, 0.7, 14, 1),
},
},
];
diff --git a/style/layers/marks.ts b/style/layers/marks.ts
index 82e81d0..d7a2569 100644
--- a/style/layers/marks.ts
+++ b/style/layers/marks.ts
@@ -1,5 +1,5 @@
import type { ExpressionSpecification, LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
-import { TOKEN, decoration, sizeRamp, withinBudget } from "./visibility.js";
+import { TOKEN, type Visibility } from "./visibility.js";
/**
* Y-offsets (sprite px, scaled by icon-size) that seat a bottom-anchored topmark on each body
@@ -88,7 +88,7 @@ const patternBodyShape: ExpressionSpecification = [
* mark; reflectors draw last, charted over the mark. minzoom 6 defers to the tile pipeline,
* which only carries cardinal/isolated-danger/safe-water marks below zoom 8 (SeamarkZoomRules).
*/
-export function marks(): LayerSpecification[] {
+export function marks({ decoration, sizeRamp, withinBudget }: Visibility): LayerSpecification[] {
return [
{
id: "topmarks",
diff --git a/style/layers/palette.ts b/style/layers/palette.ts
index c7f703e..8b23622 100644
--- a/style/layers/palette.ts
+++ b/style/layers/palette.ts
@@ -27,7 +27,11 @@ export const day = {
lightGreen: "#00a650",
lightRed: "#ed1c24",
lightYellow: "#fab20b",
- /** grey, not CHBLK black: a busy sector light should not read as a hazard boundary (S-4 B-475.1) */
+ /**
+ * grey, not CHBLK black: a busy sector light should not read as a hazard boundary (S-4 B-475.1).
+ * Also baked into sprites/icons/sector-leg.svg, which the style cannot recolour — icon-color
+ * needs an SDF sheet and spreet builds SDF per sheet, not per icon. Change the two together.
+ */
sectorLeg: "#666",
} as const;
diff --git a/style/layers/routes.ts b/style/layers/routes.ts
index 322a07e..4542892 100644
--- a/style/layers/routes.ts
+++ b/style/layers/routes.ts
@@ -1,11 +1,12 @@
import type { LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
import { colors } from "./palette.js";
+import type { Visibility } from "./visibility.js";
/**
* Things vessels follow or must stay clear of, drawn as lines: traffic separation schemes, ferry
* routes, navigation lines and tracks, and the submarine cables and pipelines below them.
*/
-export function routes(): LayerSpecification[] {
+export function routes({ symbolSize }: Visibility): LayerSpecification[] {
return [
{
id: "cables-pipes",
@@ -53,7 +54,7 @@ export function routes(): LayerSpecification[] {
"icon-image": "freenauticalchart:TSS-arrow",
// held at 0.15 (~8px on the 54px sprite) down to the z2 data floor: the arrow chain is
// the lane's only portrayal, so it has to stay legible at passage-planning zooms
- "icon-size": ["interpolate", ["linear"], ["zoom"], 2, 0.15, 6, 0.15, 14, 0.6],
+ "icon-size": symbolSize(2, 0.15, 6, 0.15, 14, 0.6),
"symbol-placement": "line",
"icon-padding": 0,
// grows with icon-size so the gap between arrows stays open as they scale up
diff --git a/style/layers/sectors.ts b/style/layers/sectors.ts
index cc24372..53ed65f 100644
--- a/style/layers/sectors.ts
+++ b/style/layers/sectors.ts
@@ -1,19 +1,15 @@
import type { ExpressionSpecification, LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
import { colors } from "./palette.js";
-import { decoration, withinBudget } from "./visibility.js";
+import type { Visibility } from "./visibility.js";
/**
- * Light sectors: decoration on a mark, drawn as ground geometry.
+ * Light sectors: decoration on a mark, in either of the two portrayals the tiles carry.
*
* S-52 fixes the arc to the display — 20 mm radius whatever the scale (PresLib 4.0.4, `LIGHTS06`)
* — but on a chart the reader zooms, a display-fixed figure is the one thing moving against
- * everything else. So the tiles carry the arc as an ordinary line at a nominal radius
+ * everything else. So the chart draws the arc as an ordinary line at a nominal radius
* (`Lights.java`), scaling with the chart like its neighbours; only the stroke widths live here.
- *
- * The zoom window is where that stops working. Below ~z10 the radius is a smudge; past ~z15 the
- * arc starts outgrowing the screen and reads as an unexplained curve with its light nowhere in
- * view — the arc never was a claim about reach, so it fades out over z15–17 rather than growing
- * into one.
+ * Standards mode takes the millimetres literally and draws sprites at the light instead.
*/
/** A sector where the light shows; obscured/faint sectors draw uncoloured (S-52 LIGHTS06). */
@@ -26,6 +22,10 @@ const visible: ExpressionSpecification = [
],
];
+export function sectors(v: Visibility): LayerSpecification[] {
+ return v.standards ? displaySectors(v) : groundSectors(v);
+}
+
const isSector: ExpressionSpecification = ["==", ["get", "subtype"], "sector"];
const common = {
@@ -49,7 +49,13 @@ const fade = (opacity: number): ExpressionSpecification => [
0,
];
-export function sectors(): LayerSpecification[] {
+/**
+ * Arcs and legs as ground geometry, inside a zoom window. Below ~z10 the radius is a smudge; past
+ * ~z15 the arc starts outgrowing the screen and reads as an unexplained curve with its light
+ * nowhere in view — the arc never was a claim about reach, so it fades out over z15–17 rather than
+ * growing into one.
+ */
+function groundSectors({ decoration, withinBudget }: Visibility): LayerSpecification[] {
return [
{
// radial legs at every limit, one per bearing (`Lights.java` dedupes shared ones)
@@ -113,3 +119,181 @@ export function sectors(): LayerSpecification[] {
},
];
}
+
+/** Angular span of each `arc-N` sprite, stepping by a factor of three. */
+const SPRITE_SPAN = [1.48, 4.44, 13.33, 40, 120];
+
+/** Sprite radius in the sheet, which the sheet rasterizes at twice the source size. */
+const SPRITE_RADIUS = 98;
+/** Length of `sector-leg` in the sheet, which is drawn for exactly this use. */
+const LEG_LENGTH = 512;
+/** CSS pixels per millimetre at 96 dpi, for turning the standard's millimetres into icon-size. */
+const PX_PER_MM = 1 / 0.2646;
+
+const mm = (target: number, spriteSize: number) =>
+ Number(((target * PX_PER_MM) / spriteSize).toFixed(3));
+
+/** Which sprite covers this sector: the largest whose three copies still fit inside it. */
+const spriteIndex: ExpressionSpecification = [
+ "step",
+ ["get", "sector_width"],
+ 0,
+ SPRITE_SPAN[1],
+ 1,
+ SPRITE_SPAN[2],
+ 2,
+ SPRITE_SPAN[3],
+ 3,
+ SPRITE_SPAN[4],
+ 4,
+];
+
+/** How much of the sector that sprite covers, which the middle and end copies rotate back by. */
+const span: ExpressionSpecification = [
+ "step",
+ ["get", "sector_width"],
+ SPRITE_SPAN[0],
+ SPRITE_SPAN[1],
+ SPRITE_SPAN[1],
+ SPRITE_SPAN[2],
+ SPRITE_SPAN[2],
+ SPRITE_SPAN[3],
+ SPRITE_SPAN[3],
+ SPRITE_SPAN[4],
+ SPRITE_SPAN[4],
+];
+
+/**
+ * Sector colour, on the same tokens the flare uses so a light and its arc agree. Orange and amber
+ * cover raw tag values; a combination the S-52 precedence can't name falls through to magenta,
+ * never a fake colour.
+ */
+const colourSuffix: ExpressionSpecification = [
+ "match",
+ ["get", "color"],
+ "green",
+ "G",
+ "red",
+ "R",
+ ["white", "yellow", "orange", "amber"],
+ "Y",
+ "M",
+];
+
+const isSectorPoint: ExpressionSpecification = ["==", ["get", "subtype"], "sector_point"];
+
+/**
+ * A sector figure draws wherever its light draws, and never reserves room: an arc quad is many
+ * times the size of the symbols around it, and MapLibre places in reverse layer order, so an arc
+ * in the collision index would suppress the marks and labels drawn after it.
+ */
+const guaranteed = {
+ "icon-overlap": "always",
+ "icon-ignore-placement": true,
+} as const;
+
+/** Which sprite family a copy belongs to, and so its filter, suffix and opacity. */
+type Family = "casing" | "colour" | "obscured";
+
+/**
+ * Arcs at the 20 mm S-52 fixes them to, drawn as sprites: a line layer cannot hold a constant
+ * screen size. Each `arc-N` spans a fixed angle, and three copies — rotated to the sector's start,
+ * its middle and its end — cover any width. A sector narrower than the smallest sprite overdraws
+ * past its limits by the difference, a fraction of a degree at the radius the arc is drawn at. The
+ * tiles carry one point per sector and one per limit with the angles (`Lights.java`); nothing here
+ * is geometry.
+ */
+function displaySectors({ decoration, withinBudget }: Visibility): LayerSpecification[] {
+ /**
+ * Two reversals that cancel, which is why the rotations below look like they are missing a
+ * conversion. Sector limits are bearings **from seaward** — the direction a vessel looks along
+ * to see the light — so drawing one outward from the light means adding 180°. S-52 warns about
+ * exactly this ("Do not forget to reverse the sector values (+/- 180 degrees) since the values
+ * are given from seaward"), and it is the classic sector-rendering bug. The arc sprite meanwhile
+ * begins at the bottom of its own image and sweeps clockwise, so its leading edge sits at 180°
+ * before any rotation. Reciprocal plus sprite origin is a full turn: rotate by the limit itself.
+ */
+ const start: ExpressionSpecification = ["get", "sector_start"];
+ const end: ExpressionSpecification = ["-", ["get", "sector_end"], span];
+ const middle: ExpressionSpecification = [
+ "-",
+ ["+", ["get", "sector_start"], ["/", ["get", "sector_width"], 2]],
+ ["/", span, 2],
+ ];
+
+ /**
+ * @param family `casing` is the wide neutral stroke S-52 puts under the colour ("First symbolize
+ * the Arc with a solid line, 4 units wide, COLOUR OUTLW; then symbolize the Arc with the
+ * COLOUR") — without it a yellow sector on pale water is barely there, so legibility rather
+ * than decoration. `obscured` is the uncoloured dashed style an obscured, part-obscured or
+ * faint sector takes instead (LIGHTS06 Continuation).
+ */
+ const arcLayer = (
+ id: string,
+ rotate: ExpressionSpecification,
+ family: Family,
+ ): LayerSpecification => ({
+ id,
+ type: "symbol",
+ source: "seamap",
+ "source-layer": "light",
+ minzoom: 8,
+ filter: [
+ "all",
+ isSectorPoint,
+ family === "obscured" ? ["!", visible] : visible,
+ decoration("sector"),
+ withinBudget,
+ ],
+ layout: {
+ "icon-image": [
+ "concat",
+ "freenauticalchart:arc-",
+ ["to-string", spriteIndex],
+ "-",
+ family === "colour" ? colourSuffix : family,
+ ],
+ // the arc is a bearing, so it turns with the map and never with the screen
+ "icon-rotation-alignment": "map",
+ "icon-rotate": rotate,
+ ...guaranteed,
+ // the smaller of an overlapping pair reaches 25 mm so the larger cannot bury it
+ "icon-size": ["case", ["has", "extended"], mm(25, SPRITE_RADIUS), mm(20, SPRITE_RADIUS)],
+ },
+ ...(family === "obscured" ? { paint: { "icon-opacity": 0.9 } } : {}),
+ });
+
+ return [
+ {
+ // radial legs at every limit, one per bearing (`Lights.java` dedupes shared ones)
+ id: "sector-legs",
+ type: "symbol",
+ source: "seamap",
+ "source-layer": "light",
+ minzoom: 8,
+ filter: ["all", ["==", ["get", "subtype"], "limit"], decoration("sector"), withinBudget],
+ layout: {
+ "icon-image": "freenauticalchart:sector-leg",
+ // anchored at its foot, so the line runs outward from the light along the bearing
+ "icon-anchor": "bottom",
+ "icon-rotation-alignment": "map",
+ // anchored at its foot the leg runs to image north, so the seaward
+ // reciprocal is the whole correction here
+ "icon-rotate": ["+", ["get", "bearing"], 180],
+ ...guaranteed,
+ "icon-size": mm(25, LEG_LENGTH),
+ },
+ },
+ // casings first, so every colour stroke lands on its own neutral backing
+ arcLayer("sector-arc-start-casing", start, "casing"),
+ arcLayer("sector-arc-middle-casing", middle, "casing"),
+ arcLayer("sector-arc-end-casing", end, "casing"),
+ arcLayer("sector-arc-start", start, "colour"),
+ arcLayer("sector-arc-middle", middle, "colour"),
+ arcLayer("sector-arc-end", end, "colour"),
+ // an obscured sector takes the same three copies: one alone covers only the sprite's own span
+ arcLayer("sector-arc-start-obscured", start, "obscured"),
+ arcLayer("sector-arc-middle-obscured", middle, "obscured"),
+ arcLayer("sector-arc-end-obscured", end, "obscured"),
+ ];
+}
diff --git a/style/layers/structures.ts b/style/layers/structures.ts
index 01d8548..08c64dd 100644
--- a/style/layers/structures.ts
+++ b/style/layers/structures.ts
@@ -1,7 +1,7 @@
import type { ExpressionSpecification, LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
import { colors } from "./palette.js";
import { anchorOffsets } from "./placement.js";
-import { TOKEN, sizeRamp, withinBudget } from "./visibility.js";
+import { TOKEN, type Visibility } from "./visibility.js";
/**
* Fixed shore and harbour works: piers and breakwaters, piles and dolphins, platforms, cranes,
@@ -15,7 +15,11 @@ const wallWaterLevel: ExpressionSpecification = [
"",
];
-export function structures(): LayerSpecification[] {
+export function structures({
+ sizeRamp,
+ symbolSize,
+ withinBudget,
+}: Visibility): LayerSpecification[] {
// access values that mean "not open to the public" (OpenSeaMap-vector's PRIVATE_TAGS).
// An untagged feature gets null, and `in` against null is false, so it reads as public.
const restricted: ExpressionSpecification = [
@@ -105,7 +109,7 @@ export function structures(): LayerSpecification[] {
layout: {
"icon-image": "freenauticalchart:crane",
"symbol-sort-key": ["coalesce", ["get", "cell_rank"], 0],
- "icon-size": ["interpolate", ["linear"], ["zoom"], 14, 0.6, 16, 1],
+ "icon-size": symbolSize(14, 0.6, 16, 1),
},
},
{
@@ -118,7 +122,7 @@ export function structures(): LayerSpecification[] {
layout: {
"icon-image": "freenauticalchart:rescue",
"symbol-sort-key": ["coalesce", ["get", "cell_rank"], 0],
- "icon-size": ["interpolate", ["linear"], ["zoom"], 10, 0.5, 13, 1],
+ "icon-size": symbolSize(10, 0.5, 13, 1),
},
},
{
@@ -131,7 +135,7 @@ export function structures(): LayerSpecification[] {
layout: {
"icon-image": "freenauticalchart:radar_scanner",
"symbol-sort-key": ["coalesce", ["get", "cell_rank"], 0],
- "icon-size": ["interpolate", ["linear"], ["zoom"], 11, 0.6, 14, 1],
+ "icon-size": symbolSize(11, 0.6, 14, 1),
},
},
{
@@ -203,7 +207,7 @@ export function structures(): LayerSpecification[] {
"freenauticalchart:poi-beach",
"freenauticalchart:poi-generic",
],
- "icon-size": ["interpolate", ["linear"], ["zoom"], 14, 0.8, 17, 1],
+ "icon-size": symbolSize(14, 0.8, 17, 1),
// which grades a dock sells decides whether it's worth the detour; the other badges say
// all they need to with the icon
"text-field": [
@@ -262,7 +266,7 @@ export function structures(): LayerSpecification[] {
"icon-overlap": "always",
"text-font": ["Noto Sans Regular"],
"text-field": ["get", "name"],
- "text-size": ["interpolate", ["linear"], ["zoom"], 8, 9, 12, 12],
+ "text-size": symbolSize(8, 9, 12, 12),
// tracks icon-size to hold ~7.5px from icon edge to glyph, leaving ~5px clear of the halo.
// Both sprites are square 32 display px at icon-size 1, so both axes match.
"text-variable-anchor-offset": [
diff --git a/style/layers/visibility.ts b/style/layers/visibility.ts
index 39ada67..9a57268 100644
--- a/style/layers/visibility.ts
+++ b/style/layers/visibility.ts
@@ -1,4 +1,8 @@
-import type { ExpressionSpecification } from "@maplibre/maplibre-gl-style-spec";
+import type {
+ DataDrivenPropertyValueSpecification,
+ ExpressionFilterSpecification,
+ ExpressionSpecification,
+} from "@maplibre/maplibre-gl-style-spec";
/**
* What survives when symbols overlap, and how much of each surviving symbol gets drawn.
@@ -10,6 +14,9 @@ import type { ExpressionSpecification } from "@maplibre/maplibre-gl-style-spec";
*
* Budgets are per family, which is what keeps a harbour from being displaced by the forty buoys
* around it: they never compete for the same allowance.
+ *
+ * S-52 answers all of this from SCAMIN and a table of fixed symbol sizes, so standards mode makes
+ * every helper here a pass-through and the layers read the same either way.
*/
/**
@@ -62,11 +69,6 @@ const LEGIBLE_FROM = {
*/
export const MAX_SAFETY_DEPTH = 30;
-/** The zoom can resolve this kind of decoration. */
-export function decoration(kind: keyof typeof LEGIBLE_FROM): ExpressionSpecification {
- return [">=", ["zoom"], LEGIBLE_FROM[kind]];
-}
-
/**
* How small a symbol can be drawn and still mean something — a property of the artwork, not of the
* feature's importance. Hue and gross silhouette survive scaling; internal detail does not, so what
@@ -97,35 +99,68 @@ export const TOKEN = {
*/
export const RAMP_FROM = 9;
-/**
- * Floor from the token until {@link RAMP_FROM}, full size at the zoom the feature's prominence
- * earns. A body never vanishes at the bottom of this ramp; only a budget removes one.
- */
-export function sizeRamp(floor: number, fullAt: number, full = 1): ExpressionSpecification {
- return ["interpolate", ["linear"], ["zoom"], RAMP_FROM, floor, fullAt, full];
-}
-
function budgetAt(step: 0 | 1 | 2 | 3): ExpressionSpecification {
const arms = Object.entries(BUDGET).flatMap(([family, steps]) => [family, steps[step]]);
return ["match", ["get", "family"], ...arms, 999] as unknown as ExpressionSpecification;
}
-/** This feature is inside its family's allowance for the cell it sits in. */
-export const withinBudget: ExpressionSpecification = [
- // `cell_rank` counts from zero, so the comparison is strict and BUDGET reads as symbol counts
- "<",
- // lines and areas hold no position in a cell, so they are never thinned
- ["coalesce", ["get", "cell_rank"], 0],
- ["step", ["zoom"], budgetAt(0), 9, budgetAt(1), 11, budgetAt(2), 13, budgetAt(3)],
-];
+/** A zoom stop's size: a constant, or an expression over the feature. */
+type Size = number | ExpressionSpecification;
+
+/** The thinning, dressing and sizing rules the layer modules share. */
+export interface Visibility {
+ /** Strict S-52 portrayal rather than the chart's own. */
+ standards: boolean;
+ /** This feature is inside its family's allowance for the cell it sits in. */
+ withinBudget: ExpressionFilterSpecification;
+ /**
+ * A name is a luxury: it is affordable where the symbols around it already fit, and it must
+ * never take the room a light characteristic or a Racon group needs, because those change the
+ * decision and a name does not.
+ *
+ * Being first in your own cell is the proxy for "there is room here". Where the chart is sparse
+ * every mark is first and every mark is named; where it is crowded only the mark its neighbours
+ * are ranked against gets one. That needs no data the tiles do not already carry.
+ */
+ topOfCell: ExpressionFilterSpecification;
+ /** The zoom can resolve this kind of decoration. */
+ decoration(kind: keyof typeof LEGIBLE_FROM): ExpressionFilterSpecification;
+ /**
+ * Floor from the token until {@link RAMP_FROM}, full size at the zoom the feature's prominence
+ * earns. A body never vanishes at the bottom of this ramp; only a budget removes one.
+ */
+ sizeRamp(
+ floor: number,
+ fullAt: number,
+ full?: number,
+ ): DataDrivenPropertyValueSpecification;
+ /**
+ * An icon or text size interpolated over `zoom, size` stops, for symbols whose growth follows
+ * the detail they carry rather than the feature's prominence. The last stop is full size.
+ */
+ symbolSize(...stops: Size[]): DataDrivenPropertyValueSpecification;
+}
/**
- * A name is a luxury: it is affordable where the symbols around it already fit, and it must never
- * take the room a light characteristic or a Racon group needs, because those change the decision
- * and a name does not.
- *
- * Being first in your own cell is the proxy for "there is room here". Where the chart is sparse
- * every mark is first and every mark is named; where it is crowded only the mark its neighbours
- * are ranked against gets one. That needs no data the tiles do not already carry.
+ * The helper set for one portrayal. Standards mode neutralizes each one: SCAMIN carries no notion
+ * of density, so nothing is thinned and nothing is undressed for want of room, and every symbol is
+ * drawn at the fixed size the standard gives it.
*/
-export const topOfCell: ExpressionSpecification = ["==", ["coalesce", ["get", "cell_rank"], 0], 0];
+export function visibility(standards = false): Visibility {
+ return {
+ standards,
+ withinBudget: standards || [
+ // `cell_rank` counts from zero, so the comparison is strict and BUDGET reads as symbol counts
+ "<",
+ // lines and areas hold no position in a cell, so they are never thinned
+ ["coalesce", ["get", "cell_rank"], 0],
+ ["step", ["zoom"], budgetAt(0), 9, budgetAt(1), 11, budgetAt(2), 13, budgetAt(3)],
+ ],
+ topOfCell: standards || ["==", ["coalesce", ["get", "cell_rank"], 0], 0],
+ decoration: (kind) => standards || [">=", ["zoom"], LEGIBLE_FROM[kind]],
+ sizeRamp: (floor, fullAt, full = 1) =>
+ standards ? full : ["interpolate", ["linear"], ["zoom"], RAMP_FROM, floor, fullAt, full],
+ symbolSize: (...stops) =>
+ standards ? stops[stops.length - 1] : ["interpolate", ["linear"], ["zoom"], ...stops],
+ };
+}
diff --git a/style/sprites/icons/arc-0-M.svg b/style/sprites/icons/arc-0-M.svg
new file mode 100644
index 0000000..5cf2c4a
--- /dev/null
+++ b/style/sprites/icons/arc-0-M.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-0-casing.svg b/style/sprites/icons/arc-0-casing.svg
new file mode 100644
index 0000000..136ff53
--- /dev/null
+++ b/style/sprites/icons/arc-0-casing.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-0-obscured.svg b/style/sprites/icons/arc-0-obscured.svg
new file mode 100644
index 0000000..fe4593e
--- /dev/null
+++ b/style/sprites/icons/arc-0-obscured.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-1-M.svg b/style/sprites/icons/arc-1-M.svg
new file mode 100644
index 0000000..22f42f1
--- /dev/null
+++ b/style/sprites/icons/arc-1-M.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-1-casing.svg b/style/sprites/icons/arc-1-casing.svg
new file mode 100644
index 0000000..255a2e3
--- /dev/null
+++ b/style/sprites/icons/arc-1-casing.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-1-obscured.svg b/style/sprites/icons/arc-1-obscured.svg
new file mode 100644
index 0000000..c1d512a
--- /dev/null
+++ b/style/sprites/icons/arc-1-obscured.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-2-M.svg b/style/sprites/icons/arc-2-M.svg
new file mode 100644
index 0000000..bd38109
--- /dev/null
+++ b/style/sprites/icons/arc-2-M.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-2-casing.svg b/style/sprites/icons/arc-2-casing.svg
new file mode 100644
index 0000000..bb06750
--- /dev/null
+++ b/style/sprites/icons/arc-2-casing.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-2-obscured.svg b/style/sprites/icons/arc-2-obscured.svg
new file mode 100644
index 0000000..b9c17fd
--- /dev/null
+++ b/style/sprites/icons/arc-2-obscured.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-3-M.svg b/style/sprites/icons/arc-3-M.svg
new file mode 100644
index 0000000..7ca00fc
--- /dev/null
+++ b/style/sprites/icons/arc-3-M.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-3-casing.svg b/style/sprites/icons/arc-3-casing.svg
new file mode 100644
index 0000000..53b8402
--- /dev/null
+++ b/style/sprites/icons/arc-3-casing.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-3-obscured.svg b/style/sprites/icons/arc-3-obscured.svg
new file mode 100644
index 0000000..a5947bc
--- /dev/null
+++ b/style/sprites/icons/arc-3-obscured.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-4-M.svg b/style/sprites/icons/arc-4-M.svg
new file mode 100644
index 0000000..43db9e2
--- /dev/null
+++ b/style/sprites/icons/arc-4-M.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-4-casing.svg b/style/sprites/icons/arc-4-casing.svg
new file mode 100644
index 0000000..0a5c6a2
--- /dev/null
+++ b/style/sprites/icons/arc-4-casing.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/arc-4-obscured.svg b/style/sprites/icons/arc-4-obscured.svg
new file mode 100644
index 0000000..0662277
--- /dev/null
+++ b/style/sprites/icons/arc-4-obscured.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style/sprites/icons/sector-leg.svg b/style/sprites/icons/sector-leg.svg
new file mode 100644
index 0000000..85282b6
--- /dev/null
+++ b/style/sprites/icons/sector-leg.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/viewer/src/main.js b/viewer/src/main.js
index f07cd44..238afc3 100644
--- a/viewer/src/main.js
+++ b/viewer/src/main.js
@@ -7,10 +7,12 @@ import { style } from "@openwaters/seamap";
// ?tiles= points the chart at another TileJSON — the dev server's local
// worker, say — instead of the published tiles. ?hillshade turns on the
// bathymetric hillshading the style ships off; ?shading=relief swaps the
-// vector depth bands for the raster DEM color-relief.
+// vector depth bands for the raster DEM color-relief; ?standards draws the
+// symbology strictly per S-52 instead of the chart's own portrayal.
const params = new URLSearchParams(location.search);
const tiles = params.get("tiles") || undefined;
const depthHillshade = params.has("hillshade");
+const standards = params.has("standards");
// anything but the known raster opt-in falls back to the style's default
const shading = params.get("shading") === "relief" ? "relief" : undefined;
@@ -25,7 +27,7 @@ const map = new maplibregl.Map({
center: [10.2351, 56.16858],
zoom: 13.4,
container: "map",
- style: await style({ tiles, depthHillshade, shading }),
+ style: await style({ tiles, depthHillshade, shading, standards }),
dragRotate: false,
touchPitch: false,
maxPitch: 0,