Skip to content

Instantly share code, notes, and snippets.

@milkbread
Last active December 19, 2015 17:58
Show Gist options
  • Save milkbread/5994807 to your computer and use it in GitHub Desktop.
Save milkbread/5994807 to your computer and use it in GitHub Desktop.
D3: TopoJSON: Animated demonstration on polygon aggregation I
<!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'>&copy; 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/'>&copy; 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