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

Merge release/mapping branch #270

Merged
merged 10 commits into from
Feb 26, 2025
1 change: 1 addition & 0 deletions .github/environments/values.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ config:
SERVER_OBJECTSTORAGE_KEY: dev
SERVER_OIDC_AUTHORITY: https://dev.loginproxy.gov.bc.ca/auth/realms/standard
SERVER_OIDC_PUBLICKEY: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuy7zfh2ZgpDV5mH/aXyLDTddZK81rGakJcTy4KvCNOkDDxt1KAhW02lmbCo8YhHCOzjNZBp1+Vi6QiMRgBqAe2GTPZYEiV70aXfROGZe3Nvwcjbtki6HoyRte3SpqLJEIPL2F+hjJkw1UPGnjPTWZkEx9p74b9i3BjuE8RnjJ0Sza2MWw83zoQUZEJRGiopSL0yuVej6t2LO2btVdVf7QuZfPt9ehkcQYlPKpVvJA+pfeqPAdnNt7OjEIeYxinjurZr8Z04hz8UhkRefcWlSbFzFQYmL7O7iArjW0bsSvq8yNUd5r0KCOQkFduwZy26yTzTxj8OLFT91fEmbBBl4rQIDAQAB
SERVER_OPENMAPS_APIPATH: https://openmaps.gov.bc.ca
SERVER_PORT: "8080"
SERVER_SSO_APIPATH: https://api.loginproxy.gov.bc.ca/api/v1
SERVER_SSO_TOKENURL: https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token
Expand Down
1 change: 1 addition & 0 deletions .github/environments/values.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ config:
SERVER_OBJECTSTORAGE_KEY: shas
SERVER_OIDC_AUTHORITY: https://loginproxy.gov.bc.ca/auth/realms/standard
SERVER_OIDC_PUBLICKEY: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmHiuPKOkpkq4GXN1ktr23rJtDl6Vdu/Y37ZAd3PnQ8/IDfAODvy1Y81aAUZicKe9egolv+OTRANN3yOg+TAbRhkeXLE5p/473EK0aQ0NazTCuWo6Am3oDQ7Yt8x0pw56/qcLtkTuXNyo5EnVV2Z2BzCnnaL31JOhyitolku0DNT6GDoRBmT4o2ItqEVHk5nM25cf1t2zbwI2790W6if1B2qVRkxxivS8tbH7nYC61Is3XCPockKptkH22cm2ZQJmtYd5sZKuXaGsvtyzHmn8/l0Kd1xnHmUu4JNuQ67YiNZGu3hOkrF0Js3BzAk1Qm4kvYRaxbJFCs/qokLZ4Z0W9wIDAQAB
SERVER_OPENMAPS_APIPATH: https://openmaps.gov.bc.ca
SERVER_PORT: "8080"
SERVER_SSO_APIPATH: https://api.loginproxy.gov.bc.ca/api/v1
SERVER_SSO_TOKENURL: https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token
Expand Down
1 change: 1 addition & 0 deletions .github/environments/values.test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ config:
SERVER_OBJECTSTORAGE_KEY: shas
SERVER_OIDC_AUTHORITY: https://test.loginproxy.gov.bc.ca/auth/realms/standard
SERVER_OIDC_PUBLICKEY: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiFdv9GA83uHuy8Eu9yiZHGGF9j6J8t7FkbcpaN81GDjwbjsIJ0OJO9dKRAx6BAtTC4ubJTBJMPvQER5ikOhIeBi4o25fg61jpgsU6oRZHkCXc9gX6mrjMjbsPaf3/bjjYxP5jicBDJQeD1oRa24+tiGggoQ7k6gDEN+cRYqqNpzC/GQbkUPk8YsgroncEgu8ChMh/3ERsLV2zorchMANUq76max16mHrhtWIQxrb/STpSt4JuSlUzzBV/dcXjJe5gywZHe0jAutFhNqjHzHdgyaC4RAd3eYQo+Kl/JOgy2AZrnx+CiPmvOJKe9tAW4k4H087ng8aVE40v4HW/FEbnwIDAQAB
SERVER_OPENMAPS_APIPATH: https://openmaps.gov.bc.ca
SERVER_PORT: "8080"
SERVER_SSO_APIPATH: https://api.loginproxy.gov.bc.ca/api/v1
SERVER_SSO_TOKENURL: https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token
Expand Down
3 changes: 3 additions & 0 deletions app/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@
"clientSecret": "SERVER_OIDC_CLIENTSECRET",
"publicKey": "SERVER_OIDC_PUBLICKEY"
},
"openMaps": {
"apiPath": "SERVER_OPENMAPS_APIPATH"
},
"port": "SERVER_PORT",
"sso": {
"apiPath": "SERVER_SSO_APIPATH",
Expand Down
26 changes: 26 additions & 0 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
},
"dependencies": {
"@prisma/client": "^6.0.1",
"@types/proj4": "^2.5.5",
"api-problem": "^9.0.2",
"axios": "^1.7.9",
"compression": "^1.7.5",
Expand All @@ -67,6 +68,7 @@
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"pg": "^8.13.1",
"proj4": "^2.15.0",
"ts-node": "^10.9.2",
"uuid": "^11.0.3",
"winston": "^3.17.0",
Expand Down
1 change: 1 addition & 0 deletions app/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as atsController } from './ats';
export { default as contactController } from './contact';
export { default as documentController } from './document';
export { default as enquiryController } from './enquiry';
export { default as mapController } from './map';
export { default as noteController } from './note';
export { default as permitController } from './permit';
export { default as permitNoteController } from './permitNote';
Expand Down
19 changes: 19 additions & 0 deletions app/src/controllers/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { mapService, submissionService } from '../services';
import type { NextFunction, Request, Response } from 'express';

const controller = {
getPIDs: async (req: Request<{ submissionId: string }>, res: Response, next: NextFunction) => {
try {
const submission = await submissionService.getSubmission(req.params.submissionId);

let response;
if (submission?.geoJSON) response = await mapService.getPIDs(submission?.geoJSON);

res.status(200).json(response);
} catch (e: unknown) {
next(e);
}
}
};

export default controller;
1 change: 1 addition & 0 deletions app/src/controllers/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const controller = {
naturalDisaster: data.location.naturalDisaster,
projectLocation: data.location.projectLocation,
projectLocationDescription: data.location.projectLocationDescription,
geoJSON: data.location.geoJSON,
locationPIDs: data.location.ltsaPIDLookup,
latitude: data.location.latitude,
longitude: data.location.longitude,
Expand Down
18 changes: 18 additions & 0 deletions app/src/db/migrations/20250219000000_026-advanced-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable max-len */
import type { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
return Promise.resolve().then(() =>
knex.schema.alterTable('submission', function (table) {
table.json('geo_json');
})
);
}

export async function down(knex: Knex): Promise<void> {
return Promise.resolve().then(() =>
knex.schema.alterTable('submission', function (table) {
table.dropColumn('geo_json');
})
);
}
2 changes: 2 additions & 0 deletions app/src/db/models/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default {
consent_to_feedback: input.consentToFeedback,
location_pids: input.locationPIDs,
company_name_registered: input.companyNameRegistered,
geo_json: input.geoJSON,
single_family_units: input.singleFamilyUnits,
has_rental_units: input.hasRentalUnits,
street_address: input.streetAddress,
Expand Down Expand Up @@ -110,6 +111,7 @@ export default {
projectName: input.project_name,
projectDescription: input.project_description,
companyNameRegistered: input.company_name_registered,
geoJSON: input.geo_json,
singleFamilyUnits: input.single_family_units,
hasRentalUnits: input.has_rental_units,
streetAddress: input.street_address,
Expand Down
1 change: 1 addition & 0 deletions app/src/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ model submission {
housing_coop_description String?
submission_type String?
consent_to_feedback Boolean @default(false)
geo_json Json? @db.Json
activity activity @relation(fields: [activity_id], references: [activity_id], onDelete: Cascade, map: "submission_activity_id_foreign")
user user? @relation(fields: [assigned_user_id], references: [user_id], onDelete: Cascade, map: "submission_assigned_user_id_foreign")

Expand Down
3 changes: 3 additions & 0 deletions app/src/routes/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import contact from './contact';
import docs from './docs';
import document from './document';
import enquiry from './enquiry';
import map from './map';
import note from './note';
import permit from './permit';
import reporting from './reporting';
Expand All @@ -32,6 +33,7 @@ router.get('/', (_req, res) => {
'/docs',
'/document',
'/enquiry',
'/map',
'/note',
'/permit',
'/reporting',
Expand All @@ -50,6 +52,7 @@ router.use('/ats', ats);
router.use('/contact', contact);
router.use('/document', document);
router.use('/enquiry', enquiry);
router.use('/map', map);
router.use('/note', note);
router.use('/permit', permit);
router.use('/reporting', reporting);
Expand Down
23 changes: 23 additions & 0 deletions app/src/routes/v1/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import express from 'express';

import { mapController } from '../../controllers';
import { hasAuthorization } from '../../middleware/authorization';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';
import { requireSomeGroup } from '../../middleware/requireSomeGroup';
import { Action, Resource } from '../../utils/enums/application';

import type { NextFunction, Request, Response } from 'express';

const router = express.Router();
router.use(requireSomeAuth);
router.use(requireSomeGroup);

router.get(
'/pids/:submissionId',
hasAuthorization(Resource.SUBMISSION, Action.READ),
(req: Request<{ submissionId: string }>, res: Response, next: NextFunction): void => {
mapController.getPIDs(req, res, next);
}
);

export default router;
1 change: 1 addition & 0 deletions app/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { default as draftService } from './draft';
export { default as emailService } from './email';
export { default as enquiryService } from './enquiry';
export { default as initiativeService } from './initiative';
export { default as mapService } from './map';
export { default as noteService } from './note';
export { default as permitService } from './permit';
export { default as permitNoteService } from './permitNote';
Expand Down
94 changes: 94 additions & 0 deletions app/src/services/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import axios from 'axios';
import config from 'config';
import proj4 from 'proj4';

import type { AxiosInstance, AxiosRequestConfig } from 'axios';

/**
* @function openMapsAxios
* Returns an Axios instance for the CHEFS API
* @param {AxiosRequestConfig} options Axios request config options
* @returns {AxiosInstance} An axios instance
*/
function openMapsAxios(options: AxiosRequestConfig = {}): AxiosInstance {
return axios.create({
baseURL: config.get('server.openMaps.apiPath'),
timeout: 10000,
...options
});
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getPolygonArray(geoJSON: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const polygonArray = geoJSON?.geometry?.coordinates[0]?.map((c: any) => {
return { lat: c[1], lng: c[0] };
});
return polygonArray;
}

const service = {
/**
* @function getParcelDataFromPMBC
* DataBC’s Open Web Services
* Accessing geographic data via WMS/WFS
* Services Provided by OCIO - Digital Platforms & Data - Data Systems & Services
* ref: https://docs.geoserver.org/main/en/user/services/wfs/reference.html#getfeature
* ref: https://catalogue.data.gov.bc.ca/dataset/parcelmap-bc-parcel-fabric
* @returns parcel data in JSON
*/

// eslint-disable-next-line @typescript-eslint/no-explicit-any
getPIDs: async (geoJSON: any) => {
const polygon = getPolygonArray(geoJSON);

// close polygon by re-adding first point to end of array
// define the source and destination layer types
// leaflet map layer
const source = proj4.Proj('EPSG:4326'); // gps format of leaflet map

// projection (BC Parcel data layer)
proj4.defs(
'EPSG:3005',
// eslint-disable-next-line max-len
'PROJCS["NAD83 / BC Albers", GEOGCS["NAD83", DATUM["North_American_Datum_1983", SPHEROID["GRS 1980",6378137,298.257222101, AUTHORITY["EPSG","7019"]], TOWGS84[0,0,0,0,0,0,0], AUTHORITY["EPSG","6269"]], PRIMEM["Greenwich",0, AUTHORITY["EPSG","8901"]], UNIT["degree",0.0174532925199433, AUTHORITY["EPSG","9122"]], AUTHORITY["EPSG","4269"]], PROJECTION["Albers_Conic_Equal_Area"], PARAMETER["standard_parallel_1",50], PARAMETER["standard_parallel_2",58.5], PARAMETER["latitude_of_center",45], PARAMETER["longitude_of_center",-126], PARAMETER["false_easting",1000000], PARAMETER["false_northing",0], UNIT["metre",1, AUTHORITY["EPSG","9001"]], AXIS["Easting",EAST], AXIS["Northing",NORTH], AUTHORITY["EPSG","3005"]]'
);

const dest = proj4.Proj('EPSG:3005');

// convert lat/long for WFS query
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = polygon.map((point: any) => {
//@ts-expect-error insufficient type definitions
return proj4(source, dest, { x: point.lng, y: point.lat });
});

// build query string for WFS request
let query = '';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result.forEach((point: any, index: any, array: any) => {
query = query.concat(point.x, ' ', point.y);
if (index < array.length - 1) query = query.concat(', ');
});

let params =
// eslint-disable-next-line max-len
'/geo/pub/wfs?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&outputFormat=json&typeName=WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW&CQL_FILTER=INTERSECTS(SHAPE, POLYGON ((query)))';

params = params.replace('query', query);

const response = await openMapsAxios().get(params);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parcelData = response.data.features?.map((f: any) => f.properties);

const PIDs = parcelData
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((p: any) => p.PID_FORMATTED)
.filter((pid: string) => pid && pid.trim().length > 0)
.join(', ');

return PIDs;
}
};

export default service;
Loading
Loading