Skip to content

Instantly share code, notes, and snippets.

@christophermanning
Last active September 13, 2019 10:31
Show Gist options
  • Save christophermanning/6203413 to your computer and use it in GitHub Desktop.
Save christophermanning/6203413 to your computer and use it in GitHub Desktop.
CTA Line Simplification
/*
Copyright (c) 2013, Vladimir Agafonkin
Simplify.js is a high-performance JS polyline simplification library
mourner.github.io/simplify-js
*/
(function (global, undefined) {
"use strict";
// to suit your point format, run search/replace for '[0]' and '[1]';
// to switch to 3D, uncomment the lines in the next 2 functions
// (configurability would draw significant performance overhead)
function getSquareDistance(p1, p2) { // square distance between 2 points
var dx = p1[0] - p2[0],
// dz = p1.z - p2.z,
dy = p1[1] - p2[1];
return dx * dx +
// dz * dz +
dy * dy;
}
function getSquareSegmentDistance(p, p1, p2) { // square distance from a point to a segment
var x = p1[0],
y = p1[1],
// z = p1.z,
dx = p2[0] - x,
dy = p2[1] - y,
// dz = p2.z - z,
t;
if (dx !== 0 || dy !== 0) {
t = ((p[0] - x) * dx +
// (p.z - z) * dz +
(p[1] - y) * dy) /
(dx * dx +
// dz * dz +
dy * dy);
if (t > 1) {
x = p2[0];
y = p2[1];
// z = p2.z;
} else if (t > 0) {
x += dx * t;
y += dy * t;
// z += dz * t;
}
}
dx = p[0] - x;
dy = p[1] - y;
// dz = p.z - z;
return dx * dx +
// dz * dz +
dy * dy;
}
// the rest of the code doesn't care for the point format
// basic distance-based simplification
function simplifyRadialDistance(points, sqTolerance) {
var i,
len = points.length,
point,
prevPoint = points[0],
newPoints = [prevPoint];
for (i = 1; i < len; i++) {
point = points[i];
if (getSquareDistance(point, prevPoint) > sqTolerance) {
newPoints.push(point);
prevPoint = point;
}
}
if (prevPoint !== point) {
newPoints.push(point);
}
return newPoints;
}
// simplification using optimized Douglas-Peucker algorithm with recursion elimination
function simplifyDouglasPeucker(points, sqTolerance) {
var len = points.length,
MarkerArray = (typeof Uint8Array !== undefined + '')
? Uint8Array
: Array,
markers = new MarkerArray(len),
first = 0,
last = len - 1,
i,
maxSqDist,
sqDist,
index,
firstStack = [],
lastStack = [],
newPoints = [];
markers[first] = markers[last] = 1;
while (last) {
maxSqDist = 0;
for (i = first + 1; i < last; i++) {
sqDist = getSquareSegmentDistance(points[i], points[first], points[last]);
if (sqDist > maxSqDist) {
index = i;
maxSqDist = sqDist;
}
}
if (maxSqDist > sqTolerance) {
markers[index] = 1;
firstStack.push(first);
lastStack.push(index);
firstStack.push(index);
lastStack.push(last);
}
first = firstStack.pop();
last = lastStack.pop();
}
for (i = 0; i < len; i++) {
if (markers[i]) {
newPoints.push(points[i]);
}
}
return newPoints;
}
// both algorithms combined for awesome performance
function simplify(points, tolerance, highestQuality) {
var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1;
points = highestQuality ? points : simplifyRadialDistance(points, sqTolerance);
points = simplifyDouglasPeucker(points, sqTolerance);
return points;
};
// export either as a Node.js module, AMD module or a global browser variable
if (typeof exports === 'object') {
module.exports = simplify;
} else if (typeof define === 'function' && define.amd) {
define(function () {
return simplify;
});
} else {
global.simplify = simplify;
}
}(this));

Created by Christopher Manning

Summary

Line simplification of the CTA train routes. Different line interpolations and simplification levels create interesting designs.

I created this because I like line simplification, d3.js, the CTA train map, and abstract geometric shapes.

extract_gtfs.rb extracts the train lines from a GTFS zip file and creates a GeoJSON file.

Controls

  • Zoom (mousewheel, double click, or pinch) adjusts the simplification percentage.

References

Run this gist at bl.ocks.org

Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
# https://github.com/nerdEd/gtfs
require 'gtfs'
require 'json'
routes = {}
# http://www.gtfs-data-exchange.com/agency/chicago-transit-authority/
filename_gtfs = ARGV[0]
puts "loading #{filename_gtfs}"
source = GTFS::Source.build(filename_gtfs)
# type == "1" for train
source.routes.select{|r| r.type == "1"}.each do |r|
puts "reading route #{r.id} #{r.color}"
routes[r.id] = {route_color: r.color} if routes[r.id] == nil
# unique shapes for all trips on a route
source.trips.select{|t| t.route_id == r.id}.each do |t|
next unless routes[r.id][:coordinates] == nil
# shapes
puts "reading shape #{t.shape_id}"
coordinates = []
source.shapes.select{|s| s.id == t.shape_id }.each do |s|
coordinates << [s.pt_lon, s.pt_lat]
end
routes[r.id][:stops] = {}
routes[r.id][:coordinates] = coordinates
# stops
puts "reading stops"
source.stop_times.select{|st| st.trip_id == t.id}.each do |st|
next unless routes[r.id][:stops][st.stop_id] == nil
source.stops.select{|s| s.id == st.stop_id}.each do |s|
puts "reading stop #{s.id} #{s.name}"
routes[r.id][:stops][st.stop_id] = { :coordinates => [s.lon, s.lat], :name => s.name }
end
end
end
end
geojson = {
"type" => "FeatureCollection",
"features" => routes.map{ |route_id, d|
[{
"type" => "Feature",
"geometry" => {
"type" => "LineString",
"coordinates" => d[:coordinates]
},
"properties" => {
"route_id" => route_id,
"route_color" => d[:route_color],
}
}] + d[:stops].map{ |stop_id, s|
{
"type" => "Feature",
"geometry" => {
"type" => "Point",
"coordinates" => s[:coordinates]
},
"properties" => {
"name" => s[:name],
"stop_id" => stop_id,
"route_id" => route_id,
}
}
}
}.flatten
}
filename_json = "#{__dir__}/#{File.basename(filename_gtfs, ".zip")}.json"
File.write(filename_json, geojson.to_json)
puts "saved train lines to #{filename_json}"
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CTA Line Simplification</title>
<style type="text/css">
body {
padding: 0;
margin: 0;
}
path {
stroke-linejoin: round;
fill: none;
}
circle {
fill: white;
stroke: black;
display: none;
stroke-width: 1px;
}
</style>
</head>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js"></script>
<script src="readme-simplify.js"></script>
<script type="text/javascript">
config = {"simplification": 9, "interpolation" : "cardinal", "strokeWidth": 4, "showStops": false};
gui = new dat.GUI();
var examples = gui.addFolder('Examples');
examples.open()
config.random = function(){
gui.__controllers.forEach(function(c){
if(typeof(c.__select) != 'undefined') {
c.setValue(c.__select[Math.floor(Math.random()*(c.__select.length-1))].value)
} else {
if(c.property!="random" && c.property!="strokeWidth" && c.property!="showStops"){
c.setValue(Math.floor(Math.random() * c.__max) + c.__min)
}
}
})
draw()
}
examples.add(config, "random")
config.accurate = function(){
config["interpolation"] = "linear"
config["simplification"] = 0
draw()
}
examples.add(config, "accurate")
config.curly = function(){
config["interpolation"] = "cardinal"
config["simplification"] = 9
draw()
}
examples.add(config, "curly")
config.minimal = function(){
config["interpolation"] = "linear"
config["simplification"] = 100
draw()
}
examples.add(config, "minimal")
maxSimplification = 53
simplificationChanger = gui.add(config, "simplification", 0, 100).step(.1).listen()
simplificationChanger.onChange(function(value) {
draw()
});
interpolationChanger = gui.add(config, "interpolation", ["linear", "step-before", "step-after", "basis", "basis-open", "basis-closed", "cardinal", "cardinal-open", "cardinal-closed", "monotone"]).listen()
interpolationChanger.onChange(function(value) {
draw()
})
strokeWidthChanger = gui.add(config, "strokeWidth", 1, 20).listen()
strokeWidthChanger.onChange(function(value) {
d3.selectAll(".routes path").style("stroke-width", value)
d3.selectAll(".stops circle").attr("r", value/2)
});
showStopsChanger = gui.add(config, "showStops").listen()
showStopsChanger.onChange(function(value) {
d3.selectAll(".stops circle").style("display", value ? "block" : "none")
});
width = window.innerWidth
height = window.innerHeight - 5
line = d3.svg.line()
.interpolate(config["interpolation"])
zoom = d3.behavior.zoom()
.scaleExtent([0, 100])
.on("zoom", function () {
config["simplification"] = d3.event.scale
draw()
})
svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom)
var projection = d3.geo.mercator()
.scale(54596)
.translate([83920, 44300])
d3.json("chicago-transit-authority_20121228_1318.json", function(json) {
routes = svg.append("g").attr("class", "routes").selectAll("path").data(json.features.filter(function(d) { return d["geometry"]["type"] == "LineString" }))
routes.enter().append("path")
.attr("id", function(d) { return d.properties.route_id })
.style("stroke", function(d) { return "#"+d.properties.route_color })
.style("stroke-width", config["strokeWidth"])
.on("mouseover", function(d) {
d3.select(this).style("stroke-width", config["strokeWidth"] * 2)
})
.on("mouseout", function(d) {
d3.select(this).style("stroke-width", config["strokeWidth"])
})
//.attr("d", function(d) { return line(d.geometry.coordinates.map(projection)) })
stops = svg.append("g").attr("class", "stops").selectAll("circle").data(json.features.filter(function(d) { return d["geometry"]["type"] == "Point" }))
stops.enter().append("circle")
.attr("id", function(d) { return d.properties.stop_id })
.attr("class", function(d) { return d.properties.route_id })
.attr("r", config["strokeWidth"]/2)
.on("mouseover", function(d) {
d3.select(this).attr("r", config["strokeWidth"]/2 * 2)
})
.on("mouseout", function(d) {
d3.select(this).attr("r", config["strokeWidth"]/2)
})
.attr("transform", function(d) {
xy = projection(d.geometry.coordinates)
return "translate("+xy[0]+","+xy[1]+")"
})
.append("svg:title")
.text(function(d, i) { return d.properties.name })
draw()
// intro animation
//var interpolator = d3.interpolateNumber(0, config["simplification"])
//svg.transition().duration(1000).tween("withchange", function() {
// return function(t) {
// config["simplification"] = interpolator(t)
// draw()
// };
//})
})
function draw() {
zoom.scale(config["simplification"])
line.interpolate(config["interpolation"])
routes.attr("d", function(d) {
return line(simplify(d.geometry.coordinates.map(projection), maxSimplification*(config["simplification"]*.01), true))
})
}
</script>
</body>
</html>
@Manjulajntuk
Copy link

Manjulajntuk commented Aug 30, 2016

Hi sir i have displayed two geojson files(flood geojson, district geojson) and then i read the features of the geojson. Using draw tool i have calculated area (using flood geojson + draw tool) Now my requirement is to calculate the flood area and subdivide the area to each and every district that is falling in the district geojson. My code is below

<title>Select features example</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/ol3/3.10.1/ol-debug.js"></script> <script src="ol.js"></script> <script src="jsts_geom_Polygon.js"></script> <script src="https://cdn.rawgit.com/bjornharrtell/jsts/gh-pages/lib/0.16.0/javascript.util.min.js"></script> <script src="https://cdn.rawgit.com/bjornharrtell/jsts/gh-pages/lib/0.16.0/jsts.min.js"></script> <script src="jsts_io_GeoJSONParser.js"></script> <style> html, body { padding: 0; margin: 0; } html, body, .map { height: 100%; width: 100%; } </style>

<script> var geojsonFormat = new ol.format.GeoJSON(); var style = new ol.style.Style({ fill: new ol.style.Fill({ color: 'rgba(255, 255, 255, 0.3)' }), stroke: new ol.style.Stroke({ color: 'rgba(255, 120, 0, 0.6)', width: 1 }) }); var styles = [style]; var highlightStyle = [ new ol.style.Style({ fill: new ol.style.Fill({ color: 'rgba(255, 120, 0, 0.3)' }), stroke: new ol.style.Stroke({ color: 'rgba(255, 120, 0, 0.6)', width: 1 }) }) ]; var vector3 = new ol.layer.Vector({ maxResolution: 0.8, source: new ol.source.Vector({ url: 'ap_taluk.geojson', format: new ol.format.GeoJSON({ defaultDataProjection:'EPSG:4326' }) }), style: styles }); var vector2 = new ol.layer.Vector({ maxResolution: 0.8, source: new ol.source.Vector({ url: 'flood.geojson', format: new ol.format.GeoJSON({ defaultDataProjection:'EPSG:4326' }) }), style: styles }); var layer1 = new ol.layer.Tile({ title: 'India', source: new ol.source.TileWMS({ url: 'http://ndem.nrsc.gov.in/geoserver/ndem50k/wms', params: {LAYERS: 'ndem50k:stateadmin50census2011'} }), transparent: false }); var dist = new ol.layer.Tile({ title: 'flood', source: new ol.source.TileWMS({ url: 'http://ndem.nrsc.gov.in/geoserver/ndem50d/wms', //http://172.31.4.37/geoserver/ndem50k/wms params: {LAYERS: 'ndem50d:apflood50dsc04122015'} //ndem50k:apdistrictadmin50census2011 }) }); var distaluk = new ol.layer.Tile({ title: 'taluk', source: new ol.source.TileWMS({ url: 'http://ndem.nrsc.gov.in/geoserver/ndem50k/wms', //http://172.31.4.37/geoserver/ndem50k/wms params: {LAYERS: 'ndem50k:aptaluk50soi2001'} //ndem50k:apdistrictadmin50census2011 }) }); intersectionLayer = new ol.layer.Vector({ source: new ol.source.Vector() }); var map = new ol.Map({ layers: [layer1,vector2,vector3,intersectionLayer,distaluk], target: 'map', view: new ol.View({ projection: 'EPSG:4326', center: [79.419,15.428], zoom: 8 }) }); var draw_inter = new ol.interaction.Draw({ type: 'Polygon' }); map.addInteraction(draw_inter); draw_inter.on('drawstart', function(evt) { intersectionLayer.getSource().clear(); }); var olParser = new jsts.io.olParser(); var geojsonParser = new jsts.io.GeoJSONParser(); draw_inter.on('drawend', function(evt) { var poly1 = olParser.read(evt.feature.getGeometry()); var extent1 = evt.feature.getGeometry().getExtent(); var source = vector2.getSource(); var source2 = vector3.getSource(); //var source4 = vector4.getSource(); var features = source.getFeatures(); var features2 = source2.getFeatures(); // var features4 = source4.getFeatures(); features.forEach(function(feature) { if (!ol.extent.intersects(extent1, feature.getGeometry().getExtent())) { return; } var poly2 = olParser.read(feature.getGeometry()); var intersection = poly1.intersection(poly2); //alert(intersection); intersection = geojsonParser.write(intersection); if(intersection.type === 'GeometryCollection' && intersection.geometries.length === 0) { return; } else { intersectionLayer.getSource().addFeature(geojsonFormat.readFeature({ type: 'Feature', properties: {}, geometry: intersection })); //alert("flood"+feature.get('geometry')); } }); features2.forEach(function(feature) { if (!ol.extent.intersects(extent1, feature.getGeometry().getExtent())) { return; } var poly3 = olParser.read(feature.getGeometry()); var intersection = poly1.intersection(poly3); //alert(intersection); intersection = geojsonParser.write(intersection); if(intersection.type === 'GeometryCollection' && intersection.geometries.length === 0) { return; } else { alert("village"+feature.get('taluk')); intersectionLayer.getSource().addFeature(geojsonFormat.readFeature({ type: 'Feature', properties: {}, geometry: intersection })); /*layersize = intersectionLayer.getSource().getFeatures().length; alert(" size"+layersize); for(i=0;i 10000) { output = (Math.round(totalarea / 1000000 * 100) / 100) + ' ' + 'km2'; } else { output = (Math.round(totalarea * 100) / 100) + ' ' + 'm2'; } alert(output); }); </script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment