Last active
December 19, 2015 17:58
-
-
Save milkbread/5994807 to your computer and use it in GitHub Desktop.
D3: TopoJSON: Animated demonstration on polygon aggregation I
This file contains hidden or 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> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://d3js.org/topojson.v1.min.js"></script> | |
<script src="http://cdn.leafletjs.com/leaflet-0.5.1/leaflet.js"></script> | |
<!--<script src="https://gist.github.com/milkbread/5713629/raw/RKMapping_0.4.js"></script>--> | |
<script src="http://bl.ocks.org/milkbread/raw/5779939/RKMapping_0.4.4.js"></script> <!--http://bl.ocks.org/milkbread/raw/5779939--> | |
<script src="http://bl.ocks.org/milkbread/raw/5829814/RKAggregation_1.0.js"></script> <!--http://bl.ocks.org/milkbread/raw/5829814 this function needs RKMapping.js additionally--> | |
<style> | |
@import url(http://bl.ocks.org/milkbread/raw/5957651/simplyArcDemoStyles.css); | |
@import url(http://cdn.leafletjs.com/leaflet-0.5/leaflet.css); | |
#map { | |
width: 960px; | |
height: 400px; | |
} | |
#polys:hover{ | |
fill:rgba(200,0,0,0.8); | |
} | |
#polys{ | |
stroke-width:1px; | |
stroke:rgba(255,255,255,1); | |
} | |
.overlay_poly{ | |
fill:rgba(100,100,100,0.3); | |
} | |
.overlay_multipoly{ | |
fill:rgba(0,0,0,0.7); | |
} | |
.clipped_poly{ | |
fill:rgba(255,0,0,0.7); | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Animated Demonstration of<br> Polygon Aggregation with the help of TopoJSON</h1> | |
<div id=selection></div> | |
<div id=map></div> | |
<script> | |
var minArea = 0; | |
var selCont = d3.select('#selection').append('svg').attr('width',960).attr('height',50) | |
.style('border-radius','15 15 0 0') | |
.style('-moz-border-radius','15 15 0 0'); | |
selCont.append('rect') | |
.attr('x',0).attr('y',0) | |
.attr('width',960).attr('height',50) | |
.style('fill','#c8ba09'); | |
var progressor = selCont.append('rect') | |
.attr('x',0).attr('y',0) | |
.attr('width',0).attr('height',50) | |
.style('fill','#f00') | |
.style('opacity',0.3); | |
var animReloadButton = selCont.append('text') | |
.attr('x',900).attr('y',30) | |
.style('fill','#000') | |
.text('Animate'); | |
var curVal = selCont.append('text') | |
.attr('x',10).attr('y',30) | |
.style('fill','#fff') | |
.style('text-anchor','left') | |
var clickArea = selCont.append('rect') | |
.attr('x',900).attr('y',0) | |
.attr('width',960).attr('height',50) | |
.style('opacity',0); | |
var path = d3.geo.path().projection(project); | |
//The background map | |
var map = L.map('map').setView([49.36, 7.36], 9); //#1 | |
//var map = L.map('map').setView([50.23, 7.68], 8); //#2 | |
//var map = L.map('map').setView([50.13, 7.66], 7); //#3 | |
var data_attrib = " | Data: <a href='http://www.geodatenzentrum.de/geodaten/gdz_rahmen.gdz_div?gdz_spr=eng&gdz_akt_zeile=5&gdz_anz_zeile=1&gdz_unt_zeile=18&gdz_user_id=0' target='_blank'>© GeoBasis-DE / BKG 2013</a> | <a href='http://d3js.org/'>D3.js</a> | <a href='https://github.com/mbostock/topojson/wiki'>TopoJSON</a> | <a href='http://mourner.github.io/simplify-js/'>Simplify</a>" | |
var osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution: "Map: <a href='http://www.openstreetmap.org/'>© OpenStreetMap </a>contributers" + data_attrib}); | |
var esri = L.tileLayer('http://services.arcgisonline.com/ArcGIS/rest/services/World_Physical_Map/MapServer/tile/{z}/{y}/{x}.png', {attribution: "Map: <a href='http://www.arcgis.com/home/item.html?id=c4ec722a1cd34cf0a23904aadf8923a0'>ArcGIS - World Physical Map</a>" + data_attrib}); | |
var stamen = L.tileLayer('http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png', {attribution: "Map: <a href='http://maps.stamen.com/#toner/12/37.7706/-122.3782'>Stamen Design</a>" + data_attrib}).addTo(map); | |
var baseLayers = {"stamen": stamen, "OpenStreetMap":osm, "World Physical Map":esri}; | |
var control = L.control.layers(baseLayers).addTo(map); | |
//initial D3 stuff | |
var overLayer = new mapOverlay(map); | |
console.log(overLayer.overlaySVG) | |
var infoContainer = d3.select("body").append("info").text("This will show you some information!") | |
d3.select("body").append("br") | |
var mouseInfo = d3.select("body").append("info").text("") | |
var thousendsSep = d3.format(","), maximum = 0; | |
//load data | |
d3.json("../5754788/vg250_clipped_mini_topo.json", function(error, topology) { //#1 | |
//d3.json("/milkbread/raw/5754788/vg250_clipped3_topo.json", function(error, topology) { //#2 | |
//d3.json("/milkbread/raw/5754788/vg250_clipped2_topo.json", function(error, topology) { //#3 | |
//get the pure 'data-object', containing only id, type & arcs | |
var pure_states = topology.objects.vg250_gem_clipped3; | |
//initialise a storage for the removed Features | |
var removedFeatureStorage = new remFeatureStorage(); | |
//create a feature storage | |
var featureStorage = new featureObjStorage(); | |
//store the highest value of 'SHAPE_AREA' | |
var maxVal = 0; | |
//add each pure 'data-object' to the feature storage | |
pure_states.geometries.forEach(function(d){ | |
featureStorage.addFeatObj(d) | |
if(d.properties.SHAPE_AREA>maxVal)maxVal=d.properties.SHAPE_AREA; | |
}) | |
//get all feature objects from the storage ... feature objects contain all necessary objects and functions for the aggregation | |
var featuresObjects = featureStorage.featureObjects; | |
//sort the features in dependence of their shape size | |
featuresObjects.sort(function(a,b){return (b.properties.SHAPE_AREA-a.properties.SHAPE_AREA)}) | |
//get all arc-geometries of the selected objects 'pure_states'...can be simplified directly | |
var arcCollection = getArcs(topology, featuresObjects); | |
//########AGGREGATION - MODULS######################## | |
function hoverAction(){ | |
var indicatorPositionHover=minArea; | |
curVal.text('Aggregate features smaller than ' + (minArea/1000000).toFixed(4) + ' km^2') | |
//++++++++FEATURE FILTERING and SORTING+++++++++++++++ | |
//check whose size is too small...it it is, push its id into an array | |
var filteredFeatures = []; | |
featuresObjects.forEach(function(d){ | |
if(d.properties.SHAPE_AREA < indicatorPositionHover){ | |
if(d.type=='Polygon')filteredFeatures.push([d.id,-1]) | |
else if(d.type=='MultiPolygon'){ | |
//console.log(d.geometry.length) | |
for(var i=d.geometry.length-1;i>=0;i--){ | |
filteredFeatures.push([d.id,i]) | |
} | |
//filteredFeatures.push([d.id,1]) | |
//filteredFeatures.push([d.id,0]) | |
} | |
} | |
else return; | |
}) | |
//++++++++FINISHED FILTERING --> 'filteredFeatures'++ | |
//~~~~~~~~INITIATE MAIN AGGREGATION MODULS~~~~~~~~~~~~ | |
filteredFeatures.forEach(function(d){ //aggregate each feature, whose id is stored in this array...feature objects are directly edited by this function | |
//console.log("####Begin feature aggregation#####") | |
doAggregation(d[0], d[1]) | |
//console.log("###Finished feature aggregation###") | |
}) | |
//re-show the aggreagated features | |
overLayer.removeAll(); | |
polygons = getGeometries(featuresObjects, arcCollection) | |
overLayer.addGeometries(polygons,"path"); | |
overLayer.showAll(); | |
doStyling(); | |
} | |
function doAggregation(id, indexOfMultiPolyPart){ //having this all as seperate function makes it applicable flixible...so I can use it also on click or something else | |
//console.log("indexOfMultiPolyPart: ",indexOfMultiPolyPart) | |
var featureObjectsIDs = featureStorage.getIndizes(); //stores the IDs of all features ... improves search & -speed | |
var selectedFeatureIndex = featureObjectsIDs.indexOf(id); //get the index of the current feature in the storage | |
var analysedFeature = featuresObjects[selectedFeatureIndex]; //get the current feature from the storage, position in array = 'selectedFeatureIndex' | |
//console.log("analysedFeature: ",analysedFeature, "multipolypart: ",indexOfMultiPolyPart) | |
//++++++++GET NEIGHBORS+++++++++++++++ | |
var neighbors = analysedFeature.getNeighbors(arcCollection, indexOfMultiPolyPart); //get all neighbors of the current feature ... 'indexOfMultiPolyPart' indicates the position of the chosen MultiPolygon-part | |
//console.log(neighbors) | |
//++++++++FINISHED GET NEIGHBORS --> 'neighbors'++ | |
//++++++++ANALYSE NEIGHBORS+++++++++++++++ | |
if(analysedFeature.type=='Polygon') var analysed_geometry = analysedFeature.geometry[0]; | |
else if(analysedFeature.type=='MultiPolygon') var analysed_geometry = analysedFeature.geometry[indexOfMultiPolyPart][0]; | |
var neighType = analyseNeighbors(neighbors, analysedFeature.type, analysed_geometry); //possible results: isle, hole, mesh_element, undefined | |
//console.log(neighType); | |
//++++++++FINISHED ANALYSE NEIGHBORS --> 'neighbor type'++ | |
if(neighType=='hole' || neighType=='mesh_element'){ //only go on when there are some neighbors | |
//++++++++FIND BEST FITTING PARTNER FOR AGGREGATION (NEIGHBOR)+++++++++++++++... Different solutions are possible | |
var sorted_neighbors = sortArcs(neighbors, arcCollection) | |
var aggregationPartner = {}; | |
aggregationPartner.id = neighbors[0].id; | |
aggregationPartner.direction = neighbors[0].direction; | |
aggregationPartner.arc_id = neighbors[0].arc_id; | |
aggregationPartner.index = neighbors[0].index; | |
//++++++++FINISHED SEARCH --> 'aggregationPartner'++ | |
//++++++++CONTROLL EXISTENCE & CORRECT ID OF AGGREGATION PARTNER+++++++++++++++ | |
var control = false, next = 1; | |
while (control == false){ | |
aggregationPartner = controlExistenceAndCorrect(id, aggregationPartner, arcCollection, removedFeatureStorage, sorted_neighbors, featuresObjects, featureObjectsIDs, indexOfMultiPolyPart, neighType) | |
if(aggregationPartner!=undefined) control=true; | |
else if(sorted_neighbors[next]!=undefined){aggregationPartner = getNeighborCopy(sorted_neighbors[next]); next++;} | |
else control=true; | |
} | |
//console.log("aggregation partner: ",aggregationPartner) | |
//++++++++FINISHED CONTROLL & CORRECTION --> 'aggregationPartner' with correct id+++++++++++++++ | |
if(aggregationPartner==undefined) console.log("The existence control did gave back some bogus...but why?") | |
else{ | |
//++++++++AGGREGATION - CORE+++++++++++++++ | |
var aggregationPartnerIndex = featureObjectsIDs.indexOf(aggregationPartner.id); //now we have to find the position of the corresponding feature in the array | |
var aggregationPartnerFeature = featuresObjects[aggregationPartnerIndex]; //by the found index...we get the corresponding feature | |
aggregateFeatures(analysedFeature, aggregationPartner, aggregationPartnerFeature, arcCollection, neighType); | |
//++++++++FINISHED AGGREGATION - CORE+++++++++++++++ | |
//++++++++REMOVE FROM 'allFeatures' & ADD to 'removedFeature' (analysed feature)+++++++++++++++ | |
featureStorage.removeFeatObj(analysedFeature, removedFeatureStorage, aggregationPartnerFeature, indexOfMultiPolyPart) | |
//++++++++FINISHED REMOVE & ADD+++++++++++++++ | |
} | |
}else console.log(id + " - There is no neighbor!!!") | |
} | |
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
//~~~~~~~~STANDARD VISUALISATION OF FEATURES~~~~~~~~~~ | |
//build geometries from feature and their arcs | |
polygons = getGeometries(featuresObjects, arcCollection) | |
//add the geometries to the data overlay | |
overLayer.addGeometries(polygons,"path"); | |
//build a GeometryCollection, for the visualisation of features | |
var polyCollection = {type: 'GeometryCollection', geometries:polygons}; | |
map.on("viewreset", reset); | |
doStyling(); | |
reset(); | |
console.log(overLayer) | |
clickArea.on('mousedown',reload) | |
//animation(); | |
function animation() { | |
curVal.style('opacity',0).text('Start animation...').transition().duration(500).style('opacity',1) | |
/*overLayer.features | |
.style('opacity',0) | |
.attr("transform","translate(0,0) scale(0,1)") | |
.transition().duration(5000) | |
.attr("transform","translate(0,0) scale(1,1)") | |
.style('opacity',1)*/ | |
overLayer.overlaySVG.transition() | |
.delay(2000) | |
.duration(30000) | |
.tween("precision", function() { | |
var area = d3.interpolateRound(0, 120000000); | |
var prog = d3.interpolateRound(0, 960); | |
return function(t) { | |
minArea = area(t); | |
progressor.attr('width',prog(t)) | |
hoverAction(); | |
}; | |
}) | |
/*.transition() | |
.duration(2500) | |
.each("end", animation);*/ | |
} | |
function reload(){ | |
console.log(animReloadButton.text()) | |
if(animReloadButton.text()=='Animate'){animReloadButton.text('Reload').style('fill','#c8ba09'); animation();} | |
else { | |
curVal.text('Reload!').transition().duration(2000).style('fill','#f00') | |
overLayer.overlaySVG.transition() | |
.duration(5000) | |
.tween("precision", function() { | |
var prog = d3.interpolate(1, 0); | |
return function(t) { | |
overLayer.features.attr("transform","translate(0,0) scale("+prog(t)+",1)") | |
if(prog(t)==0)window.location.reload(); | |
}; | |
}) | |
} | |
} | |
//additional function for aggregation on click | |
function visualiseFeatures(data){ | |
//This is a test on detecting which part of a MultiPolygon has to be edited ... !!!HAS TO BE REPLACED!!! against a check who fits to the used criterium ... currently the criterium is the click! | |
//console.log(data, d3.geo.bounds(data),d3.mouse(this)); | |
var mouse_coords = map.layerPointToLatLng(d3.mouse(this)) | |
//console.log(mouse_coords, mouse_coords.lat) | |
var fixed_multiPolyIndex = -1; //this variable is only defined for MultiPolygons and tells me which one of the 'multi'-possibilities the selected on is...by its index | |
if(data.type=='MultiPolygon')data.coordinates.forEach(function(d,i){ | |
var this_bounds = d3.geo.bounds({type: 'Polygon', coordinates: d}) | |
////console.log(this_bounds) | |
if((mouse_coords.lng>this_bounds[0][0] && mouse_coords.lng<this_bounds[1][0]) && (mouse_coords.lat>this_bounds[0][1] && mouse_coords.lat<this_bounds[1][1]))fixed_multiPolyIndex = i; | |
}) | |
//++++++++++++++ | |
overLayer.removeAll(); | |
doAggregation(data.id, fixed_multiPolyIndex); | |
polygons = getGeometries(featuresObjects, arcCollection) | |
overLayer.addGeometries(polygons,"path"); | |
overLayer.showAll(); | |
doStyling(); | |
} | |
//###Basic functions ... that are not related to the polygonal aggregation! | |
function doStyling(){ | |
overLayer.features.attr("class",function(d){if(d.clipped==true) return "clipped_poly"; else {if(d.type=="Polygon")return "overlay_poly"; else if(d.type=="MultiPolygon") return "overlay_multipoly"; }}) | |
.attr("id","polys") | |
.on("mouseover",showInfo) | |
.on("mouseup",visualiseFeatures); | |
} | |
function reset(){ | |
var start_time = Date.now(); | |
overLayer.resetView(d3.geo.bounds(polyCollection)); | |
overLayer.showAll(); | |
console.log("initial load took: "+((Date.now() - start_time)/1000).toFixed(2)+"s") | |
} | |
function showInfo(value){ | |
mouseInfo.text("Center: "+map.getCenter()+" Zoom: "+map.getZoom()) | |
infoContainer.text("You hovered over: "+value.properties.GEN +' - Type: '+value.type +' - ID: '+value.id +' - Area: ' + thousendsSep((value.properties.SHAPE_AREA/1000000).toFixed(4))) | |
//console.log(' - neighbors: ', polygons_indexed[value.id].neighbors) | |
} | |
function zoomToObject(d){ | |
var local_bounds = d3.geo.bounds(d), | |
center = new L.LatLng(local_bounds[0][1]+((local_bounds[1][1]-local_bounds[0][1])/2),local_bounds[0][0]+((local_bounds[1][0]-local_bounds[0][0])/2)), | |
southWest = new L.LatLng(local_bounds[0][1], local_bounds[0][0]), | |
northEast = new L.LatLng(local_bounds[1][1], local_bounds[1][0]); | |
var local_bounds = new L.LatLngBounds(southWest, northEast); | |
map.setView( center, map.getBoundsZoom( local_bounds)); | |
} | |
}) | |
function reShow(maxVal){ | |
maximum = maxVal; | |
overLayer.showAllFiltered(maxVal); | |
} | |
function project(point) { | |
var latlng = new L.LatLng(point[1], point[0]); | |
var layerPoint = map.latLngToLayerPoint(latlng); | |
return [layerPoint.x, layerPoint.y]; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment