Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIMS-683 - Geocoder Service #2157

Merged
merged 31 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
557fc1f
initial commit
LawrenceLau2020 Jan 30, 2024
ec385d0
Updating From column for project reports to nullable because first re…
LawrenceLau2020 Jan 30, 2024
183cd3a
Merge branch 'main' into PIMS-683
LawrenceLau2020 Jan 30, 2024
6e1a0c5
refining the service
LawrenceLau2020 Jan 31, 2024
a5d0c64
PIMS-669: User & Roles Services (#2037)
GrahamS-Quartech Feb 2, 2024
59db332
PIMS-545 Transfer Keycloak Users-Roles (#2166)
dbarkowsky Feb 2, 2024
c26631e
PIMS-408 Establish Migration Options (#2164)
dbarkowsky Feb 3, 2024
0baf563
Sprint 14 Updating express-api minor NPM Dependencies (#2170)
TaylorFries Feb 6, 2024
3d3c105
Sprint 14 Updating Frontend NPM Packages (minor updates) (#2169)
TaylorFries Feb 6, 2024
4904225
PIMS-1260: React API Hooks (#2167)
GrahamS-Quartech Feb 6, 2024
5baad42
PIMS-1201 Update react-app Dependencies (#2174)
dbarkowsky Feb 6, 2024
88fb798
PIMS-668 Users Table (#2148)
dbarkowsky Feb 7, 2024
c6ce531
PIMS-1275 Update react-datepicker to 6.1.0 (frontend) (#2180)
dbarkowsky Feb 12, 2024
238744b
PIMS-1298 Update react-router-dom to 6.22.0 (frontend) (#2179)
dbarkowsky Feb 12, 2024
ff6f3e7
PIMS-1287 Updated @types/supertest to 6.0.2 (express-api) (#2178)
dbarkowsky Feb 12, 2024
43796c8
PIMS-1236 Updated prettier to 3.2.4 (express-api) (#2177)
dbarkowsky Feb 12, 2024
dd1f409
PIMS-407: Seed Data in Postgres (#2165)
Sharala-Perumal Feb 12, 2024
e940027
PIMS-1311 Updating Role Mapper Names (#2181)
dbarkowsky Feb 12, 2024
2de3d3e
Express api Github action workflow (#2175)
ManishSihag Feb 12, 2024
a1cfb81
Merge branch 'main' into PIMS-683
TaylorFries Feb 13, 2024
a399e79
update to getSiteAddressesAsync in geocoderService
TaylorFries Feb 13, 2024
e3ecfad
adding getPids function to geocoderService
TaylorFries Feb 15, 2024
1841510
linting fix
TaylorFries Feb 15, 2024
3b12402
removing dev comment
TaylorFries Feb 15, 2024
fabc1c1
removing error catch wrap from getSiteAddressesAsync in geocoderService
TaylorFries Feb 15, 2024
b8c4eaf
adding tests for geocoderService
TaylorFries Feb 16, 2024
d193057
finalizing testing for geocoderService
TaylorFries Feb 16, 2024
b85362c
linting fix
TaylorFries Feb 16, 2024
2f80f0c
Merge branch 'main' into PIMS-683
TaylorFries Feb 16, 2024
994407e
update env template
dbarkowsky Feb 16, 2024
c87a5db
Remove commented line from geocoderService
TaylorFries Feb 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env-template
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ POSTGRES_DB=
POSTGRES_PORT=
POSTGRES_SERVICE= # only when in a container

# For Express API Development
# For Express API Development & Express Keycloak

FRONTEND_URL=
BACKEND_URL=
Expand All @@ -23,3 +23,10 @@ BACKEND_URL=
SSO_CLIENT_ID= # Keycloak client_id
SSO_CLIENT_SECRET= # Keycloak client_secret
SSO_AUTH_SERVER_URL= # Keycloak auth URL, see example below.

# CSS Keycloak API
CSS_API_CLIENT_ID= # Keycloak CSS API Service Account client_id
CSS_API_CLIENT_SECRET= # Keycloak CSS API Service Account client_secret

# BC Geocoder
GEOCODER_KEY=
4 changes: 2 additions & 2 deletions express-api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Base #
#############################################
# Use an official Node.js runtime as a base image
FROM node:18.17.1-bullseye-slim as base
FROM node:21.6-bullseye-slim as base

# Set the working directory in the container
WORKDIR /express-api
Expand All @@ -25,7 +25,7 @@ RUN npm run build
#############################################
# Prod Build #
#############################################
FROM node:18.17.1-bullseye-slim as Prod
FROM node:21.6-bullseye-slim as Prod

# Set the working directory to /express-api
WORKDIR /express-api
Expand Down
2 changes: 2 additions & 0 deletions express-api/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import networking from '@/constants/networking';
import switches from '@/constants/switches';
import urls from '@/constants/urls';

const constants = {
...networking,
...switches,
...urls,
};
export default constants;
6 changes: 6 additions & 0 deletions express-api/src/constants/urls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const urls = {
GEOCODER: {
HOSTURI: 'https://geocoder.api.gov.bc.ca',
},
};
export default urls;
75 changes: 75 additions & 0 deletions express-api/src/services/geocoder/geocoderService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { IAddressModel } from '@/services/geocoder/interfaces/IAddressModel';
import { IFeatureCollectionModel } from '@/services/geocoder/interfaces/IFeatureCollectionModel';
import constants from '@/constants';
import { IFeatureModel } from '@/services/geocoder/interfaces/IFeatureModel';
import { getLongitude, getLatitude, getAddress1 } from './geocoderUtils';
import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
import { ISitePidsResponseModel } from './interfaces/ISitePidsResponseModel';

const mapFeatureToAddress = (feature: IFeatureModel): IAddressModel => {
return {
siteId: feature.properties.siteID,
fullAddress: feature.properties.fullAddress,
address1: getAddress1(feature.properties),
administrativeArea: feature.properties.localityName,
provinceCode: feature.properties.provinceCode,
longitude: getLongitude(feature.geometry),
latitude: getLatitude(feature.geometry),
score: feature.properties.score,
};
};

/**
* @description Sends a request to Geocoder for addresses that match the specified 'address'.
* @param address String of searchable address. eg. "4000 Seymour St Victoria BC"
* @returns address information matching IAddressModel format
* @throws ErrorWithCode if the response is not 200 OK
*/
export const getSiteAddresses = async (address: string) => {
const url = new URL('/addresses.json', constants.GEOCODER.HOSTURI);
url.searchParams.append('addressString', address);

const response = await fetch(url.toString(), {
headers: {
apiKey: process.env.GEOCODER__KEY,
},
});

if (!response.ok) {
throw new ErrorWithCode('Failed to fetch data', response.status);
}

const responseData = await response.json();
const featureCollection: IFeatureCollectionModel = responseData;
const addressInformation: IAddressModel = mapFeatureToAddress(featureCollection.features[0]);

return addressInformation;
};

/**
* @description Sends a request to Geocoder for all parcel identifiers (PIDs) associated with an individual site.
* @param siteId a unique identifier assigned to every site in B.C.
* @returns Valid 'siteId' values for an address
* @throws ErrorWithCode if result is not 200 OK
*/
export const getPids = async (siteId: string) => {
const url = new URL(`/parcels/pids/${siteId}.json`, constants.GEOCODER.HOSTURI);
const result = await fetch(url.toString(), {
headers: {
apiKey: process.env.GEOCODER_KEY,
},
});

if (result.status != 200) {
throw new ErrorWithCode(result.statusText, result.status);
}

const resultData = await result.json();
const pidInformation: ISitePidsResponseModel = resultData;
return pidInformation;
};

export const GeocoderService = {
getSiteAddresses,
getPids,
};
77 changes: 77 additions & 0 deletions express-api/src/services/geocoder/geocoderUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* geocoderUtils.ts - Utility functions for working with geocoding data
*/

import { IPropertyModel } from '@/services/geocoder/interfaces/IPropertyModel';
import { IGeometryModel } from '@/services/geocoder/interfaces/IGeometryModel';
import { IFeatureModel } from '@/services/geocoder/interfaces/IFeatureModel';
import { IAddressModel } from '@/services/geocoder/interfaces/IAddressModel';

/**
* Constructs address string based on property data.
* @param properties - The property data.
* @returns The constructed address string.
*/
export const getAddress1 = (properties: IPropertyModel): string => {
const address = [];

if (properties.civicNumber) address.push(properties.civicNumber);

if (properties.isStreetTypePrefix && properties.streetType) address.push(properties.streetType);

if (properties.isStreetDirectionPrefix && properties.streetDirection)
address.push(properties.streetDirection);

if (properties.streetName) address.push(properties.streetName);

if (properties.streetQualifier) address.push(properties.streetQualifier);

if (!properties.isStreetDirectionPrefix && properties.streetDirection)
address.push(properties.streetDirection);

if (!properties.isStreetTypePrefix && properties.streetType) address.push(properties.streetType);

return address.join(' ');
};

/**
* Retrieves latitude from geometry data.
* @param geometry - The geometry data.
* @returns The latitude value.
*/
export const getLatitude = (geometry: IGeometryModel): number => {
if (geometry.coordinates && geometry.coordinates.length === 2) {
return geometry.coordinates[1];
}
return 0;
};

/**
* Retrieves longitude from geometry data.
* @param geometry - The geometry data.
* @returns The longitude value.
*/
export const getLongitude = (geometry: IGeometryModel): number => {
if (geometry.coordinates && geometry.coordinates.length === 2) {
return geometry.coordinates[0];
}
return 0;
};

/**
* Maps feature data to address data.
* @param feature - The feature data.
* @returns The mapped address data.
*/
export const mapFeatureToAddress = (feature: IFeatureModel): IAddressModel => {
return {
siteId: feature.properties.siteID,
fullAddress: feature.properties.fullAddress,
address1: getAddress1(feature.properties),
administrativeArea: feature.properties.localityName,
provinceCode: feature.properties.provinceCode,
longitude: getLongitude(feature.geometry),
latitude: getLatitude(feature.geometry),
score: feature.properties.score,
};
};
10 changes: 10 additions & 0 deletions express-api/src/services/geocoder/interfaces/IAddressModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface IAddressModel {
siteId: string;
fullAddress: string;
address1: string;
administrativeArea: string;
provinceCode: string;
latitude: number;
longitude: number;
score: number;
}
4 changes: 4 additions & 0 deletions express-api/src/services/geocoder/interfaces/ICrsModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ICrsModel {
type: string;
properties: { [key: string]: unknown };
}
5 changes: 5 additions & 0 deletions express-api/src/services/geocoder/interfaces/IFaultModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IFaultModel {
element: string;
fault: string;
penalty: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ICrsModel } from '@/services/geocoder/interfaces/ICrsModel';
import { IFeatureModel } from '@/services/geocoder/interfaces/IFeatureModel';

export interface IFeatureCollectionModel {
type: string;
queryAddress: string;
searchTimestamp: string;
executionTime: number;
version: string;
baseDataDate: string;
crs: ICrsModel;
interpolation: string;
echo: string;
locationDescripture: string;
setback: number;
minScore: number;
maxResults: number;
disclaimer: string;
privacyStatement: string;
copyrightNotice: string;
copyrightLicense: string;
features: IFeatureModel[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IGeometryModel } from '@/services/geocoder/interfaces/IGeometryModel';
import { IPropertyModel } from '@/services/geocoder/interfaces/IPropertyModel';

export interface IFeatureModel {
type: string;
geometry: IGeometryModel;
properties: IPropertyModel;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ICrsModel } from '@/services/geocoder/interfaces/ICrsModel';

export interface IGeometryModel {
type: string;
crs: ICrsModel;
coordinates: number[];
}
35 changes: 35 additions & 0 deletions express-api/src/services/geocoder/interfaces/IPropertyModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { IFaultModel } from '@/services/geocoder/interfaces/IFaultModel';

export interface IPropertyModel {
fullAddress: string;
score: number;
matchPrecision: string;
precisionPoints: number;
faults: IFaultModel[];
siteName: string;
unitDesignator: string;
unitNumber: string;
unitNumberSuffix: string;
civicNumber: string;
civicNumberSuffix: string;
streetName: string;
streetType: string;
isStreetTypePrefix: boolean;
streetDirection: string;
isStreetDirectionPrefix: boolean;
streetQualifier: string;
localityName: string;
localityType: string;
electoralArea: string;
provinceCode: string;
locationPositionalAccuracy: string;
locationDescriptor: string;
siteID: string;
blockID: string;
fullSiteDescriptor: string;
accessNotes: string;
siteStatus: string;
siteRetireDate: string;
changeDate: string;
isOfficial: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ISitePidsResponseModel {
siteID: string;
pids: string;
}
Loading
Loading