On Github erictheise / geostack-deck
Download and install Xcode from the Mac App Store.
Download and install a VirtualBox platform package.
prev | ubuntu | nextDownload and install a VirtualBox platform package.
prev | windows | nextInstalling this OS X package manager is a one-liner.
Download Windows. You'll need your product key.
prev | windows | nextDownload and install Python 2.7.X.
prev | windows | nextDownload and install TileMill from MapBox.
prev | ubuntu | nextDownload the installer. The filenames are very long, so make sure you get a python2.7 version.
prev | windows | nextDownload PostgreSQL 9.3.X.
prev | windows | nextRun the installer. When it completes, use Stack Builder to install PostGIS 2.1
prev | windows | nextDownload and install node.js.
prev | windows | nextDownload and install TileMill from MapBox.
Download and install TileMill from MapBox.
prev | windows | nextFour elements are central to OpenStreetMap's data model:
osm2pgsql massages these elements tocreate tables suitable for rendering.
More detail is available.
Let's try and make this data visible.
Launch TileMill and create a New Project.
Select the project, then pan and zoom to Portland. Add a PostGIS layer for lines. (Lines: immediate gratification.)
Can I get a close-up of that?
Time to look at tags. SQL and tags. And MSS.
Delete #planetosmline, add a PostGIS layer using:
( SELECT * FROM planet_osm_line
WHERE highway IN ('motorway', 'primary', 'secondary', 'tertiary', 'service', 'residential')
) AS roads
We need to be selective about what we suck down from our OSM database. Our basic dilemma is
Delete the #planetosmline & #roads blocks from style.mss. Click on the + to create a new Carto stylesheet, roads.mss. Into it, copy and paste this block:
@motorway: #ff8c00;
@primary: #ffd700;
@secondary: #555555;
@tertiary: #676767;
@service: #888888;
@residential: #999999;
#roads.line {
[highway = 'motorway'] {
[zoom >= 9] { line-width:2; line-color:@motorway; }
[zoom >= 10] { line-width:3; line-color:@motorway; }
[zoom >= 11] { line-width:3.5; line-color:@motorway; }
[zoom >= 12] { line-width:4; line-color:@motorway; }
[zoom >= 13] { line-width:4.5; line-color:@motorway; }
[zoom >= 14] { line-width:5; line-color:@motorway; }
[zoom >= 15] { line-width:6; line-color:@motorway; }
[zoom >= 16] { line-width:8; line-color:@motorway; }
[zoom >= 17] { line-width:10; line-color:@motorway; }
[zoom >= 18] { line-width:12; line-color:@motorway; }
}
[highway = 'primary'] {
[zoom >= 9] { line-width:2; line-color:@primary; }
[zoom >= 10] { line-width:3; line-color:@primary; }
[zoom >= 11] { line-width:3.5; line-color:@primary; }
[zoom >= 12] { line-width:4; line-color:@primary; }
[zoom >= 13] { line-width:4.5; line-color:@primary; }
[zoom >= 14] { line-width:5; line-color:@primary; }
[zoom >= 15] { line-width:6; line-color:@primary; }
[zoom >= 16] { line-width:8; line-color:@primary; }
[zoom >= 17] { line-width:10; line-color:@primary; }
[zoom >= 18] { line-width:12; line-color:@primary; }
}
[highway = 'secondary'] {
[zoom >= 9] { line-width:0.75; line-color:@secondary; }
[zoom >= 10] { line-width:2; line-color:@secondary; }
[zoom >= 11] { line-width:2.5; line-color:@secondary; }
[zoom >= 12] { line-width:3; line-color:@secondary; }
[zoom >= 13] { line-width:3.5; line-color:@secondary; }
[zoom >= 14] { line-width:4; line-color:@secondary; }
[zoom >= 15] { line-width:5; line-color:@secondary; }
[zoom >= 16] { line-width:6; line-color:@secondary; }
[zoom >= 17] { line-width:7; line-color:@secondary; }
[zoom >= 18] { line-width:9; line-color:@secondary; }
}
[highway = 'tertiary'] {
[zoom >= 9] { line-width:0.25; line-color:@tertiary; }
[zoom >= 10] { line-width:1; line-color:@tertiary; }
[zoom >= 11] { line-width:1.5; line-color:@tertiary; }
[zoom >= 12] { line-width:2; line-color:@tertiary; }
[zoom >= 13] { line-width:2.5; line-color:@tertiary; }
[zoom >= 14] { line-width:3; line-color:@tertiary; }
[zoom >= 15] { line-width:4; line-color:@tertiary; }
[zoom >= 16] { line-width:5; line-color:@tertiary; }
[zoom >= 17] { line-width:6; line-color:@tertiary; }
[zoom >= 18] { line-width:8; line-color:@tertiary; }
}
[highway = 'service'] {
[zoom >= 10] { line-width:.5; line-color:@service; }
[zoom >= 11] { line-width:.3; line-color:@service; }
[zoom >= 12] { line-width:.4; line-color:@service; }
[zoom >= 13] { line-width:.6; line-color:@service; }
[zoom >= 14] { line-width:.8; line-color:@service; }
[zoom >= 15] { line-width:1.2; line-color:@service; }
[zoom >= 16] { line-width:2; line-color:@service; }
[zoom >= 17] { line-width:4; line-color:@service; }
[zoom >= 18] { line-width:6; line-color:@service; }
}
[highway = 'residential'] {
[zoom >= 10] { line-width:.2; line-color:@residential; }
[zoom >= 11] { line-width:.3; line-color:@residential; }
[zoom >= 12] { line-width:.4; line-color:@residential; }
[zoom >= 13] { line-width:.6; line-color:@residential; }
[zoom >= 14] { line-width:.8; line-color:@residential; }
[zoom >= 15] { line-width:1.2; line-color:@residential; }
[zoom >= 16] { line-width:2; line-color:@residential; }
[zoom >= 17] { line-width:4; line-color:@residential; }
[zoom >= 18] { line-width:6; line-color:@residential; }
}
}
Let's lay in the Willamette.
Add a PostGIS layer using:
( SELECT way, "natural", waterway, landuse, name
FROM planet_osm_polygon
WHERE waterway IN ('dock', 'riverbank', 'canal') OR
landuse IN ('reservoir', 'basin') OR
"natural" IN ('lake', 'water', 'land', 'glacier', 'mud')
) AS waterway
Click on the + to create a new Carto stylesheet, water.mss. Into it, copy and paste this block:
@water-color: #16b;
#water-areas {
[waterway = 'dock'],
[waterway = 'canal'] {
[zoom >= 9]::waterway {
polygon-fill: @water-color;
}
}
[landuse = 'basin'][zoom >= 7]::landuse {
polygon-fill: @water-color;
}
[natural = 'lake']::natural,
[natural = 'water']::natural,
[landuse = 'reservoir']::landuse,
[waterway = 'riverbank']::waterway {
[zoom >= 6] {
polygon-fill: @water-color;
}
}
}
Do drag the #water-areas layer below #roads.line.
Let's pick up streams and creeks, too.
Add a PostGIS layer using:
(SELECT way, waterway, lock, name, case WHEN tunnel IN ('yes','culvert') THEN 'yes' ELSE 'no' END AS int_tunnel, 'no' AS bridge
FROM planet_osm_line
WHERE waterway IN ('weir', 'river', 'canal', 'derelict_canal', 'stream', 'drain', 'ditch', 'wadi')
AND (bridge IS NULL OR bridge NOT IN ('yes','aqueduct'))
) AS water_lines
Append this block into water.mss:
.water-lines {
[waterway = 'weir'][zoom >= 15] {
line-color: #aaa;
line-width: 2;
line-join: round;
line-cap: round;
}
[waterway = 'canal'][zoom >= 12],
[waterway = 'river'][zoom >= 12] {
[bridge = 'yes'] {
[zoom >= 14] {
bridgecasing/line-color: black;
bridgecasing/line-join: round;
bridgecasing/line-width: 6;
[zoom >= 15] { bridgecasing/line-width: 7; }
[zoom >= 17] { bridgecasing/line-width: 11; }
[zoom >= 18] { bridgecasing/line-width: 13; }
}
}
line-color: @water-color;
line-width: 2;
[zoom >= 13] { line-width: 3; }
[zoom >= 14] { line-width: 5; }
[zoom >= 15] { line-width: 6; }
[zoom >= 17] { line-width: 10; }
[zoom >= 18] { line-width: 12; }
line-cap: round;
line-join: round;
[int_tunnel = 'yes'] {
line-dasharray: 4,2;
line-cap: butt;
line-join: miter;
a/line-color: #f3f7f7;
a/line-width: 1;
[zoom >= 14] { a/line-width: 2; }
[zoom >= 15] { a/line-width: 3; }
[zoom >= 17] { a/line-width: 7; }
[zoom >= 18] { a/line-width: 8; }
}
}
[waterway = 'stream'],
[waterway = 'ditch'],
[waterway = 'drain'] {
[zoom >= 13] {
[bridge = 'yes'] {
[zoom >= 14] {
bridgecasing/line-color: black;
bridgecasing/line-join: round;
bridgecasing/line-width: 3;
[waterway = 'stream'][zoom >= 15] { bridgecasing/line-width: 4; }
bridgeglow/line-color: white;
bridgeglow/line-join: round;
bridgeglow/line-width: 2;
[waterway = 'stream'][zoom >= 15] { bridgeglow/line-width: 3; }
}
}
line-width: 1;
line-color: @water-color;
[waterway = 'stream'][zoom >= 15] {
line-width: 2;
}
[int_tunnel = 'yes'][zoom >= 15] {
line-width: 2.5;
[waterway = 'stream'] { line-width: 3.5; }
line-dasharray: 4,2;
a/line-width: 1;
[waterway = 'stream'] { a/line-width: 2; }
a/line-color: #f3f7f7;
}
}
}
}
Drag the #water-lines layer below #water-areas.
Each of the planet_osm_ tables has a name field, and we need to tap into those to begin rendering labels.
( SELECT way, CASE WHEN SUBSTR(highway, length(highway)-3, 4) = 'link' THEN substr(highway,0,length(highway)-4) ELSE highway END, name
FROM planet_osm_line
WHERE highway IN ('motorway', 'motorway_link', 'trunk', 'trunk_link', 'primary', 'primary_link', 'secondary', 'secondary_link',
'tertiary', 'tertiary_link', 'residential', 'unclassified', 'road', 'service', 'pedestrian', 'raceway', 'living_street', 'construction', 'proposed')
AND name IS NOT NULL
) AS roads_text_name
Prepend this block into style.mss:
@book-fonts: "DejaVu Sans Book", "Arundina Sans Regular", "Padauk Regular", "Khmer OS Metal Chrieng Regular",
"Mukti Narrow Regular", "gargi Medium", "TSCu_Paranar Regular", "Tibetan Machine Uni Regular", "Mallige Normal",
"Droid Sans Fallback Regular", "Unifont Medium", "unifont Medium";
Append this block into road.mss:
#roads-text-name {
[highway = 'motorway'],
[highway = 'trunk'],
[highway = 'primary'] {
[zoom >= 13] {
text-name: "[name]";
text-size: 8;
text-fill: black;
text-spacing: 300;
text-clip: false;
text-placement: line;
text-face-name: @book-fonts;
text-halo-radius: 0;
}
[zoom >= 14] {
text-size: 9;
}
[zoom >= 15] {
text-size: 10;
}
[zoom >= 17] {
text-size: 11;
}
}
[highway = 'secondary'] {
[zoom >= 13] {
text-name: "[name]";
text-size: 8;
text-fill: black;
text-spacing: 300;
text-clip: false;
text-placement: line;
text-face-name: @book-fonts;
text-halo-radius: 1;
}
[zoom >= 14] {
text-size: 9;
}
[zoom >= 15] {
text-size: 10;
}
[zoom >= 17] {
text-size: 11;
}
}
[highway = 'tertiary'],
[highway = 'tertiary_link'] {
[zoom >= 14] {
text-name: "[name]";
text-size: 9;
text-fill: black;
text-spacing: 300;
text-clip: false;
text-placement: line;
text-face-name: @book-fonts;
text-halo-radius: 1;
}
[zoom >= 17] {
text-size: 11;
}
}
[highway = 'proposed'],
[highway = 'construction'] {
[zoom >= 13] {
text-name: "[name]";
text-size: 9;
text-fill: black;
text-spacing: 300;
text-clip: false;
text-placement: line;
text-halo-radius: 1;
text-face-name: @book-fonts;
}
[zoom >= 17] {
text-size: 11;
}
}
[highway = 'residential'],
[highway = 'unclassified'],
[highway = 'road'] {
[zoom >= 15] {
text-name: "[name]";
text-size: 8;
text-fill: black;
text-spacing: 300;
text-clip: false;
text-placement: line;
text-halo-radius: 1;
text-face-name: @book-fonts;
}
[zoom >= 16] {
text-size: 9;
}
[zoom >= 17] {
text-size: 11;
text-spacing: 400;
}
}
[highway = 'raceway'],
[highway = 'service'] {
[zoom >= 16] {
text-name: "[name]";
text-size: 9;
text-fill: black;
text-spacing: 300;
text-clip: false;
text-placement: line;
text-halo-radius: 1;
text-face-name: @book-fonts;
}
[zoom >= 17] {
text-size: 11;
}
}
[highway = 'living_street'],
[highway = 'pedestrian'] {
[zoom >= 15] {
text-name: "[name]";
text-size: 8;
text-fill: black;
text-spacing: 300;
text-clip: false;
text-placement: line;
text-halo-radius: 1;
text-face-name: @book-fonts;
}
[zoom >= 16] {
text-size: 9;
}
[zoom >= 17] {
text-size: 11;
}
}
}
Let's look at the contents of static.html.
<!DOCTYPE html> <html> <head> Static Data with Leaflet </head> <body><script> var tileUrl, map = L.map('map').setView([45.521969, -122.683424], 13); // @todo: Pick a tileserver by uncommenting one of these values for tileUrl: // // To use the MapBox example tiles: // tileUrl = 'https://{s}.tiles.mapbox.com/v3/{id}/{z}/{x}/{y}.png'; // // To use the Python SimpleHTTPServer you've set up on port 8887: // tileUrl = 'http://localhost:8887/tiles/{z}/{x}/{y}.png'; // // To use the TileStream server you've set up on port 8888: tileUrl = 'http://localhost:8888/v2/portland_from_osm/{z}/{x}/{y}.png'; // L.tileLayer(tileUrl, { minZoom: 10, maxZoom: 16, attribution: 'Map data © OpenStreetMap contributors, ' + 'Imagery © Mapbox', id: 'examples.map-i86knfo3' }).addTo(map); L.marker([45.521969, -122.683424]).addTo(map) .bindPopup( 'Ración' + '1205 SW Washington StPortland, OR' ); var popup = L.popup(); </script> </body> </html>
Open it in your browser.
Let's look at the contents of dynamic.html.
<!DOCTYPE html> <html> <head> Dynamic Data with Leaflet </head> <body><script> function initMap() { var tileUrl; // Note: the [Wikipedia page on Downtown Portland Oregon](http://en.wikipedia.org/wiki/Downtown_Portland) // gives its location as 45.51935°N 122.67962°W. We'll center the map at that location with zoom level 12. // var map = L.map('map').setView([45.51935, -122.67962], 12); // @todo: Pick a tileserver by uncommenting one of these values for tileUrl: // // To use the MapBox example tiles: // tileUrl = 'https://{s}.tiles.mapbox.com/v3/{id}/{z}/{x}/{y}.png'; // // To use the Python SimpleHTTPServer you've set up on port 8887: // tileUrl = 'http://localhost:8887/tiles/{z}/{x}/{y}.png'; // // To use the TileStream server you've set up on port 8888: tileUrl = 'http://localhost:8888/v2/portland_from_osm/{z}/{x}/{y}.png'; // L.tileLayer(tileUrl, { minZoom: 10, maxZoom: 16, attribution: 'Map data © OpenStreetMap contributors, ' + 'Imagery © Mapbox', id: 'examples.map-i86knfo3' }).addTo(map); $.ajax({ url: 'http://localhost:3000/amenities', dataType: 'json', type: 'get' }).done(function (data) { var geojson = L.geoJson(data, { pointToLayer: function (feature, latlng) { var fillColor, fillOpacity = 0.75; // Note: as this is a quick & dirty demo, fill colors have been assigned arbitrarily // to the top 12 cuisines found tagged to restaurants in Sep 2014 OpenStreetMap // data for Portland, OR. Any other cuisine, or restaurants that have not been tagged // with a cuisine, get a dark gray fill color having low opacity. The color values // come from the 12 data class/qualitative nature/Set3 at http://colorbrewer2.org/ // // SELECT tags->'cuisine' AS cuisine, count(tags->'cuisine') // FROM planet_osm_point // WHERE amenity = 'restaurant' // GROUP BY tags->'cuisine' // ORDER BY count desc // LIMIT 12; // // cuisine | count //----------+------- // pizza | 47 // mexican | 42 // american | 32 // chinese | 23 // thai | 20 // japanese | 18 // italian | 17 // burger | 17 // sushi | 16 // asian | 11 // regional | 9 // sandwich | 8 // (12 rows) // switch (feature.properties.cuisine) { case 'pizza': fillColor = '#8dd3c7'; break; case 'mexican': fillColor = '#ffffb3'; break; case 'american': fillColor = '#bebada'; break; case 'chinese': fillColor = '#fb8072'; break; case 'thai': fillColor = '#80b1d3'; break; case 'japanese': fillColor = '#fdb462'; break; case 'italian': fillColor = '#b3de69'; break; case 'burger': fillColor = '#fccde5'; break; case 'sushi': fillColor = '#d9d9d9'; break; case 'asian': fillColor = '#bc80bd'; break; case 'regional': fillColor = '#ccebc5'; break; case 'sandwich': fillColor = '#ffed6f'; break; default: fillColor = '#222'; fillOpacity = 0.1; break; } var marker = L.circleMarker(latlng, { radius: 6, weight: 1, color: "#000", opacity: 1, fillColor: fillColor, fillOpacity: fillOpacity }); map.on('load zoomend', function () { var currentZoom = map.getZoom(); if (currentZoom < 13) { marker.setRadius(6); } else { if (currentZoom < 15) { marker.setRadius(9); } else { marker.setRadius(12); } } }); return marker; }, onEachFeature: function (feature, layer) { layer.bindPopup( '
Note the use of L.geoJson(), the switch statement to determine how to color each circleMarker, and the use of map.getZoom() to set the radius of each circleMarker.