RideLogicAVLS.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. //
  2. // Copyright (c) 2021 Clementine Computing LLC.
  3. //
  4. // This file is part of RideLogic.
  5. //
  6. // RideLogic is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // RideLogic is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with RideLogic. If not, see <https://www.gnu.org/licenses/>.
  18. //
  19. var MAX_ICON = 128;
  20. var g_info = {
  21. "geojson" : {},
  22. "route" : {},
  23. "route_path": {},
  24. "active_route" : [],
  25. "ol_route_layer" : {},
  26. "json_gtfs": {}
  27. };
  28. var Feature = ol.Feature;
  29. var Map = ol.Map;
  30. var Overlay = ol.Overlay;
  31. var Point = ol.geom.Point;
  32. var TileJSON = ol.source.TileJSON;
  33. var VectorSource = ol.source.Vector;
  34. var View = ol.View;
  35. var Icon = ol.style.Icon;
  36. var Style = ol.style.Style;
  37. var TileLayer = ol.layer.Tile;
  38. var VectorLayer = ol.layer.Vector;
  39. var OSM = ol.source.OSM;
  40. var fromLonLat = ol.proj.fromLonLat;
  41. var Control = ol.control.Control;
  42. var LineString = ol.geom.LineString;
  43. //var defaultLonLat = [ -76.5019, 42.4440];
  44. //var defaultLonLat = [ -76.1805, 42.6012 ];
  45. var defaultLonLat = [ -76.2705, 42.5512 ];
  46. var defaultWebMerc = fromLonLat(defaultLonLat);
  47. function _rnd(a,b) {
  48. a = ((typeof a == "undefined") ? 1.0 : a);
  49. if (typeof b === "undefined") {
  50. return Math.random()*a;
  51. }
  52. return Math.random()*(b-a) + a;
  53. }
  54. function example_change_pos() {
  55. let w = 1/16.0;
  56. let lonlat = [
  57. defaultLonLat[0] + _rnd(-w,w),
  58. defaultLonLat[1] + _rnd(-w,w)];
  59. let merc = fromLonLat(lonlat);
  60. _icon[0].feature.getGeometry().setCoordinates(merc);
  61. busLayer.getSource().changed();
  62. }
  63. function example_change_opacity(p) {
  64. p = ((typeof p === "undefined") ? 0.0 : p);
  65. _icon[0].style.getImage().setOpacity(p);
  66. busLayer.getSource().changed();
  67. }
  68. // icon can't change image src dynamically,
  69. // so have to replace.
  70. // See https://stackoverflow.com/questions/57341190/how-can-i-change-icon-source-dynamically
  71. //
  72. function example_change_image() {
  73. let _nuimg = new Icon({
  74. anchor: [0.5, 46],
  75. anchorXUnits: 'fraction',
  76. anchorYUnits: 'pixels',
  77. src: 'data/bus_gw_90.png',
  78. });
  79. _icon[0].style.setImage(_nuimg);
  80. busLayer.getSource().changed();
  81. }
  82. //---
  83. var vectorSource = new VectorSource();
  84. var g_icon = [];
  85. for (let ii=0; ii<MAX_ICON; ii++) {
  86. let w = 1/32.0;
  87. let lonlat = [
  88. defaultLonLat[0] + _rnd(-w,w),
  89. defaultLonLat[1] + _rnd(-w,w)];
  90. let merc = fromLonLat(lonlat);
  91. let iconFeature = new Feature({
  92. geometry: new Point([merc[0], merc[1]]),
  93. name: 'bus'
  94. });
  95. let iconStyle = new Style({
  96. image: new Icon({
  97. //scale: [2, 2],
  98. anchor: [0.5, 46],
  99. anchorXUnits: 'fraction',
  100. anchorYUnits: 'pixels',
  101. src: 'data/icon.png',
  102. }),
  103. });
  104. iconStyle.getImage().setOpacity(0.0);
  105. g_icon.push({ "style": iconStyle, "feature": iconFeature });
  106. iconFeature.setStyle(iconStyle);
  107. vectorSource.addFeature(iconFeature);
  108. }
  109. //---
  110. const busLayer = new VectorLayer({
  111. source: vectorSource,
  112. zIndex: 2
  113. });
  114. //--
  115. const rasterLayer = new TileLayer({
  116. //source: ol.source.OSM()
  117. source: new ol.source.OSM(),
  118. });
  119. //--
  120. //-------------------
  121. //-------------------
  122. //-------------------
  123. const g_map = new Map({
  124. //layers: [rasterLayer, vectorLayer, routeLayer],
  125. layers: [rasterLayer, busLayer ],
  126. target: document.getElementById('map'),
  127. controls:[],
  128. view: new View({
  129. center: defaultWebMerc,
  130. zoom: 11,
  131. }),
  132. });
  133. var g_side_toggle = true;
  134. function toggle_sidebar(tf) {
  135. g_side_toggle = (g_side_toggle ? false : true);
  136. if (typeof tf !== "undefined") {
  137. g_side_toggle = tf;
  138. }
  139. // my perpetual fight with CSS.
  140. // I can't seem to get these values in when I put them in the
  141. // CSS, so we do it here programatically.
  142. //
  143. // Create a scrollable bar only for the list and not
  144. // for the map.
  145. // Hide the scroll bar when collapsing.
  146. // Hide the x scrollbar but dumping x values that overflow.
  147. //
  148. let ele = document.getElementById("sidebar-wrapper");
  149. if (g_side_toggle) {
  150. ele.style["overflow-y"] = "auto";
  151. ele.style["overflow-x"] = "hidden";
  152. ele.style["margin-left"] = 0;
  153. ele.style["min-width"] = "15rem";
  154. ele.style["max-height"] = "100vh";
  155. }
  156. else {
  157. ele.style["overflow-y"] = "hidden";
  158. ele.style["overflow-x"] = "hidden";
  159. ele.style["margin-left"] = "-15rem";
  160. ele.style["min-width"] = "0";
  161. ele.style["max-height"] = "100vh";
  162. }
  163. }
  164. class CustomToggle extends Control {
  165. constructor(opt) {
  166. opt = opt || {};
  167. let ele = document.createElement("button");
  168. ele.id = 'ui_route_toggle';
  169. ele.innerHTML = "Route";
  170. // class="btn" id="sidebarToggle" style='display:float;'
  171. ele.classList.add("custom-button");
  172. super({
  173. element: ele,
  174. target: opt.target
  175. });
  176. ele.addEventListener('click', this.fire.bind(this), false);
  177. }
  178. fire() {
  179. toggle_sidebar();
  180. // hacky, but functional
  181. //
  182. setTimeout(resize_map, 250);
  183. }
  184. }
  185. // button to toggle side bar route list
  186. //
  187. g_map.addControl(new CustomToggle({ className: 'custom-button' }));
  188. //-------------------
  189. //-------------------
  190. //-------------------
  191. g_map.addControl(new ol.control.Zoom({
  192. className: 'custom-zoom'
  193. }));
  194. const element = document.getElementById('popup');
  195. const popup = new Overlay({
  196. element: element,
  197. positioning: 'bottom-center',
  198. stopEvent: false,
  199. });
  200. g_map.addOverlay(popup);
  201. // display popup on click
  202. //
  203. g_map.on('click', function (evt) {
  204. // not working (wip)
  205. //
  206. /*
  207. const feature = map.forEachFeatureAtPixel(evt.pixel, function (feature) {
  208. return feature;
  209. });
  210. */
  211. });
  212. // change mouse cursor when over marker
  213. g_map.on('pointermove', function (e) {
  214. const pixel = g_map.getEventPixel(e.originalEvent);
  215. const hit = g_map.hasFeatureAtPixel(pixel);
  216. g_map.getTarget().style.cursor = hit ? 'pointer' : '';
  217. });
  218. // Close the popup when the map is moved
  219. //
  220. g_map.on('movestart', function () {
  221. //$(element).popover('dispose');
  222. });
  223. g_map.on("postrender", function(event) {
  224. //console.log("bang...");
  225. });
  226. //-----
  227. function update_buses(json_gtfs) {
  228. let bearingIdxLookup = [ "0", "45", "90", "135", "180", "225", "270", "315" ];
  229. for (let ii=0; ii<json_gtfs.entityList.length; ii++) {
  230. let id = json_gtfs.entityList[ii].id;
  231. let p = json_gtfs.entityList[ii].vehicle.position;
  232. let lat = p.latitude;
  233. let lon = p.longitude;
  234. let bearing = p.bearing;
  235. let route_id = -1;
  236. let route_str = "";
  237. if ((typeof json_gtfs.entityList[ii] !== "undefined") &&
  238. (typeof json_gtfs.entityList[ii].vehicle !== "undefined") &&
  239. (typeof json_gtfs.entityList[ii].vehicle.trip !== "undefined") &&
  240. (typeof json_gtfs.entityList[ii].vehicle.trip.routeId !== "undefined")) {
  241. route_str = "_r" + json_gtfs.entityList[ii].vehicle.trip.routeId;
  242. route_id = json_gtfs.entityList[ii].vehicle.trip.routeId;
  243. }
  244. else { }
  245. let lonlat = [ lon , lat ];
  246. let merc = fromLonLat(lonlat);
  247. if (typeof g_icon[ii] === "undefined") { continue; }
  248. g_icon[ii].feature.getGeometry().setCoordinates(merc);
  249. bearing = (bearing % 360 );
  250. if (bearing < 0) { bearing += 360.0; }
  251. let iheading = Math.floor( (parseInt( bearing ) + 23) / 45 );
  252. if (iheading > 7) { iheading = 0; }
  253. let bearing_str = "_stat"
  254. if (p.speed != 0) {
  255. bearing_str = "_" + bearingIdxLookup[iheading];
  256. }
  257. let _nuimg = new Icon({
  258. //scale: [1.5, 1.5],
  259. anchor: [0.5, 46],
  260. anchorXUnits: 'fraction',
  261. anchorYUnits: 'pixels',
  262. //src: 'data/bus_gw' + route_str + "_" + bearingIdxLookup[iheading] + '.png',
  263. src: 'data/bus_gw' + route_str + bearing_str + '.png',
  264. });
  265. g_icon[ii].style.setImage(_nuimg);
  266. if (g_info.active_route.length == 0) {
  267. g_icon[ii].style.getImage().setOpacity(0.8);
  268. }
  269. else {
  270. let is_active = false;
  271. for (let _aridx=0; _aridx < g_info.active_route.length; _aridx++) {
  272. if ( g_info.active_route[_aridx] == route_id) {
  273. is_active = true;
  274. }
  275. }
  276. if (is_active) {
  277. g_icon[ii].style.getImage().setOpacity(0.8);
  278. }
  279. else {
  280. g_icon[ii].style.getImage().setOpacity(0.0);
  281. }
  282. }
  283. }
  284. for (let ii=json_gtfs.entityList.length; ii<MAX_ICON; ii++) {
  285. g_icon[ii].style.getImage().setOpacity(0.0);
  286. }
  287. busLayer.getSource().changed();
  288. }
  289. function fetch_gtfs_vehicle_position() {
  290. let xhr = new XMLHttpRequest();
  291. xhr.onload = function() {
  292. let ab = xhr.response;
  293. let json_pb = pb2json.pb2json(ab);
  294. g_info.json_gtfs = json_pb;
  295. update_buses(json_pb)
  296. };
  297. xhr.responseType = "arraybuffer";
  298. xhr.open("GET", "VehiclePosition.pb");
  299. xhr.send();
  300. }
  301. //--
  302. function ui_clear_routes() {
  303. for (let i=0; i<g_info.active_route; i++) {
  304. let route_id = g_info.active_route[i];
  305. let route_layer = g_info.ol_route_layer[route_id];
  306. g_map.removeLayer( route_layer );
  307. }
  308. g_info.active_route = [ ];
  309. }
  310. function ui_show_single_route(route_id) {
  311. if (!(route_id in g_info.ol_route_layer)) {
  312. console.log("route", route_id, "not found in g_info.ol_route_layer");
  313. return;
  314. }
  315. for (let i=0; i<g_info.active_route; i++) {
  316. let route_id = g_info.active_route[i];
  317. let route_layer = g_info.ol_route_layer[route_id];
  318. g_map.removeLayer( route_layer );
  319. }
  320. g_info.active_route = [ route_id ];
  321. g_map.addLayer( g_info.ol_route_layer[route_id] );
  322. }
  323. function init_route_layer() {
  324. for (let route_id in g_info.route_path) {
  325. let route_points = g_info.route_path[route_id];
  326. let merc_points = [];
  327. for (let ii=0; ii<route_points.length; ii++) {
  328. merc_points.push(ol.proj.transform(route_points[ii], 'EPSG:4326', 'EPSG:3857'));
  329. }
  330. let routeLine = new ol.Feature({
  331. geometry: new ol.geom.LineString(merc_points)
  332. });
  333. let routeLineVector = new ol.source.Vector({});
  334. routeLineVector.addFeature(routeLine);
  335. let routeLayer = new VectorLayer({
  336. source: routeLineVector,
  337. style: new ol.style.Style({
  338. fill: new ol.style.Fill({ color: 'rgb(0,0,255,0.25)', weight: 4, opacity: 0.5 }),
  339. stroke: new ol.style.Stroke({ color: 'rgb(0,0,255,0.25)', width: 6, opacity: 0.5 })
  340. }),
  341. zIndex: 0
  342. });
  343. g_info.ol_route_layer[route_id] = routeLayer;
  344. }
  345. }
  346. function init_stop_layer(geojson) {
  347. //DEBUG
  348. return;
  349. if (!geojson) { return; }
  350. let stop_points = geojson.features[0].properties.route_stops;
  351. if (!stop_points) { return; }
  352. let merc_points = [];
  353. for (let ii=0; ii<stop_points.length; ii++) {
  354. let pnt = stop_points[ii].stop.geometry.coordinates;
  355. merc_points.push(ol.proj.transform(pnt, 'EPSG:4326', 'EPSG:3857'));
  356. }
  357. let stopVectorSource = new VectorSource();
  358. for (let ii=0; ii<merc_points.length; ii++) {
  359. let _stop = stop_points[ii].stop;
  360. let _stop_name = _stop.stop_name;
  361. let iconFeature = new Feature({
  362. geometry: new Point([merc_points[ii][0], merc_points[ii][1]]),
  363. name: _stop_name
  364. });
  365. let iconStyle = new Style({
  366. image: new Icon({
  367. anchor: [0.5, 46],
  368. anchorXUnits: 'fraction',
  369. anchorYUnits: 'pixels',
  370. src: 'data/star-3.png',
  371. }),
  372. });
  373. iconStyle.getImage().setOpacity(0.8);
  374. iconFeature.setStyle(iconStyle);
  375. stopVectorSource.addFeature(iconFeature);
  376. }
  377. let stopLayer = new VectorLayer({
  378. source: stopVectorSource,
  379. zIndex: 0
  380. });
  381. g_map.addLayer(stopLayer);
  382. }
  383. function fetch_geojson(url) {
  384. let xhr = new XMLHttpRequest();
  385. xhr.onload = function() {
  386. let geojson = xhr.response;
  387. g_info.geojson = geojson;
  388. //post_process_geojson();
  389. ui_add_route();
  390. init_route_layer(geojson);
  391. init_stop_layer(geojson);
  392. };
  393. xhr.responseType = "json";
  394. xhr.open("GET", url, true);
  395. xhr.send();
  396. }
  397. //---
  398. function post_process_geojson() {
  399. let trip = g_info.geojson.features;
  400. for (let i=0; i<trip.length; i++) {
  401. if (trip[i].type != "trip") { continue; }
  402. let prop = trip[i].properties;
  403. let _route = prop.route;
  404. if (_route.route_id in g_info.route) { continue; }
  405. g_info.route[_route.route_id] = prop.route_stops;
  406. g_info.route_path[_route.route_id] = trip[i].geometry.coordinates;
  407. }
  408. }
  409. // add routes to side menu
  410. //
  411. function ui_add_route() {
  412. let ele = document.getElementById("ui_route_list");
  413. ele.innerHTML = '';
  414. let _route_list = [];
  415. let _route_long_name = {};
  416. let trip = g_info.geojson.features;
  417. for (let i=0; i<trip.length; i++) {
  418. if (trip[i].type != "trip") { continue; }
  419. let prop = trip[i].properties;
  420. let _route = prop.route;
  421. if (_route.route_id in g_info.route) { continue; }
  422. g_info.route[_route.route_id] = prop.route_stops;
  423. g_info.route_path[_route.route_id] = trip[i].geometry.coordinates;
  424. _route_list.push( _route.route_id );
  425. _route_long_name[ _route.route_id ] = prop.route.route_long_name;
  426. }
  427. _route_list.sort( (a,b) => {
  428. let _a = parseInt(a), _b = parseInt(b);
  429. if (a<b) { return -1; }
  430. if (a>b) { return 1; }
  431. return 0;
  432. });
  433. for (let i=0; i<_route_list.length; i++) {
  434. let _rln = _route_list[i];
  435. let _a = document.createElement("a");
  436. _a.classList.add("list-group-item");
  437. _a.classList.add("list-gorup-item-action");
  438. _a.classList.add("list-gorup-item-light");
  439. _a.classList.add("p-3");
  440. _a.innerText = _route_long_name[_rln];
  441. _a.onclick = (function(_r) {
  442. return function() {
  443. if ( g_info.active_route == _r) {
  444. ui_clear_routes();
  445. update_buses(g_info.json_gtfs);
  446. }
  447. else {
  448. ui_show_single_route(_r);
  449. update_buses(g_info.json_gtfs);
  450. }
  451. }
  452. })(_route_list[i]);
  453. ele.appendChild(_a);
  454. }
  455. toggle_sidebar(g_side_toggle);
  456. }
  457. //---
  458. // resize map
  459. //
  460. function resize_map() {
  461. let h = $(window).height();
  462. let m = document.getElementById("map");
  463. m.style.height = (h-10) + "px";
  464. g_map.updateSize();
  465. }
  466. function init() {
  467. setInterval(fetch_gtfs_vehicle_position, 1000);
  468. setTimeout( function() { fetch_geojson("geojson/test_route.geojson"); }, 100);
  469. $(window).on('resize', resize_map);
  470. }
  471. $(document).ready(function() {
  472. init();
  473. resize_map();
  474. toggle_sidebar(g_side_toggle);
  475. });