SlideShare a Scribd company logo
+
terrafrost@php.net
Jim Wigginton
• Creator and maintainer of phpseclib/phpseclib library
• PHP for ~20 years
• Born and raised in Austin, TX
• terrafrost@php.net
Leaflet JS (GIS) and Capital MetroRail
Leaflet Setup
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"></script>
<style>
body {
margin: 0;
}
</style>
<div id="map" style="width: 100%; height: 100%"></div>
<script>
var map = L.map('map').setView([51.505, -0.09], 13);
var tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
</script>
• The above is based off of https://leafletjs.com/examples/quick-start/
• [51.505, -09] is the latitude / longitude, 13 is the zoom
• Current location can be obtained by doing this:
• Real time location updates can be obtained by doing this:
navigator.geolocation.getCurrentPosition(pos => {
map.setView([pos.coords.latitude, pos.coords.longitude], 14);
});
navigator.geolocation.watchPosition(pos => {
polyline.addLatLng([pos.coords.latitude, pos.coords.longitude]);
});
Tile Sets
Mapbox Satellite OpenStreetMap + TomTom Traffic Flow
USGSTopo (MapServer) Mapbox Light + ISU Environmental Mesonet
Database Setup
1. Install the necessary software:
• Windows: Install OSGeo4W and then open "OSGeo4W Shell".
• Ubuntu: Run the following commands:
2. Download shapefiles from https://www.capmetro.org/metrolabs. Sort by "Recently Updated“
3. Import Routes.shp into MySQL:
sudo add-apt-repository ppa:ubuntugis/ppa
sudo apt-get update
sudo apt-get install gdal-bin
sudo apt-get install libgdal-dev
ogr2ogr -f MySQL MySQL:dbname,host=localhost,user=user,password=pass 
Routes.shp -nln tablename -update -append 
-t_srs "EPSG:4326" -lco engine=InnoDB -skipfailures
Coordinate Systems
Coordinate System Austin, TX Example
EPSG:4326 WGS 84 30.267222, -97.743056 TxDOT
EPSG:4269 NAD83 30.260336, -97.7458308 NHD, TIGER
EPSG:3857 WGS 84 / Pseudo-Mercator 3537945.1867267964, -10880707.234867256 US DOT
EPSG:3081 NAD83 / Texas State Mapping System 902702.814304697, 1216728.9256139707 THC
EPSG:32614 WGS 84 / UTM zone 14N 3349064.8428058745, 620908.0252681801 CapMetro
Conversions done with https://epsg.io/transform
Database
OGR_FID SHAPE route_id routename direction routecolor textcolor routetype routetheme servicenm servicetyp sign_id service_id source sourcedate
1 BLOB 1
North Lamar/South
Congress
Southbound 004A97 FFFFFF Local NULL Weekday Weekday 153 1-153
Capital
Metro
6/3/2022
2 BLOB 324 Georgian/Ohlen Westbound 004A97 FFFFFF Crosstown NULL Sunday Sunday 153 5-153
Capital
Metro
6/3/2022
3 BLOB 19 Bull Creek Northbound 004A97 FFFFFF Local NULL Sunday Sunday 153 5-153
Capital
Metro
6/3/2022
4 BLOB 243 Wells Branch Westbound 004A97 FFFFFF Feeder NULL Sunday Sunday 153 5-153
Capital
Metro
6/3/2022
5 BLOB 6 East 12th Westbound 004A97 FFFFFF Local NULL Saturday Saturday 153 4-153
Capital
Metro
6/3/2022
6 BLOB 310 Parker/Wickersham Eastbound 004A97 FFFFFF Crosstown NULL Weekday Weekday 153 1-153
Capital
Metro
6/3/2022
7 BLOB 339 Tuscany Westbound 004A97 FFFFFF Crosstown NULL Saturday Saturday 153 4-153
Capital
Metro
6/3/2022
8 BLOB 243 Wells Branch Eastbound 004A97 FFFFFF Feeder NULL Weekday Weekday 153 1-153
Capital
Metro
6/3/2022
9 BLOB 322 Chicon/Cherrywood Southbound 004A97 FFFFFF Crosstown NULL Sunday Sunday 153 5-153
Capital
Metro
6/3/2022
10 BLOB 550 Metro Rail Red Line Northbound E2231A FFFFFF Rail NULL RAIL AFC 2 Other 153 55004-153
Capital
Metro
6/3/2022
GeoJSON
1. Export from the DB with this SQL:
2. Load the GeoJSON in Leaflet by doing this:
SELECT ST_AsGeoJSON(SHAPE), servicenm, servicetyp
FROM capmetro_routes
WHERE route_id = 550
AND servicenm = '6TRAINMON to THURS';
L.geoJSON(route).addTo(map);
GeoJSON Primitives
Type Example
Point
{
"type": "Point",
"coordinates": [30.0, 10.0]
}
LineString
{
"type": "LineString",
"coordinates": [ [30.0, 10.0], [10.0, 30.0], [40.0, 40.0] ]
}
Polygon
{
"type": "Polygon",
"coordinates": [ [[30.0, 10.0], [40.0, 40.0], [20.0, 40.0], [10.0, 20.0], [30.0, 10.0]] ]
}
{
"type": "Polygon",
"coordinates": [
[[35.0, 10.0], [45.0, 45.0], [15.0, 40.0],
[10.0, 20.0], [35.0, 10.0]], [[20.0, 30.0], [35.0, 35.0], [30.0, 20.0], [20.0, 30.0]]
]
}
Markers
1. Export from the DB with this SQL:
2. Load the GeoJSON in Leaflet by doing this:
SELECT
stop_name,
CONCAT(latitude, ',', longitude) AS pos
FROM capmetro_stops
WHERE stop_type = 'Rail Station';
L.marker([30.264843,-97.738448])
.bindPopup('Downtown Station’)
.addTo(map);
GTFS Realtime
• General Transit Feed Specification
• Developed by Google in 2006
• Uses Protocol Buffers
• Get the download link for "CapMetro Vehicle Positions PB File"
from https://www.capmetro.org/metrolabs. Sort by "Recently Updated"
• Updated every 15s
• composer require google/gtfs-realtime-bindings
• Deprecated: As of February 2019, the official google-protobuf
Google protoc tool doesn’t support proto2 files. As a result we are
deprecating the PHP bindings until official support for proto2 files is
implemented in the Google protocol buffer tools.
GTFS Realtime: Server Side
<?php
require_once 'vendor/autoload.php’;
use transit_realtimeFeedMessage;
$output = [];
$data = file_get_contents('https://data.texas.gov/download/eiei-9rpf/application%2Foctet-stream’);
$feed = new FeedMessage();
$feed->parse($data);
foreach ($feed->getEntityList() as $entity) {
if ($entity->vehicle->trip && $entity->vehicle->trip->route_id == 550) {
$pos = $entity->vehicle->position; $output[] = [
'latitude' => $pos->latitude,
'longitude' => $pos->longitude,
'bearing' => $pos->bearing,
'speed' => $pos->speed
];
}
}
echo json_encode($output);
GTFS Realtime: Client Side
var realtimeUpdate = function() {
fetch('realtime.php’)
.then(response => response.json())
.then(data => {
data.forEach(train => {
var arrow = new L.Icon({
iconUrl: 'arrow-up.svg’,
iconSize: [25,28.975],
iconAnchor: [13, 0],
});
var marker = L.marker(
[train.latitude, train.longitude],
{icon: arrow, rotationAngle: train.bearing}
)
.bindPopup('<strong>Coordinates</strong>: ‘ +
train.latitude + ',' + train.longitude +
'<br><strong>Bearing</strong>: ' + train.bearing +
'<br><strong>Speed</strong>: ' + train.speed)
.addTo(map);
});
});
}
realtimeUpdate();
setInterval(realtimeUpdate, 15000);
Putting It Together
• arrow-up.svg is from Font Awesome ( ) and was
edited with Inkscape
• https://github.com/bbecquet/Leaflet.RotatedMar
ker is used to rotate the arrow
• NYC: https://api.mta.info/
• Los Angeles: https://developer.metro.net/api/
• Chicago: https://www.transitchicago.com/developers/bustracker/
• Houston: https://api-portal.ridemetro.org/
• London: https://api.tfl.gov.uk/
• France: https://prim.iledefrance-mobilites.fr/fr
Austin Western Railroad
• Download and import shapefile from https://hub.arcgis.com/datasets/fedmaps::north-american-rail-
lines-1/explore
• Query:
SELECT *
FROM trains
WHERE rrowner1 = 'AWRR';
OGR_FID SHAPE objectid fraarcid frfranode tofranode cntyfips stateab country rrowner1 trkrghts1 subdiv passngr tracks net miles km shape_leng
123293 BLOB 123293 423812 360530 360533 287 TX US AWRR NULL
GIDDINGS
INDUSTRIA
L SPUR
NULL 1 I 0.040001 0.064375 0.000666
123294 BLOB 123294 423813 360533 360541 287 TX US AWRR NULL
GIDDINGS
INDUSTRIA
L SPUR
NULL 1 I 0.144155 0.231995 0.002261
123386 BLOB 123386 423905 355408 355502 453 TX US AWRR CMRX CENTRAL C 1 M 1.476048 2.375474 0.023111
123413 BLOB 123413 423932 355403 355406 453 TX US AWRR NULL NULL NULL 1 M 0.334526 0.538368 0.004952
123604 BLOB 123604 424123 353382 353376 53 TX US AWRR NULL MAIN LINE NULL 0 O 0.350358 0.563848 0.005183
123705 BLOB 123705 424224 358097 358085 21 TX US AWRR NULL NULL NULL 0 O 0.143777 0.231386 0.002201
123706 BLOB 123706 424225 358629 358637 21 TX US AWRR NULL NULL NULL 0 O 0.073028 0.117527 0.001145
123707 BLOB 123707 424226 358637 358651 21 TX US AWRR NULL NULL NULL 0 O 0.223662 0.35995 0.003704
123708 BLOB 123708 424227 358637 358648 21 TX US AWRR NULL NULL NULL 0 O 0.194351 0.312778 0.003207
124262 BLOB 124262 424783 355418 355416 453 TX US AWRR NULL NULL NULL 1 M 0.204555 0.329199 0.003038
GeoJSON Multipart Geometries
Type Example
MultiPoint
{
"type": "MultiPoint",
"coordinates": [ [10.0, 40.0], [40.0, 30.0], [20.0, 20.0], [30.0, 10.0] ]
}
MultiLineString
{
"type": "MultiLineString",
"coordinates": [ [[10.0, 10.0], [20.0, 20.0], [10.0, 40.0]], [[40.0, 40.0], [30.0, 30.0], [40.0, 20.0], [30.0, 10.0]] ]
}
MultiPolygon
{
"type": "MultiPolygon",
"coordinates": [
[
[[30.0, 20.0], [45.0, 40.0], [10.0, 40.0], [30.0, 20.0]]
],
[
[[15.0, 5.0], [40.0, 10.0], [10.0, 20.0], [5.0, 10.0], [15.0, 5.0]]
]
]
}
GeoJSON Multipart Geometries: Part 2
Type Example
GeometryCollection
{
"type": "GeometryCollection",
"geometries": [
{
"type": "Point",
"coordinates": [40.0, 10.0]
},
{
"type": "LineString",
"coordinates": [ [10.0, 10.0], [20.0, 20.0], [10.0, 40.0] ]
},
{
"type": "Polygon",
"coordinates": [ [[40.0, 40.0], [20.0, 45.0], [45.0, 30.0], [40.0, 40.0]] ]
}
]
}
Styles
• Colorize layers:
• Colorize markers:
Colorized markers are from https://github.com/pointhi/leaflet-color-markers
L.geoJSON(route, {
color: 'red’,
}).addTo(map);
var redIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png’,
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png’,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
L.marker([30.264843,-97.738448], {icon: redIcon}).bindPopup('Downtown Station').addTo(map);
The Result
More Styles
• Bring train location markers to the front:
• Colorize markers:
• Add Control Layer:
var marker = L.marker(
[train.latitude, train.longitude],
{icon: arrow, rotationAngle: train.bearing, zIndexOffset: 1000}
);
var stations = L.layerGroup();
stations.addLayer(L.marker([30.264843,-97.738448]).bindPopup('Downtown Station’));
stations.addTo(map);
layerControl = L.control.layers().addTo(map);
layerControl.addOverlay(stations, 'Stations');
The Result
Counties
• Download and import "Counties (and equivalent)" shapefile from https://www.census.gov/cgi-
bin/geo/shapefiles/index.php
• TIGER: Topologically Integrated Geographic Encoding and Referencing
• Query:
SELECT *
FROM counties
WHERE statefp = 48;
OGR_FID SHAPE statefp countyfp countyns geoid name namelsad lsad aland awater intptlat intptlon
8BLOB 48327 1383949 48327 Menard Menard County 6 2.34E+09 613559 30.88527 -99.8589
12BLOB 48189 1383880 48189 Hale Hale County 6 2.6E+09 246678 34.06844 -101.823
14BLOB 4811 1383791 48011 Armstrong
Armstrong
County
6 2.35E+09 12183672 34.96418 -101.357
36BLOB 4857 1383814 48057 Calhoun Calhoun County 6 1.31E+09 1.36E+09 28.44172 -96.5796
39BLOB 4877 1383824 48077 Clay Clay County 6 2.82E+09 72506860 33.7859 -98.2129
56BLOB 48361 1383966 48361 Orange Orange County 6 8.65E+08 1.18E+08 30.12232 -93.8941
64BLOB 48177 1383874 48177 Gonzales
Gonzales
County
6 2.76E+09 8204086 29.46191 -97.4919
69BLOB 48147 1383859 48147 Fannin Fannin County 6 2.31E+09 20847065 33.59116 -96.105
71BLOB 48265 1383918 48265 Kerr Kerr County 6 2.86E+09 10231764 30.05995 -99.3533
100BLOB 48391 1383981 48391 Refugio Refugio County 6 2E+09 1.24E+08 28.32212 -97.1625
Counties: Client Side
var exclude = [], counties = L.layerGroup(), temp;
layerControl.addOverlay(counties, 'Counties’);
getCounties(map.getBounds());
map.on('moveend', function() {
getCounties(map.getBounds());
});
function getCounties(bounds) {
var ne = bounds.getNorthEast().lat + ',' + bounds.getNorthEast().lng;
var sw = bounds.getSouthWest().lat + ',' + bounds.getSouthWest().lng;
fetch('counties.php?ne=' + ne + '&sw=' + sw + '&exclude=' + exclude.join(',’))
.then(response => response.json())
.then(data => {
data.forEach(county => {
counties.addLayer(temp = L.geoJson(county.shape, {fill: false, color: "gray"}));
counties.addLayer(
L.marker(temp.getBounds().getCenter(), {opacity: 0})
.bindTooltip(county.name, {permanent: true, direction: 'center', className: 'countyName’})
);
exclude.push(county.fips);
});
counties.addTo(map);
});
}
Counties: Server Side
<?php
$db = new PDO('mysql:dbname=dbname;host=host', 'user', 'pass’);
$ne = explode(',', $_GET['ne’]);
$sw = explode(',', $_GET['sw’]);
$shape = "Polygon((
$sw[0] $sw[1],
$ne[0] $sw[1],
$ne[0] $ne[1],
$sw[0] $ne[1],
$sw[0] $sw[1]
))";
$sql_where = ‘’;
$exclude = [];
if (strlen($_GET['exclude'])) {
$exclude = explode(',', $_GET['exclude’]);
$sql_in = array_fill(0, count($exclude), '?’);
$sql_in = implode(',', $sql_in);
$sql_where = "AND countyfp NOT IN ($sql_in)";
}
$q = $db->prepare(“
SELECT
name,
countyfp AS fips,
ST_AsGeoJson(SHAPE) AS shape
FROM county_shapes
WHERE MBRIntersects(ST_GeomFromText(?, 4269), SHAPE)
AND statefp = 48
AND ST_Area(ST_GeomFromText(?, 4269)) < 40075210158
$sql_where
");
$q->execute(array_merge([$shape, $shape], $exclude));
$result = [];
while ($row = $q->fetch(PDO::FETCH_ASSOC)) {
$row['shape'] = json_decode($row['shape’]);
$result[] = $row;
}
echo json_encode($result);
The Result
The Narrows
The Narrows: Mapped
Polygon Labels
var countyNames;
function labelCounties() {
var bounds = map.getBounds();
if (countyNames) {
map.removeLayer(countyNames);
}
countyNames = L.layerGroup();
for (const [name, layer] of Object.entries(countyLayers)) {
var newCoords = [], coords = countyLayers[name].toGeoJSON()['features'][0]['geometry']['coordinates'][0]
coords.forEach(function (coord) {
coord = L.latLng(coord[1], coord[0]);
coord.lat = Math.min(coord.lat, bounds.getNorthWest().lat);
coord.lat = Math.max(coord.lat, bounds.getSouthEast().lat);
coord.lng = Math.max(coord.lng, bounds.getNorthWest().lng);
coord.lng = Math.min(coord.lng, bounds.getSouthEast().lng);
newCoords.push(coord);
});
var polygon = L.polygon(newCoords, {fill: false, opacity: 0}).addTo(map);
marker = L.marker(polygon.getCenter(), {opacity: 0}).bindTooltip(name, {permanent: true, direction: 'center', className: 'countyName’});
countyNames.addLayer(marker);
map.removeLayer(polygon);
}
countyNames.addTo(map);
}
labelCounties();
map.on('moveend', function() {
labelCounties();
});
Polygon Labels Visualized
The Narrows: With Counties
Labels for Lines
• Using https://github.com/3mapslab/Leaflet.streetlabels
• Merge LineString’s:
• Wrap GeoJSON:
$coords = $rows[0];
unset($rows[0]);
while (count($rows)) {
foreach ($rows as $i => $line) {
if ($coords[count($coords) - 1] == $line[0]) {
$coords = array_merge($coords, array_slice($line, 1));
unset($rows[$i]);
} else if ($coords[0] == $line[count($line) - 1]) {
$coords = array_merge(array_slice($line, 0, -1), $coords);
unset($rows[$i]);
}
}
}
function featureWrapper(geometry, name) {
return feature = {
type: 'Feature’,
properties: {name: name},
geometry: geometry
};
}
$coords = [
'type' => 'LineString’,
'coordinates' => $cords
];
The Narrows: With Labeled Lines
Third Street Railroad Trestle
Sanborn Fire Insurance Maps
Georeferencing
Georeferencing
1 Choose Tool
• ArcGIS
• QGIS
• Georeferencer.com
• MapWarper.net
3 Place Image
1. Extract bounding coordinates:
2. Convert to PNG or WebP
3. Place on map:
gdalinfo exported.tiff
L.imageOverlay(
'exported.webp’, [
// top left
[30.2725146, -97.7578187],
// bottom right
[30.2644510, -97.7496347]
],
{opacity: 0.5}
).addTo(map);
2 Basic Technique
The Result
Merging Overlapping GeoTIFFs
• Perform the merge:
• "In areas of overlap, the last image will be copied over earlier ones"
• Copy nextPage.tif as a new layer over temp.tif and trim away at nextPage.tif
• Copy the trimmed layer back to nextPage.tif, delete the old layer, and save as new.tif.
GeoTIFF data will be lost.
gdal_merge -o temp.tif nextPage.tif prevPage.tif
Creating Tile Layers
1
2
3
Copy GeoTIFF data from orig.tif to new.tif
listgeo -no_norm orig.tif > orig.geo
geotifcp -g orig.geo new.tif temp.tif
Merge the TIFFs
gdal_merge -o merged.tif master.tif temp.tif
Rm master.tif; rm temp.tif; mv merged.tif master.tif
Create Tile Layer
Gdal2tiles master.tif
4 Use Tile Layer
L.tileLayer('https://domain.tld/sanborn/{z}/{x}/{y}.png’, {
tms: 1,
opacity: 0.7,
minZoom: 13,
maxZoom: 19
});
The Result
Tile Layer Caveats
"OSM does NOT pre-render every tile. Pre-rendering all tiles would use around 54
TB of storage. As the following table shows, the majority of tiles are never viewed.
In fact just 1.79% are viewed. It works out this way because the majority of tiles
are at zoom level 18 and actually the majority contain nothing of interest. By
following an on-the-fly rendering approach we can avoid rendering these tiles
unnecessarily. The tile view count column shows how many tiles have been
produced on the OSM Tile server."
Source: https://wiki.openstreetmap.org/wiki/Tile_disk_usage
Thank You
• Slides & Feedback:
https://joind.in/talk/44a76
• Questions? terrafrost@php.net

More Related Content

PPTX
Lakehouse Analytics with Dremio
PPTX
Snowflake Data Access.pptx
PDF
Data engineering
PDF
SDM (Standardized Data Management) - A Dynamic Adaptive Ingestion Frameworks ...
PPT
Enterprise service bus(esb)
PPTX
What makes it worth becoming a Data Engineer?
PPTX
Tableau Architecture
PDF
Tableau file types
Lakehouse Analytics with Dremio
Snowflake Data Access.pptx
Data engineering
SDM (Standardized Data Management) - A Dynamic Adaptive Ingestion Frameworks ...
Enterprise service bus(esb)
What makes it worth becoming a Data Engineer?
Tableau Architecture
Tableau file types

What's hot (17)

PDF
The Missing Link in Enterprise Data Governance - Automated Metadata Management
PPTX
Intro to Data Vault 2.0 on Snowflake
PPTX
How to build a data dictionary
PDF
GoldenGate and Stream Processing with Special Guest Rakuten
PDF
MySQL X protocol - Talking to MySQL Directly over the Wire
PPTX
Dash plotly data visualization
PPTX
Introduction to Data Engineering
PDF
Data Modeling Fundamentals
PDF
Data Governance in an Agile SCRUM Lean MVP World
PDF
Lambda Architecture in the Cloud with Azure Databricks with Andrei Varanovich
PDF
Modularized ETL Writing with Apache Spark
PPTX
Data Observability.pptx
PDF
Data Catalog in Denodo Platform 7.0: Creating a Data Marketplace with Data Vi...
PDF
Snowflake for Data Engineering
PPTX
Elastic Data Warehousing
PDF
Monitoring Oracle Database Instances with Zabbix
PPTX
Column Level Encryption in Microsoft SQL Server
The Missing Link in Enterprise Data Governance - Automated Metadata Management
Intro to Data Vault 2.0 on Snowflake
How to build a data dictionary
GoldenGate and Stream Processing with Special Guest Rakuten
MySQL X protocol - Talking to MySQL Directly over the Wire
Dash plotly data visualization
Introduction to Data Engineering
Data Modeling Fundamentals
Data Governance in an Agile SCRUM Lean MVP World
Lambda Architecture in the Cloud with Azure Databricks with Andrei Varanovich
Modularized ETL Writing with Apache Spark
Data Observability.pptx
Data Catalog in Denodo Platform 7.0: Creating a Data Marketplace with Data Vi...
Snowflake for Data Engineering
Elastic Data Warehousing
Monitoring Oracle Database Instances with Zabbix
Column Level Encryption in Microsoft SQL Server
Ad

Similar to Leaflet JS (GIS) and Capital MetroRail (20)

PDF
CEI Email 3.14.03
KEY
Handling Real-time Geostreams
KEY
Handling Real-time Geostreams
PPTX
HTML5 - Pedro Rosa
PDF
Ruby Robots
PPT
Basics of html5, data_storage, css3
PDF
Inspec one tool to rule them all
PPTX
Java bytecode Malware Analysis
PPTX
XML-Free Programming
PDF
Explain this!
PDF
Website Performance Basics
PDF
Five Pound App talk: hereit.is, Web app architecture, REST, CSS3
ODP
Illuminated Hacks -- Where 2.0 101 Tutorial
PDF
Smashing the stats for fun (and profit)
PDF
AtlasCamp 2015 Docker continuous integration training
KEY
Agile Tour Shanghai December 2011
PDF
How I make a podcast website using serverless technology in 2023
KEY
Czzawk
PPTX
Next Level Curl
PDF
Microformats: what are they and why do I care?
CEI Email 3.14.03
Handling Real-time Geostreams
Handling Real-time Geostreams
HTML5 - Pedro Rosa
Ruby Robots
Basics of html5, data_storage, css3
Inspec one tool to rule them all
Java bytecode Malware Analysis
XML-Free Programming
Explain this!
Website Performance Basics
Five Pound App talk: hereit.is, Web app architecture, REST, CSS3
Illuminated Hacks -- Where 2.0 101 Tutorial
Smashing the stats for fun (and profit)
AtlasCamp 2015 Docker continuous integration training
Agile Tour Shanghai December 2011
How I make a podcast website using serverless technology in 2023
Czzawk
Next Level Curl
Microformats: what are they and why do I care?
Ad

Recently uploaded (20)

PDF
Assigned Numbers - 2025 - Bluetooth® Document
PPTX
Modernising the Digital Integration Hub
PPTX
MicrosoftCybserSecurityReferenceArchitecture-April-2025.pptx
PDF
Video forgery: An extensive analysis of inter-and intra-frame manipulation al...
PDF
Developing a website for English-speaking practice to English as a foreign la...
PDF
Transform Your ITIL® 4 & ITSM Strategy with AI in 2025.pdf
PDF
NewMind AI Weekly Chronicles – August ’25 Week III
PPTX
O2C Customer Invoices to Receipt V15A.pptx
PDF
August Patch Tuesday
PDF
From MVP to Full-Scale Product A Startup’s Software Journey.pdf
PPTX
Final SEM Unit 1 for mit wpu at pune .pptx
PDF
NewMind AI Weekly Chronicles - August'25-Week II
PDF
DASA ADMISSION 2024_FirstRound_FirstRank_LastRank.pdf
PPTX
OMC Textile Division Presentation 2021.pptx
PDF
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
PPTX
Tartificialntelligence_presentation.pptx
PPTX
The various Industrial Revolutions .pptx
PPTX
1. Introduction to Computer Programming.pptx
PDF
1 - Historical Antecedents, Social Consideration.pdf
PDF
Getting started with AI Agents and Multi-Agent Systems
Assigned Numbers - 2025 - Bluetooth® Document
Modernising the Digital Integration Hub
MicrosoftCybserSecurityReferenceArchitecture-April-2025.pptx
Video forgery: An extensive analysis of inter-and intra-frame manipulation al...
Developing a website for English-speaking practice to English as a foreign la...
Transform Your ITIL® 4 & ITSM Strategy with AI in 2025.pdf
NewMind AI Weekly Chronicles – August ’25 Week III
O2C Customer Invoices to Receipt V15A.pptx
August Patch Tuesday
From MVP to Full-Scale Product A Startup’s Software Journey.pdf
Final SEM Unit 1 for mit wpu at pune .pptx
NewMind AI Weekly Chronicles - August'25-Week II
DASA ADMISSION 2024_FirstRound_FirstRank_LastRank.pdf
OMC Textile Division Presentation 2021.pptx
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
Tartificialntelligence_presentation.pptx
The various Industrial Revolutions .pptx
1. Introduction to Computer Programming.pptx
1 - Historical Antecedents, Social Consideration.pdf
Getting started with AI Agents and Multi-Agent Systems

Leaflet JS (GIS) and Capital MetroRail

  • 2. Jim Wigginton • Creator and maintainer of phpseclib/phpseclib library • PHP for ~20 years • Born and raised in Austin, TX • terrafrost@php.net
  • 4. Leaflet Setup <link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css"/> <script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"></script> <style> body { margin: 0; } </style> <div id="map" style="width: 100%; height: 100%"></div> <script> var map = L.map('map').setView([51.505, -0.09], 13); var tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); </script> • The above is based off of https://leafletjs.com/examples/quick-start/ • [51.505, -09] is the latitude / longitude, 13 is the zoom • Current location can be obtained by doing this: • Real time location updates can be obtained by doing this: navigator.geolocation.getCurrentPosition(pos => { map.setView([pos.coords.latitude, pos.coords.longitude], 14); }); navigator.geolocation.watchPosition(pos => { polyline.addLatLng([pos.coords.latitude, pos.coords.longitude]); });
  • 5. Tile Sets Mapbox Satellite OpenStreetMap + TomTom Traffic Flow USGSTopo (MapServer) Mapbox Light + ISU Environmental Mesonet
  • 6. Database Setup 1. Install the necessary software: • Windows: Install OSGeo4W and then open "OSGeo4W Shell". • Ubuntu: Run the following commands: 2. Download shapefiles from https://www.capmetro.org/metrolabs. Sort by "Recently Updated“ 3. Import Routes.shp into MySQL: sudo add-apt-repository ppa:ubuntugis/ppa sudo apt-get update sudo apt-get install gdal-bin sudo apt-get install libgdal-dev ogr2ogr -f MySQL MySQL:dbname,host=localhost,user=user,password=pass Routes.shp -nln tablename -update -append -t_srs "EPSG:4326" -lco engine=InnoDB -skipfailures
  • 7. Coordinate Systems Coordinate System Austin, TX Example EPSG:4326 WGS 84 30.267222, -97.743056 TxDOT EPSG:4269 NAD83 30.260336, -97.7458308 NHD, TIGER EPSG:3857 WGS 84 / Pseudo-Mercator 3537945.1867267964, -10880707.234867256 US DOT EPSG:3081 NAD83 / Texas State Mapping System 902702.814304697, 1216728.9256139707 THC EPSG:32614 WGS 84 / UTM zone 14N 3349064.8428058745, 620908.0252681801 CapMetro Conversions done with https://epsg.io/transform
  • 8. Database OGR_FID SHAPE route_id routename direction routecolor textcolor routetype routetheme servicenm servicetyp sign_id service_id source sourcedate 1 BLOB 1 North Lamar/South Congress Southbound 004A97 FFFFFF Local NULL Weekday Weekday 153 1-153 Capital Metro 6/3/2022 2 BLOB 324 Georgian/Ohlen Westbound 004A97 FFFFFF Crosstown NULL Sunday Sunday 153 5-153 Capital Metro 6/3/2022 3 BLOB 19 Bull Creek Northbound 004A97 FFFFFF Local NULL Sunday Sunday 153 5-153 Capital Metro 6/3/2022 4 BLOB 243 Wells Branch Westbound 004A97 FFFFFF Feeder NULL Sunday Sunday 153 5-153 Capital Metro 6/3/2022 5 BLOB 6 East 12th Westbound 004A97 FFFFFF Local NULL Saturday Saturday 153 4-153 Capital Metro 6/3/2022 6 BLOB 310 Parker/Wickersham Eastbound 004A97 FFFFFF Crosstown NULL Weekday Weekday 153 1-153 Capital Metro 6/3/2022 7 BLOB 339 Tuscany Westbound 004A97 FFFFFF Crosstown NULL Saturday Saturday 153 4-153 Capital Metro 6/3/2022 8 BLOB 243 Wells Branch Eastbound 004A97 FFFFFF Feeder NULL Weekday Weekday 153 1-153 Capital Metro 6/3/2022 9 BLOB 322 Chicon/Cherrywood Southbound 004A97 FFFFFF Crosstown NULL Sunday Sunday 153 5-153 Capital Metro 6/3/2022 10 BLOB 550 Metro Rail Red Line Northbound E2231A FFFFFF Rail NULL RAIL AFC 2 Other 153 55004-153 Capital Metro 6/3/2022
  • 9. GeoJSON 1. Export from the DB with this SQL: 2. Load the GeoJSON in Leaflet by doing this: SELECT ST_AsGeoJSON(SHAPE), servicenm, servicetyp FROM capmetro_routes WHERE route_id = 550 AND servicenm = '6TRAINMON to THURS'; L.geoJSON(route).addTo(map);
  • 10. GeoJSON Primitives Type Example Point { "type": "Point", "coordinates": [30.0, 10.0] } LineString { "type": "LineString", "coordinates": [ [30.0, 10.0], [10.0, 30.0], [40.0, 40.0] ] } Polygon { "type": "Polygon", "coordinates": [ [[30.0, 10.0], [40.0, 40.0], [20.0, 40.0], [10.0, 20.0], [30.0, 10.0]] ] } { "type": "Polygon", "coordinates": [ [[35.0, 10.0], [45.0, 45.0], [15.0, 40.0], [10.0, 20.0], [35.0, 10.0]], [[20.0, 30.0], [35.0, 35.0], [30.0, 20.0], [20.0, 30.0]] ] }
  • 11. Markers 1. Export from the DB with this SQL: 2. Load the GeoJSON in Leaflet by doing this: SELECT stop_name, CONCAT(latitude, ',', longitude) AS pos FROM capmetro_stops WHERE stop_type = 'Rail Station'; L.marker([30.264843,-97.738448]) .bindPopup('Downtown Station’) .addTo(map);
  • 12. GTFS Realtime • General Transit Feed Specification • Developed by Google in 2006 • Uses Protocol Buffers • Get the download link for "CapMetro Vehicle Positions PB File" from https://www.capmetro.org/metrolabs. Sort by "Recently Updated" • Updated every 15s • composer require google/gtfs-realtime-bindings • Deprecated: As of February 2019, the official google-protobuf Google protoc tool doesn’t support proto2 files. As a result we are deprecating the PHP bindings until official support for proto2 files is implemented in the Google protocol buffer tools.
  • 13. GTFS Realtime: Server Side <?php require_once 'vendor/autoload.php’; use transit_realtimeFeedMessage; $output = []; $data = file_get_contents('https://data.texas.gov/download/eiei-9rpf/application%2Foctet-stream’); $feed = new FeedMessage(); $feed->parse($data); foreach ($feed->getEntityList() as $entity) { if ($entity->vehicle->trip && $entity->vehicle->trip->route_id == 550) { $pos = $entity->vehicle->position; $output[] = [ 'latitude' => $pos->latitude, 'longitude' => $pos->longitude, 'bearing' => $pos->bearing, 'speed' => $pos->speed ]; } } echo json_encode($output);
  • 14. GTFS Realtime: Client Side var realtimeUpdate = function() { fetch('realtime.php’) .then(response => response.json()) .then(data => { data.forEach(train => { var arrow = new L.Icon({ iconUrl: 'arrow-up.svg’, iconSize: [25,28.975], iconAnchor: [13, 0], }); var marker = L.marker( [train.latitude, train.longitude], {icon: arrow, rotationAngle: train.bearing} ) .bindPopup('<strong>Coordinates</strong>: ‘ + train.latitude + ',' + train.longitude + '<br><strong>Bearing</strong>: ' + train.bearing + '<br><strong>Speed</strong>: ' + train.speed) .addTo(map); }); }); } realtimeUpdate(); setInterval(realtimeUpdate, 15000);
  • 15. Putting It Together • arrow-up.svg is from Font Awesome ( ) and was edited with Inkscape • https://github.com/bbecquet/Leaflet.RotatedMar ker is used to rotate the arrow • NYC: https://api.mta.info/ • Los Angeles: https://developer.metro.net/api/ • Chicago: https://www.transitchicago.com/developers/bustracker/ • Houston: https://api-portal.ridemetro.org/ • London: https://api.tfl.gov.uk/ • France: https://prim.iledefrance-mobilites.fr/fr
  • 16. Austin Western Railroad • Download and import shapefile from https://hub.arcgis.com/datasets/fedmaps::north-american-rail- lines-1/explore • Query: SELECT * FROM trains WHERE rrowner1 = 'AWRR'; OGR_FID SHAPE objectid fraarcid frfranode tofranode cntyfips stateab country rrowner1 trkrghts1 subdiv passngr tracks net miles km shape_leng 123293 BLOB 123293 423812 360530 360533 287 TX US AWRR NULL GIDDINGS INDUSTRIA L SPUR NULL 1 I 0.040001 0.064375 0.000666 123294 BLOB 123294 423813 360533 360541 287 TX US AWRR NULL GIDDINGS INDUSTRIA L SPUR NULL 1 I 0.144155 0.231995 0.002261 123386 BLOB 123386 423905 355408 355502 453 TX US AWRR CMRX CENTRAL C 1 M 1.476048 2.375474 0.023111 123413 BLOB 123413 423932 355403 355406 453 TX US AWRR NULL NULL NULL 1 M 0.334526 0.538368 0.004952 123604 BLOB 123604 424123 353382 353376 53 TX US AWRR NULL MAIN LINE NULL 0 O 0.350358 0.563848 0.005183 123705 BLOB 123705 424224 358097 358085 21 TX US AWRR NULL NULL NULL 0 O 0.143777 0.231386 0.002201 123706 BLOB 123706 424225 358629 358637 21 TX US AWRR NULL NULL NULL 0 O 0.073028 0.117527 0.001145 123707 BLOB 123707 424226 358637 358651 21 TX US AWRR NULL NULL NULL 0 O 0.223662 0.35995 0.003704 123708 BLOB 123708 424227 358637 358648 21 TX US AWRR NULL NULL NULL 0 O 0.194351 0.312778 0.003207 124262 BLOB 124262 424783 355418 355416 453 TX US AWRR NULL NULL NULL 1 M 0.204555 0.329199 0.003038
  • 17. GeoJSON Multipart Geometries Type Example MultiPoint { "type": "MultiPoint", "coordinates": [ [10.0, 40.0], [40.0, 30.0], [20.0, 20.0], [30.0, 10.0] ] } MultiLineString { "type": "MultiLineString", "coordinates": [ [[10.0, 10.0], [20.0, 20.0], [10.0, 40.0]], [[40.0, 40.0], [30.0, 30.0], [40.0, 20.0], [30.0, 10.0]] ] } MultiPolygon { "type": "MultiPolygon", "coordinates": [ [ [[30.0, 20.0], [45.0, 40.0], [10.0, 40.0], [30.0, 20.0]] ], [ [[15.0, 5.0], [40.0, 10.0], [10.0, 20.0], [5.0, 10.0], [15.0, 5.0]] ] ] }
  • 18. GeoJSON Multipart Geometries: Part 2 Type Example GeometryCollection { "type": "GeometryCollection", "geometries": [ { "type": "Point", "coordinates": [40.0, 10.0] }, { "type": "LineString", "coordinates": [ [10.0, 10.0], [20.0, 20.0], [10.0, 40.0] ] }, { "type": "Polygon", "coordinates": [ [[40.0, 40.0], [20.0, 45.0], [45.0, 30.0], [40.0, 40.0]] ] } ] }
  • 19. Styles • Colorize layers: • Colorize markers: Colorized markers are from https://github.com/pointhi/leaflet-color-markers L.geoJSON(route, { color: 'red’, }).addTo(map); var redIcon = new L.Icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png’, shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png’, iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] }); L.marker([30.264843,-97.738448], {icon: redIcon}).bindPopup('Downtown Station').addTo(map);
  • 21. More Styles • Bring train location markers to the front: • Colorize markers: • Add Control Layer: var marker = L.marker( [train.latitude, train.longitude], {icon: arrow, rotationAngle: train.bearing, zIndexOffset: 1000} ); var stations = L.layerGroup(); stations.addLayer(L.marker([30.264843,-97.738448]).bindPopup('Downtown Station’)); stations.addTo(map); layerControl = L.control.layers().addTo(map); layerControl.addOverlay(stations, 'Stations');
  • 23. Counties • Download and import "Counties (and equivalent)" shapefile from https://www.census.gov/cgi- bin/geo/shapefiles/index.php • TIGER: Topologically Integrated Geographic Encoding and Referencing • Query: SELECT * FROM counties WHERE statefp = 48; OGR_FID SHAPE statefp countyfp countyns geoid name namelsad lsad aland awater intptlat intptlon 8BLOB 48327 1383949 48327 Menard Menard County 6 2.34E+09 613559 30.88527 -99.8589 12BLOB 48189 1383880 48189 Hale Hale County 6 2.6E+09 246678 34.06844 -101.823 14BLOB 4811 1383791 48011 Armstrong Armstrong County 6 2.35E+09 12183672 34.96418 -101.357 36BLOB 4857 1383814 48057 Calhoun Calhoun County 6 1.31E+09 1.36E+09 28.44172 -96.5796 39BLOB 4877 1383824 48077 Clay Clay County 6 2.82E+09 72506860 33.7859 -98.2129 56BLOB 48361 1383966 48361 Orange Orange County 6 8.65E+08 1.18E+08 30.12232 -93.8941 64BLOB 48177 1383874 48177 Gonzales Gonzales County 6 2.76E+09 8204086 29.46191 -97.4919 69BLOB 48147 1383859 48147 Fannin Fannin County 6 2.31E+09 20847065 33.59116 -96.105 71BLOB 48265 1383918 48265 Kerr Kerr County 6 2.86E+09 10231764 30.05995 -99.3533 100BLOB 48391 1383981 48391 Refugio Refugio County 6 2E+09 1.24E+08 28.32212 -97.1625
  • 24. Counties: Client Side var exclude = [], counties = L.layerGroup(), temp; layerControl.addOverlay(counties, 'Counties’); getCounties(map.getBounds()); map.on('moveend', function() { getCounties(map.getBounds()); }); function getCounties(bounds) { var ne = bounds.getNorthEast().lat + ',' + bounds.getNorthEast().lng; var sw = bounds.getSouthWest().lat + ',' + bounds.getSouthWest().lng; fetch('counties.php?ne=' + ne + '&sw=' + sw + '&exclude=' + exclude.join(',’)) .then(response => response.json()) .then(data => { data.forEach(county => { counties.addLayer(temp = L.geoJson(county.shape, {fill: false, color: "gray"})); counties.addLayer( L.marker(temp.getBounds().getCenter(), {opacity: 0}) .bindTooltip(county.name, {permanent: true, direction: 'center', className: 'countyName’}) ); exclude.push(county.fips); }); counties.addTo(map); }); }
  • 25. Counties: Server Side <?php $db = new PDO('mysql:dbname=dbname;host=host', 'user', 'pass’); $ne = explode(',', $_GET['ne’]); $sw = explode(',', $_GET['sw’]); $shape = "Polygon(( $sw[0] $sw[1], $ne[0] $sw[1], $ne[0] $ne[1], $sw[0] $ne[1], $sw[0] $sw[1] ))"; $sql_where = ‘’; $exclude = []; if (strlen($_GET['exclude'])) { $exclude = explode(',', $_GET['exclude’]); $sql_in = array_fill(0, count($exclude), '?’); $sql_in = implode(',', $sql_in); $sql_where = "AND countyfp NOT IN ($sql_in)"; } $q = $db->prepare(“ SELECT name, countyfp AS fips, ST_AsGeoJson(SHAPE) AS shape FROM county_shapes WHERE MBRIntersects(ST_GeomFromText(?, 4269), SHAPE) AND statefp = 48 AND ST_Area(ST_GeomFromText(?, 4269)) < 40075210158 $sql_where "); $q->execute(array_merge([$shape, $shape], $exclude)); $result = []; while ($row = $q->fetch(PDO::FETCH_ASSOC)) { $row['shape'] = json_decode($row['shape’]); $result[] = $row; } echo json_encode($result);
  • 29. Polygon Labels var countyNames; function labelCounties() { var bounds = map.getBounds(); if (countyNames) { map.removeLayer(countyNames); } countyNames = L.layerGroup(); for (const [name, layer] of Object.entries(countyLayers)) { var newCoords = [], coords = countyLayers[name].toGeoJSON()['features'][0]['geometry']['coordinates'][0] coords.forEach(function (coord) { coord = L.latLng(coord[1], coord[0]); coord.lat = Math.min(coord.lat, bounds.getNorthWest().lat); coord.lat = Math.max(coord.lat, bounds.getSouthEast().lat); coord.lng = Math.max(coord.lng, bounds.getNorthWest().lng); coord.lng = Math.min(coord.lng, bounds.getSouthEast().lng); newCoords.push(coord); }); var polygon = L.polygon(newCoords, {fill: false, opacity: 0}).addTo(map); marker = L.marker(polygon.getCenter(), {opacity: 0}).bindTooltip(name, {permanent: true, direction: 'center', className: 'countyName’}); countyNames.addLayer(marker); map.removeLayer(polygon); } countyNames.addTo(map); } labelCounties(); map.on('moveend', function() { labelCounties(); });
  • 31. The Narrows: With Counties
  • 32. Labels for Lines • Using https://github.com/3mapslab/Leaflet.streetlabels • Merge LineString’s: • Wrap GeoJSON: $coords = $rows[0]; unset($rows[0]); while (count($rows)) { foreach ($rows as $i => $line) { if ($coords[count($coords) - 1] == $line[0]) { $coords = array_merge($coords, array_slice($line, 1)); unset($rows[$i]); } else if ($coords[0] == $line[count($line) - 1]) { $coords = array_merge(array_slice($line, 0, -1), $coords); unset($rows[$i]); } } } function featureWrapper(geometry, name) { return feature = { type: 'Feature’, properties: {name: name}, geometry: geometry }; } $coords = [ 'type' => 'LineString’, 'coordinates' => $cords ];
  • 33. The Narrows: With Labeled Lines
  • 37. Georeferencing 1 Choose Tool • ArcGIS • QGIS • Georeferencer.com • MapWarper.net 3 Place Image 1. Extract bounding coordinates: 2. Convert to PNG or WebP 3. Place on map: gdalinfo exported.tiff L.imageOverlay( 'exported.webp’, [ // top left [30.2725146, -97.7578187], // bottom right [30.2644510, -97.7496347] ], {opacity: 0.5} ).addTo(map); 2 Basic Technique
  • 39. Merging Overlapping GeoTIFFs • Perform the merge: • "In areas of overlap, the last image will be copied over earlier ones" • Copy nextPage.tif as a new layer over temp.tif and trim away at nextPage.tif • Copy the trimmed layer back to nextPage.tif, delete the old layer, and save as new.tif. GeoTIFF data will be lost. gdal_merge -o temp.tif nextPage.tif prevPage.tif
  • 40. Creating Tile Layers 1 2 3 Copy GeoTIFF data from orig.tif to new.tif listgeo -no_norm orig.tif > orig.geo geotifcp -g orig.geo new.tif temp.tif Merge the TIFFs gdal_merge -o merged.tif master.tif temp.tif Rm master.tif; rm temp.tif; mv merged.tif master.tif Create Tile Layer Gdal2tiles master.tif 4 Use Tile Layer L.tileLayer('https://domain.tld/sanborn/{z}/{x}/{y}.png’, { tms: 1, opacity: 0.7, minZoom: 13, maxZoom: 19 });
  • 42. Tile Layer Caveats "OSM does NOT pre-render every tile. Pre-rendering all tiles would use around 54 TB of storage. As the following table shows, the majority of tiles are never viewed. In fact just 1.79% are viewed. It works out this way because the majority of tiles are at zoom level 18 and actually the majority contain nothing of interest. By following an on-the-fly rendering approach we can avoid rendering these tiles unnecessarily. The tile view count column shows how many tiles have been produced on the OSM Tile server." Source: https://wiki.openstreetmap.org/wiki/Tile_disk_usage
  • 43. Thank You • Slides & Feedback: https://joind.in/talk/44a76 • Questions? terrafrost@php.net