Web applications have greatly benefited from the inclusion of maps, providing users with valuable location-based information. Maps have transformed our interaction with the world, from navigating unfamiliar places to discovering nearby eateries. As a result, the integration of maps into websites has become increasingly popular in recent times. Nevertheless, designing maps that are both functional and user-friendly can present a challenge, especially for those lacking experience in this domain. In this article, I will share useful tips on how to create effective maps within your browser.
Let’s discuss technologies. When working with maps, we typically use three layers:
Rendering the user interface, which includes buttons and forms. In our stack, React performs this role;
Rendering the map and enabling user interaction. We use Mapbox for this;
Fetching data from the server, such as information about markers or polygons. To retrieve data, we use the browser’s built-in fetch feature.
Let’s review each item to gain a better understanding of our technology stack when working with maps.
The
As changing elements on a page is the most expensive operation for a browser, it’s essential to do it as efficiently as possible. To address this issue, Facebook engineers developed the React library, which enables fast and straightforward element changes on a page. Besides providing rapid state changes on a page, React allows us to do this declaratively without working with DOM elements directly. Instead, we use an abstraction, usually
// It's our state. Is the user our friend or not?
// false by default
const [isFriend, setIsFriend] = useState(false)
// Depending on the state, we show the text on the button
const buttonText = isFriend ? 'Your my Friend' : 'Add as Friend'
// There is JSX, syntax for UI
// In this case, we display a button, when clicked, we change the state
return (
<button onClick={() => setIsFriend(true)}>{buttonText}</button>
)
It is possible to nest components with ordinary DOM elements like forms, buttons, and inputs at the bottom of the hierarchy. By assembling these simple elements, we can create more complex ones, such as a complete form:
const Form = () => (
<form>
<input name="Email"/>
<input name="Password"/>
</form>
)
const App = () => (
<main>
<h1>My form!</h1>
<Form />
</main>
)
How does React assist us in the context of maps? As the map on the page is interactive, similar to a button or form, we aim to optimize its rendering and interaction through events like clicks on the map. React can help achieve this optimization. Here is an example of how it works:
// Use React to render the map with different event handlers
// and render markers
return (
<BaseMap
onInitMap={() => console.log('I am alive!')}
onClickMap={() => console.log('Click!')}
onDestroyMap={() => console.log('Oh no!')}
>
<ClustersMarkers />
<PostsMarkers />
<ListingsMarkers />
</BaseMap>
)
When working with React, it is essential to remember that it allows for efficient manipulation of elements on the page, quick changes, and interaction with them through events. This is achieved through an abstraction that resembles HTML, making it easy to create complex components from simpler ones.
Now, let’s discuss the map itself. Creating and using maps can be challenging, and only a few product companies can design maps from scratch. Typically, most people rely on pre-made libraries with a user-friendly API that has been tried and tested.
Numerous dynamic map providers are available, including Google Maps, Leaflet, Bing Maps, Mapbox, and more. However, we will focus on
Mapbox offers
Additionally, Mapbox provides a
Mapbox provides a variety of
Let’s revisit Mapbox maps. What does the
It initializes the map in an HTML element on the page;
It loads and renders images that make up the map;
It draws additional elements, such as markers, using GeoJson as input data;
It generates events, such as clicks or zoom changes, that can be handled.
Let’s take a closer look at each of these items.
Mapbox specializes in map rendering using tiles. Tiles are small square images that make up the larger map. The default size of a tile is 512x512 pixels, and it can be either
Just so you know, Mapbox Studio allows us to choose the specific data we want to include in the map tiles. These tiles are then placed onto a
<canvas width="100" height="100" />
Mapbox handles the loading, insertion, and updating of tiles. All we have to do is specify where we want the map to be displayed and the initial conditions, like the zoom level or the map’s coordinates. To use Mapbox, you’ll need an
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
const map = new mapboxgl.Map({
container: 'map', // we can use an Id or an element
style: 'mapbox://styles/mapbox/streets-v11', // URL for styles
center: [-74.5, 40], // initial coordinates [lng, lat]
zoom: 9, // initial zoom
});
After this, we will get a map on the page in an element with the id ‘map.’
To provide users with more information on the map, we often display the location of a certain establishment or the boundaries of a specific area. To achieve this, we use a specific data format called
GeoJSON is the standard format for storing geographic structures on maps. It can store various primitive types that describe geographic objects such as addresses, locations, streets, highways, borders, countries, states, and combinations of these, known as multipart. GeoJSON was introduced in 2008 and is represented like this:
{
"type": "Feature", // also can be FeatureCollection, it's collection of Feature
"geometry": {
"type": "Point", // also can be LineString, Polygon, MultiPolygon
"coordinates": [125.6, 10.1] // for other types you can use Array with coordinates
},
"properties": { // it's metadata, we can you that to show something on the map
"name": "Dinagat Islands"
}
}
Let’s talk about the coordinate system used in Mapbox. By default, Mapbox employs map.setProjection
method.
Now, we will discuss how to display GeoJSON on the map. Mapbox offers two entities that we will find helpful:
To display polygons or markers on the map, we must retrieve the data in GeoJson format from the server. Then, we create a source, input the data into it, and connect it to the required layer.
const geoJsonFeature = {
'type': 'Feature',
'geometry': {
'type': 'Polygon',
'coordinates': [
[-67.13734, 45.13745],
[-66.96466, 44.8097],
[-68.03252, 44.3252],
[-67.13734, 45.13745]
]
}
}
// Create source with our data
map.addSource('ourSource', {
'type': 'geojson',
'data': geoJsonFeature
});
// Add layer for background
map.addLayer({
'id': 'background',
'type': 'fill',
'source': 'ourSource', // название нашего source
'layout': {},
'paint': {
'fill-color': '#0080ff',
'fill-opacity': 0.5
}
});
// Add layer for border
map.addLayer({
'id': 'border',
'type': 'line',
'source': 'ourSource',
'layout': {},
'paint': {
'line-color': '#000',
'line-width': 3
}
});
After running this code, we get the result:
To learn more about this topic, you can refer to the on
method with the event type we want, similar to how we work with DOM elements.
map.on('mousemove', (e) => {
console.log(JSON.stringify(e.point));
});
// Result: {"x":330,"y":49}
In summary, what do we need to remember? Mapbox allows us to display a map, draw our data on top of it, and process map events. At the same time, Mapbox takes care of loading and displaying images (tiles).
A word about
What to remember? We retrieve data from the server, and there are many libraries for this, but we will use fetch. Next, we will look at how we do this specifically when working with maps, as there are nuances.
Now let’s see how the technologies described above work together. First, we will retrieve the data for displaying the polygon using fetch. Then we will declare the initialization of the map, and after it loads, we will add the polygon to the map.
You can also find a working example at the
const useFetch = () => {
/*
Our data
{
'type': 'Feature',
'geometry': {
'type': 'Polygon',
'coordinates': [
[
[-67.13734, 45.13745],
[-68.03252, 44.3252],
[-68.90478, 47.18479],
[-67.13734, 45.13745],
]
]
}
}
*/
const [data, setData] = useState(null)
useEffect(() => {
fetch('https://our-api.com/polygon')
.then(response => response.json())
.then(setData)
.catch(e => {
console.error(e)
})
}, [setData])
return { data }
}
const BaseMap = () => {
// Use the hook to fetch data
const { data } = useFetch(GET_REGION);
// Map instance
const map = useRef(null);
// DOM element
const mapContainer = useRef(null);
// Main logic - init the map and add the event
useEffect(() => {
if (map.current) {
return; // initialize map only once
}
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/light-v10', // style URL (it's Mapbox's core style)
center: [-68.137343, 45.137451], // starting position
zoom: 5 // starting zoom
});
// Handle event
map.on('load', () => {
const sourceId = 'source-region'
// Add a data source containing GeoJSON data
map.addSource(sourceId, {
'type': 'geojson',
'data': data.region // our data from Apollo
});
// Add a new layer to visualize the polygon
map.addLayer({
'id': 'background',
'type': 'fill',
'source': sourceId, // reference the data source
'paint': {
'fill-color': '#0080ff', // blue color fill
'fill-opacity': 0.5
}
});
// Add a black outline around the polygon
map.addLayer({
'id': 'outline',
'type': 'line',
'source': sourceId,
'paint': {
'line-color': '#000',
'line-width': 3
}
});
});
});
return <div ref={mapContainer} />;
}
We looked at the technology stack that underpins our future architecture. In the following article, we will discuss the principles that help design a map architecture, how to achieve maximum low coupling and high cohesion of modules, and how to maintain and develop a scalable map system.
Thank you very much for your attention! Have a great day.