Last active
May 2, 2018 19:22
-
-
Save wboykinm/ae69cce5f5b419c071bf to your computer and use it in GitHub Desktop.
Census reporter GeoJSON tiles
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
}, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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