This blog was originally posted on JavaScript in Plain English. Also check out this first post on how to Add a Map to Your React App - For Free.
Because Google makes it so easy, many websites contain maps with just one point. However, the downfalls include needing an API key and hitting a paywall once a specified number of calls have been made. Great news, though — since you are already running a web app, you can do the job yourself with no added cost.
Fortunately, on top of being free, adding a map with a point to your website is fairly simple to do, whether you want a basic drop-in for your HTML or you feel up to the challenge of building your own in React.
Basic Drop-In
This comparatively simple solution works for sites such as WordPress, single-page apps (e.g. Angular, Vue.js, and React), and basic HTML pages. Start by pasting this iframe into your HTML source.
<iframe src="https://one-point-map.herokuapp.com/?lat=0&long=0" width="800px" height="800px" />
Changing the values in the src URL alters the location and look of the map widget. To customize the query parameters you include in your URL, experiment with these options:
Custom React Build
For a custom React build, check out ol-kit, a library that wraps React and OpenLayers.
To create a blank React project (once you have node installed), use version 4․0․3 of react-scripts because ol-kit does not provide its own webpack v5 polyfills. Paste this in your terminal:
npx create-react-app@4.0.3 point-map
cd point-map
npm install --save --save-exact react-scripts@4.0.3
Next, run this command to install ol-kit and its peer dependencies:
npm i @bayer/ol-kit ol react react-dom styled-components @material-ui/core @material-ui/icons @material-ui/styles --save
Replace the contents of App․js with the below file. It includes everything needed to render a point on the map using query string data.
import React from 'react'
import {
Map,
BasemapContainer,
ContextMenu,
Controls,
Popup,
VectorLayer
} from '@bayer/ol-kit'
import { fromLonLat } from 'ol/proj'
import olFeature from 'ol/Feature'
import olGeomPoint from 'ol/geom/Point'
import olSourceVector from 'ol/source/Vector'
const App = () => {
const onMapInit = map => {
console.log('we got a map!', map)
// nice to have map set on the window while debugging
window.map = map
const queryParams = window.location.search.split(/([?&])/g).filter(e => e.length > 1).reduce((acc, str) => {
const stringSplit = str.split('=')
acc[stringSplit[0]] = stringSplit[1]
return acc
}, {})
if ((queryParams.long || queryParams.lon) && queryParams.lat) {
const point = new olFeature({
geometry: new olGeomPoint(fromLonLat([queryParams.long || queryParams.lon, queryParams.lat]))
})
const layer = new VectorLayer({
source: new olSourceVector({
features: [point]
})
})
map.addLayer(layer)
map.getView().fit(point.getGeometry().getExtent())
map.getView().setResolution(14)
} else {
throw new Error("Missing coordinates")
}
}
return (
<Map onMapInit={onMapInit} fullScreen>
<BasemapContainer />
<ContextMenu />
<Controls />
<Popup />
</Map>
)
}
export default App
This outline shows the default map styles with the most basic code. A few basic questions to answer before parameterization are:
- When does onMapInit get called?
As soon as the OpenLayers map initializes. Because the map instance passes down, it can be modified and have methods called on it, such as adding a point. This simplification of initiating the map is ol-kit’s most valuable feature.
- How is the point on the map?
The OpenLayers framework puts all features (in this case, a point) onto layers of a map (thus, the name OpenLayers). The hierarchy works like this: The map owns one or more layers, each layer owns only one source, which in turn can own multiple features (the point). Check out the Subclasses on these OpenLayers docs if you want a fuller picture.
- What does map.getView() do?
By default, ol-kit centers the map on the 48 contiguous states of the United States. If you add a point to anywhere else in the world, you presumably want the map to re-center on where the point was placed. Null Island (a weather buoy, not a real island) serves as a great place to test this, as its values are lat = 0, long = 0 making it very easy to remember.
Editing Map Subcomponents
This code incorporates the ReactHooks useState and useEffect to control the visibility of React child components. Here are the general modifications to the above snippet:
const [showBasemapContainer, setShowBasemapContainer] = useState(true)
const [showContextMenu, setShowContextMenu] = useState(true)
const [showControls, setShowControls] = useState(true)
const [showPopup, setShowPopup] = useState(true)
...
useEffect(() => {
const queryParams = window.location.search.split(/([?&])/g).filter(e => e.length > 1).reduce((acc, str) => {
const stringSplit = str.split('=')
acc[stringSplit[0]] = stringSplit[1]
return acc
}, {})
if (queryParams.show_basemap && queryParams.show_basemap === 'false') {
setShowBasemapContainer(false)
}
if (queryParams.show_popup && queryParams.show_popup === 'false') {
setShowPopup(false)
}
if (queryParams.show_context && queryParams.show_context === 'false') {
setShowContextMenu(false)
}
if (queryParams.show_controls && queryParams.show_controls === 'false') {
setShowControls(false)
}
}, []) // [] it will run on mount, but at no other time; URL params won't change without a refresh
...
<Map onMapInit={onMapInit} fullScreen>
{showBasemapContainer &&<BasemapContainer />}
{showContextMenu &&<ContextMenu />}
{showControls &&<Controls />}
{showPopup &&<Popup />}
</Map>
Adding support for a few other query params produces the following React component. The icon and its size, the zoom level, and the title shown in the popup are all customizable; every component that covers up the map can be hidden with a query param.
import React, { useState, useEffect } from 'react'
import {
Map,
BasemapContainer,
ContextMenu,
Controls,
Popup,
VectorLayer
} from '@bayer/ol-kit'
import { fromLonLat } from 'ol/proj'
import olFeature from 'ol/Feature'
import olGeomPoint from 'ol/geom/Point'
import olSourceVector from 'ol/source/Vector'
import olStyle from 'ol/style/Style'
import olStroke from 'ol/style/Stroke'
import olIcon from 'ol/style/Icon'
const App = () => {
const [showBasemapContainer, setShowBasemapContainer] = useState(true)
const [showContextMenu, setShowContextMenu] = useState(true)
const [showControls, setShowControls] = useState(true)
const [showPopup, setShowPopup] = useState(true)
useEffect(() => {
const queryParams = window.location.search.split(/([?&])/g).filter(e => e.length > 1).reduce((acc, str) => {
const stringSplit = str.split('=')
acc[stringSplit[0]] = stringSplit[1]
return acc
}, {})
if (queryParams.show_basemap && queryParams.show_basemap === 'false') {
setShowBasemapContainer(false)
}
if (queryParams.show_popup && queryParams.show_popup === 'false') {
setShowPopup(false)
}
if (queryParams.show_context && queryParams.show_context === 'false') {
setShowContextMenu(false)
}
if (queryParams.show_controls && queryParams.show_controls === 'false') {
setShowControls(false)
}
}, [])
const onMapInit = map => {
const queryParams = window.location.search.split(/([?&])/g).filter(e => e.length > 1).reduce((acc, str) => {
const stringSplit = str.split('=')
acc[stringSplit[0]] = stringSplit[1]
return acc
}, {})
if ((queryParams.long || queryParams.lon) && queryParams.lat) {
const point = new olFeature({
geometry: new olGeomPoint(fromLonLat([queryParams.long || queryParams.lon, queryParams.lat])),
...(queryParams.title ? { title: queryParams.title || undefined } : {})
})
if (queryParams.icon_url) {
point.setStyle(new olStyle({
stroke: new olStroke(),
image: new olIcon({
opacity: 1,
src: queryParams.icon_url,
scale: (queryParams.icon_scale && !isNaN(queryParams.icon_scale)) ? Number(queryParams.icon_scale) : 1
})
}))
}
const layer = new VectorLayer({
source: new olSourceVector({
features: [point]
})
})
map.addLayer(layer)
map.getView().fit(point.getGeometry().getExtent())
if (queryParams.zoom && !isNaN(queryParams.zoom)) {
map.getView().setZoom(Number(queryParams.zoom))
} else {
map.getView().setResolution(14)
}
} else {
throw new Error("Missing coordinates")
}
}
return (
<Map onMapInit={onMapInit} fullScreen>
{showBasemapContainer &&<BasemapContainer />}
{showContextMenu &&<ContextMenu />}
{showControls &&<Controls />}
{showPopup &&<Popup />}
</Map>
)
}
export default App
Some things to note:
- point.setStyle() changes the appearance of the point:
Calling setStyle allows for an infinite number of styles to be applied to points; this example only demonstrates how to change the default point style to an icon/image. For more information on styling features in OpenLayers, check out this documentation. - olFeature({ ... }) accepts any custom properties:
Adding data to the popup is that easy — a geometry of some kind (like a point or a line string) defines all features, which can then have other values applied. Though ol-kit comes pre-configured to prominently display the title property in the popup, different configurations modifies the popup behavior.
Most projects which need a map probably won’t be using URL query params to control data or component visibility, but this should be a good jumping-off point.
Stop paying for Google’s APIs; host your own software, be agile, and feel free to reach out with any questions.