Advertisement

Leaflet JS (GIS) and Capital MetroRail

Nov. 3, 2022
Advertisement

More Related Content

Advertisement

Leaflet JS (GIS) and Capital MetroRail

  1. + terrafrost@php.net
  2. Jim Wigginton • Creator and maintainer of phpseclib/phpseclib library • PHP for ~20 years • Born and raised in Austin, TX • terrafrost@php.net
  3. 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]); });
  4. Tile Sets Mapbox Satellite OpenStreetMap + TomTom Traffic Flow USGSTopo (MapServer) Mapbox Light + ISU Environmental Mesonet
  5. 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
  6. 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
  7. 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
  8. 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);
  9. 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]] ] }
  10. 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);
  11. 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.
  12. 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);
  13. 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);
  14. 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
  15. 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
  16. 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]] ] ] }
  17. 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]] ] } ] }
  18. 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);
  19. The Result
  20. 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');
  21. The Result
  22. 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
  23. 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); }); }
  24. 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);
  25. The Result
  26. The Narrows
  27. The Narrows: Mapped
  28. 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(); });
  29. Polygon Labels Visualized
  30. The Narrows: With Counties
  31. 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 ];
  32. The Narrows: With Labeled Lines
  33. Third Street Railroad Trestle
  34. Sanborn Fire Insurance Maps
  35. Georeferencing
  36. 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
  37. The Result
  38. 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
  39. 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 });
  40. The Result
  41. 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
  42. Thank You • Slides & Feedback: https://joind.in/talk/44a76 • Questions? terrafrost@php.net
Advertisement