Skip to content

Instantly share code, notes, and snippets.

@wboykinm
Last active May 2, 2018 19:22
Show Gist options
  • Save wboykinm/ae69cce5f5b419c071bf to your computer and use it in GitHub Desktop.
Save wboykinm/ae69cce5f5b419c071bf to your computer and use it in GitHub Desktop.
Census reporter GeoJSON tiles
CensusReporter = {
GeoIDLayer: L.GeoJSON.extend({
addGeoID: function(geoid) {
var request = new XMLHttpRequest();
var url = this.options.api_url + "/1.0/geo/show/tiger2013?geo_ids=" + geoid;
request.open('GET', url, true);
var self = this;
request.onreadystatechange = function() {
if (this.readyState === 4) {
if (this.status >= 200 && this.status < 400) {
// Success!
var data = JSON.parse(this.responseText);
self.addData(data);
} else {
if (typeof(console) != 'undefined' && typeof(console.log) == 'function') {
console.log("Error ("+this.status+") getting data")
console.log(this.responseText)
}
}
}
};
request.send();
request = null;
},
removeGeoID: function(geoid) {
var layers = this.getLayers();
for (var i = 0; i < layers.length; i++) {
if (layers[i].feature.properties.geoid == geoid) {
this.removeLayer(layers[i]);
return layers[i];
}
}
return null;
},
initialize: function(geoid_spec, options) {
L.GeoJSON.prototype.initialize.call(this);
var options = L.extend({
api_url: 'http://api.censusreporter.org',
censusreporter_url: 'http://censusreporter.org',
autoclick: true
}, options);
if (options.autoclick) {
var censusreporter_url = options.censusreporter_url;
var autoclick_handler = function(data, layer) {
layer.on('click',function() {
window.open(censusreporter_url + "/profiles/" + data.properties.geoid);
});
}
if (options.onEachFeature) {
var old_handler = options.onEachFeature;
options.onEachFeature = function(data, layer) {
old_handler(data, layer);
autoclick_handler(data, layer);
}
} else {
options.onEachFeature = autoclick_handler;
}
}
this.options = options;
this.addGeoID(geoid_spec);
}
})
}
CensusReporter.AjaxLayer = L.TileLayer.extend({
_requests: [],
_addTile: function (tilePoint) {
var tile = { datum: null, processed: false };
this._tiles[tilePoint.x + ':' + tilePoint.y] = tile;
this._loadTile(tile, tilePoint);
},
// XMLHttpRequest handler; closure over the XHR object, the layer, and the tile
_xhrHandler: function (req, layer, tile, tilePoint) {
return function () {
if (req.readyState !== 4) {
return;
}
var s = req.status;
if ((s >= 200 && s < 300) || s === 304) {
tile.datum = JSON.parse(req.responseText);
layer._tileLoaded(tile, tilePoint);
} else {
layer._tileLoaded(tile, tilePoint);
}
};
},
// Load the requested tile via AJAX
_loadTile: function (tile, tilePoint) {
this._adjustTilePoint(tilePoint);
var layer = this;
var req = new XMLHttpRequest();
this._requests.push(req);
req.onreadystatechange = this._xhrHandler(req, layer, tile, tilePoint);
req.open('GET', this.getTileUrl(tilePoint), true);
req.send();
},
_reset: function () {
L.TileLayer.prototype._reset.apply(this, arguments);
for (var i in this._requests) {
this._requests[i].abort();
}
this._requests = [];
},
_update: function () {
if (this._map._panTransition && this._map._panTransition._inProgress) { return; }
if (this._tilesToLoad < 0) { this._tilesToLoad = 0; }
L.TileLayer.prototype._update.apply(this, arguments);
}
});
CensusReporter.GeoJSONLayer = CensusReporter.AjaxLayer.extend({
// Store each GeometryCollection's layer by key, if options.unique function is present
_keyLayers: {},
// Used to calculate svg path string for clip path elements
_clipPathRectangles: {},
initialize: function (url, options, geojsonOptions) {
CensusReporter.AjaxLayer.prototype.initialize.call(this, url, options);
this.geojsonLayer = new L.GeoJSON(null, geojsonOptions);
},
onAdd: function (map) {
this._map = map;
CensusReporter.AjaxLayer.prototype.onAdd.call(this, map);
map.addLayer(this.geojsonLayer);
},
onRemove: function (map) {
map.removeLayer(this.geojsonLayer);
CensusReporter.AjaxLayer.prototype.onRemove.call(this, map);
},
_reset: function () {
this.geojsonLayer.clearLayers();
this._keyLayers = {};
this._removeOldClipPaths();
CensusReporter.AjaxLayer.prototype._reset.apply(this, arguments);
},
// Remove clip path elements from other earlier zoom levels
_removeOldClipPaths: function () {
for (var clipPathId in this._clipPathRectangles) {
var clipPathZXY = clipPathId.split('_').slice(1);
var zoom = parseInt(clipPathZXY[0], 10);
if (zoom !== this._map.getZoom()) {
var rectangle = this._clipPathRectangles[clipPathId];
this._map.removeLayer(rectangle);
var clipPath = document.getElementById(clipPathId);
if (clipPath !== null) {
clipPath.parentNode.removeChild(clipPath);
}
delete this._clipPathRectangles[clipPathId];
}
}
},
// Recurse LayerGroups and call func() on L.Path layer instances
_recurseLayerUntilPath: function (func, layer) {
if (layer instanceof L.Path) {
func(layer);
}
else if (layer instanceof L.LayerGroup) {
// Recurse each child layer
layer.getLayers().forEach(this._recurseLayerUntilPath.bind(this, func), this);
}
},
_clipLayerToTileBoundary: function (layer, tilePoint) {
// Only perform SVG clipping if the browser is using SVG
if (!L.Path.SVG) { return; }
var svg = this._map._pathRoot;
// create the defs container if it doesn't exist
var defs = null;
if (svg.getElementsByTagName('defs').length === 0) {
defs = document.createElementNS(L.Path.SVG_NS, 'defs');
svg.insertBefore(defs, svg.firstChild);
}
else {
defs = svg.getElementsByTagName('defs')[0];
}
// Create the clipPath for the tile if it doesn't exist
var clipPathId = 'tileClipPath_' + tilePoint.z + '_' + tilePoint.x + '_' + tilePoint.y;
var clipPath = document.getElementById(clipPathId);
if (clipPath === null) {
clipPath = document.createElementNS(L.Path.SVG_NS, 'clipPath');
clipPath.id = clipPathId;
// Create a hidden L.Rectangle to represent the tile's area
var tileSize = this.options.tileSize,
nwPoint = tilePoint.multiplyBy(tileSize),
sePoint = nwPoint.add([tileSize, tileSize]),
nw = this._map.unproject(nwPoint),
se = this._map.unproject(sePoint);
this._clipPathRectangles[clipPathId] = new L.Rectangle(new L.LatLngBounds([nw, se]), {
opacity: 0,
fillOpacity: 0,
clickable: false,
noClip: true
});
this._map.addLayer(this._clipPathRectangles[clipPathId]);
// Add a clip path element to the SVG defs element
// With a path element that has the hidden rectangle's SVG path string
var path = document.createElementNS(L.Path.SVG_NS, 'path');
var pathString = this._clipPathRectangles[clipPathId].getPathString();
path.setAttribute('d', pathString);
clipPath.appendChild(path);
defs.appendChild(clipPath);
}
// Add the clip-path attribute to reference the id of the tile clipPath
this._recurseLayerUntilPath(function (pathLayer) {
pathLayer._container.setAttribute('clip-path', 'url(#' + clipPathId + ')');
}, layer);
},
// Add a geojson object from a tile to the GeoJSON layer
// * If the options.unique function is specified, merge geometries into GeometryCollections
// grouped by the key returned by options.unique(feature) for each GeoJSON feature
// * If options.clipTiles is set, and the browser is using SVG, perform SVG clipping on each
// tile's GeometryCollection
addTileData: function (geojson, tilePoint) {
var features = L.Util.isArray(geojson) ? geojson : geojson.features,
i, len, feature;
if (features) {
for (i = 0, len = features.length; i < len; i++) {
// Only add this if geometry or geometries are set and not null
feature = features[i];
if (feature.geometries || feature.geometry || feature.features || feature.coordinates) {
this.addTileData(features[i], tilePoint);
}
}
return this;
}
var options = this.geojsonLayer.options;
if (options.filter && !options.filter(geojson)) { return; }
var parentLayer = this.geojsonLayer;
var incomingLayer = null;
if (this.options.unique && typeof(this.options.unique) === 'function') {
var key = this.options.unique(geojson);
// When creating the layer for a unique key,
// Force the geojson to be a geometry collection
if (!(key in this._keyLayers && geojson.geometry.type !== 'GeometryCollection')) {
geojson.geometry = {
type: 'GeometryCollection',
geometries: [geojson.geometry]
};
}
// Transform the geojson into a new Layer
try {
incomingLayer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng);
}
// Ignore GeoJSON objects that could not be parsed
catch (e) {
return this;
}
// Add the incoming Layer to existing key's GeometryCollection
if (key in this._keyLayers) {
parentLayer = this._keyLayers[key];
parentLayer.feature.geometry.geometries.push(geojson.geometry);
}
// Convert the incoming GeoJSON feature into a new GeometryCollection layer
else {
incomingLayer.feature = L.GeoJSON.asFeature(geojson);
this._keyLayers[key] = incomingLayer;
}
}
// Add the incoming geojson feature to the L.GeoJSON Layer
else {
// Transform the geojson into a new layer
try {
incomingLayer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng);
}
// Ignore GeoJSON objects that could not be parsed
catch (e) {
return this;
}
incomingLayer.feature = L.GeoJSON.asFeature(geojson);
}
incomingLayer.defaultOptions = incomingLayer.options;
this.geojsonLayer.resetStyle(incomingLayer);
if (options.onEachFeature) {
options.onEachFeature(geojson, incomingLayer);
}
parentLayer.addLayer(incomingLayer);
// If options.clipTiles is set and the browser is using SVG
// then clip the layer using SVG clipping
if (this.options.clipTiles) {
this._clipLayerToTileBoundary(incomingLayer, tilePoint);
}
return this;
},
_tileLoaded: function (tile, tilePoint) {
CensusReporter.AjaxLayer.prototype._tileLoaded.apply(this, arguments);
if (tile.datum === null) { return null; }
this.addTileData(tile.datum, tilePoint);
}
});
CensusReporter.SummaryLevelLayer = CensusReporter.GeoJSONLayer.extend({
summary_levels: {
'020': 'region',
'030': 'division',
'040': 'state',
'050': 'county',
'060': 'county subdivision',
'140': 'census tract',
'150': 'block group',
'160': 'place',
'170': 'consolidated city',
'230': 'Alaska native regional corporation',
'250': 'native area',
'251': 'tribal subdivision',
'256': 'tribal tract',
'310': 'metro area (CBSA)',
'314': 'metropolitan division',
'330': 'combined statistical area',
'335': 'combined NECTA',
'350': 'NECTA',
'364': 'NECTA division',
'400': 'urban area',
'500': 'congressional district',
'610': 'state house (upper)',
'620': 'state house (lower)',
'795': 'PUMA',
'860': 'ZIP code',
'950': 'school district (elementary)',
'960': 'school district (secondary)',
'970': 'school district (unified)'
},
_defaultOptions: {
clipTiles: true,
unique: function(feature) {
return feature.properties.geoid;
}
},
_defaultFeatureStyle: {
"clickable": true,
"color": "#00d",
"fillColor": "#ccc",
"weight": 1.0,
"opacity": 0.3,
"fillOpacity": 0.3,
},
_defaultGeojsonOptions: {
onEachFeature: function(feature, layer) {
// you can wire behavior to each "feature", or place outline.
var profileURL = 'http://censusreporter.org/profiles/' + feature.properties.geoid;
layer.bindPopup("<a href='" + profileURL + "'>" + feature.properties.name + "</a>");
if (this.style && this.mouseoverStyle) {
layer.on('mouseover', function() {
layer.setStyle(this.mouseoverStyle);
});
layer.on('mouseout', function() {
layer.setStyle(this.style);
});
}
}
},
initialize: function (summary_level, options, geojsonOptions) {
if (typeof(this.summary_levels[summary_level]) == "undefined") {
throw "Unsupported or invalid summary level."
}
var url = 'http://embed.censusreporter.org/1.0/geo/tiger2013/tiles/' + summary_level + '/{z}/{x}/{y}.geojson';
options = L.Util.extend(this._defaultOptions, options);
geojsonOptions = L.Util.extend(this._defaultGeojsonOptions, geojsonOptions);
if (!('style' in geojsonOptions)) {
geojsonOptions.style = this._defaultFeatureStyle;
}
CensusReporter.GeoJSONLayer.prototype.initialize.call(this, url, options, geojsonOptions);
if ('style' in geojsonOptions) {
this.style = geojsonOptions.style;
}
if ('mouseoverStyle' in options) {
this.mouseoverStyle = options.mouseoverStyle;
}
},
});
<!DOCTYPE html>
<html>
<head>
<style>
html, body, #map {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
.menu-ui {
position:absolute;
top:10px;
right:10px;
z-index: 9;
}
.reference-pane {
pointer-events: none;
}
</style>
<title>Example of using Census Reporter GeoJSON tiles on your own maps.</title>
</head>
<body>
<div id='coverageList' class='menu-ui'>
<select id="sumlev-picker">
<option> -- pick a summary level -- </option>
</select>
</div>
<div id="map"></div>
<script src='https://api.tiles.mapbox.com/mapbox.js/v2.1.6/mapbox.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox.js/v2.1.6/mapbox.css' rel='stylesheet' />
<script src="//cdn.jsdelivr.net/g/jquery@1.11.1,underscorejs"></script>
<script src='https://api.tiles.mapbox.com/mapbox.js/plugins/leaflet-hash/v0.2.1/leaflet-hash.js'></script>
<script src="cr-leaflet.js"></script>
<script type="text/javascript">
// make it big
var sumlevs = [
{ 'level': '020', 'name': 'region', zoom: 4 },
{ 'level': '030', 'name': 'division', zoom: 4 },
{ 'level': '040', 'name': 'state', zoom: 4 },
{ 'level': '050', 'name': 'county', zoom: 7 },
{ 'level': '060', 'name': 'county subdivision', zoom: 7 },
{ 'level': '140', 'name': 'census tract', zoom: 10 },
{ 'level': '150', 'name': 'block group', zoom: 15 },
{ 'level': '160', 'name': 'place', zoom: 9 },
{ 'level': '170', 'name': 'consolidated city' },
{ 'level': '230', 'name': 'Alaska native regional corporation' },
{ 'level': '250', 'name': 'native area', zoom: 4 },
{ 'level': '251', 'name': 'tribal subdivision', zoom: 4 },
{ 'level': '256', 'name': 'tribal tract', zoom: 4 },
{ 'level': '310', 'name': 'metro (CBSA) area', zoom: 7 },
{ 'level': '314', 'name': 'metropolitan division', zoom: 4 },
{ 'level': '330', 'name': 'combined statistical area', zoom: 4 },
{ 'level': '335', 'name': 'combined NECTA', zoom: 4 },
{ 'level': '350', 'name': 'NECTA', zoom: 4 },
{ 'level': '364', 'name': 'NECTA division', zoom: 4 },
{ 'level': '400', 'name': 'urban area', zoom: 7 },
{ 'level': '500', 'name': 'congressional district', zoom: 7 },
{ 'level': '610', 'name': 'state house (upper)', zoom: 7 },
{ 'level': '620', 'name': 'state house (lower)', zoom: 7 },
{ 'level': '795', 'name': 'PUMA', zoom: 8 },
{ 'level': '860', 'name': 'ZIP code', zoom: 12 },
{ 'level': '950', 'name': 'school district (elementary)', zoom: 10 },
{ 'level': '960', 'name': 'school district (secondary)', zoom: 10 },
{ 'level': '970', 'name': 'school district (unified)', zoom: 10 }
]
var div = document.getElementById("map");
div.style.height = (window.innerHeight) + "px";
L.mapbox.accessToken = 'pk.eyJ1IjoiZmFyYWRheTIiLCJhIjoiTUVHbDl5OCJ9.buFaqIdaIM3iXr1BOYKpsQ';
var map = L.mapbox.map('map','faraday2.cbab4841')
map.setView([39.833333, -98.583333], 5);
var defaultStyle = {
"clickable": true,
"color": "#00d",
"fillColor": "#ccc",
"weight": 1.0,
"opacity": 0.3,
"fillOpacity": 0.3,
};
var makeLayer = function(sumlev) {
var geojsonTileLayer = new CensusReporter.SummaryLevelLayer(sumlev);
return geojsonTileLayer;
}
_.each(sumlevs,function(l) {
$('<option>').val(l.level).text(l.level + " - " + l.name).appendTo('#sumlev-picker');
});
$("#sumlev-picker").change(function(e) {
var sumlev = _.findWhere(sumlevs,{level: $(e.target).val()})
if (sumlev) {
if (sumlev.zoom) {
map.setZoom(sumlev.zoom);
}
if (typeof sumlev.layer == 'undefined') {
sumlev.layer = makeLayer(sumlev.level);
}
_.each(sumlevs,function(sl) {
if (sl.layer && map.hasLayer(sl.layer)) {
map.removeLayer(sl.layer);
}
})
map.addLayer(sumlev.layer);
}
});
var referencePane = L.DomUtil.create('div', 'reference-pane', map.getPanes()
.mapPane);
var referenceLayer = new L.mapbox.tileLayer('faraday2.2d480026')
.addTo(map);
referencePane.appendChild(referenceLayer.getContainer());
referenceLayer.setZIndex(7);
var hash = L.hash(map);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment