Implementation of OpenLayers library in web-based mapping application

·

7 min read

Our team was leading a web-based mapping application. We were developing a platform that displayed interactive maps and photos taken from drones for working with various geo-projects.

Main tasks were:

  1. Displaying map and photos from drone

  2. Adding markers

  3. Making measurements

  4. Drawing/modifying geometric figures

  5. Processing clicks on different objects

  6. Displaying 3D objects

We were choosing between three libraries: Leaflet, OpenLayers, Mapbox GL JS. The main criteria was an ability to work with geospatial data such as vectors, rasters on different layers and 3D models support. Nice bonus would be having a developer-friendly API documentation, tutorials, and examples to help developers get started quickly. So the choice fell on OpenLayers as more powerful and feature-rich than others library.

At the beginning we started with writing proof of concept by displaying a map.

First step was setting up the positioning:

const view = new View({
    center: [220000, 360000],
    extent: [13.9458, 55.1943, 24.1586, 48.5861],
    maxResolution,
    maxZoom: 25,
    minResolution,
    minZoom: 10,
    projection,
    resolution,
    zoom: 14,
    centerFromLatLong,
});

Here we described the properties of our future map in detail for a more accurate display.

Next step was to register a raster background layer with the map itself:

const rasterLayer = new TileLayer({
    source: new OSM()
})

Then we put everything together to get our working map:

const map = new Map({
    view,
    layers: [rasterLayer]
})

After that we added vector layers for displaying our figures, markers on a map, making drawings and modifications:

// Marker
const markerFeature = new Feature({
    geometry: new Point([0, 0]),
    name: 'Country point',
});
// Displaying markers
const markersVectorSource = new VectorSource({
    features: [markerFeature],
    wrapX: false,
})
const markersVectorLayer = new VectorLayer({
    source: markersVectorSource,
});
// Figure
const figureFeature = new Feature({
    geometry: new Polygon([
        [
            [-5e6, 6e6],
            [-5e6, 8e6],
            [-3e6, 8e6],
            [-3e6, 6e6],
            [-5e6, 6e6],
        ],
    ]),
}),
// Displaying figures
const figuresVectorSource = new VectorSource({
    features: [figureFeature],
    wrapX: false,
})
const figuresVectorLayer = new VectorLayer({
    source: figuresVectorSource,
});
// Creating layer for drawing/modifying new markers and figures
const drawModifyVectorSource = new VectorSource({
    features,
    wrapX: false,
})
const drawModifyVectorLayer = new VectorLayer({
    source: drawModifyVectorSource,
});

Also we needed to add some styles to our markers and figures:

// Style for markers
const markerStyle = new Style({
    image: new Icon({
        anchor: [0.5, 46],
        anchorXUnits: 'fraction',
        anchorYUnits: 'pixels',
        scale: 0.05,
        src: 'https://www.pngall.com/wp-content/uploads/2017/05/Map-Marker-PNG-File.png',
  }),
})

// Set style for markers
markerFeature.setStyle(markerStyle);

// Style for figures
const figureStyle = new Style({
    stroke: new Stroke({
        color: "#ff0000",
        width: 2,
    }),
    fill: new Fill({
        color: "#ff000066",
    }),
})

// Set style for figures
figureFeature.setStyle(figureStyle);

Then we started drawing new markers and figures:

const markerDraw = new Draw({
    type: 'Point',
    source: drawModifyVectorSource,
});

map.addInteraction(markerDraw)

const figureDraw = new Draw({
    type: 'Polygon',
    source: drawModifyVectorSource,
});

// These are very useful methods for implementing any business needs 
figureDraw.on("drawstart", () => {})
figureDraw.on("drawend", () => {})

map.addInteraction(figureDraw)

We could easily modify them as well:

const figureModify = new Modify({
    source: drawModifyVectorSource
})

figureModify.on("modifystart", () => {})
figureModify.on("modifyend", () => {})

map.addInteraction(figureModify)

Here is our main map with markers:

And with figures:

We got a bit stuck when our client wanted to implement drawings of not only polygons and circles, but also squares and rectangles. However a solution was found in documentation.

In a nutshell to draw squares/rectangles we used type: 'Circle' with a geometryFunction that creates a box-shaped polygon (or for square — 4-sided regular polygon) instead of a circle:

import Draw, { createBox, createRegularPolygon } from "ol/interaction/Draw";

// Drawing rectangle
const rectangleSource = new VectorSource({wrapX: false});

const rectangleDraw = new Draw({
    type: 'Circle',
    source: rectangleSource,
    geometryFunction: createBox(),
});

map.addInteraction(rectangleDraw);

// Drawing square
const squareSource = new VectorSource({wrapX: false});

const squareDraw = new Draw({
    type: 'Circle',
    source: squareSource,
    geometryFunction: createRegularPolygon(4);,
});

map.addInteraction(squareDraw);

All good! We could draw all the needed figures:

Next problem was related to modifications of squares and rectangles:

The figure behaved as polygon and it was absolutely not what we wanted. After reading a documentation and googling relevant articles we came up with the next solution:

const rectangleModify = new Modify({
    source: rectangleSource,
    style: (feature) => {
        source.getFeatures().forEach((modifyFeature) => {
            const modifyGeometry = modifyFeature.get('modifyGeometry');
            if (modifyGeometry) {
                const point = feature.getGeometry().getCoordinates();
                let modifyPoint = modifyGeometry.point;
                if (!modifyPoint) {
                    // save the initial geometry and vertex position
                    modifyPoint = point;
                    modifyGeometry.point = modifyPoint;
                    modifyGeometry.geometry0 = modifyGeometry.geometry;
                    // get anchor and minimum radius of vertices
                    const result = calculateCenter(modifyGeometry.geometry0);
                    modifyGeometry.center = result.center;
                    modifyGeometry.deltas = result.deltas;
                }

                const center = modifyGeometry.center;
                const minRadius = modifyGeometry.minRadius;

                const dx0 = modifyPoint[0] - center[0];
                const dy0 = modifyPoint[1] - center[1];

                const dx = point[0] - center[0];
                const dy = point[1] - center[1];
                const geometry = modifyGeometry.geometry0.clone();
                geometry.scale(dx / dx0, dy / dy0, center);
                modifyGeometry.geometry = geometry;
            }
        });
        return defaultModifyStyle(feature);
    }
});

map.addInteraction(rectangleModify);
addEventsToModInteraction();

function addEventsToModInteraction(){
    rectangleModify.on('modifystart', (event) => {
        event.features.forEach((feature) => {
            // geometry --> modifyGeometry
            feature.set(
                'modifyGeometry',
                {geometry: feature.getGeometry().clone()},
                true
            );
        });
    });
    rectangleModify.on('modifyend', (event) {
        event.features.forEach((feature) {
            const modifyGeometry = feature.get('modifyGeometry');
            // modifyGeometry --> geometry
            if (modifyGeometry) {
                feature.setGeometry(modifyGeometry.geometry);
                feature.unset('modifyGeometry', true);
            }
        });
    });
}

function calculateCenter(geometry) {
    let center, coordinates
    const type = geometry.getType();
    if (type === 'Polygon') {
        let x = 0;
        let y = 0;
        let i = 0;
        coordinates = geometry.getCoordinates()[0].slice(1);
        coordinates.forEach((coordinate) {
            x += coordinate[0];
            y += coordinate[1];
            i++;
        });
        center = [x / i, y / i];
    }

    const deltas = coordinates?.map((coordinate) => {
        const dx = coordinate[0] - center[0];
        const dy = coordinate[1] - center[1];
        return dx, dy;
    });

    return {
        center: center,
        deltas: deltas,
    };
}

Yes! Finally we fixed this, but the client was still not satisfied with the modification of these figures: since square and rectangle both are type of circle so the scaling occurred from the center, but the client wanted the movement to go from the diagonally opposite corner to the corner that was pulled.

So initially modification process went like this:

This time I had to think what calculations to apply to correctly position the scaling and this is what came out of it:

enum Corner {
  topLeft = 0,
  topRight = 1,
  bottomRight = 2,
  bottomLeft = 3,
}

const MapperToOppositeCorner = {
  [Corner.topLeft]: Corner.bottomRight,
  [Corner.topRight]: Corner.bottomLeft,
  [Corner.bottomRight]: Corner.topLeft,
  [Corner.bottomLeft]: Corner.topRight,
};

const rectangleModify = new Modify({
    source: rectangleSource,
    style: (feature) => {
        source.getFeatures().forEach((modifyFeature) => {
            const modifyGeometry = modifyFeature.get('modifyGeometry');
            if (modifyGeometry) {
                const point = feature.getGeometry().getCoordinates();
                let modifyPoint = modifyGeometry.point;
                if (!modifyPoint) {
                    modifyPoint = point;
                    modifyGeometry.point = modifyPoint;
                    modifyGeometry.geometry0 = modifyGeometry.geometry;

                    const result = calculateCenter(
                        modifyGeometry.geometry0,
                        modifyGeometry.point
                    );
                    modifyGeometry.center = result.center;
                    modifyGeometry.deltas = result.deltas;
                }

                const center = modifyGeometry.center;

                const dx0 = modifyPoint[0] - center[0];
                const dy0 = modifyPoint[1] - center[1];

                const dx = point[0] - center[0];
                const dy = point[1] - center[1];

                const x = dx0 === 0 ? 1 : dx / dx0;
                const y = dy0 === 0 ? 1 : dy / dy0;

                const geometry = modifyGeometry.geometry0.clone();
                geometry.scale(x, y, center);
                modifyGeometry.geometry = geometry;
            }
        });
        return defaultModifyStyle(feature);
    }
});

function calculateCenter(
  geometry,
  pointCoordinates
) {
  let center = [],
      coordinates = [];
  const type = geometry.getType();
  if (type === "Polygon") {
    coordinates = geometry.getCoordinates()[0].slice(1);
    const topLeft =
      Math.abs(pointCoordinates[0] - coordinates[0][0]) +
      Math.abs(pointCoordinates[1] - coordinates[0][1]);

    const topRight =
      Math.abs(pointCoordinates[0] - coordinates[1][0]) +
      Math.abs(pointCoordinates[1] - coordinates[1][1]);

    const bottomRight =
      Math.abs(pointCoordinates[0] - coordinates[2][0]) +
      Math.abs(pointCoordinates[1] - coordinates[2][1]);

    const bottomLeft =
      Math.abs(pointCoordinates[0] - coordinates[3][0]) +
      Math.abs(pointCoordinates[1] - coordinates[3][1]);

    const options = [
      {
        label: Corner.topLeft,
        value: topLeft,
      },
      {
        label: Corner.topRight,
        value: topRight,
      },
      {
        label: Corner.bottomRight,
        value: bottomRight,
      },
      {
        label: Corner.bottomLeft,
        value: bottomLeft,
      },
    ];
    const corner =
      options.sort((a, b) => a.value - b.value)?.[0]?.label || Corner.topLeft;
    const oppositeCorner = MapperToOppositeCorner[corner];
    const coordinateX = coordinates[oppositeCorner][0];
    const coordinateY = coordinates[oppositeCorner][1];
    center = [coordinateX, coordinateY];
  }

  const deltas = coordinates?.map((coordinate) => {
      const dx = coordinate[0] - center[0];
      const dy = coordinate[1] - center[1];
      return { dx, dy };
  });

  return {
    center,
    deltas,
  };
}

I improved calculateCenter function and it gave us the desired result:

In this article I wanted to show that you shouldn’t be afraid of the situation where documentation ends and full creativity begins. The advantage of using such flexible libraries is that you can always adapt it to the client and business. The main thing is to try different options and experiment, and then you may even get more than you expected.