From 181b245466d92db0bd9364b93e8896b060d2e97c Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Tue, 19 Nov 2024 11:33:11 +0100 Subject: [PATCH] chore: retire custom ESRI GIS integrations (#3977) --- api.planx.uk/modules/gis/service/helpers.js | 213 ------ .../modules/gis/service/helpers.test.js | 315 --------- api.planx.uk/modules/gis/service/helpers.ts | 33 + api.planx.uk/modules/gis/service/index.js | 55 +- .../modules/gis/service/index.test.ts | 66 -- .../service/local_authorities/braintree.js | 147 ----- .../local_authorities/metadata/braintree.js | 202 ------ .../local_authorities/metadata/scotland.js | 104 --- .../gis/service/local_authorities/scotland.js | 119 ---- .../fetching-direct-gis-data.ed67b9a5.json | 614 ------------------ .../components/PlanningConstraints/Public.tsx | 40 +- 11 files changed, 51 insertions(+), 1857 deletions(-) delete mode 100644 api.planx.uk/modules/gis/service/helpers.js delete mode 100644 api.planx.uk/modules/gis/service/helpers.test.js create mode 100644 api.planx.uk/modules/gis/service/helpers.ts delete mode 100644 api.planx.uk/modules/gis/service/local_authorities/braintree.js delete mode 100644 api.planx.uk/modules/gis/service/local_authorities/metadata/braintree.js delete mode 100644 api.planx.uk/modules/gis/service/local_authorities/metadata/scotland.js delete mode 100644 api.planx.uk/modules/gis/service/local_authorities/scotland.js delete mode 100644 api.planx.uk/tests/nocks/fetching-direct-gis-data.ed67b9a5.json diff --git a/api.planx.uk/modules/gis/service/helpers.js b/api.planx.uk/modules/gis/service/helpers.js deleted file mode 100644 index 914a7ad0bb..0000000000 --- a/api.planx.uk/modules/gis/service/helpers.js +++ /dev/null @@ -1,213 +0,0 @@ -// Source name used in metadata templates for pre-checked/human-verified data sources -const PRECHECKED_SOURCE = "manual"; - -// Set the geometry type to polygon if we have a valid site boundary polygon drawing, -// else do an envelope query using the buffered address point -// Ref https://developers.arcgis.com/documentation/common-data-types/geometry-objects.htm -const setEsriGeometryType = (siteBoundary = []) => { - return siteBoundary.length === 0 - ? "esriGeometryEnvelope" - : "esriGeometryPolygon"; -}; - -// Set the geometry object based on our geometry type -// Ref https://developers.arcgis.com/documentation/common-data-types/geometry-objects.htm -const setEsriGeometry = (geometryType, x, y, radius, siteBoundary) => { - return geometryType === "esriGeometryEnvelope" - ? bufferPoint(x, y, radius) - : JSON.stringify({ - rings: siteBoundary, - spatialReference: { - wkid: 4326, - }, - }); -}; - -// Build up the URL used to query an ESRI feature -// Ref https://developers.arcgis.com/rest/services-reference/enterprise/query-feature-service-.htm -const makeEsriUrl = (domain, id, serverIndex = 0, overrideParams = {}) => { - let url = `${domain}/rest/services/${id}/MapServer/${serverIndex}/query`; - - const defaultParams = { - where: "1=1", - geometryType: "esriGeometryEnvelope", - inSR: 27700, - spatialRel: "esriSpatialRelIntersects", - returnGeometry: false, - outSR: 4326, - f: "json", - outFields: [], - geometry: [], - }; - - const params = { ...defaultParams, ...overrideParams }; - - if (Array.isArray(params.outFields)) - params.outFields = params.outFields.join(","); - if (Array.isArray(params.geometry)) - params.geometry = params.geometry.join(","); - - url = [ - url, - Object.keys(params) - .map((key) => key + "=" + escape(params[key])) - .join("&"), - ].join("?"); - - return url; -}; - -// Buffer an address point as a proxy for curtilage -const bufferPoint = (x, y, radius = 0.05) => { - return [x - radius, y + radius, x + radius, y - radius]; -}; - -// Build a bbox (string) around a point -const makeBbox = (x, y, radius = 1.5) => { - return `${x - radius},${y - radius},${x + radius},${y + radius}`; -}; - -// For a dictionary of planning constraint objects, return the items with preset { value: false } aka unknown data source -const getFalseConstraints = (metadata) => { - let falseConstraints = {}; - Object.keys(metadata).forEach((constraint) => { - if (metadata[constraint].value === false) { - falseConstraints[constraint] = { value: false }; - } - }); - - return falseConstraints; -}; - -// For a dictionary of planning constraint objects, return the items with known data sources -const getQueryableConstraints = (metadata) => { - let queryableConstraints = {}; - Object.keys(metadata).forEach((constraint) => { - if ( - "source" in metadata[constraint] && - metadata[constraint]["source"] !== PRECHECKED_SOURCE - ) { - queryableConstraints[constraint] = metadata[constraint]; - } - }); - - return queryableConstraints; -}; - -// For a dictionary of planning constraint objects, return the items that have been manually verified and do not apply to this geographic region -const getManualConstraints = (metadata) => { - let manualConstraints = {}; - Object.keys(metadata).forEach((constraint) => { - if ( - "source" in metadata[constraint] && - metadata[constraint]["source"] === PRECHECKED_SOURCE - ) { - // Make object shape consistent with queryable data sources - delete metadata[constraint]["source"]; - delete metadata[constraint]["key"]; - - metadata[constraint]["text"] = metadata[constraint]["neg"]; - delete metadata[constraint]["neg"]; - - metadata[constraint]["value"] = false; - metadata[constraint]["type"] = "check"; - metadata[constraint]["data"] = []; - - manualConstraints[constraint] = metadata[constraint]; - } - }); - - return manualConstraints; -}; - -// Adds "designated" variable to response object, so we can auto-answer less granular questions like "are you on designated land" -const addDesignatedVariable = (responseObject) => { - const resObjWithDesignated = { - ...responseObject, - designated: { value: false }, - }; - - const subVariables = ["conservationArea", "AONB", "nationalPark", "WHS"]; - - // If any of the subvariables are true, then set "designated" to true - subVariables.forEach((s) => { - if (resObjWithDesignated[`designated.${s}`]?.value) { - resObjWithDesignated["designated"] = { value: true }; - } - }); - - // Ensure that our response includes all the expected subVariables before returning "designated" - // so we don't incorrectly auto-answer any questions for individual layer queries that may have failed - let subVariablesFound = 0; - Object.keys(responseObject).forEach((key) => { - if (key.startsWith(`designated.`)) { - subVariablesFound++; - } - }); - - if (subVariablesFound < subVariables.length) { - return responseObject; - } else { - return resObjWithDesignated; - } -}; - -// Squash multiple layers into a single result -const squashResultLayers = (originalOb, layers, layerName) => { - const ob = { ...originalOb }; - // Check to see if we have any intersections - const match = layers.find((layer) => ob[layer].value); - // If we do, return this as the result. Otherwise take the first (negative) value. - ob[layerName] = match ? ob[match] : ob[layers[0]]; - // Tidy up the redundant layers - layers.forEach((layer) => delete ob[layer]); - return ob; -}; - -// Rollup multiple layers into a single result, whilst preserving granularity -const rollupResultLayers = (originalOb, layers, layerName) => { - const ob = { ...originalOb }; - const granularLayers = layers.filter((layer) => layer != layerName); - - if (ob[layerName]?.value) { - // If the parent layer is in the original object & intersects, preserve all properties for rendering PlanningConstraints and debugging - // ob[layerName] = ob[layerName]; - } else { - // Check to see if any granular layers intersect - const match = granularLayers.find((layer) => ob[layer].value); - // If there is a granular match, set it as the parent result. Otherwise take the first (negative) value - ob[layerName] = match ? ob[match] : ob[layers[0]]; - } - - // Return a simple view of the granular layers to avoid duplicate PlanningConstraint entries - granularLayers.forEach((layer) => (ob[layer] = { value: ob[layer].value })); - - return ob; -}; - -// Handle Article 4 subvariables -// Return an object with a simple result for each A4 subvariable -const getA4Subvariables = (features, articleFours, a4Key) => { - const result = {}; - const a4Keys = features.map((feature) => feature.attributes[a4Key]); - Object.entries(articleFours).forEach(([key, value]) => { - const isMatch = a4Keys.includes(value); - result[key] = { value: isMatch }; - }); - return result; -}; - -export { - setEsriGeometryType, - setEsriGeometry, - makeEsriUrl, - bufferPoint, - makeBbox, - getQueryableConstraints, - getFalseConstraints, - getManualConstraints, - squashResultLayers, - rollupResultLayers, - getA4Subvariables, - addDesignatedVariable, -}; diff --git a/api.planx.uk/modules/gis/service/helpers.test.js b/api.planx.uk/modules/gis/service/helpers.test.js deleted file mode 100644 index 6e6179cebc..0000000000 --- a/api.planx.uk/modules/gis/service/helpers.test.js +++ /dev/null @@ -1,315 +0,0 @@ -import { - squashResultLayers, - rollupResultLayers, - getA4Subvariables, -} from "./helpers.js"; - -describe("squashResultLayer helper function", () => { - test("It should squash the list of layers passed in", () => { - // Arrange - const input = { - "tpo.tenMeterBuffer": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: {}, - }, - "tpo.areas": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: {}, - }, - "tpo.woodland": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: {}, - }, - "tpo.group": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: {}, - }, - }; - const layersToSquash = Object.keys(input); - const key = "tpo"; - - // Act - const result = squashResultLayers(input, layersToSquash, key); - - // Assert - expect(result).toHaveProperty(key); - layersToSquash.forEach((layer) => expect(result).not.toHaveProperty(layer)); - }); - - test("It should correctly squash layers based on their value", () => { - // Arrange - const input1 = { - "tpo.tenMeterBuffer": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: {}, - }, - "tpo.areas": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: true, // We expect our test to pick this up - type: "check", - data: { - OBJECTID: 123, - }, - }, - "tpo.woodland": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: {}, - }, - "tpo.group": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: {}, - }, - }; - const input2 = { - "tpo.tenMeterBuffer": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: { - OBJECTID: "ABC", // We expect our test to pick this up - }, - }, - "tpo.areas": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: {}, - }, - "tpo.woodland": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: {}, - }, - "tpo.group": { - text: "is not in a TPO (Tree Preservation Order) Zone", - value: false, - type: "check", - data: {}, - }, - }; - const layersToSquash1 = Object.keys(input1); - const layersToSquash2 = Object.keys(input2); - const key = "tpo"; - - // Act - const result1 = squashResultLayers(input1, layersToSquash1, key); - const result2 = squashResultLayers(input2, layersToSquash2, key); - - // Assert - expect(result1[key].value).toBe(true); - expect(result1[key]).toMatchObject(input1["tpo.areas"]); - expect(result2[key].value).toBe(false); - expect(result2[key]).toMatchObject(input2["tpo.tenMeterBuffer"]); - }); -}); - -describe("rollupResultLayer helper function", () => { - test("It should roll up the list of layers passed in", () => { - // Arrange - const input = { - "listed.grade1": { - text: "is not in, or within, a Listed Building", - value: false, - type: "check", - data: {}, - }, - "listed.grade2": { - text: "is, or is within, a Listed Building (Grade 2)", - description: null, - value: false, - type: "warning", - data: {}, - }, - "listed.grade2star": { - text: "is not in, or within, a Listed Building", - value: false, - type: "check", - data: {}, - }, - }; - const layersToRollup = Object.keys(input); - const key = "listed"; - - // Act - const result = rollupResultLayers(input, layersToRollup, key); - - // Assert - expect(result).toHaveProperty(key); - layersToRollup.forEach((layer) => { - expect(result).toHaveProperty([layer]); - expect(result[layer]).toMatchObject({ value: false }); - }); - }); - - test("It should correctly roll up layers which have a match", () => { - // Arrange - const input = { - "listed.grade1": { - text: "is not in, or within, a Listed Building", - value: false, - type: "check", - data: {}, - }, - "listed.grade2": { - text: "is, or is within, a Listed Building (Grade 2)", - description: null, - value: true, - type: "warning", - data: { - OBJECTID: 3398, - }, - }, - "listed.grade2star": { - text: "is not in, or within, a Listed Building", - value: false, - type: "check", - data: {}, - }, - }; - const layersToRollup = Object.keys(input); - const key = "listed"; - - // Act - const result = rollupResultLayers(input, layersToRollup, key); - - // Assert - expect(result[key]).toMatchObject(input["listed.grade2"]); - }); - - test("It should correctly roll up layers which have do not have a match", () => { - // Arrange - const input = { - "listed.grade1": { - text: "is not in, or within, a Listed Building", - value: false, - type: "check", - data: {}, - }, - "listed.grade2": { - text: "is, or is within, a Listed Building (Grade 2)", - description: null, - value: false, - type: "warning", - data: {}, - }, - "listed.grade2star": { - text: "is not in, or within, a Listed Building", - value: false, - type: "check", - data: {}, - }, - }; - const layersToRollup = Object.keys(input); - const key = "listed"; - - // Act - const result = rollupResultLayers(input, layersToRollup, key); - - // Assert - expect(result[key]).toMatchObject(input["listed.grade1"]); - }); - - test("It should correctly rollup when the layerName matches a provided layer", () => { - // Arrange - const input = { - article4: { - text: "is subject to local permitted development restrictions (known as Article 4 directions)", - description: - "BLACKFRIARS STREET 28,29,30, KING STREET 10 TO 15, MILL LANE 19,20", - value: true, - type: "warning", - data: { - OBJECTID: 72963, - REF: "Article 4 Direction 1985", - LOCATION_1: - "BLACKFRIARS STREET 28,29,30, KING STREET 10 TO 15, MILL LANE 19,20", - DESCRIPTIO: "Effective 29 November 1985", - }, - }, - "article4.canterbury.hmo": { - text: "is subject to local permitted development restrictions (known as Article 4 directions)", - description: "Canterbury and surrounding area", - value: true, - type: "warning", - data: { - OBJECTID: 73412, - REF: "The Canterbury HMO Article 4 D", - LOCATION_1: "Canterbury and surrounding area", - DESCRIPTIO: "Effective 25 February 2016", - }, - }, - }; - const layersToRollup = Object.keys(input); - const key = "article4"; - - // Act - const result = rollupResultLayers(input, layersToRollup, key); - - // Assert - expect(result[key]).toMatchObject(input["article4"]); // parent key maintains all original properties - expect(result["article4.canterbury.hmo"]).toMatchObject({ value: true }); // granular key is simplified - }); -}); - -describe("getA4Subvariables helper function", () => { - const A4_KEY = "OBJECTID"; - const articleFours = { - "article4.test.a": 1, - "article4.test.b": 5, - "article4.test.c": 13, - }; - it("returns a property for each Article 4 passed in", () => { - // Arrange - const features = [ - { attributes: { OBJECTID: 1, name: "Hackney Road" } }, - { attributes: { OBJECTID: 5, name: "Peckham Way" } }, - { attributes: { OBJECTID: 13, name: "Chelsea Park" } }, - ]; - // Act - const result = getA4Subvariables(features, articleFours, A4_KEY); - // Assert - Object.keys(articleFours).forEach((key) => - expect(result).toHaveProperty([key]), - ); - }); - - it("correctly matches features which have a hit on an Article 4", () => { - // Arrange - const features = [ - { attributes: { OBJECTID: 1, name: "Hackney Road" } }, - { attributes: { OBJECTID: 13, name: "Chelsea Park" } }, - ]; - // Act - const result = getA4Subvariables(features, articleFours, A4_KEY); - // Assert - expect(result).toMatchObject({ - ["article4.test.a"]: { value: true }, - ["article4.test.b"]: { value: false }, - ["article4.test.c"]: { value: true }, - }); - }); - - it("handles no matching Article 4 results", () => { - const result = getA4Subvariables([], articleFours, A4_KEY); - expect(result).toMatchObject({ - ["article4.test.a"]: { value: false }, - ["article4.test.b"]: { value: false }, - ["article4.test.c"]: { value: false }, - }); - }); -}); diff --git a/api.planx.uk/modules/gis/service/helpers.ts b/api.planx.uk/modules/gis/service/helpers.ts new file mode 100644 index 0000000000..4f62cf3c11 --- /dev/null +++ b/api.planx.uk/modules/gis/service/helpers.ts @@ -0,0 +1,33 @@ +// Adds "designated" variable to response object, so we can auto-answer less granular questions like "are you on designated land" +const addDesignatedVariable = (responseObject: any) => { + const resObjWithDesignated = { + ...responseObject, + designated: { value: false }, + }; + + const subVariables = ["conservationArea", "AONB", "nationalPark", "WHS"]; + + // If any of the subvariables are true, then set "designated" to true + subVariables.forEach((s) => { + if (resObjWithDesignated[`designated.${s}`]?.value) { + resObjWithDesignated["designated"] = { value: true }; + } + }); + + // Ensure that our response includes all the expected subVariables before returning "designated" + // so we don't incorrectly auto-answer any questions for individual layer queries that may have failed + let subVariablesFound = 0; + Object.keys(responseObject).forEach((key) => { + if (key.startsWith(`designated.`)) { + subVariablesFound++; + } + }); + + if (subVariablesFound < subVariables.length) { + return responseObject; + } else { + return resObjWithDesignated; + } +}; + +export { addDesignatedVariable }; diff --git a/api.planx.uk/modules/gis/service/index.js b/api.planx.uk/modules/gis/service/index.js index da53b7b081..6332259c10 100644 --- a/api.planx.uk/modules/gis/service/index.js +++ b/api.planx.uk/modules/gis/service/index.js @@ -1,8 +1,6 @@ import * as digitalLand from "./digitalLand.js"; -import * as scotland from "./local_authorities/scotland.js"; -import * as braintree from "./local_authorities/braintree.js"; -const localAuthorities = { digitalLand, braintree, scotland }; +const localAuthorities = { digitalLand }; /** * @swagger @@ -22,11 +20,10 @@ const localAuthorities = { digitalLand, braintree, scotland }; * description: Well-Known Text (WKT) formatted polygon or point */ export async function locationSearch(req, res, next) { - // 'geom' param signals this localAuthority has data available via DigitalLand, "ready" teams are configured in PlanningConstraints component in editor - // swagger doc intentionally doesn't cover legacy custom GIS hookups for Braintree and Scotland demo + // 'geom' param signals this localAuthority has data available via Planning Data if (req.query.geom) { try { - const resp = await locationSearchWithoutTimeout( + const resp = await locationSearchViaPlanningData( req.params.localAuthority, req.query, ); @@ -37,22 +34,6 @@ export async function locationSearch(req, res, next) { message: err ? err : "unknown error", }); } - // check if this local authority is supported via our custom GIS hookup - } else if (localAuthorities[req.params.localAuthority]) { - try { - const timeout = Number(process.env.TIMEOUT_DURATION) || 15000; - const resp = await locationSearchWithTimeout( - req.params.localAuthority, - req.query, - timeout, - ); - res.send(resp); - } catch (err) { - next({ - status: 500, - message: err ? err : "unknown error", - }); - } } else { next({ status: 400, @@ -61,37 +42,11 @@ export async function locationSearch(req, res, next) { } } -// Digital Land is a single request with standardized geometry, so remove timeout & simplify query params -export function locationSearchWithoutTimeout(localAuthority, queryParams) { +// Planning Data is a single request with standardized geometry, so timeout is not necessary +export function locationSearchViaPlanningData(localAuthority, queryParams) { return localAuthorities["digitalLand"].locationSearch( localAuthority, queryParams.geom, queryParams, ); } - -// custom GIS hookups require many requests to individual data layers which are more likely to timeout -export function locationSearchWithTimeout( - localAuthority, - { x, y, siteBoundary, extras = "{}" }, - time, -) { - let extraInfo = extras; - extraInfo = JSON.parse(unescape(extras)); - - const timeout = new Promise((resolve, reject) => { - const timeoutID = setTimeout(() => { - clearTimeout(timeoutID); - reject("location search timeout"); - }, time); - }); - - const promise = localAuthorities[localAuthority].locationSearch( - parseInt(x, 10), - parseInt(y, 10), - JSON.parse(siteBoundary), - extraInfo, - ); - - return Promise.race([promise, timeout]); -} diff --git a/api.planx.uk/modules/gis/service/index.test.ts b/api.planx.uk/modules/gis/service/index.test.ts index 6b6944bc5f..e35fff3f25 100644 --- a/api.planx.uk/modules/gis/service/index.test.ts +++ b/api.planx.uk/modules/gis/service/index.test.ts @@ -2,72 +2,6 @@ import supertest from "supertest"; import loadOrRecordNockRequests from "../../../tests/loadOrRecordNockRequests.js"; import app from "../../../server.js"; -import { locationSearchWithTimeout } from "./index.js"; - -// Tests commented out due to reliance on external API calls and fallibility of nocks -// Please comment in and run locally if making changes to /gis functionality -describe("locationSearchWithTimeout", () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - test.skip("a successful call", async () => { - const timeout = 500; - const localAuthority = "braintree"; - const promise = locationSearchWithTimeout( - localAuthority, - { x: 50, y: 50, siteBoundary: "[]" }, - timeout, - ); - await expect(promise).resolves.toStrictEqual(expect.any(Object)); - }); - - test.skip("an immediate timeout", async () => { - const timeout = 500; - const localAuthority = "braintree"; - const promise = locationSearchWithTimeout( - localAuthority, - { x: 50, y: 50, siteBoundary: "[]" }, - timeout, - ); - vi.runAllTimers(); - await expect(promise).rejects.toEqual("location search timeout"); - }); -}); - -describe.skip("fetching GIS data from local authorities directly", () => { - const locations = [ - { - council: "braintree", - x: 575629.54, - y: 223122.85, - siteBoundary: [], - }, - ]; - - loadOrRecordNockRequests("fetching-direct-gis-data", locations); - - locations.forEach((location) => { - it(`returns MVP planning constraints for ${location.council}`, async () => { - await supertest(app) - .get( - `/gis/${location.council}?x=${location.x}&y=${ - location.y - }&siteBoundary=${JSON.stringify(location.siteBoundary)}`, - ) - .expect(200) - .then((res) => { - expect(res.body["article4"]).toBeDefined(); - expect(res.body["listed"]).toBeDefined(); - expect(res.body["designated.conservationArea"]).toBeDefined(); - }); - }, 20_000); // 20s request timeout - }); -}); describe.skip("fetching GIS data from Digital Land for supported local authorities", () => { const locations = [ diff --git a/api.planx.uk/modules/gis/service/local_authorities/braintree.js b/api.planx.uk/modules/gis/service/local_authorities/braintree.js deleted file mode 100644 index 4e01d2e5dc..0000000000 --- a/api.planx.uk/modules/gis/service/local_authorities/braintree.js +++ /dev/null @@ -1,147 +0,0 @@ -import "isomorphic-fetch"; -import { - getQueryableConstraints, - getManualConstraints, - makeEsriUrl, - setEsriGeometryType, - setEsriGeometry, - squashResultLayers, - rollupResultLayers, - addDesignatedVariable, -} from "../helpers.js"; -import { planningConstraints, A4_KEY } from "./metadata/braintree.js"; - -// Process local authority metadata -const gisLayers = getQueryableConstraints(planningConstraints); -const preCheckedLayers = getManualConstraints(planningConstraints); -const articleFours = planningConstraints.article4.records; - -// Fetch a data layer -async function search( - mapServer, - featureName, - serverIndex, - outFields, - geometry, - geometryType, -) { - const { id } = planningConstraints[featureName]; - - let url = makeEsriUrl(mapServer, id, serverIndex, { - outFields, - geometry, - geometryType, - }); - - return fetch(url) - .then((response) => response.text()) - .then((data) => [featureName, data]) - .catch((error) => { - console.log("Error:", error); - }); -} - -// For this location, iterate through our planning constraints and aggregate/format the responses -async function go(x, y, siteBoundary, extras) { - // If we have a siteBoundary from drawing, - // then query using the polygon, else fallback to the buffered address point - const geomType = setEsriGeometryType(siteBoundary); - const geom = setEsriGeometry(geomType, x, y, 0.05, siteBoundary); - - const results = await Promise.all( - // TODO: Better to have logic here to iterate over listed buildings and TPOs? - Object.keys(gisLayers).map((layer) => - search( - gisLayers[layer].source, - layer, - gisLayers[layer].serverIndex, - gisLayers[layer].fields, - geom, - geomType, - ), - ), - ); - - const ob = results - .filter(([_key, result]) => !(result instanceof Error)) - .reduce( - (acc, [key, result]) => { - const data = JSON.parse(result); - const k = `${planningConstraints[key].key}`; - - try { - if (data.features.length > 0) { - const { attributes: properties } = data.features[0]; - acc[k] = { - ...planningConstraints[key].pos(properties), - value: true, - type: "warning", - data: properties, - category: planningConstraints[key].category, - }; - } else { - if (!acc[k]) { - acc[k] = { - text: planningConstraints[key].neg, - value: false, - type: "check", - data: {}, - category: planningConstraints[key].category, - }; - } - } - } catch (e) { - console.log(e); - } - - return acc; - }, - { - ...preCheckedLayers, - ...extras, - }, - ); - - // Set granular A4s, accounting for one variable matching to many possible shapes - if (ob["article4"].value) { - Object.keys(articleFours).forEach((key) => { - if ( - articleFours[key].includes(ob["article4"].data[A4_KEY]) || - ob[key]?.value - ) { - ob[key] = { value: true }; - } else { - ob[key] = { value: false }; - } - }); - } - - // Braintree host multiple TPO layers, but we only want to return a single result - const tpoLayers = [ - "tpo.tenMeterBuffer", - "tpo.areas", - "tpo.woodland", - "tpo.group", - ]; - const obWithSingleTPO = squashResultLayers(ob, tpoLayers, "tpo"); - - // Likewise, multiple layers are provided for "listed" - // Roll these up to preserve their granularity - const listedLayers = ["listed.grade1", "listed.grade2", "listed.grade2star"]; - const obWithSingleListed = rollupResultLayers( - obWithSingleTPO, - listedLayers, - "listed", - ); - - // Add summary "designated" key to response - const obWithDesignated = addDesignatedVariable(obWithSingleListed); - - return obWithDesignated; -} - -async function locationSearch(x, y, siteBoundary, extras) { - return go(x, y, siteBoundary, extras); -} - -export { locationSearch }; diff --git a/api.planx.uk/modules/gis/service/local_authorities/metadata/braintree.js b/api.planx.uk/modules/gis/service/local_authorities/metadata/braintree.js deleted file mode 100644 index 5cc21ab898..0000000000 --- a/api.planx.uk/modules/gis/service/local_authorities/metadata/braintree.js +++ /dev/null @@ -1,202 +0,0 @@ -/* -LAD20CD: E07000067 -LAD20NM: Braintree -LAD20NMW: -FID: 239 - -https://mapping.braintree.gov.uk/arcgis/rest/services/PlanX/PlanX/FeatureServer -https://environment.data.gov.uk/arcgis/rest/services -*/ - -const braintreeDomain = "https://mapping.braintree.gov.uk/arcgis"; -const _environmentDomain = "https://environment.data.gov.uk"; -const A4_KEY = "PARISH"; - -const planningConstraints = { - "listed.grade1": { - key: "listed.grade1", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 0, - fields: ["OBJECTID"], - neg: "is not in, or within, a Listed Building", - pos: (_data) => ({ - text: "is, or is within, a Listed Building (Grade 1)", - description: null, - }), - category: "Heritage and conservation", - }, - "listed.grade2": { - key: "listed.grade2", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 1, - fields: ["OBJECTID"], - neg: "is not in, or within, a Listed Building", - pos: (_data) => ({ - text: "is, or is within, a Listed Building (Grade 2)", - description: null, - }), - category: "Heritage and conservation", - }, - "listed.grade2star": { - key: "listed.grade2star", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 2, - fields: ["OBJECTID"], - neg: "is not in, or within, a Listed Building", - pos: (_data) => ({ - text: "is, or is within, a Listed Building (Grade 2*)", - description: null, - }), - category: "Heritage and conservation", - }, - "designated.conservationArea": { - key: "designated.conservationArea", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 3, - fields: ["OBJECTID", "DESIGNATED"], - neg: "is not in a Conservation Area", - pos: (_data) => ({ - text: "is in a Conservation Area", - description: null, - }), - category: "Heritage and conservation", - }, - "nature.SSSI": { - key: "nature.SSSI", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 8, - fields: ["OBJECTID", "LOCATION"], - neg: "is not a Site of Special Scientific Interest", - pos: (data) => ({ - text: "is a Site of Special Scientific Interest", - description: data.LOCATION, - }), - category: "Ecology", - }, - monument: { - key: "monument", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 9, - fields: ["OBJECTID", "LOCATION"], - neg: "is not the site of a Scheduled Ancient Monument", - pos: (data) => ({ - text: "is the site of a Scheduled Ancient Monument", - description: data.LOCATION, - }), - category: "Heritage and conservation", - }, - "tpo.tenMeterBuffer": { - key: "tpo.tenMeterBuffer", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 4, - fields: ["OBJECTID"], - neg: "is not in a TPO (Tree Preservation Order) Zone", - pos: (_data) => ({ - text: "is in a TPO (Tree Preservation Order) Zone", - description: null, - }), - category: "Trees", - }, - "tpo.areas": { - key: "tpo.areas", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 5, - fields: ["OBJECTID", "AREAS"], - neg: "is not in a TPO (Tree Preservation Order) Zone", - pos: (_data) => ({ - text: "is in a TPO (Tree Preservation Order) Zone", - description: null, - }), - category: "Trees", - }, - "tpo.woodland": { - key: "tpo.woodland", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 6, - fields: ["OBJECTID", "SPECIES", "REFERENCE_"], - neg: "is not in a TPO (Tree Preservation Order) Zone", - pos: (_data) => ({ - text: "is in a TPO (Tree Preservation Order) Zone", - description: null, - }), - category: "Trees", - }, - "tpo.group": { - key: "tpo.group", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 7, - fields: ["OBJECTID", "GROUPS", "SPECIES"], - neg: "is not in a TPO (Tree Preservation Order) Zone", - pos: (_data) => ({ - text: "is in a TPO (Tree Preservation Order) Zone", - description: null, - }), - category: "Trees", - }, - article4: { - key: "article4", - source: braintreeDomain, - id: "PlanX/PlanX", - serverIndex: 10, - fields: ["OBJECTID", "PARISH", "INFORMATIO"], - neg: "is not subject to local permitted development restrictions (known as Article 4 directions)", - pos: (data) => ({ - text: "is subject to local permitted development restrictions (known as Article 4 directions)", - description: data.INFORMATIO, - }), - records: { - // planx value to "PARISH" lookup - "article4.braintree.silverEnd": ["Silver End"], - "article4.braintree.stisted": [ - "Stisted", - "Braintree", - "Gosfield", - "Middleton", - "Shalford", - "Greenstead Green", - "Coggeshall", - "Ashen", - ], - }, - category: "General policy", - }, - "designated.AONB": { - key: "designated.AONB", - source: "manual", // there are no AONBs in Braintree - neg: "is not in an Area of Outstanding Natural Beauty", - category: "Heritage and conservation", - }, - "designated.nationalPark": { - key: "designated.nationalPark", - source: "manual", // there are no National Parks in Braintree - neg: "is not in a National Park", - category: "Heritage and conservation", - }, - "designated.broads": { - key: "designated.broads", - source: "manual", // there are no Broads in Braintree - neg: "is not in a Broad", - category: "Heritage and conservation", - }, - "designated.WHS": { - key: "designated.WHS", - source: "manual", // there are no WHS in Braintree - neg: "is not an UNESCO World Heritage Site", - category: "Heritage and conservation", - }, - "defence.explosives": { value: false }, - "defence.safeguarded": { value: false }, - hazard: { value: false }, -}; - -export { planningConstraints, A4_KEY }; diff --git a/api.planx.uk/modules/gis/service/local_authorities/metadata/scotland.js b/api.planx.uk/modules/gis/service/local_authorities/metadata/scotland.js deleted file mode 100644 index 6769d0f48c..0000000000 --- a/api.planx.uk/modules/gis/service/local_authorities/metadata/scotland.js +++ /dev/null @@ -1,104 +0,0 @@ -/* -TEST ONLY currently for TPX Impact -*/ - -// const scotGovDomain = "https://maps.gov.scot/server"; -const inspireHESDomain = "https://inspire.hes.scot/arcgis"; - -const planningConstraints = { - article4: { value: false }, - "designated.conservationArea": { - key: "designated.conservationArea", - source: inspireHESDomain, - id: "HES/HES_Designations", - serverIndex: 2, - fields: ["OBJECTID", "DES_TITLE", "LINK"], - neg: "is not in a Conservation Area", - pos: (data) => ({ - text: "is in a Conservation Area", - description: data.DES_TITLE, - }), - category: "Heritage and conservation", - }, - // "designated.nationalPark.cairngorms": { - // key: "designated.nationalPark.cairngorms", - // source: scotGovDomain, - // id: "ScotGov/ProtectedSites", - // serverIndex: 0, - // fields: ["objectid", "npcode", "npname"], - // neg: "is not in a National Park", - // pos: (data) => ({ - // text: "is in Cairngorms National Park", - // description: data.npname, - // }), - // category: "Heritage and conservation", - // }, - // "designated.nationalPark.lochLomondTrossachs": { - // key: "designated.nationalPark.lochLomondTrossachs", - // source: scotGovDomain, - // id: "ScotGov/ProtectedSites", - // serverIndex: 1, - // fields: ["objectid", "npcode", "npname"], - // neg: "is not in a National Park", - // pos: (data) => ({ - // text: "is in Loch Lomond and The Trossachs National Park", - // description: data.npname, - // }), - // category: "Heritage and conservation", - // }, - // "designated.nationalScenicArea": { - // key: "designated.nationalScenicArea", - // source: scotGovDomain, - // id: "ScotGov/ProtectedSites", - // serverIndex: 3, - // fields: ["objectid", "nsacode", "nsaname"], - // neg: "is not in a National Scenic Area", - // pos: (data) => ({ - // text: "is in a National Scenic Area", - // description: data.nsaname, - // }), - // category: "Heritage and conservation", - // }, - "designated.WHS": { - key: "designated.WHS", - source: inspireHESDomain, - id: "HES/HES_Designations", - serverIndex: 6, - fields: ["DES_REF", "DES_TITLE"], - neg: "is not, or is not within, an UNESCO World Heritage Site", - pos: (data) => ({ - text: "is, or is within, an UNESCO World Heritage Site", - description: data.DES_TITLE, - }), - category: "Heritage and conservation", - }, - listed: { - key: "listed", - source: inspireHESDomain, - id: "HES/HES_Designations", - serverIndex: 7, - fields: ["DES_REF", "DES_TITLE", "CATEGORY", "LINK"], - neg: "is not, or is not within, a Listed Building", - pos: (data) => ({ - text: `is, or is within, a Listed Building (Category ${data.CATEGORY})`, - description: data.DES_TITLE, - }), - category: "Heritage and conservation", - }, - monument: { - key: "monument", - source: inspireHESDomain, - id: "HES/HES_Designations", - serverIndex: 5, - fields: ["DES_REF", "DES_TITLE", "LINK"], - neg: "is not the site of a Scheduled Monument", - pos: (data) => ({ - text: "is the site of a Scheduled Monument", - description: data.DES_TITLE, - }), - category: "Heritage and conservation", - }, - tpo: { value: false }, -}; - -export { planningConstraints }; diff --git a/api.planx.uk/modules/gis/service/local_authorities/scotland.js b/api.planx.uk/modules/gis/service/local_authorities/scotland.js deleted file mode 100644 index eef564475e..0000000000 --- a/api.planx.uk/modules/gis/service/local_authorities/scotland.js +++ /dev/null @@ -1,119 +0,0 @@ -import "isomorphic-fetch"; -import { - getQueryableConstraints, - makeEsriUrl, - setEsriGeometryType, - setEsriGeometry, - rollupResultLayers, - addDesignatedVariable, -} from "../helpers.js"; -import { planningConstraints } from "./metadata/scotland.js"; - -// Process local authority metadata -const gisLayers = getQueryableConstraints(planningConstraints); - -// Fetch a data layer -async function search( - mapServer, - featureName, - serverIndex, - outFields, - geometry, - geometryType, -) { - const { id } = planningConstraints[featureName]; - - let url = makeEsriUrl(mapServer, id, serverIndex, { - outFields, - geometry, - geometryType, - }); - - return fetch(url) - .then((response) => response.text()) - .then((data) => [featureName, data]) - .catch((error) => { - console.log("Error:", error); - }); -} - -// For this location, iterate through our planning constraints and aggregate/format the responses -async function go(x, y, siteBoundary, extras) { - // If we have a siteBoundary from drawing, - // then query using the polygon, else fallback to the buffered address point - const geomType = setEsriGeometryType(siteBoundary); - const geom = setEsriGeometry(geomType, x, y, 0.05, siteBoundary); - - const results = await Promise.all( - Object.keys(gisLayers).map((layer) => - search( - gisLayers[layer].source, - layer, - gisLayers[layer].serverIndex, - gisLayers[layer].fields, - geom, - geomType, - ), - ), - ); - - const ob = results.filter(Boolean).reduce( - (acc, [key, result]) => { - const data = JSON.parse(result); - const k = `${planningConstraints[key].key}`; - - try { - if (data.features.length > 0) { - const { attributes: properties } = data.features[0]; - acc[k] = { - ...planningConstraints[key].pos(properties), - value: true, - type: "warning", - data: properties, - category: planningConstraints[key].category, - }; - } else { - if (!acc[k]) { - acc[k] = { - text: planningConstraints[key].neg, - value: false, - type: "check", - data: {}, - category: planningConstraints[key].category, - }; - } - } - } catch (e) { - console.log(e); - } - - return acc; - }, - { - ...extras, - }, - ); - - // Scotland hosts multiple national park layers - // Roll these up to preserve their granularity when true - const nationalParkLayers = [ - // "designated.nationalPark.cairngorms", - // "designated.nationalPark.lochLomondTrossachs", - ]; - const obWithOneNationalPark = rollupResultLayers( - ob, - nationalParkLayers, - "designated.nationalPark", - ); - - // Add summary "designated" key to response - const obWithDesignated = addDesignatedVariable(obWithOneNationalPark); - - return obWithDesignated; -} - -async function locationSearch(x, y, siteBoundary, extras) { - return go(x, y, siteBoundary, extras); -} - -export { locationSearch }; diff --git a/api.planx.uk/tests/nocks/fetching-direct-gis-data.ed67b9a5.json b/api.planx.uk/tests/nocks/fetching-direct-gis-data.ed67b9a5.json deleted file mode 100644 index 1ac8016059..0000000000 --- a/api.planx.uk/tests/nocks/fetching-direct-gis-data.ed67b9a5.json +++ /dev/null @@ -1,614 +0,0 @@ -[ - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/0/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b0800000000000000ab564ac92c2ec849ac74cb4ccd49f14bcc4d55b2520a700cf20cf650d2514a03093ae6642616a7162b59552bf93b79b93a8778ba00d5c099b550654005d1d54a791013e0b23a4a2595052091d4e2a24cb01d2140be3f58261164308a51b140b352134b4a8b40d645c7d6020011d8e66a9e000000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "1a0b6423", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "126" - ], - "responseIsBinary": false - }, - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/1/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b0800000000000000ab564ac92c2ec849ac74cb4ccd49f14bcc4d55b2520a700cf20cf650d2514a03093ae6642616a7162b59552bf93b79b93a8778ba00d5c099b550654005d1d54a791013e0b23a4a2595052091d4e2a24cb01d2140be3f58261164308a51b140b352134b4a8b40d645c7d6020011d8e66a9e000000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "1a0b6423", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "126" - ], - "responseIsBinary": false - }, - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/3/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID%2CDESIGNATED&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b08000000000000006d8fb10a83301086dfe56641a545aa9badb6d84107dd8a438a571b4845923888e4dd1bb54822dd2e77f77df96f82868a9e91f14a913539f9204490a46576cbe32a4dc081d73c8819250205441314e77b7aa9b244ef6da5632216af7e028d3e26e856bfc1c9b19f3b28385d1254fa5d2c13327f692e2b67135801ff294ac969d71a168b60d8b5f20dd1d153b5ce87440e1cd784446af239c8fdad87d3ee463f74bdc0f5c32004a56af505d59c2d4948010000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "8dcca684", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "188" - ], - "responseIsBinary": false - }, - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/10/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID%2CPARISH%2CINFORMATIO&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b0800000000000000ab564ac92c2ec849ac74cb4ccd49f14bcc4d55b2520a700cf20cf650d2514a03093ae6642616a7162b59552bf93b79b93a8778ba00d5c0993a30e548fa3cfddcfc837c1d433cfd81a2489c5aa89140c3a2ab95f220b621995452590012492d2eca04bb2704c8f707cb24821c81acb856076e00dc5a6cda834b8a32f3d2914c80abce49cd4b2fc950b2323640320bc9b1449a87a20366a6b9416d2cd0afa98925a545a0a08b8ead05003204d7d96a010000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "5ef885e1", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "187" - ], - "responseIsBinary": false - }, - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/2/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b0800000000000000ab564ac92c2ec849ac74cb4ccd49f14bcc4d55b2520a700cf20cf650d2514a03093ae6642616a7162b59552bf93b79b93a8778ba00d5c099b550654005d1d54a791013e0b23a4a2595052091d4e2a24cb01d2140be3f58261164308a51b140b352134b4a8b40d645c7d6020011d8e66a9e000000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "1a0b6423", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "126" - ], - "responseIsBinary": false - }, - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/4/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b0800000000000000ab564ac92c2ec849ac74cb4ccd49f14bcc4d55b252f2730d77f374f57151d2514a03093be6642616a7162b59552bf93b79b93a8778ba0055c199b550654005d1d54a791033e0b23a4a2595052091d4e2a24cb02d2140be3f58261164308a51b140b352134b4a8b40d645c7d6020054cb60e7a0000000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "4f0f77a6", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "128" - ], - "responseIsBinary": false - }, - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/7/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID%2CGROUPS%2CSPECIES&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b0800000000000000ab564ac92c2ec849ac74cb4ccd49f14bcc4d55b252720ff20f0d0856d2514a03093ae6642616a7162b59552bf93b79b93a8778ba00d5c0993a30e548fa82035c9d3d5d414230562dd430a031d1d54a79107b90cc28a92c0089a4161765825d1202e4fb83651241d6232baed5811b00b7109bf6e092a2ccbc742413e0aa7352f3d24b3294ac8c0c90cc82b99448c310ca61a6191a18d4c602bd999a58525a040aafe8d85a007c45e3fd5f010000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "a53e735b", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "183" - ], - "responseIsBinary": false - }, - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/8/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID%2CLOCATION&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b0800000000000000ab564ac92c2ec849ac74cb4ccd49f14bcc4d55b2520a700cf20cf650d2514a03093ae6642616a7162b59552bf93b79b93a8778ba00d5c0993a4a3efece8e219efe7e405138b316aa19a82dba5a290f622e929e92ca0290486a715126d8e61020df1f2c9308b20e5971ad0edc00b8f1d80d082e29cacc4b473203497d4e6a5e7a49869295b9416d2cd06da98925a545204f45c7d602008a2779f204010000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "e3cbe4df", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "168" - ], - "responseIsBinary": false - }, - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/9/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID%2CLOCATION&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b0800000000000000ab564ac92c2ec849ac74cb4ccd49f14bcc4d55b252f2f177760cf1f4f753d2514a03093be6642616a7162b59552bf93b79b93a8778ba0055c1993a080d487a6ba19a81daa2ab95f2202623e929a92c0089a416176582ed0e01f2fdc1328920eb9015d7eac00d40721a3603824b8a32f3d291cc40529f939a975e92a1646562501b0b745b6a62496911c853d1b1b500999c6bee06010000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "bfee46e4", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "161" - ], - "responseIsBinary": false - }, - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/5/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID%2CAREAS&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b0800000000000000ab564ac92c2ec849ac74cb4ccd49f14bcc4d55b252720c72750c56d2514a038939e6642616a7162b59552bf93b79b93a8778ba0095c0993a50d5305db5506d400dd1d54a79100391549754168044528b8b32c1568600f9fe6099449045c88a6b75e006c05c844d77704951665e3a920130c539a979e925194a564606b5b14057a526969416813c121d5b0b00a81ef63cf7000000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "b7115859", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "158" - ], - "responseIsBinary": false - }, - { - "scope": "https://mapping.braintree.gov.uk:443", - "method": "GET", - "path": "//arcgis/rest/services/PlanX/PlanX/MapServer/6/query?where=1%3D1&geometryType=esriGeometryEnvelope&inSR=27700&spatialRel=esriSpatialRelIntersects&returnGeometry=false&outSR=4326&f=json&outFields=OBJECTID%2CSPECIES%2CREFERENCE_&geometry=575628.95%2C223122.05%2C575629.05%2C223121.95", - "body": "", - "status": 200, - "response": [ - "1f8b08000000000000008d903d0f82301086ffcbcd1d30c6854df148700043d908314da8d8a41242eb4048ffbb2d0a94c4c1ed3ede7bdebb1ba116aa936c88059775ca9e1c42c831c61cd3086f40e0ee1a472998e20ac211b2d305a322395bdd1212a0578c12a4b63847c4c76c98e60bb5b87284f6e3e9b1f4d0b90a57bd98b62a6c9e4d1de6d6f0c5862c80d5f7d73cd5bd681b0fb1ca256f1bfd807017041e6ef383bf889b8919ba3f98ca9ecb997ef5ee7f6565dea4e3b94a73010000" - ], - "rawHeaders": [ - "Cache-Control", - "public, must-revalidate, max-age=0", - "Content-Type", - "application/json;charset=UTF-8", - "Content-Encoding", - "gzip", - "Expires", - "Thu, 01 Jan 1970 00:00:00 GMT", - "ETag", - "80e16265", - "Vary", - "Origin", - "Server", - "Microsoft-IIS/10.0", - "X-Content-Type-Options", - "nosniff", - "X-XSS-Protection", - "1; mode=block", - "x-esri-ftiles-cache-compress", - "true", - "Set-Cookie", - "AGS_ROLES=\"419jqfa+uOZgYod4xPOQ8Q==\"; Version=1; Max-Age=60; Expires=Mon, 20-Jun-2022 11:45:22 GMT; Path=/arcgis/rest; Secure; HttpOnly", - "Server", - "", - "X-AspNet-Version", - "4.0.30319", - "X-Powered-By", - "ASP.NET", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close", - "Content-Length", - "190" - ], - "responseIsBinary": false - }, - { - "scope": "http://127.0.0.1:50475", - "method": "GET", - "path": "/gis/braintree?x=575629.54&y=223122.85&siteBoundary=[]", - "body": "", - "status": 200, - "response": { - "designated.AONB": { - "text": "is not in an Area of Outstanding Natural Beauty", - "value": false, - "type": "check", - "data": [] - }, - "designated.nationalPark": { - "text": "is not in a National Park", - "value": false, - "type": "check", - "data": [] - }, - "designated.broads": { - "text": "is not in a Broad", - "value": false, - "type": "check", - "data": [] - }, - "designated.WHS": { - "text": "is not an UNESCO World Heritage Site", - "value": false, - "type": "check", - "data": [] - }, - "listed.grade1": { "value": false }, - "listed.grade2": { "value": false }, - "listed.grade2star": { "value": false }, - "designated.conservationArea": { - "text": "is in a Conservation Area", - "description": null, - "value": true, - "type": "warning", - "data": { "OBJECTID": 38, "DESIGNATED": "19/06/1969" } - }, - "nature.SSSI": { - "text": "is not a Site of Special Scientific Interest", - "value": false, - "type": "check", - "data": {} - }, - "monument": { - "text": "is not the site of a Scheduled Ancient Monument", - "value": false, - "type": "check", - "data": {} - }, - "article4": { - "text": "is not subject to local permitted development restrictions (known as Article 4 directions)", - "value": false, - "type": "check", - "data": {} - }, - "tpo": { - "text": "is not in a TPO (Tree Preservation Order) Zone", - "value": false, - "type": "check", - "data": {} - }, - "listed": { - "text": "is not in, or within, a Listed Building", - "value": false, - "type": "check", - "data": {} - }, - "designated": { "value": true } - }, - "rawHeaders": [ - "X-Powered-By", - "Express", - "Access-Control-Allow-Origin", - "*", - "Access-Control-Allow-Headers", - "Origin, X-Requested-With, Content-Type, Accept", - "Access-Control-Allow-Credentials", - "true", - "Content-Type", - "application/json; charset=utf-8", - "Content-Length", - "1230", - "ETag", - "W/\"4ce-65F+71DwzZzn+tgPMmDSUgzd+j0\"", - "Date", - "Mon, 20 Jun 2022 11:44:22 GMT", - "Connection", - "close" - ], - "responseIsBinary": false - } -] diff --git a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx index f622093b59..34ae0fd68e 100644 --- a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx +++ b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx @@ -38,22 +38,22 @@ function Component(props: Props) { currentCardId, cachedBreadcrumbs, teamSlug, - siteBoundary, - { x, y, longitude, latitude, usrn }, hasPlanningData, + siteBoundary, priorOverrides, + { longitude, latitude, usrn }, ] = useStore((state) => [ state.currentCard?.id, state.cachedBreadcrumbs, state.teamSlug, - state.computePassport().data?.["property.boundary.site"], - (state.computePassport().data?.["_address"] as SiteAddress) || {}, state.teamIntegrations?.hasPlanningData, + state.computePassport().data?.["property.boundary.site"], state.computePassport().data?.["_overrides"], + (state.computePassport().data?.["_address"] as SiteAddress) || {}, ]); // PlanningConstraints must come after at least a FindProperty in the graph - const showGraphError = !x || !y || !longitude || !latitude; + const showGraphError = !longitude || !latitude; if (showGraphError) throw new GraphError("mapInputFieldMustFollowFindProperty"); @@ -69,32 +69,22 @@ function Component(props: Props) { const urlSearchParams = new URLSearchParams(window.location.search); const params = Object.fromEntries(urlSearchParams.entries()); - // Get the coordinates of the site boundary drawing if they exist, fallback on x & y if file was uploaded - // Coords should match Esri's "rings" type https://developers.arcgis.com/javascript/3/jsapi/polygon-amd.html#rings - const coordinates: number[][][] = siteBoundary?.geometry?.coordinates || []; - - // Get the WKT representation of the site boundary drawing or address point to pass to Digital Land, when applicable + // Get the WKT representation of the site boundary drawing or address point to pass to Planning Data const wktPoint = `POINT(${longitude} ${latitude})`; const wktPolygon: string | undefined = siteBoundary && stringify(siteBoundary); - const digitalLandParams: Record = { + const planningDataParams: Record = { geom: wktPolygon || wktPoint, ...params, }; - const customGisParams: Record = { - x: x?.toString(), - y: y?.toString(), - siteBoundary: JSON.stringify(coordinates), - version: "1", - }; // Fetch planning constraints data for a given local authority const root = `${import.meta.env.VITE_APP_API_URL}/gis/${teamSlug}?`; const teamGisEndpoint: string = root + new URLSearchParams( - hasPlanningData ? digitalLandParams : customGisParams, + hasPlanningData ? planningDataParams : undefined, ).toString(); const fetcher: Fetcher = ( @@ -105,11 +95,9 @@ function Component(props: Props) { mutate, error: dataError, isValidating, - } = useSWR( - () => (x && y && latitude && longitude ? teamGisEndpoint : null), - fetcher, - { revalidateOnFocus: false }, - ); + } = useSWR(() => (latitude && longitude ? teamGisEndpoint : null), fetcher, { + revalidateOnFocus: false, + }); // If an OS address was selected, additionally fetch classified roads (available nationally) using the USRN identifier, // skip if the applicant plotted a new non-UPRN address on the map @@ -127,9 +115,9 @@ function Component(props: Props) { { revalidateOnFocus: false }, ); - // XXX handle both/either Digital Land response and custom GIS hookup responses; merge roads for a unified list of constraints + // Merge Planning Data and Roads responses for a unified list of constraints const constraints: GISResponse["constraints"] | Record = { - ...(data?.constraints || data), + ...data?.constraints, ...roads?.constraints, }; @@ -154,8 +142,6 @@ function Component(props: Props) { ...roads, planxRequest: classifiedRoadsEndpoint, } as EnhancedGISResponse); - } else { - if (data) _constraints.push(data as GISResponse["constraints"]); } const hasInaccurateConstraints =