push demos for today
This commit is contained in:
parent
98114b9f7d
commit
1a7eec7baa
BIN
13.js-mapping/XYZ_Tiles.png
Normal file
BIN
13.js-mapping/XYZ_Tiles.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 286 KiB |
188
13.js-mapping/d3.html
Normal file
188
13.js-mapping/d3.html
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<style>
|
||||||
|
/* style for tooltip as well as the base map container */
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 8px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #333333;
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div id="map"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/topojson@3"></script>
|
||||||
|
<script>
|
||||||
|
const euMemberStates = {
|
||||||
|
Belgium: 1,
|
||||||
|
France: 1,
|
||||||
|
Germany: 1,
|
||||||
|
Italy: 1,
|
||||||
|
Luxembourg: 1,
|
||||||
|
Netherlands: 1,
|
||||||
|
Denmark: 2,
|
||||||
|
Ireland: 2,
|
||||||
|
Greece: 3,
|
||||||
|
Portugal: 4,
|
||||||
|
Spain: 4,
|
||||||
|
Austria: 5,
|
||||||
|
Finland: 5,
|
||||||
|
Sweden: 5,
|
||||||
|
Cyprus: 6,
|
||||||
|
"Czech Republic": 6,
|
||||||
|
Estonia: 6,
|
||||||
|
Hungary: 6,
|
||||||
|
Latvia: 6,
|
||||||
|
Lithuania: 6,
|
||||||
|
Malta: 6,
|
||||||
|
Poland: 6,
|
||||||
|
Slovakia: 6,
|
||||||
|
Slovenia: 6,
|
||||||
|
Bulgaria: 7,
|
||||||
|
Romania: 7,
|
||||||
|
Croatia: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const width = 800;
|
||||||
|
const height = 600;
|
||||||
|
const svg = d3
|
||||||
|
.select("#map")
|
||||||
|
.append("svg")
|
||||||
|
.attr("viewBox", [0, 0, width, height])
|
||||||
|
.attr("width", width)
|
||||||
|
.attr("height", height);
|
||||||
|
|
||||||
|
// d3 tooltip pattern: create a placeholder div which will be
|
||||||
|
// hidden at first & revealed/moved as needed
|
||||||
|
const tooltip = d3
|
||||||
|
.select("body")
|
||||||
|
.append("div")
|
||||||
|
.attr("class", "tooltip")
|
||||||
|
.style("opacity", 0);
|
||||||
|
|
||||||
|
// color function used for choropleth
|
||||||
|
const colorScale = d3
|
||||||
|
.scaleSequential()
|
||||||
|
.domain([8, 1]) // Reversed domain so earlier members are darker
|
||||||
|
.interpolator(d3.interpolateViridis);
|
||||||
|
|
||||||
|
// set up projection centered on Europe
|
||||||
|
const projection = d3
|
||||||
|
// DEMO: change projection
|
||||||
|
// https://d3js.org/d3-geo/conic
|
||||||
|
// https://github.com/d3/d3-geo-projection
|
||||||
|
.geoMercator()
|
||||||
|
//.geoConicEqualArea()
|
||||||
|
.center([15, 54])
|
||||||
|
.scale(600)
|
||||||
|
.translate([width / 2, height / 2]);
|
||||||
|
const path = d3.geoPath().projection(projection);
|
||||||
|
|
||||||
|
// load TopoJSON
|
||||||
|
d3.json(
|
||||||
|
// DEMO change to 110m/10m
|
||||||
|
"https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json",
|
||||||
|
).then((data) => {
|
||||||
|
// using topojson 3rd party library to convert to GeoJSON which D3 understands
|
||||||
|
const geojson = topojson.feature(data, data.objects.countries);
|
||||||
|
|
||||||
|
// entire map render happens after data is loaded, in the then() function
|
||||||
|
const europeCountries = geojson.features.filter(
|
||||||
|
(d) => euMemberStates[d.properties.name] !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
svg
|
||||||
|
.selectAll("path")
|
||||||
|
.data(europeCountries)
|
||||||
|
.join("path")
|
||||||
|
.attr("d", path)
|
||||||
|
.attr("fill", (d) => {
|
||||||
|
const score = euMemberStates[d.properties.name];
|
||||||
|
return score ? colorScale(score) : "#ccc";
|
||||||
|
})
|
||||||
|
.attr("stroke", "white")
|
||||||
|
.attr("stroke-width", 0.5)
|
||||||
|
.on("mouseover", function (event, d) {
|
||||||
|
tooltip.transition().duration(200).style("opacity", 0.9);
|
||||||
|
tooltip
|
||||||
|
.html(
|
||||||
|
`
|
||||||
|
<strong>${d.properties.name}</strong><br/>
|
||||||
|
Wave: ${euMemberStates[d.properties.name]}
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.style("left", event.pageX + 10 + "px")
|
||||||
|
.style("top", event.pageY - 28 + "px");
|
||||||
|
})
|
||||||
|
.on("mouseout", function () {
|
||||||
|
tooltip.transition().duration(500).style("opacity", 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// example of a legend, not related to map topics
|
||||||
|
const legendWidth = 200;
|
||||||
|
const legendHeight = 20;
|
||||||
|
const legendScale = d3
|
||||||
|
.scaleLinear()
|
||||||
|
.domain([1, 8])
|
||||||
|
.range([0, legendWidth]);
|
||||||
|
|
||||||
|
const legendAxis = d3
|
||||||
|
.axisBottom(legendScale)
|
||||||
|
.ticks(8)
|
||||||
|
.tickFormat((d) => d);
|
||||||
|
|
||||||
|
const legend = svg
|
||||||
|
.append("g")
|
||||||
|
.attr(
|
||||||
|
"transform",
|
||||||
|
`translate(${width - legendWidth - 20}, ${height - 50})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const defs = svg.append("defs");
|
||||||
|
const gradient = defs
|
||||||
|
.append("linearGradient")
|
||||||
|
.attr("id", "legend-gradient")
|
||||||
|
.attr("x1", "0%")
|
||||||
|
.attr("x2", "100%")
|
||||||
|
.attr("y1", "0%")
|
||||||
|
.attr("y2", "0%");
|
||||||
|
|
||||||
|
// Add gradient stops
|
||||||
|
const stops = d3.range(0, 8);
|
||||||
|
stops.forEach((stop, i) => {
|
||||||
|
gradient
|
||||||
|
.append("stop")
|
||||||
|
.attr("offset", `${(i / (stops.length - 1)) * 100}%`)
|
||||||
|
.attr("stop-color", colorScale(stop));
|
||||||
|
});
|
||||||
|
|
||||||
|
legend
|
||||||
|
.append("rect")
|
||||||
|
.attr("width", legendWidth)
|
||||||
|
.attr("height", legendHeight)
|
||||||
|
.style("fill", "url(#legend-gradient)");
|
||||||
|
|
||||||
|
legend
|
||||||
|
.append("g")
|
||||||
|
.attr("transform", `translate(0, ${legendHeight})`)
|
||||||
|
.call(legendAxis);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
96
13.js-mapping/leaflet.html
Normal file
96
13.js-mapping/leaflet.html
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<!--
|
||||||
|
See Also:
|
||||||
|
https://leafletjs.com/examples/quick-start/
|
||||||
|
https://leafletjs.com/examples.html
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
#map-goes-here {
|
||||||
|
height: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#info {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
#controls {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map-goes-here"></div>
|
||||||
|
<div id="info">placeholder</div>
|
||||||
|
<div id="controls">
|
||||||
|
<button id="zoom-fit">Fit All Points</button>
|
||||||
|
<button id="zoom-us">Fit US</button>
|
||||||
|
</div>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script>
|
||||||
|
const chicago = [41.8781, -87.6298];
|
||||||
|
const initialZoom = 12;
|
||||||
|
// Leaflet makes a global variable "L" available.
|
||||||
|
// this creates a map in the div above
|
||||||
|
const map = L.map("map-goes-here").setView(chicago, initialZoom);
|
||||||
|
|
||||||
|
// OSM base layer
|
||||||
|
// DEMO change of base layer
|
||||||
|
const watercolorURL =
|
||||||
|
"https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg";
|
||||||
|
const osmURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||||
|
L.tileLayer(osmURL, {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: "© OpenStreetMap contributors",
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// points could be loaded from GeoJSON or any other source
|
||||||
|
// usage (below) does not depend on format
|
||||||
|
const points = [
|
||||||
|
{ lat: 41.8826, lng: -87.6233, name: "Millennium Park" },
|
||||||
|
{ lat: 41.8916, lng: -87.6083, name: "Navy Pier" },
|
||||||
|
{ lat: 41.8526, lng: -87.6188, name: "Museum of Science and Industry" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add points to the map
|
||||||
|
const markers = [];
|
||||||
|
points.forEach((point) => {
|
||||||
|
const marker = L.marker([point.lat, point.lng]).addTo(map);
|
||||||
|
|
||||||
|
// adds a popup to the marker, can use HTML
|
||||||
|
marker.bindPopup(`<strong>${point.name}</strong>`);
|
||||||
|
|
||||||
|
// add custom events to markers in typical JS fashion
|
||||||
|
marker.on("click", function () {
|
||||||
|
const infoDiv = document.getElementById("info");
|
||||||
|
infoDiv.textContent = `Selected Location: ${point.name}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
markers.push(marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
// example of binding button events to modify the map
|
||||||
|
document.getElementById("zoom-fit").addEventListener("click", () => {
|
||||||
|
const groupBounds = new L.featureGroup(markers).getBounds();
|
||||||
|
map.fitBounds(groupBounds);
|
||||||
|
});
|
||||||
|
document.getElementById("zoom-us").addEventListener("click", () => {
|
||||||
|
const usBounds = [
|
||||||
|
[24.396308, -125.0], // Southwest corner (latitude, longitude)
|
||||||
|
[49.384358, -66.93457], // Northeast corner (latitude, longitude)
|
||||||
|
];
|
||||||
|
map.fitBounds(usBounds);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
13.js-mapping/map1.png
Normal file
BIN
13.js-mapping/map1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
BIN
13.js-mapping/map2.png
Normal file
BIN
13.js-mapping/map2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 202 KiB |
142
13.js-mapping/maplibre.html
Normal file
142
13.js-mapping/maplibre.html
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<!-- maplibre needs custom CSS loaded to style its maps and controls -->
|
||||||
|
<link
|
||||||
|
href="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
#map-goes-here {
|
||||||
|
height: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#info {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
#controls {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map-goes-here"></div>
|
||||||
|
<div id="info">placeholder</div>
|
||||||
|
<div id="controls">
|
||||||
|
<button id="zoom-fit">Fit All Points</button>
|
||||||
|
<button id="zoom-us">Fit US</button>
|
||||||
|
</div>
|
||||||
|
<script src="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js"></script>
|
||||||
|
<script>
|
||||||
|
const watercolor = {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
"raster-tiles": {
|
||||||
|
type: "raster",
|
||||||
|
tiles: [
|
||||||
|
// NOTE: Layers from Stadia Maps do not require an API key for localhost development or most production
|
||||||
|
// web deployments. See https://docs.stadiamaps.com/authentication/ for details.
|
||||||
|
"https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg",
|
||||||
|
],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution:
|
||||||
|
'Map tiles by <a target="_blank" href="https://stamen.com">Stamen Design</a>; Hosting by <a href="https://stadiamaps.com/" target="_blank">Stadia Maps</a>. Data © <a href="https://www.openstreetmap.org/about" target="_blank">OpenStreetMap</a> contributors',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "simple-tiles",
|
||||||
|
type: "raster",
|
||||||
|
source: "raster-tiles",
|
||||||
|
minzoom: 0,
|
||||||
|
maxzoom: 22,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const tileJSON =
|
||||||
|
"https://tiles.stadiamaps.com/styles/alidade_smooth.json";
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
container: "map-goes-here",
|
||||||
|
center: [-87.6298, 41.8781],
|
||||||
|
zoom: 12,
|
||||||
|
style: tileJSON,
|
||||||
|
// DEMO toggle style
|
||||||
|
style: watercolor,
|
||||||
|
});
|
||||||
|
|
||||||
|
const points = [
|
||||||
|
{ lat: 41.8826, lng: -87.6233, name: "Millennium Park" },
|
||||||
|
{ lat: 41.8916, lng: -87.6083, name: "Navy Pier" },
|
||||||
|
{ lat: 41.8526, lng: -87.6188, name: "Museum of Science and Industry" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// some features expect GeoJSON in the format shown here.
|
||||||
|
// in practice we may load from an API or source file
|
||||||
|
// if possible, it is OK to embed data directly like this
|
||||||
|
// (Outside of this example I would probably move this to a function for reuse.)
|
||||||
|
const geojson = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: points.map((point) => ({
|
||||||
|
type: "Feature",
|
||||||
|
// this should be one of the OGR geometries (Point/Line/Polygon/etc.)
|
||||||
|
geometry: {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: [point.lng, point.lat],
|
||||||
|
},
|
||||||
|
// can put arbitrary data here in properties
|
||||||
|
properties: {
|
||||||
|
name: point.name,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
// looping over the GeoJSON, we add a marker & popup for each
|
||||||
|
// point in the data. the parameter here is a GeoJSON object
|
||||||
|
geojson.features.forEach((feature) => {
|
||||||
|
// these variables are just extracting data from the format seen above
|
||||||
|
// which is GeoJSON's representation of a geometry
|
||||||
|
const coordinates = feature.geometry.coordinates;
|
||||||
|
const name = feature.properties.name;
|
||||||
|
|
||||||
|
// add a marker for each point
|
||||||
|
const marker = new maplibregl.Marker()
|
||||||
|
.setLngLat(coordinates)
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
// add a popup with the point name
|
||||||
|
const popup = new maplibregl.Popup().setText(name);
|
||||||
|
marker.setPopup(popup);
|
||||||
|
|
||||||
|
// bind event listener to marker (identical to leaflet)
|
||||||
|
marker.getElement().addEventListener("click", () => {
|
||||||
|
const infoDiv = document.getElementById("info");
|
||||||
|
infoDiv.textContent = `Selected Location: ${name}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// fit all points -- a bit more complicated than Leaflet's version
|
||||||
|
document.getElementById("zoom-fit").addEventListener("click", () => {
|
||||||
|
const bounds = geojson.features.reduce((bounds, feature) => {
|
||||||
|
return bounds.extend(feature.geometry.coordinates);
|
||||||
|
}, new maplibregl.LngLatBounds());
|
||||||
|
|
||||||
|
map.fitBounds(bounds, { padding: 20 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// fit entire US
|
||||||
|
document.getElementById("zoom-us").addEventListener("click", () => {
|
||||||
|
const usBounds = [
|
||||||
|
[-125.0, 24.396308], // Southwest corner [lng, lat]
|
||||||
|
[-66.93457, 49.384358], // Northeast corner [lng, lat]
|
||||||
|
];
|
||||||
|
map.fitBounds(usBounds, { padding: 20 });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
155
13.js-mapping/slides.html
Normal file
155
13.js-mapping/slides.html
Normal file
File diff suppressed because one or more lines are too long
176
13.js-mapping/slides.md
Normal file
176
13.js-mapping/slides.md
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
# Mapping in JavaScript
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Today
|
||||||
|
|
||||||
|
- Discuss types of maps & appropriate library choices.
|
||||||
|
- Explore examples in D3, Leaflet, and MapLibreGL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Which Tool?
|
||||||
|
|
||||||
|
**What kind of map?**
|
||||||
|
|
||||||
|
![bg left fit](map1.png)
|
||||||
|
|
||||||
|
Abstract/geometric representation of data.
|
||||||
|
|
||||||
|
D3 will provide the most flexibility & can use existing tools for color scales/interactivity/etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Map w/ Cartographic Features
|
||||||
|
|
||||||
|
![bg right fit](map2.png)
|
||||||
|
|
||||||
|
A different library will be necessary. Depending on needs **raster** or **vector** tiles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Choropleths in D3
|
||||||
|
|
||||||
|
<https://d3-graph-gallery.com/graph/choropleth_basic.html>
|
||||||
|
|
||||||
|
### Projection
|
||||||
|
|
||||||
|
<https://d3-graph-gallery.com/graph/backgroundmap_changeprojection.html>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Leaflet and MapLibreGL Choropleths
|
||||||
|
|
||||||
|
- Projection choice constrained by tile layers.
|
||||||
|
- Need additional libraries (or pure JS) for interactivity & other graphics.
|
||||||
|
|
||||||
|
Demo/Tutorial: <https://leafletjs.com/examples/choropleth/>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base Maps
|
||||||
|
|
||||||
|
Instead of starting with a blank slate, it can be helpful to have a base map, but a base map is often comprised of many feature layers:
|
||||||
|
|
||||||
|
- Country/Ocean borders
|
||||||
|
- National Sub-divisions
|
||||||
|
- Roads
|
||||||
|
- Terrain
|
||||||
|
- Points of Interest
|
||||||
|
- ...
|
||||||
|
|
||||||
|
Drawing all of these layers adds up.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Raster Tiles
|
||||||
|
|
||||||
|
An innovation that made web mapping scalable in the mid-2000s was pre-rendering base layers into image tiles.
|
||||||
|
|
||||||
|
![](XYZ_Tiles.png)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Raster Tiles
|
||||||
|
|
||||||
|
### Pros
|
||||||
|
|
||||||
|
- Simple to serve: just a lot of images.
|
||||||
|
- Each image at a zoom level ~approx same size.
|
||||||
|
- 100% consistent rendering.
|
||||||
|
- Any kind of imagery (watercolor, satellite maps, etc.)
|
||||||
|
- Minimal client-side processing needed.
|
||||||
|
- Good for dense data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Cons
|
||||||
|
|
||||||
|
- Fixed zoom levels and projection.
|
||||||
|
- Large file sizes for large Z.
|
||||||
|
- Need to create variants for all desired permutations of features.
|
||||||
|
- No modifications to styling after the fact.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vector Tiles
|
||||||
|
|
||||||
|
Instead of images, data is sent as images.
|
||||||
|
|
||||||
|
- Can create multiple variations per JSON, change road colors/etc without regenerating millions of images.
|
||||||
|
- Can be more efficient on bandwidth.
|
||||||
|
- Significantly more complexity on client and server side.
|
||||||
|
- Older devices may struggle to render them.
|
||||||
|
- Rendering relies on client library, can vary.
|
||||||
|
- Not suitable for dense data such as satellite imagery.
|
||||||
|
|
||||||
|
Blog Post from *today* on OSM switch to Vector tiles: <https://tech.marksblogg.com/osm-mvt-vector-tiles.html>
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Leaflet
|
||||||
|
|
||||||
|
**Raster Tiles w/ Vector Layer on Top**
|
||||||
|
|
||||||
|
<https://leafletjs.com/reference.html>
|
||||||
|
|
||||||
|
(Has plugins to use Vector Tiles via MapLibreGL)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MapLibreGL
|
||||||
|
|
||||||
|
Vector-tile based library. Community fork of MapboxGL.
|
||||||
|
|
||||||
|
Mapbox is an innovator in the space, but also quite expensive.
|
||||||
|
|
||||||
|
MaplibreGL/MapboxGL are mostly compatible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## General Workflow
|
||||||
|
|
||||||
|
- Use library to render map(s) to div(s) on page.
|
||||||
|
- Pick base layer, either raster or vector.
|
||||||
|
- If vector, possibly apply additional styling to base.
|
||||||
|
- Add additional vector layers based on your data & its features.
|
||||||
|
- Attach event handlers for interactivity on your features and/or input elements adjacent to the map.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pop-ups
|
||||||
|
|
||||||
|
Leaflet/MapLibre offer a simple API for adding pop-ups to features.
|
||||||
|
|
||||||
|
Reminder: hover/pop-up alone is **not enough interactivity** for final product.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Leaflet & MapLibre examples
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom Base Layers
|
||||||
|
|
||||||
|
- [Stadia Maps Hosted Tiles](https://stadiamaps.com/products/map-tiles/), free for noncommercial/academic use. You will need an account though.
|
||||||
|
- [OpenMapTiles](https://openmaptiles.org/styles/) - free, meant for self hosting.
|
||||||
|
- [Mapbox](https://www.mapbox.com) - another paid solution, $$$.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Other Libraries
|
||||||
|
|
||||||
|
- [Deck.gl](https://deck.gl) - Alternative renderer similar to Map*GL. TypeScript/React focused mostly, hard to use with JS we covered.
|
||||||
|
- [OpenLayers](https://openlayers.org) -
|
||||||
|
- [Cesium](https://cesium.com) - 3D-focused geospatial.
|
||||||
|
- Note: You can also get 3D rendering w/ D3 or MapLibreGL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## More Examples
|
||||||
|
|
||||||
|
- <https://projects.fivethirtyeight.com/redistricting-maps/alabama/#Competitive>
|
||||||
|
- <https://projects.fivethirtyeight.com/partisan-gerrymandering-north-carolina/>
|
BIN
13.js-mapping/tiles.png
Normal file
BIN
13.js-mapping/tiles.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 190 KiB |
Loading…
Reference in New Issue
Block a user