The Google Maps Toolkit I Wish I Had When I Started
Every geospatial web app I've built has the same foundation: context layers that exist before the user does anything. Stream gauges, basin boundaries, precipitation grids. By the time I was building my third one, I noticed I was copy-pasting the same boilerplate over and over. So I cleaned them up, optimized them, and I'm sharing them here.
CustomOverlay (Source)CustomOverlay is used for overlaying arbitrary images on the map. You give it the image source and geographic bounds, and it handles positioning and reprojection on zoom and pan. CustomOverlay extends the OverlayView class provided by Google.
Here is an example using CustomOverlay to overlay an image of a cat over the State of Iowa:
// Load the image
const img = new Image();
img.src = "./assets/cat.png";
// Define the geographic bounding box
const bbox = {
"sw": {
"lng": -97.688726148,
"lat": 40.344952682
},
"ne": {
"lng": -90.217149722,
"lat": 43.574378869
}
};
// Initialize
const overlay = customOverlay(img.src, bbox, map);
It's that easy.
RasterGenerator (Source)Most likely, you won't be overlaying pictures of cats for work (and if you do, then please reach out, I would love to work with you), but you will have to generate images using real world data. This is where RasterGenerator comes in.
To generate an image using RasterGenerator you need a 1D array of your spatial data (being a performance geek, I use 1D over 2D arrays since iterating through them is faster), the grid width, and a color map.
Internally, RasterGenerator uses OffscreenCanvas to create the image. The benefits of using OffscreenCanvas are that it is completely decoupled from the DOM (you don't need to pollute your HTML with a <canvas> element) and rendering operations run inside a Web Worker context (outside the main thread) preventing overhead on the main application.
Here is an example generating a population heatmap and overlaying it over the United States:
// Build the color map
const colorMap = [
{ min: 0, max: 1, rgba: [0, 0, 0, 0] }, // No population - transparent
{ min: 1, max: 10, rgba: [255, 255, 212, 255] }, // Pale yellow
{ min: 10, max: 50, rgba: [254, 240, 178, 255] }, // Light yellow
{ min: 50, max: 100, rgba: [254, 217, 142, 255] }, // Yellow-orange
{ min: 100, max: 250, rgba: [253, 187, 104, 255] }, // Light orange
{ min: 250, max: 500, rgba: [253, 141, 60, 255] }, // Orange
{ min: 500, max: 1000, rgba: [252, 78, 42, 255] }, // Red-orange
{ min: 1000, max: 2500, rgba: [227, 26, 28, 255] }, // Red
{ min: 2500, max: 5000, rgba: [189, 0, 38, 255] }, // Dark red
{ min: 5000, max: 10000, rgba: [143, 0, 51, 255] }, // Crimson
{ min: 10000, max: 25000, rgba: [103, 0, 61, 255] }, // Dark magenta
{ min: 25000, max: 55001, rgba: [63, 0, 60, 255] }, // Deep purple-black
];
// Instantiate RasterGenerator and generate an image url
const image = new RasterGenerator(populationArray1D, width, populationArray.length / width, colorMap);
const imageUrl = await image.generateUrl();
// We can use the earlier example of customOverlay to put this image on the map
const bbox = {
"sw": {
"lng": -126.672711178,
"lat": 19.804486768
},
"ne": {
"lng": -65.939069416,
"lat": 56.942856349
}
};
const overlay = customOverlay(imageUrl, bbox, map);
overlay.setOpacity(0.7);
The result:
MarkerCollection (Source)MarkerCollection manages a collection of markers, but the interesting part is how it stays performant at scale and why that required ditching Google's recommended API. Although deprecated, MarkerCollection utilizes Google's Marker instead of AdvancedMarkerElement as they are more performant. Legacy Markers are rendered directly onto the map's internal canvas, whereas AdvancedMarkerElements each create a separate DOM element. Using Markers, we avoid the overhead of managing thousands of separate DOM elements. You can attach properties to each marker and register a click callback.
I will plot a marker for every college in the United States:
const colleges = await fetch("./assets/usa_colleges.json")
.then(r => r.json());
const markers = new MarkerCollection(map);
for (const college of colleges) {
const [lat, lng, name] = college;
markers.add(lat, lng, { name });
}
markers.setColor("red");
markers.setSize(2.5);
// Add an on click callback to the markers
markers.onClick(function (marker) {
console.log(marker.properties.name);
});
We just plotted over 6,000 markers. Test by zooming and panning, and see how smooth it is ;)
Tooltip (Source)Tooltip is used to add a geographically fixed informational window on the map. It's lightweight and more visually appealing than Google Maps Info Window.
For this example, instead of logging the name of each college to the console (like I did in the previous example), I will add a tooltip with the name of the college when the marker is clicked. You can also add tooltips to your vector and raster layers as well.
let tooltip = null;
markers.onClick(function (marker) {
const lat = marker.marker.position.lat();
const lng = marker.marker.position.lng();
if (tooltip !== null) {
tooltip.destroy();
tooltip = null;
}
tooltip = new Tooltip({ lat, lng }, marker.properties.name, map, () => {
tooltip.destroy();
tooltip = null;
});
});
Try it and click on a marker:
Compare it to Google's bulky Info Window. It gets the jobs done, but good luck customizing it:
These four components: CustomOverlay, RasterGenerator, MarkerCollection, and Tooltip are ones I find myself reaching for in basically every project. None of them are groundbreaking, but having them ready to go means less time fighting with the API and more time on the actual problem. If you end up using any of these, or have ideas for improvements, shoot me an email.