Introduction
Last summer I started working for an Internet of Things startup, Blues Wireless, whose goal is to make IoT development easier by providing prepaid cellular Internet connectivity to any IoT device through the use of a Notecard, which transmits sensor data as JSON to a secure cloud, Notehub.
In a previous post, I showed how I used Next.js and React Leaflet to build an asset tracker map to display where a moving Notecard was (inside my car) in near real time. This exercise ended up coming in more handy than I expected when my parents' car was stolen out of their driveway over the Thanksgiving holiday and I'd stuck a Notecard in the backseat while visiting.
Although the Notecard was discovered shortly after the car was stolen and thrown out a window, for a short period of time we (and the police) were able to follow the car around town courtesy of my dashboard map, and this experience inspired me during a company-wide hackathon a few months later.
One thing that would have been very helpful during the period when the car was stolen was if the lines between points on the map could have been in some color besides the standard blue for easier identification of where the car was after it was taken. So for the hackathon I created a new dashboard with an "SOS mode" to not only render lines in red on the map after the SOS mode was enabled, but also increase the frequency of the Notecard taking location readings for better accuracy.
Today, I'll show you how to create a map with React Leaflet in Next.js with the ability to render different colored lines at the touch of button for asset tracking under normal or emergency conditions.
Set up a map component in Next.js app
Please note: This article will not go through setting up a brand new Next project or an in-depth explanation of fetching asset tracker data from a Blues Wireless Notecard, as I've covered this already in this post.
To see the finished code, you can view my GitHub repo here
Install map project dependencies
The first thing we'll do in this tutorial is add a map to a Next project. This is going to require a few new npm packages added to our project: leaflet, react-leaflet and leaflet-defaulticon-compatibility.
Run the following lines in a terminal.
$ npm install leaflet react-leaflet leaflet-defaulticon-compatibility
Note: You'll also need
react
andreact-dom
as peer dependencies if they're not already in your project as well.
TypeScript Note:
If you're using TypeScript in your project, you'll also want to want to install the follow dev dependency to avoid TypeScript errors:
$ npm install @types/leaflet --save-dev
After installing our new project dependencies we'll set up the component to use them.
Generate a Mapbox token for the map's display style and add it to the project
For the map display that the asset tracker will be on, I chose to use Mapbox styles. It has a variety of styles to choose from, and developers can create their own Mapbox API tokens to access these styles by signing up for a free Mapbox account.
After you've signed up and created a new API token, copy the token value. In the Next.js app's next.config.js
file at the root of the project, add the API token like so:
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
env: {
MAPBOX_ACCESS_TOKEN:
"[MAPBOX_TOKEN]",
},
};
From this file, Next can access the token when it needs to call the Mapbox API endpoint. Now we can get on with creating the <Map />
component in our project.
Create the <Map>
component
This is how the map looks displaying asset locations during normal circumstances.
As this is a React project, individual, reusable components are the name of the game, so create a new file named Map.tsx
and paste in the following code.
The actual code is available by clicking the file title below.
import {
MapContainer,
TileLayer,
Marker,
Popup,
CircleMarker,
Polyline,
} from "react-leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet-defaulticon-compatibility";
const Map = ({
coords,
lastPosition,
markers,
latestTimestamp,
sosCoords,
}: {
coords: number[][];
lastPosition: [number, number];
markers: [number, number][];
latestTimestamp: string;
sosCoords?: number[][];
}) => {
const geoJsonObj: any = coords;
const sosGeoJsonObj: any = sosCoords;
const mapMarkers = markers.map((latLng, i) => (
<CircleMarker key={i} center={latLng} fillColor="navy" />
));
return (
<>
<h2>Asset Tracker Map</h2>
<MapContainer
center={lastPosition}
zoom={14}
style={{ height: "100%", width: "100%" }}
>
<TileLayer
url={`https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/{z}/{x}/{y}@2x?access_token=${process.env.MAPBOX_ACCESS_TOKEN}`}
/>
<Marker position={lastPosition} draggable={true}>
<Popup>
Last recorded position:
<br />
{lastPosition[0].toFixed(6)}°,
{lastPosition[1].toFixed(6)}°
<br />
{latestTimestamp}
</Popup>
<Polyline pathOptions={{ color: "blue" }} positions={geoJsonObj} />
<Polyline pathOptions={{ color: "red" }} positions={sosGeoJsonObj} />
{mapMarkers}
</Marker>
</MapContainer>
</>
);
};
export default Map;
Let's briefly discuss what's happening here.
At the beginning of the file we import all the necessary React Leaflet components, the Leaflet CSS, and the Leaflet Default Icon Compatibility CSS and JS (this is recommended to get Leaflet's icons to work as expected).
Then we see the props the Map
component expects:
-
coords
- a list of arrays that have GPS latitude and longitude: this draws the connecting lines between map markers. -
lastPosition
- the most recent GPS latitude and longitude to display in the popup when the user clicks the icon on the map. -
markers
- another list of arrays that have GPS latitude and longitude to display the blue circles of previous places on the map where the tracker was in the past. -
latestTimestamp
- the most recent timestamp of GPS coordinates received (also for displaying in the popup on the map). -
sosCoords
- a separate list of GPS coordinates created when the "SOS mode" is enabled within the application: it will draw connecting lines between map markers in red.
Now turn your attention down to the JSX further down in the file.
The <MapContainer />
component is responsible for creating the Leaflet Map instance. Without this component, the map won't work, and we also define the map's center
coordinates, its default zoom level, and basic styling for the component.
The <TileLayer />
component is where our Mapbox style and new API token come in. Choose whatever style suits you, replace the streets-v11
portion of the string, and make sure the Mapbox token is present in the next.config.js
file, which I showed in the previous step. Without this component no map background for the coordinates will render - instead it will just be a blank canvas.
<Marker />
takes in the lastPosition
prop to display the icon on the map of the tracker's last recorded position, and it wraps the <Popup />
component, the <Polyline />
components, and the list of <CircleMarker />
components.
The <Popup />
component is a nice looking tooltip that can display info. My <Popup />
shows the tracker's last GPS coordinates and time it was reported when a user clicks it.
The <Polyline />
components are where the coords
list or sosCoords
list of GPS coordinates are passed to draw the connecting lines between map markers. The Polyline
object takes in positions
, which in this case is either geoJsonObj
or sosGeoJsonObj
, and the pathOptions
determines the color of the line rendered.
Note: At first, I tried to use the
GeoJSON
object to render the connecting lines, but there's no way to change line color midway through (as when SOS mode is enabled and lines need to go from blue to red), so multiple separatePolyline
objects was the best way to achieve this objective.
And last but not least, the <CircleMarker >/
components, which are displayed in this component's JSX as {mapMarkers}
.
Note: In order to get all the
markers
in the list to render as individual circles on the map, I had to create this little function to iterate over the list and generate all the circles, then inject that directly into the JSX.Trying to iterate over all the values inside the JSX wouldn't work.
Now our Map
component's been dissected, let's move on to populating the map with data and going from blue lines to red and back again.
Render the map in the Next.js app
The next step to getting this map working in our Next.js app is to import the Map
component with the option of ssr: false
.
The react-leaflet
library only works on the client side so Next's dynamic import()
support with no SSR feature must be employed to ensure the component doesn't try to render server-side.
Below is the code for the index.tsx
file that this component will be displayed within, condensed for clarity. If you'd like to see the full code in GitHub, click the file name.
// imports
import dynamic from "next/dynamic";
// other imports
type dataProps = {
// condensed for code brevity
};
export default function Home({ data }: { data: dataProps[] }) {
// needed to make the Leaflet map render correctly
const MapWithNoSSR = dynamic(() => import("../src/components/Map"), {
ssr: false,
});
// logic to enable/disable sos mode and transform data into items needed to pass to map
return (
<div>
{/* extra tracker app code */}
<main>
<h1>Notelink Tracker Dashboard</h1>
{/* other tracker components */}
<MapWithNoSSR
coords={latLngMarkerPositions}
lastPosition={lastPosition}
markers={latLngMarkerPositions}
latestTimestamp={latestTimestamp}
sosCoords={sosCoords}
/>
</div>
</main>
</div>
);
}
// code to fetch tracker data: getStaticProps
Don't worry too much about the props being passed to the component yet - we'll set those up shortly.
Now our <Map />
component is being dynamically imported with Next's server-side rendering disabled, and the component can be used just like any other in the app.
Fetch data for the map
In my previous asset tracking dashboard article I went into great detail about how to create your own asset tracker to generate real data for the app using Blues Wireless hardware and fetching that data to the app via the Notehub cloud's API.
If you'd like to follow along there to build your own tracker and route data to Notehub, please be my guest.
For this post, I'll jump ahead to the part where we're already pulling data into the app via a Next.js getStaticProps
API call. The JSON data from the Notehub cloud looks like this when it first arrives:
[
{
"uid": "d7cf7475-45ff-4d8c-b02a-64de9f15f538",
"device_uid": "dev:864475ABCDEF",
"file": "_track.qo",
"captured": "2021-11-05T16:10:52Z",
"received": "2021-11-05T16:11:29Z",
"body": {
"hdop": 3,
"seconds": 90,
"motion": 76,
"temperature": 20.1875,
"time": 1636123230,
"voltage": 4.2578125
},
"gps_location": {
"when": "2021-11-05T16:10:53Z",
"name": "Sandy Springs, GA",
"country": "US",
"timezone": "America/New_York",
"latitude": 33.913747500000014,
"longitude": -84.35008984375
}
},
{
"uid": "3b1ef772-44da-455a-a846-446a85a70050",
"device_uid": "dev:864475ABCDEF",
"file": "_track.qo",
"captured": "2021-11-05T22:22:18Z",
"received": "2021-11-05T22:23:12Z",
"body": {
"hdop": 2,
"motion": 203,
"seconds": 174,
"temperature": 22,
"time": 1636150938,
"voltage": 4.2265625
},
"gps_location": {
"when": "2021-11-05T22:22:19Z",
"name": "Doraville, GA",
"country": "US",
"timezone": "America/New_York",
"latitude": 33.901052500000006,
"longitude": -84.27090234375
}
},
{
"uid": "e94b0c68-b1d0-49cb-8361-d622d2d0081e",
"device_uid": "dev:864475ABCDEF",
"file": "_track.qo",
"captured": "2021-11-05T22:40:04Z",
"received": "2021-11-05T22:46:30Z",
"body": {
"hdop": 1,
"motion": 50,
"seconds": 41,
"temperature": 21.875,
"time": 1636152004,
"voltage": 4.1875
},
"gps_location": {
"when": "2021-11-05T22:40:05Z",
"name": "Peachtree Corners, GA",
"country": "US",
"timezone": "America/New_York",
"latitude": 33.9828325,
"longitude": -84.21591015624999
}
},
{
"uid": "1344517c-adcb-4133-af6a-b1132ffc86ea",
"device_uid": "dev:864475ABCDEF",
"file": "_track.qo",
"captured": "2021-11-06T03:04:07Z",
"received": "2021-11-06T03:10:51Z",
"body": {
"hdop": 1,
"motion": 126,
"seconds": 218,
"temperature": 12.5625,
"time": 1636167847,
"voltage": 4.1875
},
"gps_location": {
"when": "2021-11-06T03:04:08Z",
"name": "Norcross, GA",
"country": "US",
"timezone": "America/New_York",
"latitude": 33.937182500000006,
"longitude": -84.25278515625
}
}
]
Each JSON object in this array is a separate _track.qo
motion event that displays the Notecard's current location and sensor readings. The part of the object that we care about in this particular post is the gps_location
values: latitude
, longitude
, and the captured
value. This is the data we'll need for the map.
Shortly we'll work on transforming this data to fit our <Map />
component's props - we'll handle that right after we create SOS mode for the app.
Configure SOS mode in the app
The SOS button to toggle SOS mode in the app.
Before we transform this JSON data, we need to give our application the option to turn SOS mode on or off (which changes the color of the polylines rendered in the map).
To build this, we'll need a new state variable, function, and button in our index.tsx
file.
// imports
import { useState } from "react";
// more imports
type dataProps = {
// condensed for code brevity
};
export default function Home({ data }: { data: dataProps[] }) {
// map component imported dynamically here
const [isSosModeEnabled, setIsSosModeEnabled] = useState<boolean>(false);
const toggleSosMode = () => {
const newSosState = !isSosModeEnabled;
if (newSosState === true) {
localStorage.setItem("sos-timestamp", new Date());
setIsSosModeEnabled(newSosState);
} else {
localStorage.removeItem("sos-timestamp");
setIsSosModeEnabled(newSosState);
}
};
// logic to transform data into items needed to pass to map
return (
<div>
{/* extra tracker app code */}
<main>
<h1>Notelink Tracker Dashboard</h1>
<button onClick={toggleSosMode}>
SOS Mode
</button>
{isSosModeEnabled ? <p>SOS Mode Currently On</p> : null}
{/* other tracker components */}
<MapWithNoSSR
coords={latLngMarkerPositions}
lastPosition={lastPosition}
markers={latLngMarkerPositions}
latestTimestamp={latestTimestamp}
sosCoords={sosCoords}
/>
</div>
</main>
</div>
);
}
// code to fetch tracker data: getStaticProps
In the file above, we're adding a new isSosModeEnabled
boolean: this will let the app know whether new location events are happening during an emergency tracking situation or not.
Next, we create a new function called toggleSosMode()
. This function will change the state of the isSosModeEnabled
and also store a timestamp named sos-timestamp
in the browser's local storage. I'm storing this timestamp in local storage so it can be compared to events that reach the app after SOS mode has been enabled and the app will know whether it needs to render the polylines on the map in red or blue. We'll get to the logic for this part in the following section.
Lastly, in the JSX for the component, we'll make a new <button>
element and attach the toggleSosMode()
function to its onClick()
method. I also added a <p>
tag beneath the button to display when SOS Mode is in effect in the app.
Pressing the button after app mode will turn it on, pressing it again will turn it off.
Note : In the actual repo there's also logic to update the Notecard via Notehub to take more frequent location readings, and handle if SOS mode was already enabled and the browser window was closed or app connection was lost, but for clarity in this tutorial, I've eliminated both of those considerations.
Now that we can turn SOS mode on and off in the browser at will, it's time to take our location data and transform it to render in our map.
Reshape the tracker event data
Our index.tsx
file is going to need some more state variables to fulfill all the different data props the <Map />
component needs. Once again, I've condensed the logic to make this file easier to read, but you can always click the file name to see its full contents online.
// imports
import { useEffect, useState } from "react";
import dayjs from "dayjs"; // for ease of date formatting
// more imports
type dataProps = {
// condensed for code brevity
};
export default function Home({ data }: { data: dataProps[] }) {
// map component imported dynamically here
const [lastPosition, setLastPosition] = useState<[number, number]>([
33, -84,
]);
const [latestTimestamp, setLatestTimestamp] = useState<string>("");
const [latLngMarkerPositions, setLatLngMarkerPositions] = useState<
[number, number][]
>([]);
// isSosEnabled boolean here
const [sosCoords, setSosCoords] = useState<number[][]>([]);
/* runs as soon as the location data is fetched from Notehub API
or when the sos mode is toggled on or off with the button */
useEffect(() => {
const latLngArray: [number, number][] = [];
const sosLatLngArray: [number, number][] = [];
if (data && data.length > 0) {
data
.sort((a, b) => {
return Number(a.captured) - Number(b.captured);
})
.map((event) => {
let latLngCoords: [number, number] = [];
let sosLatLngCoords: [number, number] = [];
if (!isSosModeEnabled) {
latLngCoords = [
event.gps_location.latitude,
event.gps_location.longitude,
];
latLngArray.push(latLngCoords);
} else {
const localSosTimestamp = localStorage.getItem("sos-timestamp");
if (Date.parse(event.captured) >= Date.parse(localSosTimestamp)) {
sosLatLngCoords = [
event.gps_location.latitude,
event.gps_location.longitude,
];
sosLatLngArray.push(sosLatLngCoords);
} else {
latLngCoords = [
event.gps_location.latitude,
event.gps_location.longitude,
];
latLngArray.push(latLngCoords);
}
}
});
const lastEvent = data.at(-1);
let lastCoords: [number, number] = [0, 1];
lastCoords = [
lastEvent.gps_location.latitude,
lastEvent.gps_location.longitude,
];
setLastPosition(lastCoords);
const timestamp = dayjs(lastEvent?.captured).format("MMM D, YYYY h:mm A");
setLatestTimestamp(timestamp);
}
if (sosLatLngArray.length > 0) {
setSosCoords(sosLatLngArray);
}
setLatLngMarkerPositions(latLngArray);
}, [data, isSosModeEnabled]);
// toggleSosMode function
return (
<div>
{/* extra tracker app code */}
<main>
<h1>Notelink Tracker Dashboard</h1>
{/* other tracker components */}
<MapWithNoSSR
coords={latLngMarkerPositions}
lastPosition={lastPosition}
markers={latLngMarkerPositions}
latestTimestamp={latestTimestamp}
sosCoords={sosCoords}
/>
</div>
</main>
</div>
);
}
// code to fetch tracker data: getStaticProps
In our main component, once the data is fetched from Notehub, we set the following new React useState
variables to hold the data to pass to the <Map />
component.
lastPosition
, latestTimestamp
, latLngMarkerPositions
, and sosCoords
, are the new state variables we'll need at our disposal.
After those states are declared, the useEffect()
function will run whenever data is fetched from Notehub (on component mount) or when the app's SOS mode is toggled. Inside the function, the events from Notehub are sorted and iterated through.
If the isSosModeEnabled
boolean is true, the sos-timestamp
is fetched out of the browser's local storage, and the date of that timestamp is compared to the captured
timestamp from each event so the event can be properly sorted into either the sosLatLngArray
list or the latLngArray
list.
Once those local arrays are assembled inside of the useEffect()
, they're set equal to the state variables latLngMarkerPositions
and sosCoords
.
If isSosModeEnabled
is false, then all the events are added to the latLngArray
list automatically.
The other variables lastPosition
and latestTimestamp
are set simply by pulling the last event off of the sorted data array and extracting the properties from it.
Then all these variables are passed to the <Map />
component, and it knows what do to from there regarding markers, popups, and line colors.
Test it out
Ok! I think we're reading to test out our map and multicolored lines!
If you're using our mocked data instead of real-time data streaming in from the Notehub cloud, the easiest way to test the app is to toggle SOS mode on via the button in the browser, then adjust the timestamp in the browser's DevTool local storage to be before at least some of the events captured
timestamps.
If everything goes according to plan, you should end up seeing a combination of colored lines depending on when the SOS mode's time is set to and when the event occurred.
When SOS mode is on, new events that occurred after it was enabled show up as red lines.
Please keep in mind, this app was built in a day so it's rough around the edges and certainly not ready for prime time. I'd recommend forking it and giving it some extra polish and testing before putting it into prod, if you're so inclined.
And there you have it: multicolored lines in a map in a React application. Not too shabby for a day's work.
Conclusion
After I joined an IoT startup last summer, I started building web apps to reflect the data being captured and transferred by our IoT devices in the real world and sent to the cloud, and during a company-wide hackathon I built a dashboard that not only displayed location data but also had the ability to change location lines from blue to red at will.
It seemed like a nice feature to improve readability of the map in some sort of emergency situation.
Next.js combined with the React Leaflet library made it all possible, and within the timeframe I had a working (if very rough) prototype to show my coworkers. It was a lot of fun to work on, and I learned a bunch of new things along the way. That's my idea of a successful hackathon.
Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.
If you’d like to make sure you never miss an article I write, sign up for my newsletter here: https://paigeniedringhaus.substack.com
Thanks for reading. I hope you found it useful to see how to set up an interactive map in Next.js and render multicolored travel lines between different location points depending on the situation. Happy mapping!
Top comments (0)