Add velov stations

This commit is contained in:
Gabriel Augendre 2021-11-13 15:53:10 +01:00
parent 6c96a05002
commit 11cfe19650
7 changed files with 148 additions and 30 deletions

View file

@ -1,43 +1,57 @@
import React from 'react'; import React from 'react';
import './App.css'; import './App.css';
import Ligne from "./Ligne"; import Tcl, {ILigne} from "./Tcl";
import {ILigne} from "./interfaces"; import {clearInterval, setInterval} from "timers";
import {setInterval, clearInterval} from "timers"; import {Col, Container, Dropdown, DropdownButton, Row} from "react-bootstrap";
import {Col, Row, Container, Dropdown, DropdownButton} from "react-bootstrap"; import {IStationInfo, IStationsInfoWrapper, IStationsStatusWrapper, IVelovStation} from "./IVelov";
import Velov from "./Velov";
interface ITclFilteredApi {
passages: ILigne[];
}
interface IAppState { interface IAppState {
passages: ILigne[]; passages: ILigne[];
refreshDate?: string; refreshDate?: string;
stations: IVelovStation[];
} }
class App extends React.Component<{}, IAppState> { class App extends React.Component<{}, IAppState> {
timerId?: ReturnType<typeof setInterval>; timerId?: ReturnType<typeof setInterval>;
refreshSeconds: number; refreshSeconds: number;
monitoredVelovStationIds: string[] = [];
constructor(props: {}) { constructor(props: {}) {
super(props); super(props);
this.timerId = undefined; this.timerId = undefined;
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
this.refreshSeconds = Number(urlParams.get("refreshSeconds")); this.refreshSeconds = Number(urlParams.get("refreshSeconds"));
const velovStationIds = urlParams.get("velovStationIds");
if (velovStationIds) {
this.monitoredVelovStationIds = velovStationIds.split(";");
}
if (this.refreshSeconds <= 5) { if (this.refreshSeconds <= 5) {
this.refreshSeconds = 60; this.refreshSeconds = 60;
} }
this.state = {passages: [{ligne: undefined, delais: [undefined]}]}; this.state = {passages: [{ligne: undefined, delais: [undefined]}], stations: []};
} }
render() { render() {
return <div> return <div>
<Container className="main"> <Container className="main">
<Row md={4}> <Row sm={2}>
{this.state.passages.map((ligne) => <Ligne key={ligne.ligne} ligne={ligne.ligne} {this.state.passages.map((ligne) => <Tcl key={ligne.ligne} ligne={ligne.ligne}
delais={ligne.delais}/>)} delais={ligne.delais}/>)}
</Row>
<Row sm={2}>
{this.state.stations.map((station) => <Velov key={station.info.station_id} info={station.info} status={station.status}/>)}
</Row> </Row>
<Row> <Row>
<Col> <Col>
<DropdownButton variant="secondary" size="lg" title="Refresh"> <DropdownButton variant="secondary" size="lg" title="Refresh">
<Dropdown.Item onClick={this.refreshData}>Data</Dropdown.Item> <Dropdown.Item onClick={this.refreshData}>Data</Dropdown.Item>
<Dropdown.Item onClick={this.reload}>Full page</Dropdown.Item> <Dropdown.Item onClick={this.reload}>Full page</Dropdown.Item>
</DropdownButton> </DropdownButton>
</Col> </Col>
</Row> </Row>
@ -79,10 +93,45 @@ class App extends React.Component<{}, IAppState> {
private refresh() { private refresh() {
const headers = new Headers(); const headers = new Headers();
headers.set("Authorization", `Basic ${process.env.REACT_APP_TCL_AUTH}`); headers.set("Authorization", `Basic ${process.env.REACT_APP_TCL_AUTH}`);
http<IAppState>("https://tcl.augendre.info/stop/290", {method: "GET", headers: headers}).then(json => { const tclPromise = http<ITclFilteredApi>("https://tcl.augendre.info/stop/290", {
json.refreshDate = new Date().toLocaleString("fr-fr"); method: "GET",
this.setState(json); headers: headers
}); });
const velovInfoPromise = http<IStationsInfoWrapper>("https://transport.data.gouv.fr/gbfs/lyon/station_information.json", {method: "GET"});
const velovStatusPromise = http<IStationsStatusWrapper>("https://transport.data.gouv.fr/gbfs/lyon/station_status.json", {method: "GET"});
Promise.all([tclPromise, velovInfoPromise, velovStatusPromise]).then(values => {
const tcl = values[0];
const velovInfo = values[1];
const stationsInfo: Record<string, IStationInfo> = {};
for (const stationInfo of velovInfo.data.stations) {
if (this.monitoredVelovStationIds.includes(stationInfo.station_id)) {
stationsInfo[stationInfo.station_id] = stationInfo;
}
}
const velovStatus = values[2];
const stationsDict = new Map<string, IVelovStation>();
for (const stationStatus of velovStatus.data.stations) {
if (this.monitoredVelovStationIds.includes(stationStatus.station_id)) {
const velovStation: IVelovStation = {
status: stationStatus,
info: stationsInfo[stationStatus.station_id]
};
stationsDict.set(velovStation.info.station_id, velovStation);
}
}
const stations: IVelovStation[] = [];
for (const monitoredVelovStationId of this.monitoredVelovStationIds) {
const stationInfo = stationsDict.get(monitoredVelovStationId);
if (stationInfo) {
stations.push(stationInfo);
}
}
this.setState({
refreshDate: new Date().toLocaleString("fr-fr"),
passages: tcl.passages,
stations: stations,
});
})
} }
private reload = () => { private reload = () => {

39
src/IVelov.ts Normal file
View file

@ -0,0 +1,39 @@
export interface IStationInfo {
address: string;
capacity: number;
lat: number;
lon: number;
name: string;
station_id: string;
}
export interface IStationsInfo {
stations: IStationInfo[];
}
export interface IStationsInfoWrapper {
data: IStationsInfo;
}
export interface IStationStatus {
is_installed: number;
is_renting: number;
is_returning: number;
last_reported: number;
num_bikes_available: number;
num_docks_available: number;
station_id: string;
}
export interface IStationsStatus {
stations: IStationStatus[];
}
export interface IStationsStatusWrapper {
data: IStationsStatus;
}
export interface IVelovStation {
info: IStationInfo;
status: IStationStatus;
}

View file

@ -1,11 +1,17 @@
import React from "react"; import React from "react";
import {ILigne, PassageType} from "./interfaces";
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faBus} from '@fortawesome/free-solid-svg-icons' import {faBus} from '@fortawesome/free-solid-svg-icons'
import {Card, Col, ListGroup} from "react-bootstrap"; import {Card, Col, ListGroup} from "react-bootstrap";
import {placeholder} from "./utils";
export type PassageType = string | undefined;
export default class Ligne extends React.Component<ILigne> { export interface ILigne {
ligne?: string;
delais: PassageType[];
}
export default class Tcl extends React.Component<ILigne> {
render() { render() {
return <Col> return <Col>
<Card> <Card>
@ -40,11 +46,3 @@ class Passage extends React.Component<IPassageProps> {
return <ListGroup.Item>{placeholder(this.props.passage)}</ListGroup.Item> return <ListGroup.Item>{placeholder(this.props.passage)}</ListGroup.Item>
} }
} }
function placeholder(value?: string) {
if (value === undefined) {
return <span className="placeholder"/>
} else {
return value;
}
}

3
src/Velov.css Normal file
View file

@ -0,0 +1,3 @@
.card-header {
text-transform: capitalize;
}

27
src/Velov.tsx Normal file
View file

@ -0,0 +1,27 @@
import React from "react";
import './Velov.css';
import {Card, Col, ListGroup} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAnchor, faBicycle, faBiking} from "@fortawesome/free-solid-svg-icons";
import {IVelovStation} from "./IVelov";
export default class Velov extends React.Component<IVelovStation> {
render() {
return <Col>
<Card>
<Card.Header>
<FontAwesomeIcon icon={faBiking}/> {this.props.info.name.toLowerCase()}
</Card.Header>
<ListGroup variant="flush">
<ListGroup.Item>
<FontAwesomeIcon icon={faBicycle}/> {this.props.status.num_bikes_available}
</ListGroup.Item>
<ListGroup.Item>
<FontAwesomeIcon icon={faAnchor}/> {this.props.status.num_docks_available}
</ListGroup.Item>
</ListGroup>
</Card>
</Col>
}
}

View file

@ -1,7 +0,0 @@
export type PassageType = string|undefined;
export interface ILigne {
ligne?: string;
delais: PassageType[];
}

9
src/utils.tsx Normal file
View file

@ -0,0 +1,9 @@
import React from "react";
export function placeholder(value?: string) {
if (value === undefined) {
return <span className="placeholder"/>
} else {
return value;
}
}