Drawing S57 symbols onto Leaflet or Openlayers map

Viewed 1229

I have been working on getting leaflet (or openlayers) to be able to display S-57 data (also known as ENC) symbols (Known as S-52) onto the map. I know that leaflet can place svg data onto a map, but I haven't seen these symbols being used as anything else besides two formats. (An example of what these symbols look like can be found here: https://github.com/OpenCPN/OpenCPN/blob/master/data/s57data/rastersymbols-day.png)

The first example is one used from the following link: https://raw.githubusercontent.com/OpenCPN/OpenCPN/master/data/s57data/chartsymbols.xml

<symbol RCID="1268">
    <name>BOYBAR01</name>
    <description>barrel buoy, paper-chart</description>
    <bitmap width="19" height="14">
        <distance min="0" max="0" />
        <pivot x="11" y="12" />
        <origin x="0" y="0" />
        <graphics-location x="926" y="10" />
    </bitmap>
    <color-ref>CCHBLK</color-ref>
    <vector width="615" height="440">
        <distance min="0" max="0" />
        <pivot x="1500" y="1500" />
        <origin x="1145" y="1110" />
        <HPGL>SPC;PU1500,1500;SW2;CI50;PU1200,1500;PD1200,1345;PD1210,1280;PD1245,1210;PU1290,1165;PD1335,1140;PD1390,1120;PD1435,1110;PD1480,1110;PD1530,1120;PD1590,1155;PD1635,1185;PD1660,1215;PD1685,1265;PD1700,1310;PD1705,1345;PD1705,1500;PU1275,1175;PD1310,1230;PD1345,1280;PD1365,1335;PD1380,1410;PD1390,1455;PD1390,1500;PU1545,1500;PD1760,1500;PU1245,1210;PD1290,1165;PU1445,1500;PD1145,1500;</HPGL>
    </vector>
    <definition>V</definition>
</symbol>

In the case above the path is coded as HPGL. I have tried converting just the HPGL into an SVG, but the converter either fails or prints it upside down.

The other format is taken from S-52x.stylx (This is from ArcGIS, which if you have the ENC viewer extension install you can find here: C:\Users\<yourUsername>\ArcGIS\Runtime\Data\ENC\hydrography and you can load it with DB Browser for SQLite under the table SymbolInfo)

{
  "type": "CIMPointSymbol",
  "symbolLayers": [
    {
      "type": "CIMVectorMarker",
      "enable": true,
      "anchorPoint": {
        "x": 1.346457,
        "y": -4.818898,
        "z": 0.000000
      },
      "anchorPointUnits": "Absolute",
      "dominantSizeAxis3D": "Z",
      "offsetX": 0.000000,
      "rotateClockwise": true,
      "size": 12.472441,
      "billboardMode3D": "None",
      "frame": {
        "xmin": -10.062992,
        "ymin": -1.417323,
        "xmax": 7.370079,
        "ymax": 11.055118
      },
      "markerGraphics": [
        {
          "type": "CIMMarkerGraphic",
          "geometry": {
            "paths": [
              [
                [
                  1.417323,
                  0.000000
                ],
                [
                  1.403167,
                  -0.199814
                ],
                [
                  1.360984,
                  -0.395636
                ],
                [
                  1.291614,
                  -0.583555
                ],
                [
                  1.196445,
                  -0.759818
                ],
                [
                  1.077376,
                  -0.920904
                ],
                [
                  0.936787,
                  -1.063595
                ],
                [
                  0.777486,
                  -1.185040
                ],
                [
                  0.602654,
                  -1.282814
                ],
                [
                  0.415785,
                  -1.354964
                ],
                [
                  0.220610,
                  -1.400048
                ],
                [
                  0.021028,
                  -1.417167
                ],
                [
                  -0.178973,
                  -1.405977
                ],
                [
                  -0.375400,
                  -1.366704
                ],
                [
                  -0.564328,
                  -1.300130
                ],
                [
                  -0.741984,
                  -1.207586
                ],
                [
                  -0.904818,
                  -1.090921
                ],
                [
                  -1.049579,
                  -0.952464
                ],
                [
                  -1.173374,
                  -0.794982
                ],
                [
                  -1.273731,
                  -0.621621
                ],
                [
                  -1.348646,
                  -0.435842
                ],
                [
                  -1.396621,
                  -0.241358
                ],
                [
                  -1.416699,
                  -0.042052
                ],
                [
                  -1.408478,
                  0.158094
                ],
                [
                  -1.372123,
                  0.355082
                ],
                [
                  -1.308360,
                  0.544976
                ],
                [
                  -1.218462,
                  0.723986
                ],
                [
                  -1.104225,
                  0.888533
                ],
                [
                  -0.967932,
                  1.035332
                ],
                [
                  -0.812304,
                  1.161450
                ],
                [
                  -0.640450,
                  1.264369
                ],
                [
                  -0.455803,
                  1.342031
                ],
                [
                  -0.262052,
                  1.392886
                ],
                [
                  -0.063066,
                  1.415919
                ],
                [
                  0.137179,
                  1.410669
                ],
                [
                  0.334685,
                  1.377240
                ],
                [
                  0.525505,
                  1.316301
                ],
                [
                  0.705828,
                  1.229069
                ],
                [
                  0.872052,
                  1.117286
                ],
                [
                  1.020857,
                  0.983186
                ],
                [
                  1.149271,
                  0.829446
                ],
                [
                  1.254727,
                  0.659138
                ],
                [
                  1.335121,
                  0.475664
                ],
                [
                  1.388845,
                  0.282689
                ],
                [
                  1.417323,
                  0.000000
                ]
              ],
              [
                [
                  -8.503937,
                  0.000000
                ],
                [
                  -8.503937,
                  4.393701
                ],
                [
                  -8.220472,
                  6.236220
                ],
                [
                  -7.228346,
                  8.220472
                ]
              ],
              [
                [
                  -5.952756,
                  9.496063
                ],
                [
                  -4.677165,
                  10.204724
                ],
                [
                  -3.118110,
                  10.771654
                ],
                [
                  -1.842520,
                  11.055118
                ],
                [
                  -0.566929,
                  11.055118
                ],
                [
                  0.850394,
                  10.771654
                ],
                [
                  2.551181,
                  9.779528
                ],
                [
                  3.826772,
                  8.929134
                ],
                [
                  4.535433,
                  8.078740
                ],
                [
                  5.244094,
                  6.661417
                ],
                [
                  5.669291,
                  5.385827
                ],
                [
                  5.811024,
                  4.393701
                ],
                [
                  5.811024,
                  0.000000
                ]
              ],
              [
                [
                  -6.377953,
                  9.212598
                ],
                [
                  -5.385827,
                  7.653543
                ],
                [
                  -4.393701,
                  6.236220
                ],
                [
                  -3.826772,
                  4.677165
                ],
                [
                  -3.401575,
                  2.551181
                ],
                [
                  -3.118110,
                  1.275591
                ],
                [
                  -3.118110,
                  0.000000
                ]
              ],
              [
                [
                  1.275591,
                  0.000000
                ],
                [
                  7.370079,
                  0.000000
                ]
              ],
              [
                [
                  -7.228346,
                  8.220472
                ],
                [
                  -5.952756,
                  9.496063
                ]
              ],
              [
                [
                  -1.559055,
                  0.000000
                ],
                [
                  -10.062992,
                  0.000000
                ]
              ]
            ]
          },
          "symbol": {
            "type": "CIMLineSymbol",
            "symbolLayers": [
              {
                "type": "CIMSolidStroke",
                "enable": true,
                "capStyle": "Round",
                "joinStyle": "Bevel",
                "lineStyle3D": "Tube",
                "miterLimit": 10,
                "width": 2,
                "color": [
                  0,
                  0,
                  0,
                  255
                ]
              }
            ]
          }
        }
      ],
      "respectFrame": true
    }
  ],
  "haloSize": 1,
  "scaleX": 1,
  "angleAlignment": "Display"
}

Insight onto how I can get these to either convert them into svg's that I can then load into leaflet, or be able to draw them from the raw data.

Edit 1: If you want to see the docs on how these symbols are drawn view https://iho.int/uploads/user/pubs/standards/s-52/S-52%20PresLib%20Ed%204.0.2%20Part%20I%20Addendum.pdf That contains the standard for each symbol, to view the one I reference search for "BOYBAR01"

Edit 2: Another option could be to convert the data into a local tileset so I can load that into leaflet. I've tried a couple of tests to find software to convert the data, but I have found nothing so far (that works at least).

Edit 3: OpenCPN seems like a solution that could work, however I'm unsure how to install it, or how it works after it is installed. If this route ends up working I'll end up using within my nodejs application.

Edit 4: ESRI has a beta for their CIM data plotting found here: https://www.esri.com/arcgis-blog/products/js-api-arcgis/mapping/create-points-lines-and-polygons-using-cimsymbols/ As far as I have tested, this only works with their mapping platform. Since this is in beta there maybe symbols that don't work, but I haven't seen any that don't work from their list of styles from their database yet. The only problem is how to get this data from esri to work with leaflet. I know there is a esri-leaflet project, but this is just for their mapping platform for now. So another option would be to use this CIM specification to draw the data using some leaflet polylineDecorator or something along those lines. The only problem is, how do I even do that?

Edit 5: Looking into the way that openlayers works I have added the option for openlayers. From looking into the way that openlayers styles work, it would give me a lot more control over how the symbol is displayed, however some things like Stroke Pattern, which uses a canvas, would work well for a lot of things, however I am unsure how to rotated some of the data that needs to face inwards towards the polygons. Point symbols would be simple enough. Any advice on that would be helpful.

2 Answers

Based on Mike Nunn's solution (https://gis.stackexchange.com/questions/306976/add-image-along-the-linestring/306979), the following code may solve your problem.

// Symbol

var img = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAADsQAAA7EB9YPtSQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHzSURBVFiF7ZY9SBxBFMd/s7uHGIKoB4paJJIyJNjETjkRG7FIE5MlEEQJRAiEC1xhIG0QRIQIdp6gnQgipEqRSkgXDGiXwAUEOZDoJSx+3Mez8LKep3s6c3oWuX83O/P++5v3dt4sVFXVDUuZBk7yUpqoB8Bjn1dMG3lZJkEuEQlT549rqeE9rlQM4CHt2AWhFop7tJpYmQGMMaey5Pxxlhzf+FEZgEG65Tk9ckimwERxQJqndGuXQfvDWSBW8iUb/GKcxUv7OroACZIoFA42bYQBSOGxiwfAJttafsbHEE6yscJXllit3DEsloNtHFs2gCDYNwkA4JRhcyUA15IBl8ilz/S1ZOA+dxhn+EIIhSqZAZeIzJfoHYEAW/ymjTBxohdCBGXgA0PSzyMg2CIQIMkOGbKEcFggJqMMBLqEzulncd5Ka75RbfNHH2CJVWXlpwWhneYzayS/s2KAJ3RJCBsbC0FIkNQHOJ48bm4KRQuNvOPZqSz8AygsgUtEBuj0xwrFR1YCu2TJ9jlPTF4woYbok146/OdpsqTwaOA2NhZ7HJImQx23/DUpPF4zo2aJyghTZgDFihOV8+pdLJ0b0egCecNjecBdQjh+mf6yxzoJZvik5Vn2bZhDWOMnUywbeWn/DxTqC98BmONzWRup6v/WETwrgqYBFtclAAAAAElFTkSuQmCC"

// Helpers

function splitLineString(geometry, minSegmentLength, options) {

  function calculatePointsDistance(coord1, coord2) {
    var dx = coord1[0] - coord2[0];
    var dy = coord1[1] - coord2[1];
    return Math.sqrt(dx * dx + dy * dy);
  }

  function calculateSplitPointCoords(startNode, nextNode, distanceBetweenNodes, distanceToSplitPoint) {
    var d = distanceToSplitPoint / distanceBetweenNodes;
    var x = nextNode[0] + (startNode[0] - nextNode[0]) * d;
    var y = nextNode[1] + (startNode[1] - nextNode[1]) * d;
    return [x, y];
  }

  function calculateAngle(startNode, nextNode, alwaysUp) {
    var x = (startNode[0] - nextNode[0]);
    var y = (startNode[1] - nextNode[1]);
    var angle = alwaysUp ? Math.atan(x / y) : Math.atan2(x, y);
    return angle - Math.PI / 2 * orientation
  }

  var splitPoints = [];
  var coords = geometry.getCoordinates();
  var coordIndex = 0;
  var startPoint = coords[coordIndex];
  var nextPoint = coords[coordIndex + 1];
  var angle = calculateAngle(startPoint, nextPoint, options.alwaysUp);
  var n = Math.ceil(geometry.getLength() / minSegmentLength);
  var segmentLength = geometry.getLength() / n;
  var currentSegmentLength = options.midPoints ? segmentLength / 2 : segmentLength;

  for (var i = 0; i <= n; i++) {

    var distanceBetweenPoints = calculatePointsDistance(startPoint, nextPoint);
    currentSegmentLength += distanceBetweenPoints;

    if (currentSegmentLength < segmentLength) {
      coordIndex++;
      if (coordIndex < coords.length - 1) {
        startPoint = coords[coordIndex];
        nextPoint = coords[coordIndex + 1];
        angle = calculateAngle(startPoint, nextPoint, options.alwaysUp);
        i--;
        continue;
      } else {
        if (!options.midPoints) {
          var splitPointCoords = nextPoint;
          if (!options.extent || ol.extent.containsCoordinate(options.extent, splitPointCoords)) {
            splitPointCoords.push(angle);
            splitPoints.push(splitPointCoords);
          }
        }
        break;
      }
    } else {
      var distanceToSplitPoint = currentSegmentLength - segmentLength;
      var splitPointCoords = calculateSplitPointCoords(startPoint, nextPoint, distanceBetweenPoints, distanceToSplitPoint);
      startPoint = splitPointCoords.slice();
      if (!options.extent || ol.extent.containsCoordinate(options.extent, splitPointCoords)) {
        splitPointCoords.push(angle);
        splitPoints.push(splitPointCoords);
      }
      currentSegmentLength = 0;
    }
  }

  return splitPoints;
}

function calculateOrientation(coords) {
  var area = 0;
  for (var i = 0; i < coords.length; i++) {
    const addX = coords[i][0];
    const addY = coords[i === coords.length - 1 ? 0 : i + 1][1];
    const subX = coords[i === coords.length - 1 ? 0 : i + 1][0];
    const subY = coords[i][1];
    area += (addX * addY * 0.5) - (subX * subY * 0.5);
  }
  return Math.sign(area);
}

// StyleFunction

var styleFunction = function(feature, resolution) {

  var styles = [
    new ol.style.Style({
      stroke: new ol.style.Stroke({
        color: "darkmagenta",
        width: 2,
        lineDash: [4]
      })
    })
  ];

  var size = 64;
  var mapSize = map.getSize();
  var extent = map.getView().calculateExtent([mapSize[0] + (size * 2), mapSize[1] + (size * 2)]);

  var splitPoints = splitLineString(feature.getGeometry(), size * 2 * resolution, {
    alwaysUp: false,
    midPoints: true,
    extent: extent
  });

  splitPoints.forEach(function(point, index) {
    styles.push(new ol.style.Style({
      geometry: new ol.geom.Point([point[0], point[1]]),
      image: new ol.style.Icon({
        src: img,
        scale: 0.8,
        rotation: point[2]
      })
    }));

  });

  return styles;
}

// Layer

var raster = new ol.layer.Tile({
  source: new ol.source.OSM()
});
var vectorSource = new ol.source.Vector();
var vector = new ol.layer.Vector({
  source: vectorSource,
  style: styleFunction
});

// Map

var map = new ol.Map({
  layers: [raster, vector],
  target: "map",
  view: new ol.View({
    center: ol.proj.fromLonLat([7.5, 54.0]),
    zoom: 8
  })
});

// Interaction

var draw = new ol.interaction.Draw({
  source: vectorSource,
  type: "LineString"
});

var orientation;

draw.on("drawend", function(event) {
  var featureGeom = event.feature.getGeometry();
  if (featureGeom) {
    var featureCoords = featureGeom.getCoordinates();
    if (featureCoords.length > 0) {
      featureCoords.push(featureCoords[0])
      featureGeom.setCoordinates(featureCoords);
      orientation = calculateOrientation(featureCoords)
      // map.removeInteraction(draw);
    }
  }
})

map.addInteraction(draw);
html,
body,
.map {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
<link href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.14.1/css/ol.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.14.1/build/ol.js"></script>
<div id="map" class="map" tabindex="0"></div>

If you are looking for graphic files, this link may be of interest: OpenSeaMap

Related