Skip to content

Commit

Permalink
Reverse geo components.
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Jensen committed Nov 22, 2023
1 parent 7c80106 commit 6459f8f
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 19 deletions.
4 changes: 2 additions & 2 deletions __tests__/geolocation/nominatim/NominatimClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("NominatimClient", () => {
const instance = NominatimClient.create(axios.request);
const response = await readJsonResource<NominatimResponse>(__dirname, "./nominatim-reverse-response.json");
axiosMock.onGet("./reverse").reply(200, JSON.stringify(response));
expect(await instance.getLocation([43.220, -89.765])).toBe("Mazomanie, WI");
expect(await instance.getPlace([43.220, -89.765])).toBe("Mazomanie, WI");
});
test("Throws on rate limit failure", async () => {
const axios = Axios.create({
Expand All @@ -26,7 +26,7 @@ describe("NominatimClient", () => {
const instance = NominatimClient.create(axios.request);
axiosMock.onGet("./reverse").reply(429);
try {
await instance.getLocation([43.220, -89.765]);
await instance.getPlace([43.220, -89.765]);
expect(true).toBe(false);
} catch (ex) {
if (isError(ex)) {
Expand Down
12 changes: 10 additions & 2 deletions src/geolocation/ReverseGeolocationContext.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {createContext, useContext} from "react";
import {createContext, Dispatch, useContext} from "react";
import {ReverseGeolocationAction, ReverseGeolocationState} from "./ReverseGeolocationState";

/**
* Contents of the {@link ReverseGeolocationContext}.
*/
export type ReverseGeolocationContextContents = [];
export type ReverseGeolocationContextContents = [
state: ReverseGeolocationState,
dispatch: Dispatch<ReverseGeolocationAction>
];

/**
* Context through which reverse geolocation data is exposed by a {@link ReverseGeolocationProvider} component.
Expand All @@ -20,3 +24,7 @@ export function useReverseGeolocation() {
}
return context;
}

export function useReverseGeolocationPlace() {

}
38 changes: 35 additions & 3 deletions src/geolocation/ReverseGeolocationManager.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {useCallback, useEffect} from "react";
import {useReverseGeolocation} from "./ReverseGeolocationContext";

import type {GeolocationService} from "./geolocation-types";
import {DateTime} from "luxon";
import {GeoCoordinates} from "../flight-types";

/**
* Properties for a {@link ReverseGeolocationManager} component.
Expand All @@ -11,7 +16,34 @@ interface ReverseGeolocationManagerProps {
service: GeolocationService;
}

export default function ReverseGeolocationManager(props: ReverseGeolocationManagerProps) {
const {service} = props;
export default function ReverseGeolocationManager({service}: ReverseGeolocationManagerProps) {
const [state, dispatch] = useReverseGeolocation();

const updatePlace = useCallback((coordinates: GeoCoordinates) => {
Promise.resolve()
.then(async () => {
const place = await service.getPlace(coordinates);
dispatch({
kind: "place resolved",
payload: place!
});
})
.catch(error => {
dispatch({
kind: "error occurred",
payload: {
timestamp: DateTime.utc(),
error
}
})
});
}, [dispatch, service]);

const {coordinates, place} = state;
useEffect(() => {
if (null == place) {
updatePlace(coordinates);
}
}, [coordinates, place, updatePlace]);
return null;
}
}
16 changes: 12 additions & 4 deletions src/geolocation/ReverseGeolocationProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import * as React from "react";
import {useMemo} from "react";
import {useMemo, useReducer} from "react";
import {ReverseGeolocationContext} from "./ReverseGeolocationContext";
import NominatimReverseGeolocationProvider from "./nominatim/NominatimReverseGeolocationProvider";

import type {PropsWithChildren} from "react";
import type {GeoCoordinates, Kinded} from "../flight-types";
import type {ReverseGeolocationContextContents} from "./ReverseGeolocationContext";
import type {NominatimReverseGeolocationProviderProps} from "./nominatim/NominatimReverseGeolocationProvider";
import {ReverseGeolocationState} from "./ReverseGeolocationState";
import {freeze} from "immer";

/**
* Properties for a {@link ReverseGeolocationProvider} component.
*/
export interface ReverseGeolocationProviderProps {
config:
| Kinded<NominatimReverseGeolocationProviderProps, "nominatim">;
coordinates: Record<string, GeoCoordinates>;
coordinates: GeoCoordinates;

/**
* Minimum distance, in nautical miles, which coordinates must change before a reverse geolocation update is needed.
*/
threshold: number;
}

/**
Expand All @@ -26,11 +33,12 @@ export interface ReverseGeolocationProviderProps {
*/
export default function ReverseGeolocationProvider(props: PropsWithChildren<ReverseGeolocationProviderProps>) {
const {config, children} = props;
const context = useMemo<ReverseGeolocationContextContents>(() => [], []);
const [state, dispatch] = useReducer(ReverseGeolocationState.reduce, props, ReverseGeolocationState.initial);
const contents = useMemo(() => freeze<ReverseGeolocationContextContents>([state, dispatch]), []);
switch (config.kind) {
case "nominatim":
return (
<ReverseGeolocationContext.Provider value={context}>
<ReverseGeolocationContext.Provider value={contents}>
<NominatimReverseGeolocationProvider {...config}/>
{children}
</ReverseGeolocationContext.Provider>
Expand Down
111 changes: 109 additions & 2 deletions src/geolocation/ReverseGeolocationState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,114 @@
import {immerable} from "immer";
import {freeze, immerable, produce} from "immer";
import _ from "lodash";
import {GeoCoordinates, Timestamped} from "../flight-types";
import {pointToPointCourseDistance} from "./geo-utils";

/**
* Configuration for creating an initial {@link ReverseGeolocationState}.
*/
export type ReverseGeolocationStateConfig = Omit<ReverseGeolocationState, "error" | "place" | "placeCoordinates" | typeof immerable>;

/**
* {@link ReverseGeolocationState} holds the state of a {@link ReverseGeolocationProvider} component.
*/
export class ReverseGeolocationState {
[immerable] = true;

/**
* Current coordinates.
*/
coordinates: GeoCoordinates;
error?: Timestamped<{
error: unknown;
}>;
place?: {
coordinates: GeoCoordinates;
name: string;
};
threshold: number;

private constructor(config: ReverseGeolocationStateConfig) {
this.coordinates = config.coordinates;
this.threshold = config.threshold;
}

/**
* Create an initial {@link ReverseGeolocationState}.
*
* @param config the initial property values.
*/
static initial(config: ReverseGeolocationStateConfig) {
return freeze(new ReverseGeolocationState(config));
}

/**
* Reducer for {@link ReverseGeolocationAction} against a {@link ReverseGeolocationState}.
*
* @param previous the previous state.
* @param kind the action kind.
* @param payload the action payload.
*/
static reduce(previous: ReverseGeolocationState, {kind, payload}: ReverseGeolocationAction) {
switch (kind) {
case "coordinates updated":
return produce(previous, draft => {
delete draft.error;
draft.coordinates = [...payload];
const {place, threshold} = draft;
if (null != place) {
const {distance} = pointToPointCourseDistance(place.coordinates, payload);
if (distance >= threshold) {
delete draft.place;
}
}
});
case "error occurred":
return produce(previous, draft => {
draft.error = _.cloneDeep(payload);
delete draft.place;
});
case "place resolved":
return produce(previous, draft => {
delete draft.error;
draft.place = {
coordinates: draft.coordinates,
name: payload
};
});
}
}
}

/**
* Action performed when the coordinates for which to perform reverse geolocation change.
*/
interface CoordinatesUpdated {
kind: "coordinates updated";
payload: GeoCoordinates;
}

/**
* Action performed when a Nominatim API request returns an error.
*/
interface ErrorOccurred {
kind: "error occurred";
payload: Timestamped<{
error: unknown;
}>;
}

/**
* Action performed when a reverse geolocation request resolves to a place name.
*/
interface PlaceResolved {
kind: "place resolved";
payload: string;
}

}
/**
* All actions supported by {@link ReverseGeolocationState.reduce}.
*/
export type ReverseGeolocationAction =
| CoordinatesUpdated
| ErrorOccurred
| PlaceResolved;
2 changes: 1 addition & 1 deletion src/geolocation/geo-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function pointRadialDistance(from: GeoCoordinates, course: number, distan
}

/**
* Calculate the course, return course, and distance between `from` and `to` coordinates.
* Calculate the course, return course, and distance in nautical miles between `from` and `to` coordinates.
*
* @param from the starting point.
* @param to the ending point.
Expand Down
2 changes: 1 addition & 1 deletion src/geolocation/geolocation-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ export interface GeolocationService {
*
* @param coordinates the geographic coordinates.
*/
getLocation(coordinates: GeoCoordinates): Promise<null | string>;
getPlace(coordinates: GeoCoordinates): Promise<null | string>;
}
2 changes: 1 addition & 1 deletion src/geolocation/nominatim/NominatimClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class NominatimClient {
private constructor(private readonly axios: AxiosInstance["request"]) {
}

async getLocation(coordinates: GeoCoordinates) {
async getPlace(coordinates: GeoCoordinates) {
let response: AxiosResponse<NominatimResponse>;
try {
response = await this.axios({
Expand Down
4 changes: 2 additions & 2 deletions src/geolocation/nominatim/NominatimGeolocationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export class NominatimGeolocationService implements GeolocationService {
private constructor(private readonly client: NominatimClient) {
}

getLocation(coordinates: GeoCoordinates): Promise<string | null> {
throw "TODO";
getPlace(coordinates: GeoCoordinates): Promise<string | null> {
return this.client.getPlace(coordinates);
}

static create(client: NominatimClient) {
Expand Down
2 changes: 1 addition & 1 deletion src/tracking/TrackingState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {ModeSCode, Positions} from "./tracking-types";
export type TrackingStateConfig = Omit<TrackingState, "error" | "interval" | "nextUpdate" | "positions" | "tracking" | typeof immerable>;

/**
* {@link TrackingState} holds the current state
* {@link TrackingState} holds the state of a {@link TrackingProvider} component.
*/
export class TrackingState {
[immerable] = true;
Expand Down

0 comments on commit 6459f8f

Please sign in to comment.