Compare commits

...

14 Commits
main ... db

23 changed files with 839 additions and 5140 deletions

View File

@ -5,10 +5,10 @@ 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 Table from './pages/table-page';
import TripUpdates from './pages/trip-updates';
export default function App() {
return (
@ -20,9 +20,9 @@ export default function App() {
<NavBar />
<Routes>
<Route path="/" element={<Map />} />
<Route path="/table" element={<Table />} />
<Route path="/map" element={<Map />} />
<Route path="/contact" element={<Contact />} />
<Route path="/map" element={<Map />} />
<Route path="/trip-updates" element={<TripUpdates />} />
</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

@ -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>
<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,124 @@
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';
/*the simplest way to define a component is to write a JavaScript function*/
/*destructure props object*/
export default function TripUpdatesTable ({isFetched, oset, limit, filter}) {
////console.log('TripUpdatesTable: oset: ' + oset);
//////console.log('TripUpdatesTable: limit: ' + limit);
const [ary, setAry] = useState([]);
const [aryFiltered, setAryFiltered] = useState([]);
const [fetchCompleted, setFetchCompleted] = useState(isFetched);
/*get arry in a JavaScript function*/
const getRry = async () => {
try {
/*TODO handle errors: https://www.valentinog.com/blog/await-react/*/
const address = `${config.API}gtfs-rt`;
const res = await axios.get(address,
{
responseType: 'arraybuffer'
});
if(res.data){
////console.log('getRry() res available');
const rry = readPbf(res.data);
const rryLngth= rry.length;
////console.log('getRry() rryLngh: ' + rryLngth);
const rryETU = new Array();
let countEntityTripUpdate = 0;
rry.forEach(entity => {
const entityTripUpdate = entity.trip_update;
if (entityTripUpdate) {
rryETU.push(entityTripUpdate);
countEntityTripUpdate++;
}
});
////console.log('getRry() countEntityTripUpdate: ' + countEntityTripUpdate);
const rryOstLmt = new Array();
let j = 0
for(let i = (oset - 1) * limit; i < rryLngth ; i++){
if(j < limit){
rryOstLmt.push(rryETU[i]);
j++;
}
}
const rryOstLmtLngth = rryOstLmt.length;
////console.log('getRry() rryOstLmtLngh: ' + rryOstLmtLngth);
setAry((ary) => rryOstLmt);
//TODO let data=filterData(res.data,'tdb_stops',filter);
//TODO setAryFiltered((aryFiltered) => data);
setAryFiltered((aryFiltered) => rryOstLmt);
}else{
console.error('getRry() res NOT available');
}
} catch (err) {
console.error('err.message: ' + err.message);
setAry((ary) => []);
setAryFiltered((aryFiltered) => []);
}
};
useEffect(()=>{
setAryFiltered((aryFiltered)=>{
let filtered=filterData(ary,'tdb_stops',filter);
return filtered;
});
},[filter]);
useEffect(()=>{
setFetchCompleted((fetchCompleted)=>true);
}, [ary]);
useEffect(() => {
/*effect goes here*/
getRry();
/*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 (
<>
<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://www.v1gtfs-rt-p.api.swingbe.de/'
};

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,40 +1,53 @@
import React, {useEffect,useState} from 'react';
import axios from 'axios';
import Alert from 'react-bootstrap/Alert';
import Badge from 'react-bootstrap/Badge';
import Map from '../components/map/map';
import parseMessages from '../utils/gtfs-rt-utils';
import {parseMessages, readPbf} from '../utils/gtfs-rt-utils';
import config from '../config';
export default function MapPage() {
/*storage*/
const [vehPos, setVehPos] = useState([]);
const getData= async ()=>{
//console.log('getData() start...');
////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,
const address = `${config.API}gtfs-rt`;
const res = await axios.get(address,
{
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);
//console.log('getRry() res available');
const rry = readPbf(res.data);
const rryLngth= rry.length;
//console.log('getRry() rryLngh: ' + rryLngth);
const rryEVP = new Array();
let countEntityVehiclePositions = 0;
rry.forEach(entity => {
const entityVehiclePosition = entity.vehicle;
if (entityVehiclePosition) {
rryEVP.push(entityVehiclePosition);
countEntityVehiclePositions++;
}
});
//console.log('getRry() countEntityVehiclePositions: ' + countEntityVehiclePositions);
/*set state*/
setVehPos(messages);
setVehPos((messages) => rryEVP);
}else{
console.error('getData() res NOT available');
console.error('getRry() res NOT available');
}
} catch (err) {
console.error('err.message: ' + err.message);
}
//console.log('getData() done.');
////console.log('getData() done.');
};
useEffect(()=>{
@ -51,9 +64,19 @@ export default function MapPage() {
clearInterval(intervalCall);
};
},[]);
if (vehPos.length < 0) {
return (
<>
<Map messages={vehPos}/>
</>
);
} else {
return (
<Alert variant={'secondary'} onClose={() => setShow(false)} dismissible>
<Badge bg="secondary">VehiclePostion</Badge> entities loading...
</Alert>
);
}
}

View File

@ -1,8 +1,9 @@
import React, {useEffect,useState} from 'react';
import axios from 'axios';
import parseMessages from '../utils/gtfs-rt-utils';
import {parseMessages, readPbf} from '../utils/gtfs-rt-utils';
import Table from '../components/table/table';
export default function TablePage() {
const [vehPos, setVehPos] = useState([]);
const getData= async ()=>{
@ -10,13 +11,18 @@ export default function TablePage() {
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';
//let url = 'https://api.entur.io/realtime/v1/gtfs-rt/vehicle-positions';
//let url = 'http://localhost:65533/gtfs-rt';
let url = 'https://www.v1gtfs-rt-p.api.swingbe.de/gtfs-rt'
const res = await axios.get(url,
{
responseType: 'arraybuffer'
});
if(res.data){
//console.log('getData() res available');
/*parse messages*/
readPbf(res.data);
const messages = parseMessages(res.data);
//console.log('getData() messages.length: '+messages.length);
setVehPos(messages);

View File

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

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

@ -0,0 +1,205 @@
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 'tokens':
console.log('filterData() //TODO implement 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
};

20
db/bin/trip_updates.sh Normal file
View File

@ -0,0 +1,20 @@
#!/bin/sh
#
echo "Started..."
#
# special variable $# is the number of arguments
if [ $# -lt 3 ] ; then
echo 'Call ./<script> <db name> <db user> <db schema>'
exit 1
fi
#
DB_NAME="$1"
echo "DB_NAME: ${DB_NAME}"
DB_USER="$2"
echo "DB_USER: ${DB_USER}"
DB_SCHEMA="$3"
echo "DB_SCHEMA: ${DB_SCHEMA}"
#
psql -h localhost -p 5432 -U $DB_USER -f ./sql/trip_updates.sql -d $DB_NAME -v schema=$DB_SCHEMA
#
echo "Done."

20
db/sql/trip_updates.sql Normal file
View File

@ -0,0 +1,20 @@
-- colon before variable: for a prepared statement using named placeholders, this will be a parameter name of the form :name
CREATE SCHEMA IF NOT EXISTS :schema;
SET search_path to :schema, public;
-- create table
DROP TABLE IF EXISTS :schema.trip_updates;
CREATE TABLE IF NOT EXISTS :schema.trip_updates
(
trip_trip_id text NULL,
trip_route_id text NULL,
trip_schedule_relationship text NULL,
vehicle_id text NULL,
vehicle_label text NULL,
vehicle_license_plate text NULL,
timestamp_gtfsr timestamptz NULL,
timestamp_pgsql timestamptz NOT NULL DEFAULT now(),
delay int
);

5136
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.4.0",
"main": "index.js",
"keywords": [
"public",
@ -13,12 +13,12 @@
"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/dancingCycle/gtfs-rt-display",
"bugs": "https://git.wtf-eg.de/dancingCycle/gtfs-rt-display/issues",
"license": "GPL-3.0-or-later",
"engines": {
"node": ">=10"
"node": "<=18.13.0"
},
"scripts": {
"start": "webpack serve --config config/webpack.dev.js",
@ -39,14 +39,14 @@
"webpack-merge": "5.8.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-router-bootstrap": "0.26.2"
}
}

View File

@ -1,20 +0,0 @@
{
"extends":
[
"config:base"
],
"timezone": "Europe/Berlin",
"packageRules":
[
{
"matchDepTypes": ["devDependencies"],
"matchPackagePatterns": ["lint","prettier"],
"automerge": true
},
{
"matchUpdateTypes": ["minor","patch"],
"matchCurrentVersion": "!/^0/",
"automerge": true
}
]
}