chore(assets): Upload report assets
Some checks failed
Polyglot CI / tests (push) Has been cancelled
Some checks failed
Polyglot CI / tests (push) Has been cancelled
This commit is contained in:
parent
f013955bc2
commit
32cd6bcb21
119 changed files with 4531 additions and 1 deletions
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,130 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>DroneAttrComparatorService.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">ilp-coursework</a> > <a href="index.source.html" class="el_package">io.github.js0ny.ilp_coursework.service</a> > <span class="el_source">DroneAttrComparatorService.java</span></div><h1>DroneAttrComparatorService.java</h1><pre class="source lang-java linenums">package io.github.js0ny.ilp_coursework.service;
|
||||
|
||||
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
||||
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
|
||||
import io.github.js0ny.ilp_coursework.util.AttrOperator;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class DroneAttrComparatorService {
|
||||
|
||||
private final String baseUrl;
|
||||
<span class="fc" id="L27"> private final String dronesEndpoint = "drones";</span>
|
||||
<span class="fc" id="L28"> private final RestTemplate restTemplate = new RestTemplate();</span>
|
||||
|
||||
/** Constructor, handles the base url here. */
|
||||
<span class="fc" id="L31"> public DroneAttrComparatorService() {</span>
|
||||
<span class="fc" id="L32"> String baseUrl = System.getenv("ILP_ENDPOINT");</span>
|
||||
<span class="pc bpc" id="L33" title="3 of 4 branches missed."> if (baseUrl == null || baseUrl.isBlank()) {</span>
|
||||
<span class="fc" id="L34"> this.baseUrl = "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";</span>
|
||||
} else {
|
||||
// Defensive: Add '/' to the end of the URL
|
||||
<span class="nc bnc" id="L37" title="All 2 branches missed."> if (!baseUrl.endsWith("/")) {</span>
|
||||
<span class="nc" id="L38"> baseUrl += "/";</span>
|
||||
}
|
||||
<span class="nc" id="L40"> this.baseUrl = baseUrl;</span>
|
||||
}
|
||||
<span class="fc" id="L42"> }</span>
|
||||
|
||||
/**
|
||||
* Return an array of ids of drones with a given attribute name and value.
|
||||
*
|
||||
* <p>Associated service method with {@code /queryAsPath/{attrName}/{attrVal}}
|
||||
*
|
||||
* @param attrName the attribute name to filter on
|
||||
* @param attrVal the attribute value to filter on
|
||||
* @return array of drone ids matching the attribute name and value
|
||||
* @see #dronesWithAttributeCompared(String, String, AttrOperator)
|
||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getIdByAttrMap
|
||||
*/
|
||||
public List<String> dronesWithAttribute(String attrName, String attrVal) {
|
||||
// Call the helper with EQ operator
|
||||
<span class="fc" id="L57"> return dronesWithAttributeCompared(attrName, attrVal, AttrOperator.EQ);</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of ids of drones which matches all given complex comparing rules
|
||||
*
|
||||
* @param attrComparators The filter rule with Name, Value and Operator
|
||||
* @return array of drone ids that matches all rules
|
||||
*/
|
||||
public List<String> dronesSatisfyingAttributes(AttrQueryRequest[] attrComparators) {
|
||||
<span class="fc" id="L67"> Set<String> matchingDroneIds = null;</span>
|
||||
<span class="fc bfc" id="L68" title="All 2 branches covered."> for (var comparator : attrComparators) {</span>
|
||||
<span class="fc" id="L69"> String attribute = comparator.attribute();</span>
|
||||
<span class="fc" id="L70"> String operator = comparator.operator();</span>
|
||||
<span class="fc" id="L71"> String value = comparator.value();</span>
|
||||
<span class="fc" id="L72"> AttrOperator op = AttrOperator.fromString(operator);</span>
|
||||
<span class="fc" id="L73"> List<String> ids = dronesWithAttributeCompared(attribute, value, op);</span>
|
||||
<span class="fc bfc" id="L74" title="All 2 branches covered."> if (matchingDroneIds == null) {</span>
|
||||
<span class="fc" id="L75"> matchingDroneIds = new HashSet<>(ids);</span>
|
||||
} else {
|
||||
<span class="fc" id="L77"> matchingDroneIds.retainAll(ids);</span>
|
||||
}
|
||||
}
|
||||
<span class="fc bfc" id="L80" title="All 2 branches covered."> if (matchingDroneIds == null) {</span>
|
||||
<span class="fc" id="L81"> return new ArrayList<>();</span>
|
||||
}
|
||||
<span class="fc" id="L83"> return matchingDroneIds.stream().toList();</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper that wraps the dynamic querying with different comparison operators
|
||||
*
|
||||
* <p>This method act as a concatenation of {@link
|
||||
* io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
|
||||
* AttrOperator)}
|
||||
*
|
||||
* @param attrName the attribute name to filter on
|
||||
* @param attrVal the attribute value to filter on
|
||||
* @param op the comparison operator
|
||||
* @return array of drone ids matching the attribute name and value (filtered by {@code op})
|
||||
* @see io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
|
||||
* AttrOperator)
|
||||
*/
|
||||
private List<String> dronesWithAttributeCompared(
|
||||
String attrName, String attrVal, AttrOperator op) {
|
||||
<span class="fc" id="L102"> URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);</span>
|
||||
// This is required to make sure the response is valid
|
||||
<span class="fc" id="L104"> Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);</span>
|
||||
|
||||
<span class="pc bpc" id="L106" title="1 of 2 branches missed."> if (drones == null) {</span>
|
||||
<span class="nc" id="L107"> return new ArrayList<>();</span>
|
||||
}
|
||||
|
||||
// Use Jackson's ObjectMapper to convert DroneDto to JsonNode for dynamic
|
||||
// querying
|
||||
<span class="fc" id="L112"> ObjectMapper mapper = new ObjectMapper();</span>
|
||||
|
||||
<span class="fc" id="L114"> return Arrays.stream(drones)</span>
|
||||
<span class="fc" id="L115"> .filter(</span>
|
||||
drone -> {
|
||||
<span class="fc" id="L117"> JsonNode node = mapper.valueToTree(drone);</span>
|
||||
<span class="fc" id="L118"> JsonNode attrNode = node.findValue(attrName);</span>
|
||||
<span class="pc bpc" id="L119" title="1 of 2 branches missed."> if (attrNode != null) {</span>
|
||||
// Manually handle different types of JsonNode
|
||||
<span class="fc" id="L121"> return isValueMatched(attrNode, attrVal, op);</span>
|
||||
} else {
|
||||
<span class="nc" id="L123"> return false;</span>
|
||||
}
|
||||
})
|
||||
<span class="fc" id="L126"> .map(Drone::id)</span>
|
||||
<span class="fc" id="L127"> .collect(Collectors.toList());</span>
|
||||
}
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.12.202403310830</span></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,271 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>DroneInfoService.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">ilp-coursework</a> > <a href="index.source.html" class="el_package">io.github.js0ny.ilp_coursework.service</a> > <span class="el_source">DroneInfoService.java</span></div><h1>DroneInfoService.java</h1><pre class="source lang-java linenums">package io.github.js0ny.ilp_coursework.service;
|
||||
|
||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
||||
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
|
||||
import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
|
||||
import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
|
||||
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
<span class="fc" id="L21">@Service</span>
|
||||
public class DroneInfoService {
|
||||
|
||||
private final String baseUrl;
|
||||
<span class="fc" id="L25"> private final String dronesForServicePointsEndpoint = "drones-for-service-points";</span>
|
||||
public static final String servicePointsEndpoint = "service-points";
|
||||
public static final String restrictedAreasEndpoint = "restricted-areas";
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
/** Constructor, handles the base url here. */
|
||||
public DroneInfoService() {
|
||||
<span class="fc" id="L33"> this(new RestTemplate());</span>
|
||||
<span class="fc" id="L34"> }</span>
|
||||
|
||||
<span class="fc" id="L36"> public DroneInfoService(RestTemplate restTemplate) {</span>
|
||||
<span class="fc" id="L37"> this.restTemplate = restTemplate;</span>
|
||||
<span class="fc" id="L38"> String baseUrl = System.getenv("ILP_ENDPOINT");</span>
|
||||
<span class="pc bpc" id="L39" title="3 of 4 branches missed."> if (baseUrl == null || baseUrl.isBlank()) {</span>
|
||||
<span class="fc" id="L40"> this.baseUrl = "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";</span>
|
||||
} else {
|
||||
// Defensive: Add '/' to the end of the URL
|
||||
<span class="nc bnc" id="L43" title="All 2 branches missed."> if (!baseUrl.endsWith("/")) {</span>
|
||||
<span class="nc" id="L44"> baseUrl += "/";</span>
|
||||
}
|
||||
<span class="nc" id="L46"> this.baseUrl = baseUrl;</span>
|
||||
}
|
||||
<span class="fc" id="L48"> }</span>
|
||||
|
||||
/**
|
||||
* Return an array of ids of drones with/without cooling capability
|
||||
*
|
||||
* <p>Associated service method with {@code /dronesWithCooling/{state}}
|
||||
*
|
||||
* @param state determines the capability filtering
|
||||
* @return if {@code state} is true, return ids of drones with cooling capability, else without
|
||||
* cooling
|
||||
* @see
|
||||
* io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
||||
*/
|
||||
public List<String> dronesWithCooling(boolean state) {
|
||||
// URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
||||
// Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
|
||||
<span class="fc" id="L64"> List<Drone> drones = fetchAllDrones();</span>
|
||||
|
||||
<span class="pc bpc" id="L66" title="1 of 2 branches missed."> if (drones == null) {</span>
|
||||
<span class="nc" id="L67"> return new ArrayList<>();</span>
|
||||
}
|
||||
|
||||
<span class="fc" id="L70"> return drones.stream()</span>
|
||||
<span class="fc bfc" id="L71" title="All 2 branches covered."> .filter(drone -> drone.capability().cooling() == state)</span>
|
||||
<span class="fc" id="L72"> .map(Drone::id)</span>
|
||||
<span class="fc" id="L73"> .collect(Collectors.toList());</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link Drone}-style json data structure with the given {@code id}
|
||||
*
|
||||
* <p>Associated service method with {@code /droneDetails/{id}}
|
||||
*
|
||||
* @param id The id of the drone
|
||||
* @return drone json body of given id
|
||||
* @throws NullPointerException when cannot fetch available drones from remote
|
||||
* @throws IllegalArgumentException when drone with given {@code id} cannot be found this should
|
||||
* lead to a 404
|
||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
|
||||
*/
|
||||
public Drone droneDetail(String id) {
|
||||
<span class="fc" id="L89"> List<Drone> drones = fetchAllDrones();</span>
|
||||
|
||||
<span class="fc bfc" id="L91" title="All 2 branches covered."> for (var drone : drones) {</span>
|
||||
<span class="fc bfc" id="L92" title="All 2 branches covered."> if (drone.id().equals(id)) {</span>
|
||||
<span class="fc" id="L93"> return drone;</span>
|
||||
}
|
||||
<span class="fc" id="L95"> }</span>
|
||||
|
||||
// This will result in 404
|
||||
<span class="fc" id="L98"> throw new IllegalArgumentException("drone with that ID cannot be found");</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of ids of drones that match all the requirements in the medical dispatch
|
||||
* records
|
||||
*
|
||||
* <p>Associated service method with
|
||||
*
|
||||
* @param rec array of medical dispatch records
|
||||
* @return List of drone ids that match all the requirements
|
||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#queryAvailableDrones
|
||||
*/
|
||||
public List<String> dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
|
||||
<span class="fc" id="L112"> List<Drone> drones = fetchAllDrones();</span>
|
||||
|
||||
<span class="fc bfc" id="L114" title="All 4 branches covered."> if (rec == null || rec.length == 0) {</span>
|
||||
<span class="fc" id="L115"> return drones.stream()</span>
|
||||
<span class="fc" id="L116"> .filter(Objects::nonNull)</span>
|
||||
<span class="fc" id="L117"> .map(Drone::id)</span>
|
||||
<span class="fc" id="L118"> .collect(Collectors.toList());</span>
|
||||
}
|
||||
|
||||
/*
|
||||
* Traverse and filter drones, pass every record's requirement to helper
|
||||
*/
|
||||
<span class="fc" id="L124"> return drones.stream()</span>
|
||||
<span class="pc bpc" id="L125" title="2 of 4 branches missed."> .filter(d -> d != null && d.capability() != null)</span>
|
||||
<span class="fc" id="L126"> .filter(</span>
|
||||
d ->
|
||||
<span class="fc" id="L128"> Arrays.stream(rec)</span>
|
||||
<span class="pc bpc" id="L129" title="2 of 4 branches missed."> .filter(r -> r != null && r.requirements() != null)</span>
|
||||
<span class="fc" id="L130"> .allMatch(r -> droneMatchesRequirement(d, r)))</span>
|
||||
<span class="fc" id="L131"> .map(Drone::id)</span>
|
||||
<span class="fc" id="L132"> .collect(Collectors.toList());</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if a drone meets the requirement of a medical dispatch.
|
||||
*
|
||||
* @param drone the drone to be checked
|
||||
* @param record the medical dispatch record containing the requirement
|
||||
* @return true if the drone meets the requirement, false otherwise
|
||||
* @throws IllegalArgumentException when record requirements or drone capability is invalid
|
||||
* (capacity and id cannot be null in {@code MedDispathRecDto})
|
||||
*/
|
||||
public boolean droneMatchesRequirement(Drone drone, MedDispatchRecRequest record) {
|
||||
<span class="fc" id="L145"> var requirements = record.requirements();</span>
|
||||
<span class="fc bfc" id="L146" title="All 2 branches covered."> if (requirements == null) {</span>
|
||||
<span class="fc" id="L147"> throw new IllegalArgumentException("requirements cannot be null");</span>
|
||||
}
|
||||
<span class="fc" id="L149"> var capability = drone.capability();</span>
|
||||
<span class="fc bfc" id="L150" title="All 2 branches covered."> if (capability == null) {</span>
|
||||
<span class="fc" id="L151"> throw new IllegalArgumentException("drone capability cannot be null");</span>
|
||||
}
|
||||
|
||||
<span class="fc" id="L154"> float requiredCapacity = requirements.capacity();</span>
|
||||
<span class="pc bpc" id="L155" title="1 of 4 branches missed."> if (requiredCapacity <= 0 || capability.capacity() < requiredCapacity) {</span>
|
||||
<span class="fc" id="L156"> return false;</span>
|
||||
}
|
||||
|
||||
// Use boolean wrapper to allow null (not specified) values
|
||||
<span class="fc" id="L160"> boolean requiredCooling = requirements.cooling();</span>
|
||||
<span class="fc" id="L161"> boolean requiredHeating = requirements.heating();</span>
|
||||
|
||||
// Case 1: required is null: We don't care about it
|
||||
// Case 2: required is false: We don't care about it (high capability adapts to
|
||||
// low requirements)
|
||||
// Case 3: capability is true: Then always matches
|
||||
// See: https://piazza.com/class/me9vp64lfgf4sn/post/100
|
||||
<span class="pc bpc" id="L168" title="2 of 4 branches missed."> boolean matchesCooling = !requiredCooling || capability.cooling();</span>
|
||||
<span class="pc bpc" id="L169" title="3 of 4 branches missed."> boolean matchesHeating = !requiredHeating || capability.heating();</span>
|
||||
|
||||
// Conditions: All requirements matched + availability matched, use helper
|
||||
// For minimal privilege, only pass drone id to check availability
|
||||
<span class="pc bpc" id="L173" title="3 of 6 branches missed."> return (matchesCooling && matchesHeating && checkAvailability(drone.id(), record)); // &&</span>
|
||||
// checkCost(drone, record) // checkCost is more expensive than
|
||||
// checkAvailability
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if a drone is available at the required date and time
|
||||
*
|
||||
* @param droneId the id of the drone to be checked
|
||||
* @param record the medical dispatch record containing the required date and time
|
||||
* @return true if the drone is available, false otherwise
|
||||
*/
|
||||
private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
|
||||
<span class="fc" id="L186"> URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);</span>
|
||||
<span class="fc" id="L187"> ServicePointDrones[] servicePoints =</span>
|
||||
<span class="fc" id="L188"> restTemplate.getForObject(droneUrl, ServicePointDrones[].class);</span>
|
||||
|
||||
<span class="fc" id="L190"> LocalDate requiredDate = record.date();</span>
|
||||
<span class="fc" id="L191"> DayOfWeek requiredDay = requiredDate.getDayOfWeek();</span>
|
||||
<span class="fc" id="L192"> LocalTime requiredTime = record.time();</span>
|
||||
|
||||
<span class="pc bpc" id="L194" title="1 of 2 branches missed."> assert servicePoints != null;</span>
|
||||
<span class="pc bpc" id="L195" title="1 of 2 branches missed."> for (var servicePoint : servicePoints) {</span>
|
||||
<span class="fc" id="L196"> var drone = servicePoint.locateDroneById(droneId); // Nullable</span>
|
||||
<span class="pc bpc" id="L197" title="1 of 2 branches missed."> if (drone != null) {</span>
|
||||
<span class="fc" id="L198"> return drone.checkAvailability(requiredDay, requiredTime);</span>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="nc" id="L202"> return false;</span>
|
||||
}
|
||||
|
||||
private LngLat queryServicePointLocationByDroneId(String droneId) {
|
||||
<span class="nc" id="L206"> URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);</span>
|
||||
<span class="nc" id="L207"> ServicePointDrones[] servicePoints =</span>
|
||||
<span class="nc" id="L208"> restTemplate.getForObject(droneUrl, ServicePointDrones[].class);</span>
|
||||
|
||||
<span class="nc bnc" id="L210" title="All 2 branches missed."> assert servicePoints != null;</span>
|
||||
<span class="nc bnc" id="L211" title="All 2 branches missed."> for (var sp : servicePoints) {</span>
|
||||
<span class="nc" id="L212"> var drone = sp.locateDroneById(droneId); // Nullable</span>
|
||||
<span class="nc bnc" id="L213" title="All 2 branches missed."> if (drone != null) {</span>
|
||||
<span class="nc" id="L214"> return queryServicePointLocation(sp.servicePointId());</span>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="nc" id="L218"> return null;</span>
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LngLat queryServicePointLocation(int id) {
|
||||
<span class="nc" id="L223"> URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);</span>
|
||||
|
||||
<span class="nc" id="L225"> ServicePoint[] servicePoints =</span>
|
||||
<span class="nc" id="L226"> restTemplate.getForObject(servicePointUrl, ServicePoint[].class);</span>
|
||||
|
||||
<span class="nc bnc" id="L228" title="All 2 branches missed."> assert servicePoints != null;</span>
|
||||
<span class="nc bnc" id="L229" title="All 2 branches missed."> for (var sp : servicePoints) {</span>
|
||||
<span class="nc bnc" id="L230" title="All 2 branches missed."> if (sp.id() == id) {</span>
|
||||
// We dont consider altitude
|
||||
<span class="nc" id="L232"> return new LngLat(sp.location());</span>
|
||||
}
|
||||
}
|
||||
<span class="nc" id="L235"> return null;</span>
|
||||
}
|
||||
|
||||
public List<Drone> fetchAllDrones() {
|
||||
<span class="fc" id="L239"> System.out.println("fetchAllDrones called");</span>
|
||||
<span class="fc" id="L240"> String dronesEndpoint = "drones";</span>
|
||||
<span class="fc" id="L241"> URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);</span>
|
||||
<span class="fc" id="L242"> System.out.println("Fetching from URL: " + droneUrl);</span>
|
||||
<span class="fc" id="L243"> Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);</span>
|
||||
<span class="fc bfc" id="L244" title="All 2 branches covered."> return drones == null ? new ArrayList<>() : Arrays.asList(drones);</span>
|
||||
}
|
||||
|
||||
public List<RestrictedArea> fetchRestrictedAreas() {
|
||||
<span class="fc" id="L248"> URI restrictedUrl = URI.create(baseUrl).resolve(restrictedAreasEndpoint);</span>
|
||||
<span class="fc" id="L249"> RestrictedArea[] restrictedAreas =</span>
|
||||
<span class="fc" id="L250"> restTemplate.getForObject(restrictedUrl, RestrictedArea[].class);</span>
|
||||
<span class="pc bpc" id="L251" title="1 of 2 branches missed."> assert restrictedAreas != null;</span>
|
||||
<span class="fc" id="L252"> return Arrays.asList(restrictedAreas);</span>
|
||||
}
|
||||
|
||||
public List<ServicePoint> fetchServicePoints() {
|
||||
<span class="fc" id="L256"> URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);</span>
|
||||
<span class="fc" id="L257"> ServicePoint[] servicePoints =</span>
|
||||
<span class="fc" id="L258"> restTemplate.getForObject(servicePointUrl, ServicePoint[].class);</span>
|
||||
<span class="pc bpc" id="L259" title="1 of 2 branches missed."> assert servicePoints != null;</span>
|
||||
<span class="fc" id="L260"> return Arrays.asList(servicePoints);</span>
|
||||
}
|
||||
|
||||
public List<ServicePointDrones> fetchDronesForServicePoints() {
|
||||
<span class="fc" id="L264"> URI servicePointDronesUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);</span>
|
||||
<span class="fc" id="L265"> ServicePointDrones[] servicePointDrones =</span>
|
||||
<span class="fc" id="L266"> restTemplate.getForObject(servicePointDronesUrl, ServicePointDrones[].class);</span>
|
||||
<span class="pc bpc" id="L267" title="1 of 2 branches missed."> assert servicePointDrones != null;</span>
|
||||
<span class="fc" id="L268"> return Arrays.asList(servicePointDrones);</span>
|
||||
}
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.12.202403310830</span></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,190 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>GpsCalculationService.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">ilp-coursework</a> > <a href="index.source.html" class="el_package">io.github.js0ny.ilp_coursework.service</a> > <span class="el_source">GpsCalculationService.java</span></div><h1>GpsCalculationService.java</h1><pre class="source lang-java linenums">package io.github.js0ny.ilp_coursework.service;
|
||||
|
||||
import io.github.js0ny.ilp_coursework.data.common.Angle;
|
||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||
import io.github.js0ny.ilp_coursework.data.common.Region;
|
||||
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
|
||||
import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
|
||||
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class that handles calculations about Coordinates
|
||||
*
|
||||
* @see LngLat
|
||||
* @see Region
|
||||
*/
|
||||
@Service
|
||||
<span class="fc" id="L21">public class GpsCalculationService {</span>
|
||||
|
||||
/**
|
||||
* Given step size
|
||||
*
|
||||
* @see #nextPosition(LngLat, double)
|
||||
*/
|
||||
private static final double STEP = 0.00015;
|
||||
|
||||
/**
|
||||
* Given threshold to judge if two points are close to each other
|
||||
*
|
||||
* @see #isCloseTo(LngLat, LngLat)
|
||||
*/
|
||||
private static final double CLOSE_THRESHOLD = 0.00015;
|
||||
|
||||
/**
|
||||
* Calculate the Euclidean distance between {@code position1} and {@code position2}, which are
|
||||
* coordinates defined as {@link LngLat}
|
||||
*
|
||||
* @param position1 The coordinate of the first position
|
||||
* @param position2 The coordinate of the second position
|
||||
* @return The Euclidean distance between {@code position1} and {@code position2}
|
||||
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getDistance(DistanceRequest)
|
||||
*/
|
||||
public double calculateDistance(LngLat position1, LngLat position2) {
|
||||
<span class="fc" id="L47"> double lngDistance = position2.lng() - position1.lng();</span>
|
||||
<span class="fc" id="L48"> double latDistance = position2.lat() - position1.lat();</span>
|
||||
// Euclidean: \sqrt{a^2 + b^2}
|
||||
<span class="fc" id="L50"> return Math.sqrt(lngDistance * lngDistance + latDistance * latDistance);</span>
|
||||
}
|
||||
|
||||
public double calculateSteps(LngLat position1, LngLat position2) {
|
||||
<span class="nc" id="L54"> double distance = calculateDistance(position1, position2);</span>
|
||||
<span class="nc" id="L55"> return distance / STEP;</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if {@code position1} and {@code position2} are close to each other, the threshold is <
|
||||
* 0.00015
|
||||
*
|
||||
* <p>Note that = 0.00015 will be counted as not close to and will return {@code false}
|
||||
*
|
||||
* @param position1 The coordinate of the first position
|
||||
* @param position2 The coordinate of the second position
|
||||
* @return {@code true} if {@code position1} and {@code position2} are close to each other
|
||||
* @see #CLOSE_THRESHOLD
|
||||
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsCloseTo(DistanceRequest)
|
||||
*/
|
||||
public boolean isCloseTo(LngLat position1, LngLat position2) {
|
||||
<span class="fc" id="L71"> double distance = calculateDistance(position1, position2);</span>
|
||||
<span class="fc bfc" id="L72" title="All 2 branches covered."> return distance < CLOSE_THRESHOLD;</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next position moved from {@code start} in the direction with {@code angle}, with
|
||||
* step size 0.00015
|
||||
*
|
||||
* @param start The coordinate of the original start point.
|
||||
* @param angle The direction to be moved in angle.
|
||||
* @return The next position moved from {@code start}
|
||||
* @see #STEP
|
||||
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequest)
|
||||
*/
|
||||
public LngLat nextPosition(LngLat start, Angle angle) {
|
||||
<span class="fc" id="L86"> double rad = angle.toRadians();</span>
|
||||
<span class="fc" id="L87"> double newLng = Math.cos(rad) * STEP + start.lng();</span>
|
||||
<span class="fc" id="L88"> double newLat = Math.sin(rad) * STEP + start.lat();</span>
|
||||
<span class="fc" id="L89"> return new LngLat(newLng, newLat);</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check if the given {@code position} is inside the {@code region}, on edge and vertex
|
||||
* is considered as inside.
|
||||
*
|
||||
* @param position The coordinate of the position.
|
||||
* @param region A {@link Region} that contains name and a list of {@code LngLatDto}
|
||||
* @return {@code true} if {@code position} is inside the {@code region}.
|
||||
* @throws IllegalArgumentException If {@code region} is not closed
|
||||
* @see
|
||||
* io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest)
|
||||
* @see Region#isClosed()
|
||||
*/
|
||||
public boolean checkIsInRegion(LngLat position, Region region) throws IllegalArgumentException {
|
||||
<span class="fc bfc" id="L105" title="All 2 branches covered."> if (!region.isClosed()) {</span>
|
||||
// call method from RegionDto to check if not closed
|
||||
<span class="fc" id="L107"> throw new IllegalArgumentException("Region is not closed.");</span>
|
||||
}
|
||||
<span class="fc" id="L109"> return rayCasting(position, region.vertices());</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to {@code checkIsInRegion}, use of ray-casting algorithm to check if inside
|
||||
* the polygon
|
||||
*
|
||||
* @param point The point to check
|
||||
* @param polygon The region that forms a polygon to check if {@code point} sits inside.
|
||||
* @return If the {@code point} sits inside the {@code polygon} then return {@code true}
|
||||
* @see #isPointOnEdge(LngLat, LngLat, LngLat)
|
||||
* @see #checkIsInRegion(LngLat, Region)
|
||||
*/
|
||||
private boolean rayCasting(LngLat point, List<LngLat> polygon) {
|
||||
<span class="fc" id="L123"> int intersections = 0;</span>
|
||||
<span class="fc" id="L124"> int n = polygon.size();</span>
|
||||
<span class="fc bfc" id="L125" title="All 2 branches covered."> for (int i = 0; i < n; ++i) {</span>
|
||||
<span class="fc" id="L126"> LngLat a = polygon.get(i);</span>
|
||||
<span class="fc" id="L127"> LngLat b = polygon.get((i + 1) % n); // Next vertex</span>
|
||||
|
||||
<span class="fc bfc" id="L129" title="All 2 branches covered."> if (isPointOnEdge(point, a, b)) {</span>
|
||||
<span class="fc" id="L130"> return true;</span>
|
||||
}
|
||||
|
||||
// Ensure that `a` is norther than `b`, in order to easy classification
|
||||
<span class="fc bfc" id="L134" title="All 2 branches covered."> if (a.lat() > b.lat()) {</span>
|
||||
<span class="fc" id="L135"> LngLat temp = a;</span>
|
||||
<span class="fc" id="L136"> a = b;</span>
|
||||
<span class="fc" id="L137"> b = temp;</span>
|
||||
}
|
||||
|
||||
// The point is not between a and b in latitude mean, skip this loop
|
||||
<span class="fc bfc" id="L141" title="All 4 branches covered."> if (point.lat() < a.lat() || point.lat() >= b.lat()) {</span>
|
||||
<span class="fc" id="L142"> continue;</span>
|
||||
}
|
||||
|
||||
// Skip the case of horizontal edge, already handled in `isPointOnEdge`:w
|
||||
<span class="pc bpc" id="L146" title="1 of 2 branches missed."> if (a.lat() == b.lat()) {</span>
|
||||
<span class="nc" id="L147"> continue;</span>
|
||||
}
|
||||
|
||||
<span class="fc" id="L150"> double xIntersection =</span>
|
||||
<span class="fc" id="L151"> a.lng() + ((point.lat() - a.lat()) * (b.lng() - a.lng())) / (b.lat() - a.lat());</span>
|
||||
|
||||
<span class="fc bfc" id="L153" title="All 2 branches covered."> if (xIntersection > point.lng()) {</span>
|
||||
<span class="fc" id="L154"> ++intersections;</span>
|
||||
}
|
||||
}
|
||||
// If intersections are odd, ray-casting returns true, which the point sits
|
||||
// inside the polygon;
|
||||
// If intersections are even, the point does not sit inside the polygon.
|
||||
<span class="fc bfc" id="L160" title="All 2 branches covered."> return intersections % 2 == 1;</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function from {@code rayCasting} that used to simply calculation <br>
|
||||
* Used to check if point {@code p} is on the edge formed by {@code a} and {@code b}
|
||||
*
|
||||
* @param p point to be checked on the edge
|
||||
* @param a point that forms the edge
|
||||
* @param b point that forms the edge
|
||||
* @return {@code true} if {@code p} is on {@code ab}
|
||||
* @see #rayCasting(LngLat, List)
|
||||
*/
|
||||
private boolean isPointOnEdge(LngLat p, LngLat a, LngLat b) {
|
||||
// Cross product: (p - a) × (b - a)
|
||||
<span class="fc" id="L175"> double crossProduct =</span>
|
||||
<span class="fc" id="L176"> (p.lng() - a.lng()) * (b.lat() - a.lat())</span>
|
||||
<span class="fc" id="L177"> - (p.lat() - a.lat()) * (b.lng() - a.lng());</span>
|
||||
<span class="fc bfc" id="L178" title="All 2 branches covered."> if (Math.abs(crossProduct) > 1e-9) {</span>
|
||||
<span class="fc" id="L179"> return false;</span>
|
||||
}
|
||||
|
||||
<span class="fc" id="L182"> boolean isWithinLng =</span>
|
||||
<span class="pc bpc" id="L183" title="1 of 4 branches missed."> p.lng() >= Math.min(a.lng(), b.lng()) && p.lng() <= Math.max(a.lng(), b.lng());</span>
|
||||
<span class="fc" id="L184"> boolean isWithinLat =</span>
|
||||
<span class="fc bfc" id="L185" title="All 4 branches covered."> p.lat() >= Math.min(a.lat(), b.lat()) && p.lat() <= Math.max(a.lat(), b.lat());</span>
|
||||
|
||||
<span class="fc bfc" id="L187" title="All 4 branches covered."> return isWithinLng && isWithinLat;</span>
|
||||
}
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.12.202403310830</span></div></body></html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>PathFinderService.PathSegment</title><script type="text/javascript" src="../jacoco-resources/sort.js"></script></head><body onload="initialSort(['breadcrumb'])"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">ilp-coursework</a> > <a href="index.html" class="el_package">io.github.js0ny.ilp_coursework.service</a> > <span class="el_class">PathFinderService.PathSegment</span></div><h1>PathFinderService.PathSegment</h1><table class="coverage" cellspacing="0" id="coveragetable"><thead><tr><td class="sortable" id="a" onclick="toggleSort(this)">Element</td><td class="down sortable bar" id="b" onclick="toggleSort(this)">Missed Instructions</td><td class="sortable ctr2" id="c" onclick="toggleSort(this)">Cov.</td><td class="sortable bar" id="d" onclick="toggleSort(this)">Missed Branches</td><td class="sortable ctr2" id="e" onclick="toggleSort(this)">Cov.</td><td class="sortable ctr1" id="f" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="g" onclick="toggleSort(this)">Cxty</td><td class="sortable ctr1" id="h" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="i" onclick="toggleSort(this)">Lines</td><td class="sortable ctr1" id="j" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="k" onclick="toggleSort(this)">Methods</td></tr></thead><tfoot><tr><td>Total</td><td class="bar">0 of 27</td><td class="ctr2">100%</td><td class="bar">0 of 2</td><td class="ctr2">100%</td><td class="ctr1">0</td><td class="ctr2">3</td><td class="ctr1">0</td><td class="ctr2">4</td><td class="ctr1">0</td><td class="ctr2">2</td></tr></tfoot><tbody><tr><td id="a0"><a href="PathFinderService.java.html#L538" class="el_method">appendSkippingStart(List)</a></td><td class="bar" id="b0"><img src="../jacoco-resources/greenbar.gif" width="120" height="10" title="18" alt="18"/></td><td class="ctr2" id="c0">100%</td><td class="bar" id="d0"><img src="../jacoco-resources/greenbar.gif" width="120" height="10" title="2" alt="2"/></td><td class="ctr2" id="e0">100%</td><td class="ctr1" id="f0">0</td><td class="ctr2" id="g0">2</td><td class="ctr1" id="h0">0</td><td class="ctr2" id="i0">3</td><td class="ctr1" id="j0">0</td><td class="ctr2" id="k0">1</td></tr><tr><td id="a1"><a href="PathFinderService.java.html#L530" class="el_method">PathFinderService.PathSegment(List, int)</a></td><td class="bar" id="b1"><img src="../jacoco-resources/greenbar.gif" width="60" height="10" title="9" alt="9"/></td><td class="ctr2" id="c1">100%</td><td class="bar" id="d1"/><td class="ctr2" id="e1">n/a</td><td class="ctr1" id="f1">0</td><td class="ctr2" id="g1">1</td><td class="ctr1" id="h1">0</td><td class="ctr2" id="i1">1</td><td class="ctr1" id="j1">0</td><td class="ctr2" id="k1">1</td></tr></tbody></table><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.12.202403310830</span></div></body></html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>PathFinderService.TripResult</title><script type="text/javascript" src="../jacoco-resources/sort.js"></script></head><body onload="initialSort(['breadcrumb'])"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">ilp-coursework</a> > <a href="index.html" class="el_package">io.github.js0ny.ilp_coursework.service</a> > <span class="el_class">PathFinderService.TripResult</span></div><h1>PathFinderService.TripResult</h1><table class="coverage" cellspacing="0" id="coveragetable"><thead><tr><td class="sortable" id="a" onclick="toggleSort(this)">Element</td><td class="down sortable bar" id="b" onclick="toggleSort(this)">Missed Instructions</td><td class="sortable ctr2" id="c" onclick="toggleSort(this)">Cov.</td><td class="sortable bar" id="d" onclick="toggleSort(this)">Missed Branches</td><td class="sortable ctr2" id="e" onclick="toggleSort(this)">Cov.</td><td class="sortable ctr1" id="f" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="g" onclick="toggleSort(this)">Cxty</td><td class="sortable ctr1" id="h" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="i" onclick="toggleSort(this)">Lines</td><td class="sortable ctr1" id="j" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="k" onclick="toggleSort(this)">Methods</td></tr></thead><tfoot><tr><td>Total</td><td class="bar">0 of 12</td><td class="ctr2">100%</td><td class="bar">0 of 0</td><td class="ctr2">n/a</td><td class="ctr1">0</td><td class="ctr2">1</td><td class="ctr1">0</td><td class="ctr2">1</td><td class="ctr1">0</td><td class="ctr2">1</td></tr></tfoot><tbody><tr><td id="a0"><a href="PathFinderService.java.html#L548" class="el_method">PathFinderService.TripResult(DeliveryPathResponse.DronePath, int, float)</a></td><td class="bar" id="b0"><img src="../jacoco-resources/greenbar.gif" width="120" height="10" title="12" alt="12"/></td><td class="ctr2" id="c0">100%</td><td class="bar" id="d0"/><td class="ctr2" id="e0">n/a</td><td class="ctr1" id="f0">0</td><td class="ctr2" id="g0">1</td><td class="ctr1" id="h0">0</td><td class="ctr2" id="i0">1</td><td class="ctr1" id="j0">0</td><td class="ctr2" id="k0">1</td></tr></tbody></table><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.12.202403310830</span></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,550 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>PathFinderService.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">ilp-coursework</a> > <a href="index.source.html" class="el_package">io.github.js0ny.ilp_coursework.service</a> > <span class="el_source">PathFinderService.java</span></div><h1>PathFinderService.java</h1><pre class="source lang-java linenums">package io.github.js0ny.ilp_coursework.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
import io.github.js0ny.ilp_coursework.controller.DroneController;
|
||||
import io.github.js0ny.ilp_coursework.data.common.Angle;
|
||||
import io.github.js0ny.ilp_coursework.data.common.DroneAvailability;
|
||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||
import io.github.js0ny.ilp_coursework.data.common.Region;
|
||||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
||||
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
|
||||
import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
|
||||
import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
|
||||
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
|
||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath;
|
||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath.Delivery;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Class that handles calculations about deliverypath
|
||||
*
|
||||
* @see DroneInfoService
|
||||
* @see DroneController
|
||||
* @see DeliveryPathResponse
|
||||
*/
|
||||
@Service
|
||||
public class PathFinderService {
|
||||
|
||||
/**
|
||||
* Hard stop on how many pathfinding iterations we attempt for a single segment before bailing,
|
||||
* useful for preventing infinite loops caused by precision quirks or unexpected map data.
|
||||
*
|
||||
* @see #computePath(LngLat, LngLat)
|
||||
*/
|
||||
private static final int MAX_SEGMENT_ITERATIONS = 8_000;
|
||||
|
||||
// Services
|
||||
private final GpsCalculationService gpsCalculationService;
|
||||
private final DroneInfoService droneInfoService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final List<Drone> drones;
|
||||
private final Map<String, Drone> droneById;
|
||||
private final Map<String, Integer> droneServicePointMap;
|
||||
private final Map<Integer, LngLat> servicePointLocations;
|
||||
private final List<Region> restrictedRegions;
|
||||
|
||||
@Autowired(required = false)
|
||||
private TelemetryService telemetryService;
|
||||
|
||||
/**
|
||||
* Constructor for PathFinderService. The dependencies are injected by Spring and the
|
||||
* constructor pre-computes reference maps used throughout the request lifecycle.
|
||||
*
|
||||
* @param gpsCalculationService Service handling geometric operations.
|
||||
* @param droneInfoService Service that exposes drone metadata and capability information.
|
||||
*/
|
||||
public PathFinderService(
|
||||
<span class="fc" id="L75"> GpsCalculationService gpsCalculationService, DroneInfoService droneInfoService) {</span>
|
||||
<span class="fc" id="L76"> this.gpsCalculationService = gpsCalculationService;</span>
|
||||
<span class="fc" id="L77"> this.droneInfoService = droneInfoService;</span>
|
||||
<span class="fc" id="L78"> this.objectMapper = new ObjectMapper();</span>
|
||||
<span class="fc" id="L79"> objectMapper.registerModule(new JavaTimeModule());</span>
|
||||
<span class="fc" id="L80"> objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);</span>
|
||||
<span class="fc" id="L81"> objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);</span>
|
||||
|
||||
<span class="fc" id="L83"> this.drones = droneInfoService.fetchAllDrones();</span>
|
||||
<span class="fc" id="L84"> List<ServicePoint> servicePoints = droneInfoService.fetchServicePoints();</span>
|
||||
<span class="fc" id="L85"> List<ServicePointDrones> servicePointAssignments =</span>
|
||||
<span class="fc" id="L86"> droneInfoService.fetchDronesForServicePoints();</span>
|
||||
<span class="fc" id="L87"> List<RestrictedArea> restrictedAreas = droneInfoService.fetchRestrictedAreas();</span>
|
||||
|
||||
<span class="fc" id="L89"> this.droneById = this.drones.stream().collect(Collectors.toMap(Drone::id, drone -> drone));</span>
|
||||
|
||||
<span class="fc" id="L91"> this.droneServicePointMap = new HashMap<>();</span>
|
||||
<span class="fc bfc" id="L92" title="All 2 branches covered."> for (ServicePointDrones assignment : servicePointAssignments) {</span>
|
||||
<span class="pc bpc" id="L93" title="2 of 4 branches missed."> if (assignment == null || assignment.drones() == null) {</span>
|
||||
<span class="nc" id="L94"> continue;</span>
|
||||
}
|
||||
<span class="fc bfc" id="L96" title="All 2 branches covered."> for (DroneAvailability availability : assignment.drones()) {</span>
|
||||
<span class="pc bpc" id="L97" title="2 of 4 branches missed."> if (availability == null || availability.id() == null) {</span>
|
||||
<span class="nc" id="L98"> continue;</span>
|
||||
}
|
||||
<span class="fc" id="L100"> droneServicePointMap.put(availability.id(), assignment.servicePointId());</span>
|
||||
}
|
||||
<span class="fc" id="L102"> }</span>
|
||||
|
||||
<span class="fc" id="L104"> this.servicePointLocations =</span>
|
||||
<span class="fc" id="L105"> servicePoints.stream()</span>
|
||||
<span class="fc" id="L106"> .collect(</span>
|
||||
<span class="fc" id="L107"> Collectors.toMap(</span>
|
||||
<span class="fc" id="L108"> ServicePoint::id, sp -> new LngLat(sp.location())));</span>
|
||||
|
||||
<span class="fc" id="L110"> this.restrictedRegions = restrictedAreas.stream().map(RestrictedArea::toRegion).toList();</span>
|
||||
<span class="fc" id="L111"> }</span>
|
||||
|
||||
/**
|
||||
* Produce a delivery plan for the provided dispatch records. Deliveries are grouped per
|
||||
* compatible drone and per trip to satisfy each drone move limit.
|
||||
*
|
||||
* @param records Dispatch records to be fulfilled.
|
||||
* @return Aggregated path response with cost and move totals.
|
||||
* @see #calculateDeliveryPathAsGeoJson(MedDispatchRecRequest[])
|
||||
*/
|
||||
public DeliveryPathResponse calculateDeliveryPath(MedDispatchRecRequest[] records) {
|
||||
<span class="pc bpc" id="L122" title="2 of 4 branches missed."> if (records == null || records.length == 0) {</span>
|
||||
<span class="nc" id="L123"> return new DeliveryPathResponse(0f, 0, new DronePath[0]);</span>
|
||||
}
|
||||
|
||||
<span class="fc" id="L126"> Map<Integer, LocalDateTime> deliveryTimestamps = new HashMap<>();</span>
|
||||
<span class="fc bfc" id="L127" title="All 2 branches covered."> for (var r : records) {</span>
|
||||
<span class="pc bpc" id="L128" title="1 of 2 branches missed."> if (isRestricted(r.delivery())) {</span>
|
||||
<span class="nc" id="L129"> throw new IllegalStateException(</span>
|
||||
"Delivery "
|
||||
<span class="nc" id="L131"> + r.id()</span>
|
||||
+ " is located within a restricted area and cannot be fulfilled");
|
||||
}
|
||||
<span class="pc bpc" id="L134" title="2 of 4 branches missed."> if (r.date() != null && r.time() != null) {</span>
|
||||
<span class="fc" id="L135"> deliveryTimestamps.put(r.id(), LocalDateTime.of(r.date(), r.time()));</span>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="fc" id="L139"> Map<String, List<MedDispatchRecRequest>> assigned = assignDeliveries(records);</span>
|
||||
|
||||
<span class="fc" id="L141"> List<DronePath> paths = new ArrayList<>();</span>
|
||||
<span class="fc" id="L142"> float totalCost = 0f;</span>
|
||||
<span class="fc" id="L143"> int totalMoves = 0;</span>
|
||||
|
||||
<span class="fc bfc" id="L145" title="All 2 branches covered."> for (Map.Entry<String, List<MedDispatchRecRequest>> entry : assigned.entrySet()) {</span>
|
||||
<span class="fc" id="L146"> String droneId = entry.getKey();</span>
|
||||
<span class="fc" id="L147"> Drone drone = droneById.get(droneId);</span>
|
||||
<span class="pc bpc" id="L148" title="1 of 2 branches missed."> if (drone == null) {</span>
|
||||
<span class="nc" id="L149"> continue;</span>
|
||||
}
|
||||
<span class="fc" id="L151"> Integer spId = droneServicePointMap.get(droneId);</span>
|
||||
<span class="pc bpc" id="L152" title="1 of 2 branches missed."> if (spId == null) {</span>
|
||||
<span class="nc" id="L153"> continue;</span>
|
||||
}
|
||||
<span class="fc" id="L155"> LngLat servicePointLocation = servicePointLocations.get(spId);</span>
|
||||
<span class="pc bpc" id="L156" title="1 of 2 branches missed."> if (servicePointLocation == null) {</span>
|
||||
<span class="nc" id="L157"> continue;</span>
|
||||
}
|
||||
|
||||
<span class="fc" id="L160"> List<MedDispatchRecRequest> sortedDeliveries =</span>
|
||||
<span class="fc" id="L161"> entry.getValue().stream()</span>
|
||||
<span class="fc" id="L162"> .sorted(</span>
|
||||
<span class="fc" id="L163"> Comparator.comparingDouble(</span>
|
||||
rec ->
|
||||
<span class="nc" id="L165"> gpsCalculationService.calculateDistance(</span>
|
||||
<span class="nc" id="L166"> servicePointLocation, rec.delivery())))</span>
|
||||
<span class="fc" id="L167"> .toList();</span>
|
||||
|
||||
<span class="fc" id="L169"> List<List<MedDispatchRecRequest>> trips =</span>
|
||||
<span class="fc" id="L170"> splitTrips(sortedDeliveries, drone, servicePointLocation);</span>
|
||||
|
||||
<span class="fc bfc" id="L172" title="All 2 branches covered."> for (List<MedDispatchRecRequest> trip : trips) {</span>
|
||||
<span class="fc" id="L173"> TripResult result = buildTrip(drone, servicePointLocation, trip);</span>
|
||||
<span class="pc bpc" id="L174" title="1 of 2 branches missed."> if (result != null) {</span>
|
||||
<span class="fc" id="L175"> totalCost += result.cost();</span>
|
||||
<span class="fc" id="L176"> totalMoves += result.moves();</span>
|
||||
<span class="fc" id="L177"> paths.add(result.path());</span>
|
||||
}
|
||||
<span class="fc" id="L179"> }</span>
|
||||
<span class="fc" id="L180"> }</span>
|
||||
|
||||
<span class="fc" id="L182"> var resp = new DeliveryPathResponse(totalCost, totalMoves, paths.toArray(new DronePath[0]));</span>
|
||||
|
||||
<span class="pc bpc" id="L184" title="1 of 2 branches missed."> if (telemetryService != null) {</span>
|
||||
<span class="nc" id="L185"> telemetryService.sendEventAsyncByPathResponse(resp, deliveryTimestamps);</span>
|
||||
}
|
||||
|
||||
<span class="fc" id="L188"> return resp;</span>
|
||||
}
|
||||
|
||||
/*
|
||||
* Convenience wrapper around {@link #calculateDeliveryPath} that serializes the
|
||||
* result into a
|
||||
* GeoJSON FeatureCollection suitable for mapping visualization.
|
||||
*
|
||||
* @param records Dispatch records to be fulfilled.
|
||||
*
|
||||
* @return GeoJSON payload representing every delivery flight path.
|
||||
*
|
||||
* @throws IllegalStateException When the payload cannot be serialized.
|
||||
*/
|
||||
public String calculateDeliveryPathAsGeoJson(MedDispatchRecRequest[] records) {
|
||||
<span class="fc" id="L203"> DeliveryPathResponse response = calculateDeliveryPath(records);</span>
|
||||
<span class="fc" id="L204"> Map<String, Object> featureCollection = new LinkedHashMap<>();</span>
|
||||
<span class="fc" id="L205"> featureCollection.put("type", "FeatureCollection");</span>
|
||||
<span class="fc" id="L206"> List<Map<String, Object>> features = new ArrayList<>();</span>
|
||||
|
||||
<span class="pc bpc" id="L208" title="2 of 4 branches missed."> if (response != null && response.dronePaths() != null) {</span>
|
||||
<span class="fc bfc" id="L209" title="All 2 branches covered."> for (DronePath dronePath : response.dronePaths()) {</span>
|
||||
<span class="pc bpc" id="L210" title="2 of 4 branches missed."> if (dronePath == null || dronePath.deliveries() == null) {</span>
|
||||
<span class="nc" id="L211"> continue;</span>
|
||||
}
|
||||
<span class="fc bfc" id="L213" title="All 2 branches covered."> for (Delivery delivery : dronePath.deliveries()) {</span>
|
||||
<span class="fc" id="L214"> Map<String, Object> feature = new LinkedHashMap<>();</span>
|
||||
<span class="fc" id="L215"> feature.put("type", "Feature");</span>
|
||||
|
||||
<span class="fc" id="L217"> Map<String, Object> properties = new LinkedHashMap<>();</span>
|
||||
<span class="fc" id="L218"> properties.put("droneId", dronePath.droneId());</span>
|
||||
<span class="fc" id="L219"> properties.put("deliveryId", delivery.deliveryId());</span>
|
||||
<span class="fc" id="L220"> feature.put("properties", properties);</span>
|
||||
|
||||
<span class="fc" id="L222"> Map<String, Object> geometry = new LinkedHashMap<>();</span>
|
||||
<span class="fc" id="L223"> geometry.put("type", "LineString");</span>
|
||||
|
||||
<span class="fc" id="L225"> List<List<Double>> coordinates = new ArrayList<>();</span>
|
||||
<span class="pc bpc" id="L226" title="1 of 2 branches missed."> if (delivery.flightPath() != null) {</span>
|
||||
<span class="fc bfc" id="L227" title="All 2 branches covered."> for (LngLat point : delivery.flightPath()) {</span>
|
||||
<span class="fc" id="L228"> coordinates.add(List.of(point.lng(), point.lat()));</span>
|
||||
<span class="fc" id="L229"> }</span>
|
||||
}
|
||||
<span class="fc" id="L231"> geometry.put("coordinates", coordinates);</span>
|
||||
<span class="fc" id="L232"> feature.put("geometry", geometry);</span>
|
||||
<span class="fc" id="L233"> features.add(feature);</span>
|
||||
<span class="fc" id="L234"> }</span>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="fc" id="L238"> featureCollection.put("features", features);</span>
|
||||
|
||||
try {
|
||||
<span class="fc" id="L241"> return objectMapper.writeValueAsString(featureCollection);</span>
|
||||
<span class="nc" id="L242"> } catch (JsonProcessingException e) {</span>
|
||||
<span class="nc" id="L243"> throw new IllegalStateException("Failed to generate GeoJSON payload", e);</span>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group dispatch records by their assigned drone, ensuring every record is routed through
|
||||
* {@link #findBestDrone(MedDispatchRecRequest)} exactly once and discarding invalid entries.
|
||||
*
|
||||
* @param records Dispatch records to be grouped.
|
||||
* @return Map keyed by drone ID with the deliveries it should service.
|
||||
*/
|
||||
private Map<String, List<MedDispatchRecRequest>> assignDeliveries(
|
||||
MedDispatchRecRequest[] records) {
|
||||
<span class="fc" id="L256"> Map<String, List<MedDispatchRecRequest>> assignments = new LinkedHashMap<>();</span>
|
||||
<span class="fc bfc" id="L257" title="All 2 branches covered."> for (MedDispatchRecRequest record : records) {</span>
|
||||
<span class="pc bpc" id="L258" title="2 of 4 branches missed."> if (record == null || record.delivery() == null) {</span>
|
||||
<span class="nc" id="L259"> continue;</span>
|
||||
}
|
||||
<span class="fc" id="L261"> String droneId = findBestDrone(record);</span>
|
||||
<span class="fc" id="L262"> assignments.computeIfAbsent(droneId, id -> new ArrayList<>()).add(record);</span>
|
||||
}
|
||||
<span class="fc" id="L264"> return assignments;</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the best drone for the provided record. Currently that equates to picking the closest
|
||||
* compatible drone to the delivery location.
|
||||
*
|
||||
* @param record Dispatch record that needs fulfillment.
|
||||
* @return Identifier of the drone that should fly the mission.
|
||||
* @throws IllegalStateException If no available drone can serve the request.
|
||||
*/
|
||||
private String findBestDrone(MedDispatchRecRequest record) {
|
||||
<span class="fc" id="L276"> double bestScore = Double.MAX_VALUE;</span>
|
||||
<span class="fc" id="L277"> String bestDrone = null;</span>
|
||||
|
||||
<span class="fc bfc" id="L279" title="All 2 branches covered."> for (Drone drone : drones) {</span>
|
||||
<span class="pc bpc" id="L280" title="1 of 2 branches missed."> if (!droneInfoService.droneMatchesRequirement(drone, record)) {</span>
|
||||
<span class="nc" id="L281"> continue;</span>
|
||||
}
|
||||
<span class="fc" id="L283"> String droneId = drone.id();</span>
|
||||
<span class="fc" id="L284"> Integer servicePointId = droneServicePointMap.get(droneId);</span>
|
||||
<span class="pc bpc" id="L285" title="1 of 2 branches missed."> if (servicePointId == null) {</span>
|
||||
<span class="nc" id="L286"> continue;</span>
|
||||
}
|
||||
<span class="fc" id="L288"> LngLat servicePointLocation = servicePointLocations.get(servicePointId);</span>
|
||||
<span class="pc bpc" id="L289" title="1 of 2 branches missed."> if (servicePointLocation == null) {</span>
|
||||
<span class="nc" id="L290"> continue;</span>
|
||||
}
|
||||
|
||||
<span class="fc" id="L293"> double distance =</span>
|
||||
<span class="fc" id="L294"> gpsCalculationService.calculateDistance(</span>
|
||||
<span class="fc" id="L295"> servicePointLocation, record.delivery());</span>
|
||||
|
||||
<span class="pc bpc" id="L297" title="1 of 2 branches missed."> if (distance < bestScore) {</span>
|
||||
<span class="fc" id="L298"> bestScore = distance;</span>
|
||||
<span class="fc" id="L299"> bestDrone = droneId;</span>
|
||||
}
|
||||
<span class="fc" id="L301"> }</span>
|
||||
<span class="pc bpc" id="L302" title="1 of 2 branches missed."> if (bestDrone == null) {</span>
|
||||
<span class="nc" id="L303"> throw new IllegalStateException("No available drone for delivery " + record.id());</span>
|
||||
}
|
||||
<span class="fc" id="L305"> return bestDrone;</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Break a sequence of deliveries into several trips that each respect the drone move limit. The
|
||||
* deliveries should already be ordered by proximity for sensible grouping.
|
||||
*
|
||||
* @param deliveries Deliveries assigned to a drone.
|
||||
* @param drone Drone that will service the deliveries.
|
||||
* @param servicePoint Starting and ending point of every trip.
|
||||
* @return Partitioned trips with at least one delivery each.
|
||||
* @throws IllegalStateException If a single delivery exceeds the drone's move limit.
|
||||
*/
|
||||
private List<List<MedDispatchRecRequest>> splitTrips(
|
||||
List<MedDispatchRecRequest> deliveries, Drone drone, LngLat servicePoint) {
|
||||
<span class="fc" id="L320"> List<List<MedDispatchRecRequest>> trips = new ArrayList<>();</span>
|
||||
<span class="fc" id="L321"> List<MedDispatchRecRequest> currentTrip = new ArrayList<>();</span>
|
||||
<span class="fc bfc" id="L322" title="All 2 branches covered."> for (MedDispatchRecRequest delivery : deliveries) {</span>
|
||||
<span class="fc" id="L323"> currentTrip.add(delivery);</span>
|
||||
<span class="fc" id="L324"> int requiredMoves = estimateTripMoves(servicePoint, currentTrip);</span>
|
||||
<span class="pc bpc" id="L325" title="1 of 2 branches missed."> if (requiredMoves > drone.capability().maxMoves()) {</span>
|
||||
<span class="nc" id="L326"> currentTrip.remove(currentTrip.size() - 1);</span>
|
||||
<span class="nc bnc" id="L327" title="All 2 branches missed."> if (currentTrip.isEmpty()) {</span>
|
||||
<span class="nc" id="L328"> throw new IllegalStateException(</span>
|
||||
"Delivery "
|
||||
<span class="nc" id="L330"> + delivery.id()</span>
|
||||
+ " exceeds drone "
|
||||
<span class="nc" id="L332"> + drone.id()</span>
|
||||
+ " move limit");
|
||||
}
|
||||
<span class="nc" id="L335"> trips.add(new ArrayList<>(currentTrip));</span>
|
||||
<span class="nc" id="L336"> currentTrip.clear();</span>
|
||||
<span class="nc" id="L337"> currentTrip.add(delivery);</span>
|
||||
}
|
||||
<span class="fc" id="L339"> }</span>
|
||||
<span class="pc bpc" id="L340" title="1 of 2 branches missed."> if (!currentTrip.isEmpty()) {</span>
|
||||
<span class="fc" id="L341"> int requiredMoves = estimateTripMoves(servicePoint, currentTrip);</span>
|
||||
<span class="pc bpc" id="L342" title="1 of 2 branches missed."> if (requiredMoves > drone.capability().maxMoves()) {</span>
|
||||
<span class="nc" id="L343"> throw new IllegalStateException(</span>
|
||||
<span class="nc" id="L344"> "Delivery plan exceeds move limit for drone " + drone.id());</span>
|
||||
}
|
||||
<span class="fc" id="L346"> trips.add(new ArrayList<>(currentTrip));</span>
|
||||
}
|
||||
<span class="fc" id="L348"> return trips;</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a single trip for the provided drone, including the entire flight path to every
|
||||
* delivery and back home. The resulting structure contains the {@link DronePath} representation
|
||||
* as well as cost and moves consumed.
|
||||
*
|
||||
* @param drone Drone executing the trip.
|
||||
* @param servicePoint Starting/ending location of the trip.
|
||||
* @param deliveries Deliveries to include in the trip in execution order.
|
||||
* @return Trip information or {@code null} if no deliveries are provided.
|
||||
* @see DeliveryPathResponse.DronePath
|
||||
*/
|
||||
private TripResult buildTrip(
|
||||
Drone drone, LngLat servicePoint, List<MedDispatchRecRequest> deliveries) {
|
||||
<span class="pc bpc" id="L364" title="2 of 4 branches missed."> if (deliveries == null || deliveries.isEmpty()) {</span>
|
||||
<span class="nc" id="L365"> return null;</span>
|
||||
}
|
||||
<span class="fc" id="L367"> List<Delivery> flightPlans = new ArrayList<>();</span>
|
||||
<span class="fc" id="L368"> LngLat current = servicePoint;</span>
|
||||
<span class="fc" id="L369"> int moves = 0;</span>
|
||||
|
||||
<span class="fc bfc" id="L371" title="All 2 branches covered."> for (int i = 0; i < deliveries.size(); i++) {</span>
|
||||
<span class="fc" id="L372"> MedDispatchRecRequest delivery = deliveries.get(i);</span>
|
||||
<span class="fc" id="L373"> PathSegment toDelivery = computePath(current, delivery.delivery());</span>
|
||||
<span class="fc" id="L374"> List<LngLat> flightPath = new ArrayList<>(toDelivery.positions());</span>
|
||||
<span class="pc bpc" id="L375" title="1 of 2 branches missed."> if (!flightPath.isEmpty()) {</span>
|
||||
<span class="fc" id="L376"> LngLat last = flightPath.get(flightPath.size() - 1);</span>
|
||||
<span class="pc bpc" id="L377" title="1 of 2 branches missed."> if (!last.isSamePoint(delivery.delivery())) {</span>
|
||||
<span class="nc" id="L378"> flightPath.add(delivery.delivery());</span>
|
||||
}
|
||||
<span class="fc" id="L380"> } else {</span>
|
||||
<span class="nc" id="L381"> flightPath.add(current);</span>
|
||||
<span class="nc" id="L382"> flightPath.add(delivery.delivery());</span>
|
||||
}
|
||||
<span class="fc" id="L384"> flightPath.add(delivery.delivery());</span>
|
||||
<span class="fc" id="L385"> moves += toDelivery.moves();</span>
|
||||
|
||||
<span class="pc bpc" id="L387" title="1 of 2 branches missed."> if (i == deliveries.size() - 1) {</span>
|
||||
<span class="fc" id="L388"> PathSegment backHome = computePath(delivery.delivery(), servicePoint);</span>
|
||||
<span class="fc" id="L389"> backHome.appendSkippingStart(flightPath);</span>
|
||||
<span class="fc" id="L390"> moves += backHome.moves();</span>
|
||||
<span class="fc" id="L391"> current = servicePoint;</span>
|
||||
<span class="fc" id="L392"> } else {</span>
|
||||
<span class="nc" id="L393"> current = delivery.delivery();</span>
|
||||
}
|
||||
<span class="fc" id="L395"> flightPlans.add(new Delivery(delivery.id(), flightPath));</span>
|
||||
}
|
||||
|
||||
<span class="fc" id="L398"> float cost =</span>
|
||||
<span class="fc" id="L399"> drone.capability().costInitial()</span>
|
||||
<span class="fc" id="L400"> + drone.capability().costFinal()</span>
|
||||
<span class="fc" id="L401"> + (float) (drone.capability().costPerMove() * moves);</span>
|
||||
|
||||
<span class="fc" id="L403"> DronePath path = new DronePath(drone.parseId(), flightPlans);</span>
|
||||
|
||||
<span class="fc" id="L405"> return new TripResult(path, moves, cost);</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate the number of moves a prospective trip would need by replaying the path calculation
|
||||
* without mutating any persistent state.
|
||||
*
|
||||
* @param servicePoint Trip origin.
|
||||
* @param deliveries Deliveries that would compose the trip.
|
||||
* @return Total moves required to fly the proposed itinerary.
|
||||
*/
|
||||
private int estimateTripMoves(LngLat servicePoint, List<MedDispatchRecRequest> deliveries) {
|
||||
<span class="pc bpc" id="L417" title="1 of 2 branches missed."> if (deliveries.isEmpty()) {</span>
|
||||
<span class="nc" id="L418"> return 0;</span>
|
||||
}
|
||||
<span class="fc" id="L420"> int moves = 0;</span>
|
||||
<span class="fc" id="L421"> LngLat current = servicePoint;</span>
|
||||
<span class="fc bfc" id="L422" title="All 2 branches covered."> for (MedDispatchRecRequest delivery : deliveries) {</span>
|
||||
<span class="fc" id="L423"> PathSegment segment = computePath(current, delivery.delivery());</span>
|
||||
<span class="fc" id="L424"> moves += segment.moves();</span>
|
||||
<span class="fc" id="L425"> current = delivery.delivery();</span>
|
||||
<span class="fc" id="L426"> }</span>
|
||||
<span class="fc" id="L427"> moves += computePath(current, servicePoint).moves();</span>
|
||||
<span class="fc" id="L428"> return moves;</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a path between {@code start} and {@code target} by repeatedly moving in snapped
|
||||
* increments while avoiding restricted zones.
|
||||
*
|
||||
* @param start Start coordinate.
|
||||
* @param target Destination coordinate.
|
||||
* @return Sequence of visited coordinates and move count.
|
||||
* @see #nextPosition(LngLat, LngLat)
|
||||
*/
|
||||
private PathSegment computePath(LngLat start, LngLat target) {
|
||||
<span class="fc" id="L441"> List<LngLat> positions = new ArrayList<>();</span>
|
||||
<span class="pc bpc" id="L442" title="2 of 4 branches missed."> if (start == null || target == null) {</span>
|
||||
<span class="nc" id="L443"> return new PathSegment(positions, 0);</span>
|
||||
}
|
||||
<span class="fc" id="L445"> positions.add(start);</span>
|
||||
<span class="fc" id="L446"> LngLat current = start;</span>
|
||||
<span class="fc" id="L447"> int iterations = 0;</span>
|
||||
<span class="pc bpc" id="L448" title="1 of 4 branches missed."> while (!gpsCalculationService.isCloseTo(current, target)</span>
|
||||
&& iterations < MAX_SEGMENT_ITERATIONS) {
|
||||
<span class="fc" id="L450"> LngLat next = nextPosition(current, target);</span>
|
||||
<span class="pc bpc" id="L451" title="1 of 2 branches missed."> if (next.isSamePoint(current)) {</span>
|
||||
<span class="nc" id="L452"> break;</span>
|
||||
}
|
||||
<span class="fc" id="L454"> positions.add(next);</span>
|
||||
<span class="fc" id="L455"> current = next;</span>
|
||||
<span class="fc" id="L456"> iterations++;</span>
|
||||
<span class="fc" id="L457"> }</span>
|
||||
<span class="pc bpc" id="L458" title="1 of 2 branches missed."> if (!positions.get(positions.size() - 1).isSamePoint(target)) {</span>
|
||||
<span class="nc" id="L459"> positions.add(target);</span>
|
||||
}
|
||||
<span class="fc" id="L461"> return new PathSegment(positions, Math.max(0, positions.size() - 1));</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the next position on the path from {@code current} toward {@code target},
|
||||
* preferring the snapped angle closest to the desired heading that does not infiltrate a
|
||||
* restricted region.
|
||||
*
|
||||
* @param current Current coordinate.
|
||||
* @param target Destination coordinate.
|
||||
* @return Next admissible coordinate or the original point if none can be found.
|
||||
*/
|
||||
private LngLat nextPosition(LngLat current, LngLat target) {
|
||||
<span class="fc" id="L474"> double desiredAngle =</span>
|
||||
<span class="fc" id="L475"> Math.toDegrees(</span>
|
||||
<span class="fc" id="L476"> Math.atan2(target.lat() - current.lat(), target.lng() - current.lng()));</span>
|
||||
<span class="fc" id="L477"> List<Angle> candidateAngles = buildAngleCandidates(desiredAngle);</span>
|
||||
<span class="pc bpc" id="L478" title="1 of 2 branches missed."> for (Angle angle : candidateAngles) {</span>
|
||||
<span class="fc" id="L479"> LngLat next = gpsCalculationService.nextPosition(current, angle);</span>
|
||||
<span class="pc bpc" id="L480" title="1 of 2 branches missed."> if (!isRestricted(next)) {</span>
|
||||
<span class="fc" id="L481"> return next;</span>
|
||||
}
|
||||
<span class="nc" id="L483"> }</span>
|
||||
<span class="nc" id="L484"> return current;</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a sequence of candidate angles centered on the desired heading, expanding symmetrically
|
||||
* clockwise and counter-clockwise to explore alternative headings if the primary path is
|
||||
* blocked.
|
||||
*
|
||||
* @param desiredAngle Bearing in degrees between current and target positions.
|
||||
* @return Ordered list of candidate snapped angles.
|
||||
* @see Angle#snap(double)
|
||||
*/
|
||||
private List<Angle> buildAngleCandidates(double desiredAngle) {
|
||||
<span class="fc" id="L497"> List<Angle> angles = new LinkedList<>();</span>
|
||||
<span class="fc" id="L498"> Angle snapped = Angle.snap(desiredAngle);</span>
|
||||
<span class="fc" id="L499"> angles.add(snapped);</span>
|
||||
<span class="fc bfc" id="L500" title="All 2 branches covered."> for (int offset = 1; offset <= 8; offset++) {</span>
|
||||
<span class="fc" id="L501"> angles.add(snapped.offset(offset));</span>
|
||||
<span class="fc" id="L502"> angles.add(snapped.offset(-offset));</span>
|
||||
}
|
||||
<span class="fc" id="L504"> return angles;</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the provided coordinate falls inside any restricted region.
|
||||
*
|
||||
* @param position Coordinate to inspect.
|
||||
* @return {@code true} if the position intersects a restricted area.
|
||||
* @see #restrictedRegions
|
||||
*/
|
||||
private boolean isRestricted(LngLat position) {
|
||||
<span class="pc bpc" id="L515" title="1 of 2 branches missed."> for (Region region : restrictedRegions) {</span>
|
||||
<span class="nc bnc" id="L516" title="All 2 branches missed."> if (gpsCalculationService.checkIsInRegion(position, region)) {</span>
|
||||
<span class="nc" id="L517"> return true;</span>
|
||||
}
|
||||
<span class="nc" id="L519"> }</span>
|
||||
<span class="fc" id="L520"> return false;</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of a computed path segment wrapping the visited positions and the number of
|
||||
* moves taken to traverse them.
|
||||
*
|
||||
* @param positions Ordered coordinates that describe the path.
|
||||
* @param moves Number of moves consumed by the path.
|
||||
*/
|
||||
<span class="fc" id="L530"> private record PathSegment(List<LngLat> positions, int moves) {</span>
|
||||
/**
|
||||
* Append the positions from this segment to {@code target}, skipping the first coordinate
|
||||
* as it is already represented by the last coordinate in the consumer path.
|
||||
*
|
||||
* @param target Mutable list to append to.
|
||||
*/
|
||||
private void appendSkippingStart(List<LngLat> target) {
|
||||
<span class="fc bfc" id="L538" title="All 2 branches covered."> for (int i = 1; i < positions.size(); i++) {</span>
|
||||
<span class="fc" id="L539"> target.add(positions.get(i));</span>
|
||||
}
|
||||
<span class="fc" id="L541"> }</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle containing the calculated {@link DronePath}, total moves and financial cost for a
|
||||
* single trip.
|
||||
*/
|
||||
<span class="fc" id="L548"> private record TripResult(DronePath path, int moves, float cost) {}</span>
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.12.202403310830</span></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,80 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>TelemetryService.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">ilp-coursework</a> > <a href="index.source.html" class="el_package">io.github.js0ny.ilp_coursework.service</a> > <span class="el_source">TelemetryService.java</span></div><h1>TelemetryService.java</h1><pre class="source lang-java linenums">package io.github.js0ny.ilp_coursework.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.github.js0ny.ilp_coursework.data.common.DroneEvent;
|
||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Service
|
||||
public class TelemetryService {
|
||||
<span class="fc" id="L20"> private static final Logger log = LoggerFactory.getLogger(TelemetryService.class);</span>
|
||||
|
||||
private final HttpClient client;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
private final String BLACKBOX_URL;
|
||||
|
||||
<span class="fc" id="L27"> public TelemetryService() {</span>
|
||||
<span class="fc" id="L28"> this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();</span>
|
||||
|
||||
<span class="fc" id="L30"> this.mapper = new ObjectMapper();</span>
|
||||
<span class="fc" id="L31"> this.BLACKBOX_URL =</span>
|
||||
<span class="fc" id="L32"> System.getenv().getOrDefault("BLACKBOX_ENDPOINT", "http://localhost:3000");</span>
|
||||
<span class="fc" id="L33"> }</span>
|
||||
|
||||
public void sendEventAsyncByPathResponse(DeliveryPathResponse resp) {
|
||||
<span class="nc" id="L36"> var events = DroneEvent.fromPathResponse(resp);</span>
|
||||
<span class="nc bnc" id="L37" title="All 2 branches missed."> for (var event : events) {</span>
|
||||
<span class="nc" id="L38"> sendEventAsync(event);</span>
|
||||
<span class="nc" id="L39"> }</span>
|
||||
<span class="nc" id="L40"> }</span>
|
||||
|
||||
public void sendEventAsyncByPathResponse(
|
||||
DeliveryPathResponse resp, LocalDateTime baseTimestamp) {
|
||||
<span class="nc" id="L44"> var events = DroneEvent.fromPathResponseWithTimestamp(resp, baseTimestamp);</span>
|
||||
<span class="nc bnc" id="L45" title="All 2 branches missed."> for (var event : events) {</span>
|
||||
<span class="nc" id="L46"> sendEventAsync(event);</span>
|
||||
<span class="nc" id="L47"> }</span>
|
||||
<span class="nc" id="L48"> }</span>
|
||||
|
||||
public void sendEventAsyncByPathResponse(
|
||||
DeliveryPathResponse resp, Map<Integer, LocalDateTime> deliveryTimestamps) {
|
||||
<span class="nc" id="L52"> var events = DroneEvent.fromPathResponseWithTimestamps(resp, deliveryTimestamps);</span>
|
||||
<span class="nc bnc" id="L53" title="All 2 branches missed."> for (var event : events) {</span>
|
||||
<span class="nc" id="L54"> sendEventAsync(event);</span>
|
||||
<span class="nc" id="L55"> }</span>
|
||||
<span class="nc" id="L56"> }</span>
|
||||
|
||||
public void sendEventAsync(DroneEvent event) {
|
||||
<span class="nc" id="L59"> CompletableFuture.runAsync(</span>
|
||||
() -> {
|
||||
try {
|
||||
<span class="nc" id="L62"> String json = mapper.writeValueAsString(event);</span>
|
||||
<span class="nc" id="L63"> log.debug("Sending telemetry event: {}", json);</span>
|
||||
var request =
|
||||
<span class="nc" id="L65"> java.net.http.HttpRequest.newBuilder()</span>
|
||||
<span class="nc" id="L66"> .uri(java.net.URI.create(BLACKBOX_URL + "/ingest"))</span>
|
||||
<span class="nc" id="L67"> .header("Content-Type", "application/json")</span>
|
||||
<span class="nc" id="L68"> .POST(</span>
|
||||
<span class="nc" id="L69"> java.net.http.HttpRequest.BodyPublishers.ofString(</span>
|
||||
json))
|
||||
<span class="nc" id="L71"> .build();</span>
|
||||
|
||||
<span class="nc" id="L73"> client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());</span>
|
||||
<span class="nc" id="L74"> } catch (Exception e) {</span>
|
||||
<span class="nc" id="L75"> log.error("Failed to send telemetry event: {}", e.getMessage());</span>
|
||||
<span class="nc" id="L76"> }</span>
|
||||
<span class="nc" id="L77"> });</span>
|
||||
<span class="nc" id="L78"> }</span>
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.12.202403310830</span></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue