-
Notifications
You must be signed in to change notification settings - Fork 4
Estrutura
O projeto é organizado da seguinte forma:
- src: pasta principal do projeto, contendo todo o código fonte, nessa pasta além dos arquivos padrão do React(App.jsx, main.jsx) é possível encontrar components, assets, pages, hooks e services.
Os componentes descritos abaixo são os principais do app. Alguns deles apresentam variação de acordo com o tema (BRT ou SPPO).
Nome | Descrição | BRT | SPPO |
---|---|---|---|
Header | cabeçalho da tela com código pesquisado | ||
InfoCard | card para selecionar a plataforma desejada | Pontos convencionais não possuem plataforma | |
InfoCard (Plataforma selecionada) | card para selecionar a trip que deseja ver todo o trajeto |
||
SequenceCard | card com sequência de pontos em que a trip passa a partir do ponto pesquisado |
Existem duas telas no app, uma antes de pesquisar o código e outra depois, a tela depois de pesquisado é onde os componentes são chamados e renderizados de acordo com a rota e código que o passageiro deseja ver.
Aqui pode ser pesquisado ou escaneado o QR Code. Os contextos que são usados aqui são apenas para definir o código e ativar o QR Code
O plug-in de mapa usado é o Leaflet e o React-leaflet. Nessa tela é usado a maior parte do Hooks e contextos criados. Os componentes são mostrados em condicionais de acordo com os cliques do passageiro e respostas das chamadas da API.
Etapa | BRT | SPPO |
---|---|---|
Código pesquisado | ||
Plataforma BRT Selecionada | ||
Linha escolhida |
Nesta pasta estão todas as funções que permitem o app funcionar, aqui ocorre todas as chamadas para a nossa API e é definido as variáveis de estado para os componentes mostrarem corretamente os dados para os passageiros. As funções funcionam de acordo com a nomenclatura e estrutura do GTFS, então se houver alguma dúvida é recomendável acessar este site.
Esse contexto é o primeiro a ser usado, aqui é onde mostra as sugestões de nomes quando digitado no input o nome da estação ou ponto desejado, a pesquisa só mostra resultados que possuem o campo stop_code
preenchido. Também é usado o useNavigate do react-router,passando como parâmetro o código que for pesquisado na tela inicial.
O service_id
é um campo que define qual dia uma linha está disponível. É feito uma pesquisa em duas urls: /calendar/
e /calendar_dates/
, primeiro é checado se o dia de hoje está presente em calendar_dates
, caso esteja, o service_id
é definido com o valor que a data da, se caso não estiver é pesquisado em calendar
, que é definido pelo dia da semana que foi feita a pesquisa.
Função principal do hook:
function findService(todayDate) {
const service = calendar.find((item) => item.date === todayDate);
if (service) {
setServiceId(service.service_id);
} else if (weekDay) {
const dayOfWeek = new Intl.DateTimeFormat('en-US', options).format(new Date()).toLowerCase();
const serviceWorks = weekDay.filter((service) => service[dayOfWeek] === 1);
const currentDate = new Date(todayDate);
if (serviceWorks.length > 0) {
const todayService = serviceWorks.find((service) => {
const startDate = new Date(service.start_date);
const endDate = new Date(service.end_date);
return currentDate >= startDate && currentDate <= endDate;
});
if (todayService) {
setServiceId(todayService.service_id);
}
}
}
}
Nesse contexto são iniciadas as variáveis primárias do app, por exemplo:
-
code
: código que foi pesquisado, ele é recebido pela URL usando a função useSearchParams do react-router
Depois que o código é obtido é rodado o seguinte useEffect, aqui é feito a pesquisa na API e é salvo as informações sobre o ponto.
useEffect(() => {
if (code) {
api
.get("/stops/?stop_code=" + code.toUpperCase())
.then(response => {
setStopId(response.data.results[0].stop_id)
setLocationType(response.data.results[0].location_type)
setName(response.data.results[0].stop_name)
setCenter([parseFloat(response.data.results[0].stop_lat), parseFloat(response.data.results[0].stop_lon)])
setStopCoords([response.data.results[0].stop_lon, response.data.results[0].stop_lat])
})
}
}, [code])
Além disso é iniciado a variável de estado gpsUrl
, que mais tarde será usada para mostrar a localização e estimativa de chegada em tempo real dos ônibus em corredores BRT.
O tema é definido pelo route_type
, a partir do momento que o stop_id
e o service_id
são definidos a função que pesquisa por tema é disparada. A função além de servir para o tema é armazenado um array que contém os route_types
.
async function findTheme(url) {
let routeTypes = [];
await api
.get(url)
.then(({ data }) => {
data.results.forEach((item) => {
routeTypes.push(item.trip_id.route_id.route_type);
});
const brtRoute = (e) => e === 702
if (routeTypes.some(brtRoute)) {
setBrt();
setTheme("")
} else if (routeTypes === 3 || 700) {
setSppo();
setTheme("sppo")
}
setRouteType([...routeTypes]);
});
}
Aqui acontecem as funções que buscam e definem as linhas e em caso de código BRT as plataformas também. A partir de agora o service_id
e o
stop_id
são dependências que constantemente são monitoradas para mudar as informações do app. Nesse contexto é criado funções, como compareTripName
, que é usada para ordenar as linhas em ordem crescente pelo número, e getMultiplePages
nessa função é feita uma busca em /stop_times/
usando como parâmetro service_id
e stop_id
e assim que tiver a resposta é retirado as linhas que tenham direction_id
e trip_short_name
diferente:
const filteredTrips = [];
async function getMultiplePages(url) {
await api
.get(url)
.then(({ data }) => {
data.results.forEach((item) => {
const existingTrip = filteredTrips.find((trip) => trip.trip_id.trip_short_name === item.trip_id.trip_short_name && trip.trip_id.direction_id === item.trip_id.direction_id);
if (!existingTrip) {
filteredTrips.push(item);
}
});
if (data.next) {
getMultiplePages(data.next);
} else {
if (locationType === 1) {
getStations(`/stop_times/?stop_id=${stopId}&service_id=${serviceId}`)
}
filteredTrips.sort(compareTripName)
setRoutes([...filteredTrips]);
}
});
}
E se por acaso for BRT é disparada outra função que ao invés de armazenar as linhas é armazenado as plataformas (estações BRT possuem mais de uma plataforma, e são consideradas "estações pai" que são definidas por location_type: 1
) e depois que essas estações são atualizadas é criado, a partir de um reduce
, um novo objeto que usa o platform_code
como chave e o valor sendo todas as informações da plataforma:
useEffect(() => {
if (routeType) {
if (locationType != null || locationType != undefined || stations != undefined) {
const iteratee = stations.map((e) => e)
const result = iteratee.reduce((acc, curr) => {
if (routeType.includes(3) && routeType.includes(702)) {
acc[curr.stop_id.platform_code] = acc[curr.stop_id.platform_code] || {};
acc[curr.stop_id.platform_code][curr.stop_id.stop_id] = curr;
return acc;
}
}, {});
setPlataforms((prevResults) => [...prevResults, result]);
}
}
}, [stations]);
Depois que todas as linhas são mostradas é possível selecionar a linha que deseja ver. Quando selecionada esse contexto roda a função que também busca em /stop_times/
mas usando o trip_id
e direction_id
. O shape_id
é armazanedo e a variável stopInfo
também, além de toda a sequência de pontos que temos como resposta da API, os pontos são ordenados por stop_sequence
, para prevenir que apareça uma ordem diferente da rota que o ônibus segue. Essa sequência é separada a partir do ponto que foi pesquisado, as estações BRT usam o nome da plataforma( plataformas "filhas" do BRT não possuem stop_id
único) pesquisada para achar o índice dela na lista e mostrar apenas os que vem depois, enquanto os pontos de ônibus convencionais usam o índice do stop_id
pesquisado.
useEffect(() => {
if (trip) {
setStopInfo(trip)
setShape(trip.trip_id.shape_id)
getAllStops(`/stop_times/?trip_id=${trip.trip_id.trip_id}&direction_id=${trip.trip_id.direction_id}&service_id=${serviceId}`)
}
}, [trip])
useEffect(() => {
if (!isLoading) {
const sortedSequence = allSequenceStops.sort((a, b) => { a.stop_sequence - b.stop_sequence })
if (locationType === 1) {
const mapSequence = sortedSequence?.map(e => e.stop_id.stop_name).indexOf(name)
const mapSequenceIncludes = sortedSequence?.findIndex(e => e.stop_id.stop_name.includes(name))
if (mapSequence === -1) {
const filteredSequenceIncludes = sortedSequence?.splice(mapSequenceIncludes)
setSequenceInfo(filteredSequenceIncludes)
} else {
const filteredSequence = sortedSequence?.splice(mapSequence)
setSequenceInfo(filteredSequence)
}
} else {
const mapSequence = sortedSequence?.map(e => e.stop_id.stop_id).indexOf(stopId)
const filteredSequence = sortedSequence?.splice(mapSequence)
setSequenceInfo(filteredSequence)
}
}
}, [allSequenceStops])
As shapes
são o trajeto que o ônibus segue para completar a linha, para poder desenhar isso no mapa é usada a Polyline do react-leaflet. O shape
precisa ser "cortado" a partir do ponto pesquisado para mostrar apenas o trajeto que vai ser percorrido, portanto é usado o turf.js. Primeiro é feito a pesquisa em /shapes/
usando o shape_id
e depois é usado as funções que o turf disponibiliza para fazer esse recorte (em caso de pontos iniciais mostra o trajeto inteiro já que ele não possui nada anterior a ele) :
let allPoints = []
async function getAllPoints(url) {
await api
.get(url)
.then(({ data }) => {
data.results.forEach((item) => { allPoints.push(item) })
if (data.next) {
getAllPoints(data.next)
} else {
allPoints.sort((a, b) => a.shape_pt_sequence - b.shape_pt_sequence)
setRawPoints([...allPoints])
}
})
}
useEffect(() => {
if (sequenceInfo) {
getAllPoints("/shapes/?shape_id=" + shape)
}
}, [sequenceInfo])
useEffect(() => {
if (rawPoints) {
let longLat = rawPoints.map(p => [p.shape_pt_lon * 1, p.shape_pt_lat * 1])
var line = turf.lineString(longLat);
var pt = turf.point(stopCoords)
var splitter = turf.nearestPointOnLine(line, pt)
var split = turf.lineSplit(line, splitter)
if (split.features.length == 2) {
setPoints(split.features[1].geometry.coordinates.map(c => [c[1], c[0]]))
} else {
setPoints(line.geometry.coordinates.map(c => [c[1], c[0]]))
}
}
}, [rawPoints])
Contexto para mostrar ou fechar o formulário de feedback.
Esses contextos são os que mostram informações em tempo real
O gps utiliza como parâmetro o stop_id
e é atualizado a cada 6 segundos, existe uma função para começar a fazer a busca e outra que é exportada para ser usada por todo o app para parar:
async function getGPS(url) {
await gps.get(url).then(({ data }) => {
data.results.forEach((item) => {
allBuses.push(item);
});
setRealtime([...allBuses]);
allBuses = [];
});
}
function startFetching() {
getGPS(gpsUrl);
const intervalId = setInterval(() => {
getGPS(gpsUrl);
}, 6000);
setIntervalId(intervalId);
}
function stopFetching() {
clearInterval(intervalId);
setIntervalId(null);
}
useEffect(() => {
if(gpsUrl){
startFetching();
return () => {
stopFetching();
};
}
}, [gpsUrl]);
Aqui é usado as informações do gps e das linhas que chegam de stop_times
. Primeiro é feito todo o filtro dos dados em tempo real, são eles:
- se não estiverem em garagem (para conferir se usa booleanPointInPolygon).
- apenas os que passam naquela plataforma
- a distância não for negativa (se for quer dizer que o ônibus já passou pelo ponto)
- tiver sido atualizado há mais de 5 minutos
useEffect(() => {
if (realtime && routes) {
let trackedBuses = []
realtime.map((i) => {
const currentTime = new Date();
const givenTime = new Date(i.dataHora);
const timeDifference = currentTime - givenTime
const seconds = Math.floor(timeDifference / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
const result = {
code: i.codigo,
linha: i.trip_short_name,
lat: i.latitude,
lng: i.longitude,
velocidade: i.velocidade,
sentido: i.direction_id,
hora: [minutes, remainingSeconds],
chegada: i.estimated_time_arrival,
distancia: i.d_px_to_stop,
};
const alreadyExists = trackedBuses.some(r => r.lat === result.lat && r.lng === result.lng);
var pt = turf.point([i.longitude, i.latitude])
var poly = turf.polygon([garage1])
var poly2 = turf.polygon([garage2])
const scaledMultiPolygon = turf.transformScale(poly, 1.5);
const scaledMultiPolygon2 = turf.transformScale(poly2, 1.5);
if (!alreadyExists && !turf.booleanPointInPolygon(pt, scaledMultiPolygon) && !turf.booleanPointInPolygon(pt, scaledMultiPolygon2)) {
trackedBuses.push(result);
}
});
let filteredGPS = trackedBuses.filter(item => {
return routes.some(filterItem => {
return (item.linha === filterItem.trip_id.trip_short_name && item.hora[0] < 5 && item.distancia > -0.100 && item.chegada <= 15)
});
});
setTracked(filteredGPS)
}
}, [realtime])
Depois de passar por esses filtros é armazenado todos os que "sobraram" em tracked
e a partir disso é feito um map
em routes
, e usa o trip_short_name
e direction_id
para encontrar os objetos que possuam os mesmos valores mas em tracked
. Se tiver pelo menos um objeto que vem desse filtro é criado um novo objeto com todas as propriedades de routes
e uma nova que chama smallestEtas
quem contém um array com os três menores valores da propriedade chegada dos objetos filtrados em tracked
. E assim a variável de estado arrivals
é atualizada. Segue abaixo a função:
useEffect(() => {
if (routes && tracked) {
const arrivals = routes.map((obj1) => {
const matched = tracked.filter((obj2) => {
return (
obj1.trip_id.trip_short_name === obj2.linha &&
obj1.trip_id.direction_id === obj2.sentido
);
});
if (matched.length > 0) {
const smallestEtas = matched
.map((obj) => obj.chegada)
.sort((a, b) => a - b)
.slice(0, 3);
const combinedObj = {
...obj1,
smallestEtas: smallestEtas,
};
return combinedObj;
}
return obj1;
});
setArrivals(arrivals);
}
}, [tracked]);
Nesta pasta é armazenado as URLs em que são feitas as chamadas para a API, é divido em dois arquivos: gps e api. É usado o axios para definir as baseURls
:
- api
let baseURL = '';
if (window.location.hostname === 'mobilidade.rio') {
baseURL = 'https://api.mobilidade.rio/gtfs'
} else if (window.location.hostname === 'app.staging.mobilidade.rio') {
baseURL = 'https://api.staging.mobilidade.rio/gtfs'
} else if(window.location.hostname === 'localhost') {
baseURL = 'https://api.dev.mobilidade.rio/gtfs'
}
export const api = axios.create({baseURL})
- gps
import axios from "axios";
let baseURL = '';
if (window.location.hostname === 'mobilidade.rio') {
baseURL = 'https://api.mobilidade.rio/predictor/'
} else if (window.location.hostname === 'app.staging.mobilidade.rio') {
baseURL = 'https://api.staging.mobilidade.rio/predictor/'
} else if (window.location.hostname === 'localhost') {
baseURL = 'https://api.dev.mobilidade.rio/predictor/'
}
export const gps = axios.create({ baseURL })
É a pasta que possui todas as imagens e estilizações globais do app.