From 352e79c8e36acf4cba0cf541276ee422f3f3670d Mon Sep 17 00:00:00 2001 From: Sanjay Babu Date: Wed, 27 Nov 2024 14:15:12 -0800 Subject: [PATCH] Advanced Map - added pin or draw, migration added for adding geoJSON, model & types updated, externalAPIService updated --- .github/environments/values.dev.yaml | 1 + .github/environments/values.prod.yaml | 1 + .github/environments/values.test.yaml | 1 + app/app.ts | 12 +- app/config/custom-environment-variables.json | 3 + app/src/controllers/submission.ts | 1 + .../20241127000000_016-advanced-map.ts | 19 +++ app/src/db/models/submission.ts | 3 + app/src/db/prisma/schema.prisma | 1 + app/src/services/submission.ts | 6 +- app/src/types/Submission.ts | 2 + app/src/types/SubmissionIntake.ts | 1 + charts/pcns/values.yaml | 1 + frontend/src/components/housing/maps/Map.vue | 112 +++++++++++++++++- .../submission/SubmissionIntakeForm.vue | 90 ++++++++++++++ .../submission/SubmissionIntakeSchema.ts | 3 +- frontend/src/services/externalApiService.ts | 60 +++++++++- frontend/src/services/interceptors.ts | 25 ++++ frontend/src/types/Submission.ts | 5 + frontend/src/utils/constants/housing.ts | 6 +- frontend/src/utils/enums/housing.ts | 3 +- 21 files changed, 345 insertions(+), 11 deletions(-) create mode 100644 app/src/db/migrations/20241127000000_016-advanced-map.ts diff --git a/.github/environments/values.dev.yaml b/.github/environments/values.dev.yaml index 4c513e90..e098d28f 100644 --- a/.github/environments/values.dev.yaml +++ b/.github/environments/values.dev.yaml @@ -10,6 +10,7 @@ config: FRONTEND_GEOCODER_APIPATH: https://geocoder.api.gov.bc.ca FRONTEND_OIDC_AUTHORITY: https://dev.loginproxy.gov.bc.ca/auth/realms/standard FRONTEND_OIDC_CLIENTID: nr-permit-connect-navigator-service-5188 + FRONTEND_OPENMAPS_APIPATH: https://openmaps.gov.bc.ca FRONTEND_OPENSTREETMAP_APIPATH: https://tile.openstreetmap.org FRONTEND_ORGBOOK_APIPATH: https://orgbook.gov.bc.ca/api/v4 SERVER_APIPATH: /api/v1 diff --git a/.github/environments/values.prod.yaml b/.github/environments/values.prod.yaml index 5378a424..aa312cb1 100644 --- a/.github/environments/values.prod.yaml +++ b/.github/environments/values.prod.yaml @@ -10,6 +10,7 @@ config: FRONTEND_GEOCODER_APIPATH: https://geocoder.api.gov.bc.ca FRONTEND_OIDC_AUTHORITY: https://loginproxy.gov.bc.ca/auth/realms/standard FRONTEND_OIDC_CLIENTID: nr-permit-connect-navigator-service-5188 + FRONTEND_OPENMAPS_APIPATH: https://openmaps.gov.bc.ca FRONTEND_OPENSTREETMAP_APIPATH: https://tile.openstreetmap.org FRONTEND_ORGBOOK_APIPATH: https://orgbook.gov.bc.ca/api/v4 SERVER_APIPATH: /api/v1 diff --git a/.github/environments/values.test.yaml b/.github/environments/values.test.yaml index 229a040f..03eacace 100644 --- a/.github/environments/values.test.yaml +++ b/.github/environments/values.test.yaml @@ -10,6 +10,7 @@ config: FRONTEND_GEOCODER_APIPATH: https://geocoder.api.gov.bc.ca FRONTEND_OIDC_AUTHORITY: https://test.loginproxy.gov.bc.ca/auth/realms/standard FRONTEND_OIDC_CLIENTID: nr-permit-connect-navigator-service-5188 + FRONTEND_OPENMAPS_APIPATH: https://openmaps.gov.bc.ca FRONTEND_OPENSTREETMAP_APIPATH: https://tile.openstreetmap.org FRONTEND_ORGBOOK_APIPATH: https://orgbook.gov.bc.ca/api/v4 SERVER_APIPATH: /api/v1 diff --git a/app/app.ts b/app/app.ts index 0915f451..0f7426c9 100644 --- a/app/app.ts +++ b/app/app.ts @@ -39,14 +39,20 @@ app.use( new URL(config.get('frontend.oidc.authority')).origin, new URL(config.get('frontend.coms.apiPath')).origin, new URL(config.get('frontend.geocoder.apiPath')).origin, - new URL(config.get('frontend.orgbook.apiPath')).origin + new URL(config.get('frontend.orgbook.apiPath')).origin, + new URL(config.get('frontend.openMaps.apiPath')).origin ], - 'img-src': ["'self'", 'data:', new URL(config.get('frontend.openStreetMap.apiPath')).origin] // eslint-disable-line + + 'img-src': [ + "'self'", // eslint-disable-line + 'data:', + new URL(config.get('frontend.openStreetMap.apiPath')).origin, + new URL(config.get('frontend.openMaps.apiPath')).origin + ] } } }) ); - // Skip if running tests if (process.env.NODE_ENV !== 'test') { app.use(httpLogger); diff --git a/app/config/custom-environment-variables.json b/app/config/custom-environment-variables.json index 17d964ea..fa5d3032 100644 --- a/app/config/custom-environment-variables.json +++ b/app/config/custom-environment-variables.json @@ -21,6 +21,9 @@ "authority": "FRONTEND_OIDC_AUTHORITY", "clientId": "FRONTEND_OIDC_CLIENTID" }, + "openMaps": { + "apiPath": "FRONTEND_OPENMAPS_APIPATH" + }, "openStreetMap": { "apiPath": "FRONTEND_OPENSTREETMAP_APIPATH" }, diff --git a/app/src/controllers/submission.ts b/app/src/controllers/submission.ts index f9eb5812..d98ccce7 100644 --- a/app/src/controllers/submission.ts +++ b/app/src/controllers/submission.ts @@ -274,6 +274,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, diff --git a/app/src/db/migrations/20241127000000_016-advanced-map.ts b/app/src/db/migrations/20241127000000_016-advanced-map.ts new file mode 100644 index 00000000..9ece70d9 --- /dev/null +++ b/app/src/db/migrations/20241127000000_016-advanced-map.ts @@ -0,0 +1,19 @@ +/* eslint-disable max-len */ +import type { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return Promise.resolve().then(() => + knex.schema.alterTable('submission', function (table) { + table.json('geo_json'); + }) + ); +} + +export async function down(knex: Knex): Promise { + return Promise.resolve() // Drop public schema tables + .then(() => + knex.schema.alterTable('submission', function (table) { + table.dropColumn('geo_json'); + }) + ); +} diff --git a/app/src/db/models/submission.ts b/app/src/db/models/submission.ts index ce6564e6..3b48d0a4 100644 --- a/app/src/db/models/submission.ts +++ b/app/src/db/models/submission.ts @@ -56,6 +56,8 @@ export default { consent_to_feedback: input.consentToFeedback, location_pids: input.locationPIDs, company_name_registered: input.companyNameRegistered, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + geo_json: input.geoJSON as any, single_family_units: input.singleFamilyUnits, has_rental_units: input.hasRentalUnits, street_address: input.streetAddress, @@ -110,6 +112,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, diff --git a/app/src/db/prisma/schema.prisma b/app/src/db/prisma/schema.prisma index 3f7a8d88..1df9b7e1 100644 --- a/app/src/db/prisma/schema.prisma +++ b/app/src/db/prisma/schema.prisma @@ -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") diff --git a/app/src/services/submission.ts b/app/src/services/submission.ts index 34fbe3f5..c690d797 100644 --- a/app/src/services/submission.ts +++ b/app/src/services/submission.ts @@ -35,6 +35,7 @@ const service = { */ createSubmission: async (data: Partial) => { const response = await prisma.submission.create({ + //@ts-expect-error please help data: { ...submission.toPrismaModel(data as Submission), created_at: data.createdAt, created_by: data.createdBy }, include: { activity: { @@ -48,7 +49,7 @@ const service = { } } }); - + //@ts-expect-error please help return submission.fromPrismaModelWithContact(response); }, @@ -352,6 +353,7 @@ const service = { updateSubmission: async (data: Submission) => { try { const result = await prisma.submission.update({ + //@ts-expect-error please help data: { ...submission.toPrismaModel(data), updated_at: data.updatedAt, updated_by: data.updatedBy }, where: { submission_id: data.submissionId @@ -368,7 +370,7 @@ const service = { } } }); - + //@ts-expect-error please help return submission.fromPrismaModelWithContact(result); } catch (e: unknown) { throw e; diff --git a/app/src/types/Submission.ts b/app/src/types/Submission.ts index 49725c25..f40b23bb 100644 --- a/app/src/types/Submission.ts +++ b/app/src/types/Submission.ts @@ -12,6 +12,8 @@ export type Submission = { locationPIDs: string | null; companyNameRegistered: string | null; consentToFeedback: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + geoJSON: any; projectName: string | null; projectDescription: string | null; singleFamilyUnits: string | null; diff --git a/app/src/types/SubmissionIntake.ts b/app/src/types/SubmissionIntake.ts index 75d3d6c6..4b2cffd2 100644 --- a/app/src/types/SubmissionIntake.ts +++ b/app/src/types/SubmissionIntake.ts @@ -38,6 +38,7 @@ export type SubmissionIntake = { naturalDisaster?: string; projectLocation?: string; projectLocationDescription?: string; + geoJSON?: JSON; ltsaPIDLookup?: string; latitude?: number | null; longitude?: number | null; diff --git a/charts/pcns/values.yaml b/charts/pcns/values.yaml index 054dd129..133106be 100644 --- a/charts/pcns/values.yaml +++ b/charts/pcns/values.yaml @@ -139,6 +139,7 @@ config: FRONTEND_GEOCODER_APIPATH: ~ FRONTEND_OIDC_AUTHORITY: ~ FRONTEND_OIDC_CLIENTID: ~ + FRONTEND_OPENMAPS_APIPATH: ~ FRONTEND_OPENSTREETMAP_APIPATH: ~ FRONTEND_ORGBOOK_APIPATH: ~ diff --git a/frontend/src/components/housing/maps/Map.vue b/frontend/src/components/housing/maps/Map.vue index 44422d6b..13958ea2 100644 --- a/frontend/src/components/housing/maps/Map.vue +++ b/frontend/src/components/housing/maps/Map.vue @@ -1,10 +1,13 @@ diff --git a/frontend/src/components/housing/submission/SubmissionIntakeSchema.ts b/frontend/src/components/housing/submission/SubmissionIntakeSchema.ts index 27248e5c..4620fd0d 100644 --- a/frontend/src/components/housing/submission/SubmissionIntakeSchema.ts +++ b/frontend/src/components/housing/submission/SubmissionIntakeSchema.ts @@ -142,7 +142,8 @@ export const submissionIntakeSchema = object({ otherwise: () => number().nullable().min(-139).max(-114).label('Longitude') }), ltsaPIDLookup: string().max(255).nullable().label('Parcel ID'), - geomarkUrl: string().max(255).label('Geomark web service url') + geomarkUrl: string().max(255).label('Geomark web service url'), + getJSON: mixed().nullable().label('geoJSON') }), [IntakeFormCategory.PERMITS]: object({ hasAppliedProvincialPermits: string().oneOf(YES_NO_UNSURE_LIST).required().label('Applied permits') diff --git a/frontend/src/services/externalApiService.ts b/frontend/src/services/externalApiService.ts index 534bf011..d943b615 100644 --- a/frontend/src/services/externalApiService.ts +++ b/frontend/src/services/externalApiService.ts @@ -1,5 +1,8 @@ -import { geocoderAxios, orgBookAxios } from './interceptors'; +import proj4 from 'proj4'; +import { storeToRefs } from 'pinia'; +import { geocoderAxios, openMapsAxios, orgBookAxios } from './interceptors'; import { ADDRESS_CODER_QUERY_PARAMS, ORG_BOOK_QUERY_PARAMS } from '@/utils/constants/housing'; +import { useConfigStore } from '@/store'; import type { AxiosResponse } from 'axios'; @@ -21,5 +24,60 @@ export default { ...ADDRESS_CODER_QUERY_PARAMS } }); + }, + + /** + * @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 + */ + async getParcelDataFromPMBC(polygon: Array) { + const { getConfig } = storeToRefs(useConfigStore()); + // close polygon by re-adding first point to end of array + const points = polygon.concat(polygon[0]); + + // 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', + '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 + const result = points.map((point) => { + //@ts-ignore + return proj4(source, dest, { x: point.lng, y: point.lat }); + }); + + // built query string for WFS request + let query = ''; + result.forEach((point, index, array) => { + query = query.concat(point.x, ' ', point.y); + if (index < array.length - 1) query = query.concat(', '); + }); + + let params = + '/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); + + return response.data; + }, + + async getNearestOccupant(longitude: string, lattitude: string) { + return geocoderAxios().get('/occupants/nearest.json', { + params: { + point: `${longitude},${lattitude}` + } + }); } }; diff --git a/frontend/src/services/interceptors.ts b/frontend/src/services/interceptors.ts index f42e3d11..93697568 100644 --- a/frontend/src/services/interceptors.ts +++ b/frontend/src/services/interceptors.ts @@ -110,7 +110,32 @@ export function orgBookAxios(options: AxiosRequestConfig = {}): AxiosInstance { paramsSerializer, ...options }); + instance.interceptors.request.use( + async (cfg: InternalAxiosRequestConfig) => { + return Promise.resolve(cfg); + }, + (error: Error) => { + return Promise.reject(error); + } + ); + + return instance; +} +/** + * @function openMapsAxios + * Returns an Axios instance for the openMaps API + * @param {AxiosRequestConfig} options Axios request config options + * @returns {AxiosInstance} An axios instance + */ +export function openMapsAxios(options: AxiosRequestConfig = {}): AxiosInstance { + const instance = axios.create({ + baseURL: new ConfigService().getConfig().openMaps.apiPath, + timeout: 10000, + headers: { 'Access-Control-Allow-Origin': '*' }, + paramsSerializer, + ...options + }); instance.interceptors.request.use( async (cfg: InternalAxiosRequestConfig) => { return Promise.resolve(cfg); diff --git a/frontend/src/types/Submission.ts b/frontend/src/types/Submission.ts index e034a8fc..d015a9c1 100644 --- a/frontend/src/types/Submission.ts +++ b/frontend/src/types/Submission.ts @@ -15,6 +15,11 @@ export type Submission = { companyNameRegistered: string; consentToFeedback?: boolean; isDevelopedInBC: string; + contactApplicantRelationship: string; + contactPreference: string; + contactPhoneNumber: string; + contactEmail: string; + geoJSON: JSON; projectName: string; projectDescription: string; projectLocationDescription: string; diff --git a/frontend/src/utils/constants/housing.ts b/frontend/src/utils/constants/housing.ts index 3f90d233..04f9e4f5 100644 --- a/frontend/src/utils/constants/housing.ts +++ b/frontend/src/utils/constants/housing.ts @@ -105,7 +105,11 @@ export const PERMIT_STATUS_LIST = [ PermitStatus.PENDING ]; -export const PROJECT_LOCATION_LIST = [ProjectLocation.STREET_ADDRESS, ProjectLocation.LOCATION_COORDINATES]; +export const PROJECT_LOCATION_LIST = [ + ProjectLocation.LOCATION_COORDINATES, + ProjectLocation.STREET_ADDRESS, + ProjectLocation.PIN_OR_DRAW +]; export const QUEUE_PRIORITY = [1, 2, 3]; diff --git a/frontend/src/utils/enums/housing.ts b/frontend/src/utils/enums/housing.ts index 0aee7cc5..fbcbd01d 100644 --- a/frontend/src/utils/enums/housing.ts +++ b/frontend/src/utils/enums/housing.ts @@ -108,7 +108,8 @@ export enum ProjectRelationship { export enum ProjectLocation { STREET_ADDRESS = 'Street address', - LOCATION_COORDINATES = 'Location coordinates' + LOCATION_COORDINATES = 'Location coordinates', + PIN_OR_DRAW = 'Pin or draw your location' } export enum SubmissionType {