25th July 2024
When I first started working on the Map component, my focus was on displaying individual points of interest, which involved fetching geoJSON data and adding it as map markers, as well as adding some interactivity by making the markers clickable. This worked for seeing speicific locations like huts and public services, but clearly was not a good solution to displaying actual trails.
With the goal to show entire trails, not just individual points, I started by revisiting the Department of Conservation's API endpoints that I came across towards the end of setting up the map component initially, which had a lot of data on the trails themselves and the points (coordinates) that make them up.I tested the response in thunderclient, using the x-api-key to enter the API key I generated through the DOC portal and got a successful response back.
In the backend controller, getDocTrails.js, I defined the endpoint url and hardcoded "trail id" parameter (for now, will allow for dynamic searches later).
const url = `https://api.doc.govt.nz/v1/tracks/114ff80d-12f4-4f0b-8384-103f0c8e6efc/detail?api_key=${docApiKey}`
I added the API key as a header in the request to ensure that the server receives the necessary authentication for access.
try {
const response = await fetch(url, {
headers: {
Accept: 'application/json',
'x-api-key': docApiKey,
...
}
I then added the API key to the env file, but not after first publishing it to GitHub and having to create a new key and deactivate the old one. Better now than later to notice that one, but still a bit of a blunder.
For the front end, I created a new component, DocTrails.tsx which allowed me to show the data for the single trail I fetched from DOC. I'll likely need to bring this back into the main map compont at some point but for now it was good to have a separate component to work on.
I used state to keep track of the data and then used the useEffect hook to fetch the data from my endpoint and set the state to be the response data. This means that the fetched data is stored in the state and I can use it throughout the component.
const [data, setData] = useState<DocRoutesTypes | null>(null)
useEffect(() => {
axios
.get('http://localhost:3000/v1/doctrails)'
.then((response) => {
setData(response.data)
...
The main thing I wanted to get from the DOC endpoint was the latitude and longitude of the points they use to show a trail on the map. The data was being stored as arrays of “lines” which was arrays, each with a set of coordinates, which were in the same format I came across when adding map markers through geoJSON in my previous post, so planned to convert through the existing proj4 library I was using.
Resulting in not so great of a user experience, (I removed after checking it worked), I mapped over the data to show all the coordinates for the Abel Tasman Walk which is listed as being between 3-5 days duration, so a lot of coordinates. Great to know it's all working correctly though.
It made sense at this point to separate out the coordinate conversion function, having now used it in both my main map component, and the new DocTrails component so I created a utility function to look after the conversion for the whole app. This is basically looping through each coordinate pair and for each pair, it converts the coordinates from the source projection to the destination projection and then returns an array of these converted coordinates.
const convertedLineData = response.data.line.map(
(line: [number, number][]) => convertCoordinates(line)
);
With the trail data successfully fetched and converted, the next step was to show the trail on the map. To ensure the map centers correctly, I dynamically set its center to the first coordinate of the first trail, and then went through each item in the data.line array, created a Polyline object for each using the Google Maps API, set its path to the corresponding coordinates.
data.line.forEach((line) => {
if (Array.isArray(line)) {
const linePath = new window.google.maps.Polyline({
path: line.map(([lng, lat]) => ({ lat, lng })),
...
});
linePath.setMap(map);
}
});
and Voilà!! - it's not pretty but it works!
With a single trail showing, next up would involve accessing a separate endpoint from DOC to get all the trails and seeing how best to load and display the data without causing my laptop to freeze. Also, provide the ability to navigate to a trail-specific page from the main map or search results. This seems pretty straightforward, with having the bones for a single-trail page already setup, and I expect to update the current component to accept dynamic trail data but will see how it goes.
For down the line, I would love to get 3D added like the below, and continue to build out the other components within my app.