Skip to content

Estrutura

Gabriel Guimarães edited this page May 2, 2023 · 3 revisions

Estrutura do projeto

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 Captura de Tela 2023-04-28 às 12 00 45 Captura de Tela 2023-04-30 às 23 49 32
InfoCard card para selecionar a plataforma desejada Captura de Tela 2023-04-28 às 13 06 16 Pontos convencionais não possuem plataforma
InfoCard (Plataforma selecionada) card para selecionar a trip que deseja ver todo o trajeto Captura de Tela 2023-04-28 às 13 04 20 Captura de Tela 2023-04-30 às 23 54 16
SequenceCard card com sequência de pontos em que a trip passa a partir do ponto pesquisado Captura de Tela 2023-04-28 às 12 58 09 Captura de Tela 2023-04-30 às 23 56 50

Pages

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

Captura de Tela 2023-05-01 às 17 45 17

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 Captura de Tela 2023-05-01 às 17 47 56 Captura de Tela 2023-05-01 às 17 50 16
Plataforma BRT Selecionada Captura de Tela 2023-05-01 às 17 51 13
Linha escolhida Captura de Tela 2023-05-01 às 17 55 42 Captura de Tela 2023-05-01 às 17 56 27

Hooks

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_idsã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.

Hooks usados apenas no BRT

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]);

Services

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 })

Assets

É a pasta que possui todas as imagens e estilizações globais do app.

Clone this wiki locally