Skip to content

Instantly share code, notes, and snippets.

@reyemtm
Last active November 26, 2019 13:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save reyemtm/8623e996d31ef4f8b9fd57dfb5d9b319 to your computer and use it in GitHub Desktop.
Save reyemtm/8623e996d31ef4f8b9fd57dfb5d9b319 to your computer and use it in GitHub Desktop.
County Parcel Viewer | Leaflet Vector Grid

Pushing the limits of client-side rendering with Leaflet, this map uses the Leaflet.VectorGrid plugin and JSZip to create a county parcel viewer. The county-wide parcel coverage easily loads and renders in a desktop environment. On mobile, the app will load, albeit in 20-30 seconds. Once the data is loaded, however, the app works well on both platforms. I believe the parcel coverage contains over 70k polygons, and is 15MB unzipped.

The original parcel data was slightly simplified using mapshaper and converted to topojson.

<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>County Parcel Viewer | Leaflet Vector Grid</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.vectorgrid@latest/dist/Leaflet.VectorGrid.bundled.js"></script>
<script src="https://api.mapbox.com/mapbox.js/plugins/leaflet-hash/v0.2.1/leaflet-hash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.4/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.0.2/jszip-utils.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js" integrity="sha256-PieqE0QdEDMppwXrTzSZQr6tWFX3W5KkyRVyF1zN3eg=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.Spin/1.1.0/leaflet.spin.min.js"></script>
<style>
body {
margin: 0;
padding: 0;
background: black;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
#control {
margin: 10px;
border: 2px solid rgba(0,0,0,0.2);
padding: 10px;
border-radius: 5px;
background: white;
width: auto;
background-clip: padding-box;
position: absolute;
bottom: 0;
}
</style>
</head>
<body>
<div id='map'>
<div id='control' class="leaflet-control"></div>
</div>
<script>
var options = {
center: [39.723, -82.598],
zoom: 13,
minZoom: 1,
maxZoom: 19,
},
map = L.map('map', options); // Create map object and pass it our options object from above
var tiles = L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="http://cartodb.com/attributions">CartoDB</a> | Map Authored by <a href="https://getbounds.com">Malcolm Meyer</a>',
subdomains: 'abcd',
maxZoom: 19
}).addTo(map);
L.hash(map);
var opts = {
lines: 15, // The number of lines to draw
length: 38, // The length of each line
width: 3, // The line thickness
radius: 30, // The radius of the inner circle
scale: 1, // Scales overall size of the spinner
corners: 0.5, // Corner roundness (0..1)
color: 'white', // #rgb or #rrggbb or array of colors
opacity: 0.5, // Opacity of the lines
};
var step1, step2, step3, step4;
var largeLayerAdd = 0;
var id = 0;
var url = "https://reyemtm.github.io/data/topojson/";
var colors = ["goldenrod", "goldenrod"];
var c = 0;
var layerName;
var largeLayer = new L.geoJSON();
var x = 0;
var build = 1;
var layerControl = new L.control.layers(null, {
"15MB Layer (3MB Zip)": largeLayer
}, {
collapsed: false
}).addTo(map);
map.on('overlayadd', function(e) {
if (build == 0 && e.name != "15MB Layer (3MB Zip)") {
map.spin(true, opts);
}
if (largeLayerAdd == 0 && e.name == "15MB Layer (3MB Zip)") {
buildLayer("fairfieldCounty.zip", largeLayer);
largeLayerAdd = 1;
return false;
}
});
map.on('overlayremove', function() {
map.closePopup();
})
buildLayer("lancaster.zip", null);
function buildLayer(file, placeholder) {
build = 1;
x = 0;
map.closePopup();
map.spin(true, opts);
var now = Date.now();
var layerData;
file = url + file;
/*load and extract topojson form zip file*/
JSZipUtils.getBinaryContent(file, function(err, data) {
if (err) {
throw err; // or handle err
}
JSZip.loadAsync(data)
.then(function(data) {
step1 = Date.now();
document.getElementById('control').innerHTML = "Loaded: " + ((step1 - now)/1000).toFixed(2) + " seconds";
console.log(data);
for (file in data.files) {
var inner = file;
console.log(inner);
data.file(inner).async("string")
.then(function(string) {
layerData = JSON.parse(string);
//console.log(layerData);
map.eachLayer(function(layer) {
if (layer["_vectorTiles"]) {
map.removeLayer(layer)
}
});
for (name in layerData.objects) {
layerName = name;
console.log(name);
break
}
console.log("step1", step1)
step2 = Date.now();
document.getElementById('control').innerHTML += "<br>Unzipped: " + ((step2 - step1)/1000).toFixed(2) + " seconds";
var layerDataGeoJSON = topojson.feature(layerData, layerData.objects[layerName]);
//console.log(layerDataGeoJSON);
step3 = Date.now();
document.getElementById('control').innerHTML += "<br>Topo to GeoJSON: " + ((step3-step2)/1000).toFixed(2) + " seconds";
var tileLayer = L.vectorGrid.slicer(layerDataGeoJSON, {
rendererFactory: L.canvas.tile,
vectorTileLayerStyles: {
sliced: {
fillColor: "transparent",
color: colors[1],
weight: 0.5
}
},
maxZoom: 22,
indexMaxZoom: 5,
interactive: true,
getFeatureId: function(feature) {
var props = (feature.properties.index).split("|");
//console.log(props[0]);
return props[1]
}
});
if (placeholder) {
layerControl.removeLayer(placeholder);
}
layerControl.addOverlay(tileLayer, layerName);
tileLayer.addTo(map);
tileLayer.on('click', function(e) {
//console.log(e);
if (e.layer.feature) {
var props = e.layer.feature.properties;
var latlng = [e.latlng.lat,e.latlng.lng];
}else{
var props = e.layer.properties;
var latlng = [Number(e.latlng.lat),Number(e.latlng.lng)];
}
if (id != 0) {
tileLayer.setFeatureStyle(id, {
color: colors[1],
weight: .5,
});
}
var index = (props.index).split("|");
id = index[1];
var popup = "<h4>" + index[0] + "</h4>" + index[1];
setTimeout(function() {
tileLayer.setFeatureStyle(id, {
color: "red"
}, 100);
map.openPopup(popup, latlng);
});
})
map.on('popupclose', function() {
tileLayer.setFeatureStyle(id, {
color: colors[1],
weight: 0.5
});
});
tileLayer.on('load', function() {
console.log('tiles loaded');
if (x == 0) {
step4 = ((Date.now() - step3) / 1000) + " seconds";
document.getElementById('control').innerHTML += "<br>Sliced and Rendered: " + step4;
}
x = 1;
map.spin(false);
build = 0;
});
});
return false;
}
});
});
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment