Nextjs + Openlayers integration

Nextjs + Openlayers integration
Photo by Ruthie / Unsplash

Introduction

Nextjs is production-grade framework working with react, it has neat features with mixing server side and client side rendering and has integrated easy internal API creation.

Open layers is library used for displaying dynamic maps on web page, it can handle map tiles, vector data or even Geo JSON. It is open source under 2-clause BSD License (FreeBSD)

Installation and cleanup

We will use npm, but you can use package manager of your choice.

Create nextjs app (i use Tailwindcss, but you can style it yourself):

npx create-next-app@latest
  1. Install dependencie:
npm i ol
  1. Clean layout and page, and add small styling:
// Layout.tsx

import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Openlayers showcasing",
  description: "Small nextjs app showcasing open layers library functionality",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`h-full antialiased`}>
        <main className="h-view-container"
        {children}
      </body>
    </html>
  );
}

// Page.tsx

export default function Page() {
  return (
    <div className="max-w-container">
      <div className="h-full w-full">
      </div>
    </div>
  );
}
/* globals.css */

@layer base {
  .h-view-container {
    @apply min-h-screen w-full flex flex-col;
  }

  .max-w-container {
    @apply mx-auto max-w-screen-xl w-full container sm:px-6 lg:px-8 py-24;
  }
}

Openlayers component

Now that we installed and cleaned up, we need to create open layers client component which will hold our map.

If you tried yourself adding open layers to react you realized one big shortcoming, react doesn't have window API support, and many others which openlayers needs to run.

Solution that is pretty simple, we only need to run our code only when we can have instance of window, or more exactly only when web is fully loaded.

That happens when we do useEffect() hook which will run with first render.

// src/components/OpenLayersComponent.tsx

'use client';
import TileLayer from 'ol/layer/Tile';
import VectorLayer from 'ol/layer/Vector';
import Map from 'ol/Map';
import { transform } from 'ol/proj';
import { OSM } from 'ol/source';
import VectorSource from 'ol/source/Vector';
import View from 'ol/View';
import React, { useEffect } from 'react'

export default function OpenLayersComponet() {
  const mapStateRef = React.useRef<any>(null);
  const sourceRef = React.useRef<any>(null);

  useEffect(() => {
    if (mapStateRef.current === null) {
      const raster = new TileLayer({
        source: new OSM(),
      });

      sourceRef.current = new VectorSource();

      const vector = new VectorLayer({
        source: sourceRef.current,
      });

      mapStateRef.current = new Map({
        layers: [raster, vector],
        target: "map",
        view: new View({
          center: transform(
            [15.243629268374999, 49.272716766783859],
            "EPSG:4326",
            "EPSG:3857",
          ),
          zoom: 4,
        }),
      });
    }
  }, []);

  return (
    <div className="py-8 flex flex-col gap-4">
      <div id="map" className="h-96 w-full" />
    </div>
  )
}```

- Component must be client component `"use client"`
- One thing we need to do is prevent some corner cases like multiple renders with useEffect, we solve it by having map object in mapStateRef variable

Now, we need to add map to the Page, so we will be able to see what we have.

```tsx
// Page.tsx

import OpenLayersComponent from "@/components/OpenLayersComponent"

export default function Page() {
  return (
    <div className="max-w-container">
      <div className="h-full w-full">
	    <OpenLayersComponent />
      </div>
    </div>
  );
}

Now we can run app with:

npm run dev

and can see simple map somewhere above Europe.

Configuration

We can further configure openlayers map, like change where will map first spawn, how big or small is zoom on map or even load some elements in map.

To change center or zoom we need to edit map:

  mapStateRef.current = new Map({
	layers: [raster, vector],
	target: "map",
	view: new View({
	  center: transform(
		[15.243629268374999, 49.272716766783859],
		"EPSG:4326",
		"EPSG:3857",
	  ),
	  zoom: 4, // Here we can set zoom value
	}),
  });
  • View we can either use EPSG:4326 or EPSG:3857, sometimes we need to transform that format through transform function like here.
  • Zoom is easy as it sounds it means at which height you will view from, try decreasing or simply changing value to see what happens.

More complex functionalities include loading data or listening to events like click or select.

Now i will add some features to the map to show how open layers handles elements we want to show.


  useEffect(() => {
      if (mapStateRef.current === null) {
        
        ...
        
        sourceRef.current = new VectorSource();
  
        sourceRef.current.addFeature(
          new Feature({
            geometry: new Polygon([
              [
                [15.443629268374999, 49.272716766783859],
                [15.243629268374999, 49.272716766783859],
                [15.243629268374999, 56.272716766783859],
                [17.243629268374999, 49.272716766783859],
              ],
            ].map(ring => ring.map(coord => fromLonLat(coord)))),
          }),
        );
  
        const vector = new VectorLayer({
          source: sourceRef.current,
        });

        ...
      }
  })
  • Open Layers natively supports EPSG:3857, but i am using EPSG:4326 and showing how i need to transform each coordinate to right format.
  • We can either bind each feature as individual or list of features to the sourceRef.

If we want to add some events we can add select which will work as click like this:

import { click } from "ol/events/condition";
import Select from "ol/interaction/Select.js";

export default function DrawingMap() {
  ...
  
  const sourceRef = React.useRef<any>(null);
  const mapSelectedFeatureRef = React.useRef<any>(null);
  
  ...

  useEffect(() => {
    mapSelectedFeatureRef = new Select({
      condition: click
    })
    
    mapStateRef.current.addInteraction(mapSelectedFeatureRef.current);
    mapSelectedFeature.current.on("select", (e) => {
      e.preventDefault();

      // Do something on click (select event in this case)
    })
  }, [])

  ...

Geo JSON data

Maybe most important capability of open layers library is loading of Geo JSON data, we can either save our data as JSON document send it to server to some data storage or load JSON document and work with it's data.

Here is some data sample i will use:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [15.243629268374999, 49.27271676678386]
      },
      "properties": {
        "OBJECTID": 1
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [15.076066499217609, 49.3199891113441]
      },
      "properties": {
        "OBJECTID": 2
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [15.57072814608417, 49.41256216073112]
      },
      "properties": {
        "OBJECTID": 3
      }
    },
  ]
}
  • Three geometry objects of point type, with EPSG:4326 coordinates and some ID properties.
  • Geo JSON document starts with type like FeatureCollection and contains array of features.

Using data above we can through open layers library:

import data from "assets/geojson-data.json";

...

  useEffect(() => {
    ...
    
    if (mastStateRef.current === null) {
    
      sourceRef.current = new VectorSource({
        features: new GeoJSON().readFeatures(data, {
          dataProjection: 'EPSG:4326',
          featureProjection: 'EPSG:3857',
        }),
      });
      
      const vector = new VectorLayer({
        source: sourceRef.current,
      });
    }
    
    ...
  
  }, [])
  
  ...
  • Data was saved in JSON file to newly created directory assets.
  • We read Geo JSON data and feature will converted to right coordinates format through second parameter.

Conclusion

Here is some simple tutorial showing how to use open layers library with Nextjs to create maps.

I hope you enjoyed and found this post useful, if you need more functionalities of open layers look at their examples it has most of their capabilities written.


Disclaimer:
This blog post is based on my personal experience using Next.js, OpenLayers, GeoJSON, and TailwindCSS. All code snippets and instructions are provided "as is" and should be tested in a secure environment. I am not liable for any issues arising from their use. Please refer to the official documentation for each tool for the most up-to-date information. Open-source tools referenced in this post are used in accordance with their respective licenses.

This approach helps protect you legally and establishes transparency with your readers.