21. FE ‐ Main Page - bohdanabadi/doroha-simulator GitHub Wiki

Pages & Component

So the frontend is going to consists of 2 pages. The main page (including the map and the side journey list) and another page which will hold the metrics. Simple and straight forward.

The Frontend consist of React, typescript and tailwind. These are the main 3 things are holding everything together. Absoluelty love tailwind !!

Main Page

The main page will hold a leaflet map component which will draw a car icon via journey's current position. The side jounrye list will have list of journeys, ID, distance and progress bar.

Like below

Incoming Messages

The FE opens a web socket connection to receive journeys. The journeys then process and car icons are placed on the leaflet map via specific coordinates.

The code snippet below show how we get our messages

 const [journeys, setJourneys] = useState<Map<number, Journey>>(new Map());

    useEffect(() => {
        const websocketEndpoint: string | undefined = process.env.REACT_APP_API_WEBSOCKET;
        if (typeof websocketEndpoint === 'string' && websocketEndpoint.trim() !== '') {
            // Valid endpoint, proceed with the connection
            const webSocketClient = new WebSocketClient(websocketEndpoint, 5);
            const handleMessage = (message: string) => {
                const journeyData: Journey = JSON.parse(message);
                setJourneys((prevCarPosition) => {
                    const updatedCarPositions = new Map(prevCarPosition)
                    const bearing = calculateBearing(journeyData.prevPoint.Y, journeyData.prevPoint.X, journeyData.currentPoint.Y, journeyData.currentPoint.X)
                    journeyData.bearing = (bearing - 90 + 360) % 360;
                    updatedCarPositions.set(journeyData.id, journeyData)
                    return updatedCarPositions;
                })
            };

            webSocketClient.addListener(handleMessage);
            webSocketClient.connect();

            return () => {
                webSocketClient.removeListener(handleMessage);
                webSocketClient.close();
            }
        } else {
            console.log(websocketEndpoint);
            // Invalid or missing endpoint, handle the error
            console.error("WebSocket endpoint is undefined, null, or empty. Cannot establish WebSocket connection.");
        }
    }, []);

This is initiated on the HomePage which is parent of Map component. So how do we pass the data to the map component ? There are multiple options but I opted for using context. There is a deep explanation here

There is also special calcualtion to make sure the icon is angle in a forward position.

Icon angle

export function calculateBearing(startLat: number, startLng: number, destLat: number, destLng: number): number {
    const startLatRad = degreesToRadians(startLat);
    const startLngRad = degreesToRadians(startLng);
    const destLatRad = degreesToRadians(destLat);
    const destLngRad = degreesToRadians(destLng);

    const y = Math.sin(destLngRad - startLngRad) * Math.cos(destLatRad);
    const x = Math.cos(startLatRad) * Math.sin(destLatRad) - Math.sin(startLatRad) * Math.cos(destLatRad) * Math.cos(destLngRad - startLngRad);
    return (radiansToDegrees(Math.atan2(y, x)) + 360) % 360;
}

function degreesToRadians(degrees: number): number {
    return degrees * Math.PI / 180;
}

function radiansToDegrees(radians: number): number {
    return radians * 180 / Math.PI;
}

This code calculates the bearing (direction) from one geographic point to another. Here's how it breaks down into simpler terms:

  1. Convert Degrees to Radians: Since many mathematical functions expect angles in radians, the first step converts the latitude and longitude of both the starting point and destination from degrees to radians.

  2. Compute Differences and Angles:

    • It calculates the difference in longitude between the destination and the start point.
    • Then, it uses trigonometry to compute two values, x and y, which represent components of the direction vector from the start point to the destination in a two-dimensional plane. This involves:
      • Calculating y using the sine of the longitude difference multiplied by the cosine of the destination latitude. This essentially gives us a projection of the angle on the y-axis.
      • Calculating x by subtracting two terms that represent the spherical trigonometry version of the Pythagorean theorem. The first term is the product of the cosine of the start latitude and the sine of the destination latitude. The second term accounts for the earth's curvature and the direction change by multiplying the sine of the start latitude, the cosine of the destination latitude, and the cosine of the longitude difference.
  3. Calculate Bearing: The bearing (the compass direction from the start point to the destination) is calculated using the atan2 function with y and x as inputs. atan2 gives the angle in radians between the positive x-axis and the point (y, x), which effectively translates to the direction from the start point to the destination in our context.

  4. Convert Radians to Degrees and Adjust: The angle is converted back to degrees since bearings are traditionally expressed in degrees. The + 360 and % 360 operations ensure the result is a non-negative value between 0 and 359 degrees, representing the compass bearing from the start point to the destination.

In essence, this function tells you the direction you need to follow to go straight from one point on the globe to another, presented as a degree value from 0 to 359.