# GpsCalculationService.java

    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
    public class GpsCalculationService {

        /**
         * 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) {
            double lngDistance = position2.lng() - position1.lng();
            double latDistance = position2.lat() - position1.lat();
            // Euclidean: \sqrt{a^2 + b^2}
            return Math.sqrt(lngDistance * lngDistance + latDistance * latDistance);
        }

        public double calculateSteps(LngLat position1, LngLat position2) {
            double distance = calculateDistance(position1, position2);
            return distance / STEP;
        }

        /**
         * 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) {
            double distance = calculateDistance(position1, position2);
            return distance < CLOSE_THRESHOLD;
        }

        /**
         * 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) {
            double rad = angle.toRadians();
            double newLng = Math.cos(rad) * STEP + start.lng();
            double newLat = Math.sin(rad) * STEP + start.lat();
            return new LngLat(newLng, newLat);
        }

        /**
         * 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 {
            if (!region.isClosed()) {
                // call method from RegionDto to check if not closed
                throw new IllegalArgumentException("Region is not closed.");
            }
            return rayCasting(position, region.vertices());
        }

        /**
         * 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) {
            int intersections = 0;
            int n = polygon.size();
            for (int i = 0; i < n; ++i) {
                LngLat a = polygon.get(i);
                LngLat b = polygon.get((i + 1) % n); // Next vertex

                if (isPointOnEdge(point, a, b)) {
                    return true;
                }

                // Ensure that `a` is norther than `b`, in order to easy classification
                if (a.lat() > b.lat()) {
                    LngLat temp = a;
                    a = b;
                    b = temp;
                }

                // The point is not between a and b in latitude mean, skip this loop
                if (point.lat() < a.lat() || point.lat() >= b.lat()) {
                    continue;
                }

                // Skip the case of horizontal edge, already handled in `isPointOnEdge`:w
                if (a.lat() == b.lat()) {
                    continue;
                }

                double xIntersection =
                        a.lng() + ((point.lat() - a.lat()) * (b.lng() - a.lng())) / (b.lat() - a.lat());

                if (xIntersection > point.lng()) {
                    ++intersections;
                }
            }
            // 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.
            return intersections % 2 == 1;
        }

        /**
         * 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)
            double crossProduct =
                    (p.lng() - a.lng()) * (b.lat() - a.lat())
                            - (p.lat() - a.lat()) * (b.lng() - a.lng());
            if (Math.abs(crossProduct) > 1e-9) {
                return false;
            }

            boolean isWithinLng =
                    p.lng() >= Math.min(a.lng(), b.lng()) && p.lng() <= Math.max(a.lng(), b.lng());
            boolean isWithinLat =
                    p.lat() >= Math.min(a.lat(), b.lat()) && p.lat() <= Math.max(a.lat(), b.lat());

            return isWithinLng && isWithinLat;
        }
    }