GoogleMapsService.java

package com.soen390.backend.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.soen390.backend.exception.GoogleMapsDirectionEmptyException;
import com.soen390.backend.object.OutdoorDirectionResponse;
import com.soen390.backend.object.RouteStep;
import com.soen390.backend.enums.ManeuverType;
import com.soen390.backend.enums.TransportMode;
import com.soen390.backend.exception.GoogleMapsDirectionsApiException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.ArrayList;
import java.util.List;

@Service
public class GoogleMapsService {

    @Value("${google.api.key}")
    private String apiKey;

    private final RestTemplate restTemplate;

    public GoogleMapsService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public OutdoorDirectionResponse getDirections(String origin, String destination, TransportMode transportMode) {

        String url = "https://maps.googleapis.com/maps/api/directions/json" +
                "?origin=" + origin +
                "&destination=" + destination +
                "&mode=" + transportMode.getApiValue() +
                "&key=" + apiKey;
        String json = restTemplate.getForObject(url, String.class);

        try {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode root = mapper.readTree(json);
            String status = root.path("status").asText();

            checkResponseStatus(status);

            JsonNode route = root.path("routes").get(0);
            JsonNode leg = route.path("legs").get(0);

            String distance = leg.path("distance").path("text").asText();
            String duration = leg.path("duration").path("text").asText();
            String polyline = route.path("overview_polyline").path("points").asText();

            JsonNode steps = leg.path("steps");

            return new OutdoorDirectionResponse(distance, duration, polyline, transportMode, processSteps(steps));

        } catch (GoogleMapsDirectionsApiException | GoogleMapsDirectionEmptyException  e) {
            throw e;
        } catch (NullPointerException | JsonProcessingException e) {
            throw new GoogleMapsDirectionsApiException(
                    "Failed to parse Google Maps directions response: incomplete or unexpected data format.",
                    e
            );
        }
    }

    private List<RouteStep> processSteps(JsonNode steps) {

        List<RouteStep> stepList = new ArrayList<>();

        for (JsonNode step : steps) {

            String instruction = step.path("html_instructions").asText();
            String cleanInstruction = instruction.replaceAll("<[^>]*>", "");

            String stepDist = step.path("distance").path("text").asText();
            String stepDur = step.path("duration").path("text").asText();

            String stepPolyline = step.path("polyline").path("points").asText();

            ManeuverType maneuverType = handleMissingManeuver(step);

            stepList.add(new RouteStep(cleanInstruction, stepDist, stepDur, maneuverType, stepPolyline));
        }
        return stepList;

    }

    private ManeuverType handleMissingManeuver(JsonNode step) {
        ManeuverType maneuverType;
        if (!step.path("maneuver").isTextual()) {
            maneuverType = ManeuverType.STRAIGHT;
        } else {
            String maneuver = step.path("maneuver").asText();
            maneuverType = ManeuverType.fromString(maneuver);
        }
        return maneuverType;
    }

    private void checkResponseStatus(String status) {
        if (status.equals("ZERO_RESULTS")){
            throw new GoogleMapsDirectionEmptyException("Directions not found. Please check your start and end locations.");
        }
        if (!status.equals("OK")) {
            throw new GoogleMapsDirectionsApiException("Google Maps directions request failed with status: " + status);
        }
    }

}