Compare commits

...

24 Commits
main ... dev

Author SHA1 Message Date
dancingCycle 20694dac86 chore: bump version to v0.6.0 2024-02-26 22:08:07 +01:00
dancingCycle 4aaad35ae5 feat: add Badges to app/pages/vehicle-positions.jsx 2024-02-26 22:06:10 +01:00
dancingCycle eca8a07656 feat: add setInterval for constant map page reload 2024-02-26 21:51:06 +01:00
dancingCycle af533042ad feat: deleted app/pages/map-page.jsx 2024-02-26 21:45:13 +01:00
dancingCycle d2dcf47245 feat: add dependency react-leaflet-cluster 2024-02-26 21:41:58 +01:00
dancingCycle bfb15c4cf1 feat: add app/pages/vehicle-positions.jsx 2024-02-26 17:32:59 +01:00
dancingCycle 0b501ee48a chore: adjust url for API call 2024-02-23 12:27:09 +01:00
dancingCycle 168db28cd1 chore: adjust url for API call 2024-02-23 12:27:09 +01:00
dancingCycle b09004a163 chore: adjust url for API call 2024-02-23 12:27:06 +01:00
dancingCycle 0b5a4eac3e chore: adjust url for API call 2024-02-23 12:26:34 +01:00
dancingCycle 966243afd3 chore: refactor use of app/config.js 2024-02-23 12:25:32 +01:00
dancingCycle a828b309be feat: add entity TripUpdate counters to UI 2024-01-29 14:25:02 +01:00
dancingCycle b3025d9d79 feat: add entity TripUpdate counters to UI 2024-01-29 14:09:42 +01:00
dancingCycle 648d1afe66 chore: update minimum node version to 18.17.0 2024-01-26 14:04:06 +01:00
dancingCycle 134dcbcd8b chore: bump version to v0.4.0 2023-07-27 14:29:12 +02:00
dancingCycle 72815f7cbd adjust page Contact 2023-07-27 14:15:40 +02:00
dancingCycle 9ff992b39a add page TripUpdates and VehiclePositions 2023-07-27 12:34:42 +02:00
dancingCycle c5cd44e632 feat: apply function readPbf(buffer) 2023-07-26 14:36:12 +02:00
dancingCycle d67e967c7a feat: add function readPbf(buffer) 2023-07-25 19:17:16 +02:00
dancingCycle 5ed587f0ce chore: adjust url for API call 2023-07-25 18:19:45 +02:00
dancingCycle ac91c2b741 chore: adjust url for API call 2023-07-25 18:17:42 +02:00
dancingCycle 6d94dc0732 chore: adjust url for API call 2023-07-25 18:15:01 +02:00
dancingCycle 82b92eac86 feat: clear Home page 2023-07-25 18:04:06 +02:00
dancingCycle b5e4f567a3 feat: adjust home page 2023-02-15 15:38:11 +01:00
26 changed files with 3604 additions and 2513 deletions

View File

@ -5,10 +5,9 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import Contact from './pages/contact';
import Home from './pages/home';
import Table from './pages/table-page';
import Map from './pages/map-page';
import NavBar from './components/nav-bar';
import TripUpdates from './pages/trip-updates';
import VehiclePositions from './pages/vehicle-positions';
export default function App() {
return (
@ -19,10 +18,10 @@ export default function App() {
<BrowserRouter>
<NavBar />
<Routes>
<Route path="/" element={<Map />} />
<Route path="/table" element={<Table />} />
<Route path="/map" element={<Map />} />
<Route path="/" element={<VehiclePositions />} />
<Route path="/contact" element={<Contact />} />
<Route path="/trip-updates" element={<TripUpdates />} />
<Route path="/vehicle-positions" element={<VehiclePositions />} />
</Routes>
</BrowserRouter>
);

32
app/components/input.js Normal file
View File

@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import Form from 'react-bootstrap/Form';
/*controlled component: input form value controlled by React*/
const InputSearch = ({id, name, onChange, placeholder, title, type, value}) => {
return (
<>
<Form.Control
aria-label={title}
className={name}
id={id}
name={name}
onChange={onChange}
placeholder={placeholder}
title={title}
type={type}
value={value}
/>
</>
);
};
export default InputSearch;
InputSearch.propTypes = {
id: PropTypes.string,
value: PropTypes.string,
name: PropTypes.string,
placeholder: PropTypes.string,
title: PropTypes.string,
type: PropTypes.string,
onChange: PropTypes.func
};

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {MapContainer,TileLayer} from 'react-leaflet';
import MarkerClusterGroup from 'react-leaflet-cluster';
/*JS module import (vs cdn or style link)*/
import 'leaflet/dist/leaflet.css'
import './map.css';
@ -26,20 +27,25 @@ export default function Map({messages}) {
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MarkerClusterGroup
chunkedLoading
>
{
messages.map(function(value,key) {
//console.log(`key: ${key}, tripId: ${value.tripId}`);
if(hasGtfs){
return <MsgMarkerWithGtfs key={value.tripId} index={value.vehicleId} message={value}/>;
}else{
return <MsgMarkerWithoutGtfs key={value.tripId} index={value.vehicleId} message={value}/>;
return <MsgMarkerWithGtfs key={key} index={value.trip_id} message={value}/>;
} else {
return <MsgMarkerWithoutGtfs key={key} index={value.trip_id} message={value}/>;
}
})
}
</MarkerClusterGroup>
</MapContainer>
</>
);
}
};
Map.propTypes = {
messages: PropTypes.array
};

View File

@ -7,20 +7,26 @@ import getIcon from './icon';
const MarkerMsgPlus = ({ message }) => {
if(message===undefined || message===null){
if(message === undefined || message === null){
console.error('message undefined or null');
return null;
}else{
//console.log(`MarkerMsgPlus: tripId: ${message.tripId}`);
const markerIcon=getIcon();
if(markerIcon===null){
if(markerIcon === null || markerIcon === undefined){
console.error('MarkerMsgPlus: icon null');
return null;
}else if(message.latitude === null || message.latitude === undefined) {
//console.error('MarkerMsgPlus: lat unavailable');
return null;
}else if(message.longitude === null || message.longitude === undefined) {
//console.error('MarkerMsgPlus: lon unavailable');
return null;
}else{
return(
<>
<Marker
position={[message.lat,message.lon]}
position={[message.latitude, message.longitude]}
icon={markerIcon}
>
<PopupMsg message={message} />

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import MarkerMsgPlus from './marker-msg-plus';
const MarkerMsg = ({ message }) => {
if(message===undefined || message===null){
if(message === undefined || message === null){
console.error('message undefined or null');
return null;
}else{

View File

@ -2,29 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import {Popup} from 'react-leaflet';
import seconds2dmhs from '../../utils/seconds2dhms';
const PopupMsg = ({message}) => {
/*get number of ms since epoch*/
const nowTsMs=Date.now();
const nowTs=Math.round(nowTsMs/1000);
const itcsTs=message.tsMsgCreationItcs;
const itcsTsMs=itcsTs*1000;
const itcsAge=seconds2dmhs(Math.round(nowTs-itcsTs));
const itcsDate=new Date(itcsTsMs);
const itcsString=itcsDate.toString()
return (
<>
<Popup>
message id: {message.id} <br/>
vehicle id: {message.vehicleId} <br/>
trip id: {message.tripId} <br/>
route id: {message.routeId} <br/>
lat: {message.lat} <br/>
lon: {message.lon} <br/>
<br/>
GTFS Realtime age: {itcsAge} <br/>
trip id: {message.trip_id} <br/>
route id: {message.route_id} <br/>
lat: {message.latitude} <br/>
lon: {message.longitude} <br/>
</Popup>
</>
);

View File

@ -5,18 +5,17 @@ import { LinkContainer } from 'react-router-bootstrap';
function NavigationBar () {
return (
<Navbar collapseOnSelect fixed="top" bg="dark" expand="xxl" variant="dark">
//TODO make brand available through configuration
<Navbar.Brand href="/">GTFS Realtime Display</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<LinkContainer to="/table">
<Nav.Link>Table</Nav.Link>
<LinkContainer to="/trip-updates">
<Nav.Link>TripUpdates</Nav.Link>
</LinkContainer>
</Nav>
<Nav className="mr-auto">
<LinkContainer to="/map">
<Nav.Link>Map</Nav.Link>
<LinkContainer to="/vehicle-positions">
<Nav.Link>VehiclePositions</Nav.Link>
</LinkContainer>
</Nav>
<Nav className="mr-auto">

36
app/components/select.js Normal file
View File

@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import Form from 'react-bootstrap/Form';
/*controlled component: select controlled by React*/
const Select = ({defaultValue, id, name, onChange, options, title}) => {
if (options) {
return (
<Form.Select
aria-label="select table entries per page"
className={name}
defaultValue={defaultValue}
name={name}
id={id}
onChange={onChange}
title={title}
>
{options.map((item, index) => (
<option key={index} value={item}>
{item}
</option>
))}
</Form.Select>
);
} else {
return <p>Select options unavailable.</p>;
}
};
export default Select;
Select.propTypes = {
id: PropTypes.string,
name: PropTypes.string,
defaultValue: PropTypes.number,
onChange: PropTypes.func,
options: PropTypes.array,
title: PropTypes.string
};

View File

@ -0,0 +1,63 @@
import React, { useEffect, useState } from 'react';
import Button from 'react-bootstrap/Button';
import Stack from 'react-bootstrap/Stack';
import PropTypes from 'prop-types';
import Select from './select';
import {selectOptions} from '../utils/select-options';
import TripUpdatesTable from './trip-updates-table';
import Input from './input';
import config from '../config';
export default function TripUpdatesPage(){
/*store and initialise data in function component state*/
const [oset, setOset] = useState(1);
const [limit, setLimit] = useState(parseInt(selectOptions[0],10));
const [searchField, setSearchField] = useState('');
const handleClickPrev = () => {
setOset((oset) => (oset > 1 ? --oset : oset));
};
const handleClickNext = () => {
setOset((oset) => ++oset);
};
const handleChangeLimit = (event) => {
setLimit((limit) => parseInt(event.target.value,10));
};
const handleSearch = (e) => {
setSearchField((searchField)=>e.target.value);
};
return (
<>
<Stack direction="horizontal" gap={1} className="m-1">
<Button variant="secondary" onClick={handleClickPrev}>
prev
</Button>
<Button variant="secondary" onClick={handleClickNext}>
next
</Button>
<Select
defaultValue={selectOptions[0]}
id="tablePageLimit"
name="tablePageLimit"
onChange={handleChangeLimit}
options={selectOptions}
/>
<Input
id="tablePageSearch"
name="tablePageSearch"
onChange={handleSearch}
placeholder="Search table globally"
type="search"
title="Enter search value"
value={searchField}
/>
</Stack>
<TripUpdatesTable
isFetched={false}
oset={oset}
limit={limit}
filter={searchField}
/>
</>
);
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import TripUpdatesEntry from './trip-updates-table-entry';
export default function TripUpdatesTableEntries ({aryData}) {
if (aryData.length > 0) {
//iterate over array
return aryData.map((item, index) => {
if (item.trip) {
//console.log('TripUpdatesTableEntries: trip available');
const trip = item.trip;
return (
<TripUpdatesEntry
tripId={typeof trip.trip_id !== 'undefined' ? trip.trip_id : null}
routeId={typeof trip.route_id !== 'undefined' ? trip.route_id : null}
directionId={typeof trip.direction_id !== 'undefined' ? trip.direction_id : null}
startTime={typeof trip.start_time !== 'undefined' ? trip.start_time : null}
startDate={typeof trip.start_date !== 'undefined' ? trip.start_date : null}
timestamp={typeof item.timestamp !== 'undefined' ? item.timestamp : null}
delay={typeof item.delay !== 'undefined' ? item.delay : null}
key={index}
/>
);
} else {
console.log('ERROR: TripUpdatesTableEntries: REQUIRED trip NOT available');
}
});
}else{
//data is empty
return null;
}
}
TripUpdatesTableEntries.propTypes = {
aryData: PropTypes.array
};

View File

@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
/*destructure props object*/
const TripUpdatesTableEntry = ({
tripId,
routeId,
directionId,
startTime,
startDate,
timestamp,
delay
}) => {
return (
<tr>
<td>{tripId}</td>
<td>{routeId}</td>
<td>{directionId}</td>
<td>{startTime}</td>
<td>{startDate}</td>
<td>{timestamp}</td>
<td>{delay}</td>
</tr>
);
};
TripUpdatesTableEntry.propTypes = {
tripId: PropTypes.string,
routeId: PropTypes.string,
directionId: PropTypes.number,
startTime: PropTypes.string,
startDate: PropTypes.string,
timestamp: PropTypes.number,
delay: PropTypes.number
};
export default TripUpdatesTableEntry;

View File

@ -0,0 +1,17 @@
import React from 'react';
const TripUpdatesTableHead = () => {
return (
<tr>
<th>Trip:trip_id</th>
<th>Trip:route_id</th>
<th>Trip:direction_id</th>
<th>Trip:start_time</th>
<th>Trip:start_date</th>
<th>timestamp</th>
<th>delay</th>
</tr>
);
};
export default TripUpdatesTableHead;

View File

@ -0,0 +1,144 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
import Table from 'react-bootstrap/Table';
import Alert from 'react-bootstrap/Alert';
import Badge from 'react-bootstrap/Badge';
import {readPbf} from '../utils/gtfs-rt-utils';
import TripUpdatesTableHead from './trip-updates-table-head';
import TripUpdatesTableEntries from './trip-updates-table-entries';
import config from '../config';
import {filterData} from '../utils/filter-data';
export default function TripUpdatesTable ({isFetched, oset, limit, filter}) {
const [ary, setAry] = useState([]);
const [aryFiltered, setAryFiltered] = useState([]);
const [fetchCompleted, setFetchCompleted] = useState(isFetched);
const [entityCount, setEntityCount] = useState(0);
const [entityTripUpdateCount, setEntityTripUpdateCount] = useState(0);
/*fetch ary in a JavaScript function*/
const fetch = async () => {
try {
/*TODO handle errors: https://www.valentinog.com/blog/await-react/*/
//TODO Make fields available via configuration!
const optns = { responseType: 'arraybuffer' };
const address = `${config.API}`;
console.log('getData() address: ' + address );
const res = await axios.get(address, optns);
if(res.data){
//////console.log('fetch() res available');
const rry = readPbf(res.data);
const rryLngth= rry.length;
console.log('fetch() rryLngh: ' + rryLngth);
setEntityCount(rryLngth);
const rryETU = new Array();
rry.forEach(entity => {
const entityTripUpdate = entity.trip_update;
if (entityTripUpdate) {
rryETU.push(entityTripUpdate);
}
});
const rryETULngth= rryETU.length;
console.log('fetch() rryETULength: ' + rryETULngth);
setEntityTripUpdateCount(rryETULngth);
const rryOstLmt = new Array();
let j = 0
for(let i = (oset - 1) * limit; i < rryETULngth ; i++){
if(j < limit){
rryOstLmt.push(rryETU[i]);
j++;
}
}
//console.log('fetch() rryOstLmt.length: ' + rryOstLmt.length);
setAry((ary) => rryOstLmt);
let data=filterData(rryOstLmt,'trip_updates',filter);
setAryFiltered((aryFiltered) => data);
}else{
console.error('fetch() res NOT available');
}
} catch (err) {
console.error('err.message: ' + err.message);
setAry([]);
setAryFiltered([]);
}
};
useEffect(()=>{
setAryFiltered((aryFiltered)=>{
let filtered=filterData(ary,'trip_updates',filter);
return filtered;
});
},[filter]);
useEffect(() => {
/*effect goes here*/
fetch();
setFetchCompleted((fetchCompleted)=>true);
//console.log('entityCount: ' + entityCount);
//console.log('entityTripUpdateCount: ' + entityTripUpdateCount);
/*use an empty dependency array to ensure the hook is running only once*/
/*TODO study dependency array: https://reactjs.org/docs/hooks-effect.html*/
}, [oset,limit]);
if(fetchCompleted && aryFiltered.length > 0){
/*return a React element*/
return (
<>
<Badge bg="secondary">
abs entity count: {entityCount}&nbsp;
</Badge>
&nbsp;
<Badge bg="secondary">
abs TripUpdate count: {entityTripUpdateCount}&nbsp;
</Badge>
&nbsp;
<Badge bg="secondary">
page trip count: {ary.length}&nbsp;
</Badge>
&nbsp;
<Badge bg="secondary">
filtered trip count: {aryFiltered.length}
</Badge>
&nbsp;
<Table
striped
bordered
hover
size="sm"
variant="dark"
responsive
>
<thead>
<TripUpdatesTableHead />
</thead>
<tbody>
<TripUpdatesTableEntries aryData={aryFiltered} />
</tbody>
</Table>
</>
);
}else{
return (
<Alert variant={'secondary'} onClose={() => setShow(false)} dismissible>
<Badge bg="secondary">TripUpdate</Badge> entities loading...
</Alert>
);
}
};
TripUpdatesTable.propTypes = {
isFetched: PropTypes.bool,
offset: PropTypes.number,
limit: PropTypes.number,
filter: PropTypes.string
};

3
app/config.js Normal file
View File

@ -0,0 +1,3 @@
export default {
API: 'https://vm2037.swingbe.mooo.com/'
};

View File

@ -4,32 +4,36 @@ const VERSION = packageInfo.version;
const Contact = () => {
return (
<>
<h2>About this website</h2>
<p>
For questions about this website please do not hesitate to reach out to dialog (at) swingbe (dot) de.
For questions about this website please do not hesitate to reach out to dialog
(at) swingbe (dot) de.
</p>
<p>
Source code has been made public on{' '}
Source code is controlled and provided using{' '}
<a
href="https://github.com/Software-Ingenieur-Begerad/gtfs-rt-display"
href="https://git.wtf-eg.de/dancesWithCycles/gtfs-rt-display"
target="_blank"
>
GitHub
</a>.
Git
</a>
.
</p>
<h2>Imprint</h2>
<address>
<strong>Software Ingenieur Begerad</strong>
<br />
Lammer Heide 87
<br />
38116 Braunschweig
<br />
Deutschland
<br />
</address>
<h2>Other</h2>
<p>
<a
href="https://www.swingbe.de/imprint"
target="_blank"
>
Imprint
</a>
</p>
<p>
<a
href="https://www.swingbe.de/privacy-policy"
target="_blank"
>
Privacy Policy
</a>
</p>
<p>
Version: {VERSION}
</p>

View File

@ -1,8 +1,10 @@
import React from 'react';
import React, {useState} from 'react';
import axios from 'axios';
export default function Home() {
return (
<>
<h1>Home</h1>
<p>Home</p>
</>
);
}

View File

@ -1,59 +0,0 @@
import React, {useEffect,useState} from 'react';
import axios from 'axios';
import Map from '../components/map/map';
import parseMessages from '../utils/gtfs-rt-utils';
export default function MapPage() {
/*storage*/
const [vehPos, setVehPos] = useState([]);
const getData= async ()=>{
//console.log('getData() start...');
try {
/*TODO handle errors: https://www.valentinog.com/blog/await-react/*/
//TODO Make fields available via configuration!
let url = 'https://api.entur.io/realtime/v1/gtfs-rt/vehicle-positions';
const res = await axios.get(url,
{
responseType: 'arraybuffer'
//responseType: 'blob'
});
if(res.data){
//TODO remove debugging
//console.log('getData() res available');
/*parse messages*/
const messages = parseMessages(res.data);
//console.log('getData() messages.length: '+messages.length);
/*set state*/
setVehPos(messages);
}else{
console.error('getData() res NOT available');
}
} catch (err) {
console.error('err.message: ' + err.message);
}
//console.log('getData() done.');
};
useEffect(()=>{
/*do not wait the interval when component loads the first time*/
getData();
/*refresh data periodically*/
const intervalCall=setInterval(()=>{
getData();
}, 5000);
/*TODO adjust interval, make it available via config file*/
return ()=>{
/*clean up*/
clearInterval(intervalCall);
};
},[]);
return (
<>
<Map messages={vehPos}/>
</>
);
}

View File

@ -1,46 +0,0 @@
import React, {useEffect,useState} from 'react';
import axios from 'axios';
import parseMessages from '../utils/gtfs-rt-utils';
import Table from '../components/table/table';
export default function TablePage() {
const [vehPos, setVehPos] = useState([]);
const getData= async ()=>{
//console.log('getData() start...');
try {
/*TODO handle errors: https://www.valentinog.com/blog/await-react/*/
//TODO Make fields available via configuration!
let url = 'https://api.entur.io/realtime/v1/gtfs-rt/vehicle-positions';
const res = await axios.get(url,
{
responseType: 'arraybuffer'
});
if(res.data){
//console.log('getData() res available');
const messages = parseMessages(res.data);
//console.log('getData() messages.length: '+messages.length);
setVehPos(messages);
}else{
console.error('getData() res NOT available');
}
} catch (err) {
console.error('err.message: ' + err.message);
}
//console.log('getData() done.');
};
useEffect(()=>{
getData();
const intervalCall=setInterval(()=>{
getData();
}, 5000);
/*TODO adjust interval, make it available via config file*/
return ()=>{
clearInterval(intervalCall);
};
},[]);
return (
<>
<Table messages={vehPos}/>
</>
);
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import TripUpdatesPage from '../components/trip-updates-page';
export default function TripUpdates() {
return (
<>
<TripUpdatesPage />
</>
);
};

View File

@ -0,0 +1,109 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import Alert from 'react-bootstrap/Alert';
import Badge from 'react-bootstrap/Badge';
import {readPbf} from '../utils/gtfs-rt-utils';
import Map from '../components/map/map';
import config from '../config';
export default function VehiclePositions() {
const [rry, setRry] = useState([]);
const [entityCount, setEntityCount] = useState(0);
const [entityVehiclePositionCount, setEntityVehiclePositionCount] = useState(0);
/*get data*/
const getData = async () => {
try {
/*TODO handle errors: https://www.valentinog.com/blog/await-react/*/
//TODO Make fields available via configuration!
const optns = { responseType: 'arraybuffer' };
const address = `${config.API}`;
//console.log('getData() address: ' + address );
const res = await axios.get(address, optns);
if(res.data){
////console.log('getRry() res available');
const rry = readPbf(res.data);
const rryLngth= rry.length;
//console.log('getRry() rryLngh: ' + rryLngth);
setEntityCount(rryLngth);
const rryEVP = new Array();
rry.forEach(entity => {
const entityVehiclePosition = entity.vehicle;
if (entityVehiclePosition !== null && entityVehiclePosition !== undefined) {
const oEVP = {};
const trip = entityVehiclePosition.trip;
if (trip !== null && trip !== undefined) {
oEVP.trip_id = trip.trip_id;
oEVP.route_id = trip.route_id;
const position = entityVehiclePosition.position;
if (position !== null && position !== undefined) {
oEVP.latitude = position.latitude;
oEVP.longitude = position.longitude;
rryEVP.push(oEVP);
}
}
}
});
const rryEVPLngth = rryEVP.length;
//console.log('getRry() rryEVPLength: ' + rryEVPLngth);
setEntityVehiclePositionCount(rryEVPLngth);
setRry(rryEVP);
}else{
console.error('getRry() res NOT available');
}
} catch (err) {
console.error('err.message: ' + err.message);
setRry([]);
}
//////console.log('getData() done.');
};
useEffect(()=>{
//initial call
getData();
const intervalCall=setInterval(()=>{
getData();
}, 10000);
return ()=>{
clearInterval(intervalCall);
};
},[]);
if (rry.length > 0) {
/*return a React element*/
return (
<>
<Badge bg="secondary">
abs entity count: {entityCount}&nbsp;
</Badge>
&nbsp;
<Badge bg="secondary">
abs VehiclePosition count: {entityVehiclePositionCount}&nbsp;
</Badge>
&nbsp;
<Map messages={rry}/>
</>
);
} else {
return (
<Alert variant={'secondary'} onClose={() => setShow(false)} dismissible>
<Badge bg="secondary">VehiclePostion</Badge> entities loading...
</Alert>
);
}
};

216
app/utils/filter-data.js Normal file
View File

@ -0,0 +1,216 @@
function filterData(data, name,filter){
if(data.length>0){
//console.log('filterData() data.length: '+data.length);
//console.log('filterData() name: '+name);
//console.log('filterData() filter:'+filter);
switch(name){
case 'fare_zones_history':
//TODO implement
console.log('filterData() //TODO implement fare_zones_history');
return data;
break;
case 'tdb_fare_zones':
return data.filter((item, index) => {
return (
item.id.toLowerCase().includes(filter.toLowerCase()) ||
(item.external!==null && item.external.toLowerCase().includes(filter.toLowerCase())) ||
(item.internal!==null && item.internal.toLowerCase().includes(filter.toLowerCase())) ||
(item.name!==null && item.name.toLowerCase().includes(filter.toLowerCase())) ||
(item.short_name!==null && item.short_name.toLowerCase().includes(filter.toLowerCase())) ||
(item.type!==null && item.type.toLowerCase().includes(filter.toLowerCase())) ||
(item.valid_from!==null && item.valid_from.toLowerCase().includes(filter.toLowerCase())) ||
(item.valid_until!==null && item.valid_until.toLowerCase().includes(filter.toLowerCase()))
);
});
break;
case 'lct_msg':
return data.filter((item, index) => {
return (
item.bs_id.toLowerCase().includes(filter.toLowerCase()) ||
(item.vc_trip!==null && item.vc_trip.toLowerCase().includes(filter.toLowerCase())) ||
(item.vc_route!==null && item.vc_route.toLowerCase().includes(filter.toLowerCase())) ||
(item.vc_tenant!==null && item.vc_tenant.toLowerCase().includes(filter.toLowerCase())) ||
(item.vc_date!==null && item.vc_date.toLowerCase().includes(filter.toLowerCase())) ||
(item.vc_time!==null && item.vc_time.toLowerCase().includes(filter.toLowerCase())) ||
(item.vc_lon!==null && item.vc_lon.toLowerCase().includes(filter.toLowerCase())) ||
(item.vc_lat!==null && item.vc_lat.toLowerCase().includes(filter.toLowerCase()))
);
});
break;
case 'localization':
return data.filter((item, index) => {
return (
item.localization_id.toLowerCase().includes(filter.toLowerCase()) ||
(item.name!==null && item.name.toLowerCase().includes(filter.toLowerCase())) ||
(item.lang_de!==null && item.lang_de.toLowerCase().includes(filter.toLowerCase())) ||
(item.lang_en!==null && item.lang_en.toLowerCase().includes(filter.toLowerCase())) ||
(item.version_id!==null && item.version_id.toLowerCase().includes(filter.toLowerCase()))
);
});
break;
case 'relations':
return data.filter((item, index) => {
return (
(item.dtype!==null && item.dtype.toLowerCase().includes(filter.toLowerCase())) ||
(item.id!==null && item.id.toLowerCase().includes(filter.toLowerCase())) ||
(item.active!=null && item.active.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.direct_purchase!==null && item.direct_purchase.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.disabled!==null && item.disabled.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.info!==null && item.info.toLowerCase().includes(filter.toLowerCase())) ||
(item.last_modified!==null && item.last_modified.toLowerCase().includes(filter.toLowerCase())) ||
(item.price_level!==null && item.price_level.toLowerCase().includes(filter.toLowerCase())) ||
(item.start_zone!==null && item.start_zone.toLowerCase().includes(filter.toLowerCase())) ||
(item.target_zone!==null && item.target_zone.toLowerCase().includes(filter.toLowerCase())) ||
(item.valid_from!==null && item.valid_from.toLowerCase().includes(filter.toLowerCase())) ||
(item.valid_until!==null && item.valid_until.toLowerCase().includes(filter.toLowerCase())) ||
(item.via_name!==null && item.via_name.toLowerCase().includes(filter.toLowerCase())) ||
(item.via_fare_zone!==null && item.via_fare_zone.toLowerCase().includes(filter.toLowerCase())) ||
(item.zones!==null && item.zones.toLowerCase().includes(filter.toLowerCase())) ||
(item.matching_via_id!==null && item.matching_via_id.toLowerCase().includes(filter.toLowerCase())) ||
(item.variant!==null && item.variant.toLowerCase().includes(filter.toLowerCase())) ||
(item.created_user_id!==null && item.created_user_id.toLowerCase().includes(filter.toLowerCase())) ||
(item.last_modified_user_id!==null && item.last_modified_user_id.toLowerCase().includes(filter.toLowerCase())) ||
(item.comment!== null && item.comment.toLowerCase().includes(filter.toLowerCase())) ||
(item.reverse_direction_id!== null && item.reverse_direction_id.toLowerCase().includes(filter.toLowerCase())) ||
(item.via_id!==null && item.via_id.toLowerCase().includes(filter.toLowerCase())) ||
(item.all_transit_zones!=null && item.all_transit_zones.toString().toLowerCase().includes(filter.toLowerCase()))
);
});
break;
case 'price_levels':
return data.filter((item, index) => {
return (
item.price_level_id.toString().toLowerCase().includes(filter.toLowerCase()) ||
(item.short_name!==null && item.short_name.toLowerCase().includes(filter.toLowerCase())) ||
(item.name!==null && item.name.toLowerCase().includes(filter.toLowerCase()))
);
});
break;
case 'price':
return data.filter((item, index) => {
return (
item.price_id.toString().toLowerCase().includes(filter.toLowerCase()) ||
(item.product_id!==null && item.product_id.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.price_level_id!==null && item.price_level_id.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.id_ticket!==null && item.id_ticket.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.price!==null && item.price.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.duration!==null && item.duration.toLowerCase().includes(filter.toLowerCase())) ||
(item.priority!==null && item.priority.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.localization_id4add_info!==null && item.localization_id4add_info.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.localization_id4ticket_descr!==null && item.localization_id4ticket_descr.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.localization_id4sale_text1!==null && item.localization_id4sale_text1.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.localization_id4sale_text2!==null && item.localization_id4sale_text2.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.localization_id4ticket_note1!==null && item.localization_id4ticket_note1.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.localization_id4ticket_note2!==null && item.localization_id4ticket_note2.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.localization_id4note_lang!==null && item.localization_id4note_lang.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.filter_code!==null && item.filter_code.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.version_id!==null && item.version_id.toString().toLowerCase().includes(filter.toLowerCase()))
);
});
break;
case 'product':
return data.filter((item, index) => {
return (
item.product_id.toString().toLowerCase().includes(filter.toLowerCase()) ||
(item.id_prod!==null && item.id_prod.toLowerCase().includes(filter.toLowerCase())) ||
(item.ext_prod_localization_id!==null && item.ext_prod_localization_id.toLowerCase().includes(filter.toLowerCase())) ||
(item.info_prod_localization_id!==null && item.info_prod_localization_id.toLowerCase().includes(filter.toLowerCase()))
);
});
break;
case 'sales_parameter':
return data.filter((item, index) => {
return (
item.sales_parameter_id.toString().includes(filter.toLowerCase()) ||
(item.product_id!==null && item.product_id.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.cnt_presale_days!==null && item.cnt_presale_days.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.app_prsnt_after_val!==null && item.app_prsnt_after_val.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.bday!==null && item.bday.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.num_add_names!==null && item.num_add_names.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.val_type!==null && item.val_type.toLowerCase().includes(filter.toLowerCase())) ||
(item.val_days!==null && item.val_days.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.val_beg_m_f!==null && item.val_beg_m_f.toLowerCase().includes(filter.toLowerCase())) ||
(item.val_beg_s_s!==null && item.val_beg_s_s.toLowerCase().includes(filter.toLowerCase())) ||
(item.val_end!==null && item.val_end.toLowerCase().includes(filter.toLowerCase())) ||
(item.version_id!==null && item.version_id.toString().toLowerCase().includes(filter.toLowerCase()))
);
});
break;
case 'tdb_stops':
return data.filter((item, index) => {
return (
item.id.toLowerCase().includes(filter.toLowerCase()) ||
(item.active!==null && item.active.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.lon!==null && item.lon.toLowerCase().includes(filter.toLowerCase())) ||
(item.location!==null && item.location.toLowerCase().includes(filter.toLowerCase())) ||
(item.lat!==null && item.lat.toLowerCase().includes(filter.toLowerCase())) ||
(item.stop_long_name!==null && item.stop_long_name.toLowerCase().includes(filter.toLowerCase())) ||
(item.stop_name!==null && item.stop_name.toLowerCase().includes(filter.toLowerCase())) ||
(item.stop_name_extern!==null && item.stop_name_extern.toLowerCase().includes(filter.toLowerCase())) ||
(item.fare_zone_1!==null && item.fare_zone_1.toLowerCase().includes(filter.toLowerCase())) ||
(item.fare_zone_2!==null && item.fare_zone_2.toLowerCase().includes(filter.toLowerCase())) ||
(item.fare_zone_3!==null && item.fare_zone_3.toLowerCase().includes(filter.toLowerCase())) ||
(item.fare_zone_4!==null && item.fare_zone_4.toLowerCase().includes(filter.toLowerCase())) ||
(item.valid_from!==null && item.valid_from.toLowerCase().includes(filter.toLowerCase())) ||
(item.valid_until!==null && item.valid_until.toLowerCase().includes(filter.toLowerCase())) ||
(item.last_modified!==null && item.last_modified.toLowerCase().includes(filter.toLowerCase()))
);
});
break;
case 'ticket-selection':
return data.filter((item,index)=>{
return (
item.price_id.toString().toLowerCase().includes(filter.toLowerCase()) ||
(item.ext_prod_de!==null && item.ext_prod_de.toLowerCase().includes(filter.toLowerCase())) ||
(item.id_ticket!==null && item.id_ticket.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.price!==null && item.price.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.name!==null && item.name.toLowerCase().includes(filter.toLowerCase())) ||
(item.short_name!==null && item.short_name.toLowerCase().includes(filter.toLowerCase())) ||
(item.priority!==null && item.priority.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.duration!==null && item.duration.toLowerCase().includes(filter.toLowerCase())) ||
(item.val_beg_m_f!==null && item.val_beg_m_f.toLowerCase().includes(filter.toLowerCase())) ||
(item.val_beg_s_s!==null && item.val_beg_s_s.toLowerCase().includes(filter.toLowerCase())) ||
(item.val_end!==null && item.val_end.toLowerCase().includes(filter.toLowerCase()))
);
});
break;
case 'trip_updates':
return data.filter((item, index) => {
return (
(item.trip.trip_id !== null && item.trip.trip_id.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.trip.route_id !== null && item.trip.route_id.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.trip.direction_id !== null && item.trip.direction_id.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.trip.start_time !== null && item.trip.start_time.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.trip.start_date !== null && item.trip.start_date.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.timestamp !== null && item.timestamp.toString().toLowerCase().includes(filter.toLowerCase())) ||
(item.delay !== null && item.delay.toString().toLowerCase().includes(filter.toLowerCase()))
);
});
case 'tokens':
return data;
break;
case 'users':
console.log('filterData() //TODO implement users');
return data;
break;
case 'versions':
return data.filter((item, index) => {
return (
item.version_id.toString().toLowerCase().includes(filter.toLowerCase()) ||
(item.name!==null && item.name.toLowerCase().includes(filter.toLowerCase())) ||
(item.valid_from!==null && item.valid_from.toLowerCase().includes(filter.toLowerCase())) ||
(item.valid_until!==null && item.valid_until.toLowerCase().includes(filter.toLowerCase())) ||
(item.descr!==null && item.descr.toLowerCase().includes(filter.toLowerCase()))
);
});
break;
default:
console.error(`filterData() name: ${name} unknown`);
return data;
}
}
return data;
};
module.exports = {
filterData
};

View File

@ -1,41 +1,58 @@
import Pbf from 'pbf';
import { FeedMessage } from './gtfs-rt.js';
import charIntoString from './string';
//import charIntoString from './string';
export default function parseMessages(buffer){
//console.log('parseMessages() start...');
/**
* parse buffer into array of entity objects
*/
export function parseMessages(buffer){
////console.log('parseMessages() start...');
const messages = [];
const pbf = new Pbf(buffer);
const feed = FeedMessage.read(pbf);
//console.log('parseMessages() feed:'+JSON.stringify(feed));
////console.log('parseMessages() feed:'+JSON.stringify(feed));
let countEntityAlert = 0;
let countEntityShape = 0;
let countEntityTripUpdate = 0;
let countEntityVehicle = 0;
feed.entity.forEach(entity => {
/*Data about the realtime position of a vehicle.*/
const vehiclePos = entity.vehicle;
if (vehiclePos) {
//console.log('getVehPos() vehiclePos available');
const entityAlert = entity.alert;
const entityShape = entity.shape;
const entityTripUpdate = entity.trip_update;
const entityVehicle = entity.vehicle;
if (entityAlert) {
countEntityAlert++;
} else if (entityShape) {
countEntityShape++;
} else if (entityTripUpdate) {
countEntityTripUpdate++;
}else if (entityVehicle) {
countEntityVehicle++;
////console.log('parseMessage() entityVehicle available');
/*The Trip that this vehicle is serving.*/
const trip=vehiclePos.trip;
const trip=entityVehicle.trip;
/*Additional information on the vehicle that is serving this trip.*/
const vehicle=vehiclePos.vehicle;
const vehicle=entityVehicle.vehicle;
/*Current position of this vehicle.*/
const position=vehiclePos.position;
const position=entityVehicle.position;
/*Moment at which the vehicle's position was measured. In POSIX time (i.e., number of seconds since January 1st 1970 00:00:00 UTC).*/
const vehPosTimestamp=vehiclePos.timestamp;
const vehPosTimestamp=entityVehicle.timestamp;
//remove tailing dot
//match a dot when it is followed by a whitespace or the end of the string
/*TODO Is this precaution required?*/
//TODO Handle error! Placing a decimal point at a fixed place does not work in general!
//let posLat=position.latitude;
//console.log(`getVehPos() posLat:${posLat}`);
////console.log(`parseMessage() posLat:${posLat}`);
//let latFormed = position.latitude === undefined ? -360 : position.latitude.toString().replace(/\.+$/, "");
//console.log(`getVehPos() latFormed:${latFormed}`);
////console.log(`parseMessage() latFormed:${latFormed}`);
//latFormed=charIntoString(latFormed,latFormed.length - 7,'.');
//console.log(`getVehPos() latFormed:${latFormed}`);
////console.log(`parseMessage() latFormed:${latFormed}`);
//let lonFormed = position.longitude === undefined ? -720 : position.longitude.toString().replace(/\.+$/, "");
//lonFormed=charIntoString(lonFormed,lonFormed.length - 7,'.');
//console.log(`getVehPos() lonFormed:${lonFormed}`);
////console.log(`parseMessage() lonFormed:${lonFormed}`);
const now= new Date();
const message={
/*Version of the feed specification. The current version is 2.0.*/
@ -61,9 +78,27 @@ export default function parseMessages(buffer){
};
messages.push(message);
} else {
console.error('getVehPos() vehiclePos NOT available');
console.error('ERROR: parseMessage() entity NOT known');
}
});
//console.log('parseMessages() done.');
//console.log('parseMessages() countEntityAlert: ' + countEntityAlert);
//console.log('parseMessages() countEntityShape: ' + countEntityShape);
//console.log('parseMessages() countEntityTripUpdate: ' + countEntityTripUpdate);
//console.log('parseMessages() countEntityVehicle: ' + countEntityVehicle);
////console.log('parseMessages() done.');
return messages;
};
/**
* read pbf file
* @return array of GTFS RT entities
*/
export function readPbf(buffer){
//console.log('readPbfs() start...');
const messages = [];
const pbf = new Pbf(buffer);
const feed = FeedMessage.read(pbf);
//console.log('readPbfs() length: ' + feed.entity.length);
//console.log('readPbfs() done.');
return feed.entity;
};

View File

@ -0,0 +1,4 @@
const selectOptions = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000];
module.exports = {
selectOptions
};

View File

@ -38,6 +38,14 @@ module.exports = {
},
],
},
{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'file-loader',
},
],
},
]
},
resolve: {

5062
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"private": true,
"name": "gtfs-rt-display",
"description": "display data from GTFS Realtime feeds",
"version": "0.3.0",
"version": "0.6.0",
"main": "index.js",
"keywords": [
"public",
@ -13,40 +13,42 @@
"display"
],
"author": "Software Ingenieur Begerad <dialog@SwIngBe.de>",
"homepage": "https://github.com/Software-Ingenieur-Begerad/gtfs-rt-display/tree/main",
"repository": "https://github.com/Software-Ingenieur-Begerad/gtfs-rt-display",
"bugs": "https://github.com/Software-Ingenieur-Begerad/gtfs-rt-display/issues",
"homepage": "https://www.swingbe.de/activity/gtfs-rt-display/",
"repository": "https://git.wtf-eg.de/dancesWithCycles/gtfs-rt-display",
"bugs": "https://git.wtf-eg.de/dancesWithCycles/gtfs-rt-display/issues",
"license": "GPL-3.0-or-later",
"engines": {
"node": ">=10"
"node": "<=18.17.0"
},
"scripts": {
"start": "webpack serve --config config/webpack.dev.js",
"build": "webpack --config config/webpack.prod.js"
},
"devDependencies": {
"@babel/core": "7.19.1",
"@babel/preset-env": "7.19.1",
"@babel/preset-react": "7.18.6",
"babel-loader": "8.2.5",
"css-loader": "6.7.1",
"html-webpack-plugin": "5.5.0",
"style-loader": "3.3.1",
"@babel/core": "7.22.10",
"@babel/preset-env": "7.22.10",
"@babel/preset-react": "7.22.5",
"babel-loader": "9.1.3",
"css-loader": "6.8.1",
"file-loader": "6.2.0",
"html-webpack-plugin": "5.5.3",
"style-loader": "3.3.2",
"svg-url-loader": "8.0.0",
"webpack": "5.74.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.11.0",
"webpack-merge": "5.8.0"
"webpack": "5.88.2",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-merge": "5.9.0"
},
"dependencies": {
"axios": "0.27.2",
"bootstrap": "5.2.1",
"leaflet": "1.8.0",
"pbf": "^3.2.1",
"axios": "1.4.0",
"bootstrap": "5.3.1",
"leaflet": "1.9.4",
"pbf": "3.2.1",
"react": "18.2.0",
"react-bootstrap": "2.5.0",
"react-bootstrap": "2.8.0",
"react-dom": "18.2.0",
"react-leaflet": "4.0.2",
"react-leaflet": "4.2.1",
"react-leaflet-cluster": "2.1.0",
"react-router-bootstrap": "0.26.2"
}
}