controllers/all-controller.js

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchParcelData = fetchParcelData;
exports.fetchAdjoinersData = fetchAdjoinersData;
exports.fetchZoningData = fetchZoningData;
exports.fetchFloodData = fetchFloodData;
const express_1 = __importDefault(require("express"));
const database_1 = require("../config/database");
const fetch_from_mongo_1 = require("../fetch/fetch-from-mongo");
const fetch_parcel_by_pin_1 = require("../fetch/fetch-parcel-by-pin");
const fetch_parcel_by_polygon_1 = require("../fetch/fetch-parcel-by-polygon");
const axios_1 = __importDefault(require("axios"));
const fetch_zoning_by_post_1 = require("../fetch/fetch-zoning-by-post");
const fetch_flood_by_post_1 = require("../fetch/fetch-flood-by-post");
const calculate_centroid_1 = require("../utils/polyline/calculate-centroid");
const constants_1 = require("../utils/constants/constants");
const controller = express_1.default.Router();
/**
* Fetches parcel data based on state, county, and PIN
* @param state - The state parameter from the request
* @param county - The county parameter from the request
* @param pin - Optional PIN for parcel lookup
* @param res - Express Response object
*/
function fetchParcelData(state_1, county_1, pin_1) {
    return __awaiter(this, arguments, void 0, function* (state, county, pin, wgs84 = false) {
        const layer = 'VB-PROP-BNDY-LINE-E';
        try {
            const countySchema = yield (0, fetch_from_mongo_1.fetchCountySchema)(state, county);
            if (!pin) {
                pin = countySchema.examplePin;
            }
            let pinString = `${pin}`;
            const pinType = countySchema.esriPinType;
            if (pinType === 'esriFieldTypeString') {
                pinString = `'${pin}'`;
            }
            const parcelUrl = countySchema.baseUrls.parcel;
            let spatialReference = countySchema.spatialReference;
            if (wgs84) {
                spatialReference = '4269';
            }
            const schema = countySchema.keys;
            const results = yield (0, fetch_parcel_by_pin_1.fetchDataForPin)(county, pinString, parcelUrl, spatialReference, schema, layer);
            return results; // Return the data instead of sending it
        }
        catch (error) {
            console.error('Error fetching parcel data:', error);
            throw new Error('Error fetching parcel data');
        }
    });
}
function fetchAdjoinersData(state_1, county_1, pin_1) {
    return __awaiter(this, arguments, void 0, function* (state, county, pin, wgs84 = false) {
        if (!pin) {
            throw new Error('Missing required query parameter: pin');
        }
        let pinString = `${pin}`;
        try {
            const countySchema = yield (0, fetch_from_mongo_1.fetchCountySchema)(state, county);
            const layersDb = yield (0, database_1.connectToDatabase)('available_layers');
            const layerCollection = yield layersDb.collection('layers').findOne({}, { projection: { _id: 0 } });
            const layer = layerCollection && layerCollection['parcel'] ? layerCollection['parcel'] : '0';
            const pinType = countySchema.esriPinType;
            if (pinType === 'esriFieldTypeString') {
                pinString = `'${pin}'`;
            }
            const parcelUrl = countySchema.baseUrls.parcel;
            let spatialReference = countySchema.spatialReference;
            if (wgs84) {
                spatialReference = '4269';
            }
            const schema = countySchema.keys;
            const pinKey = schema['pinKey'];
            const response = yield axios_1.default.get(`${parcelUrl}?where=${pinKey}=${pinString}&inSR=${spatialReference}&outSR=${spatialReference}&outFields=*&f=pjson`, { httpsAgent: constants_1.agent });
            const rings = response.data.features[0].geometry;
            const { features } = yield (0, fetch_parcel_by_polygon_1.fetchParcelByPolygon)(county, parcelUrl, spatialReference, rings, schema, layer, 'adjoiners');
            return { features }; // Return the features instead of sending them
        }
        catch (e) {
            console.error('Error fetching adjoiners data:', e);
            throw new Error('Error fetching adjoiners data');
        }
    });
}
/**
* Fetches zoning data based on state, county, and geometry
* @param state - The state parameter from the request
* @param county - The county parameter from the request
* @param geometry - Geometry query parameter
* @param res - Express Response object
* @returns Promise<void>
*/
function fetchZoningData(state_1, county_1, geometry_1) {
    return __awaiter(this, arguments, void 0, function* (state, county, geometry, wgs84 = false) {
        let parsedGeometry;
        if (!geometry) {
            throw new Error('Missing required query parameter: geometry');
        }
        try {
            parsedGeometry = JSON.parse(decodeURIComponent(geometry));
        }
        catch (e) {
            console.error('Error parsing geometry:', e);
            throw new Error('Invalid geometry parameter');
        }
        try {
            const countySchema = yield (0, fetch_from_mongo_1.fetchCountySchema)(state, county);
            const layersDb = yield (0, database_1.connectToDatabase)('available_layers');
            const layerCollection = yield layersDb.collection('layers').findOne({}, { projection: { _id: 0 } });
            const layer = layerCollection && layerCollection['zoning'] ? layerCollection['zoning'] : '0';
            const zoningUrl = countySchema.baseUrls.zoning;
            ;
            let spatialReference = countySchema.spatialReference;
            if (wgs84) {
                spatialReference = '4269';
            }
            const schema = countySchema.keys;
            const features = yield (0, fetch_zoning_by_post_1.fetchZoningByPost)(county, zoningUrl, spatialReference, parsedGeometry, schema, layer);
            return features;
        }
        catch (e) {
            console.error('Error fetching zoning data:', e);
            throw new Error('An error occurred while fetching zoning data');
        }
        finally {
            //await closeClient();
        }
    });
}
/**
* Fetches flood data based on state, county, and geometry
* if wgs84 is true, the spatial reference will be 4269, this is mainly usef for leaflet maps to prevent projection manipulation on the front end
* @param state - The state parameter from the request
* @param county - The county parameter from the request
* @param geometry - Geometry query parameter
* @param res - Express Response object
* @returns Promise<void>
* @example
* fetchFloodData('south-carolina', 'horry', 'geometry');
*/
function fetchFloodData(state_1, county_1, geometry_1) {
    return __awaiter(this, arguments, void 0, function* (state, county, geometry, wgs84 = false) {
        let parsedGeometry;
        if (!geometry) {
            throw new Error('Missing required query parameter: geometry');
        }
        try {
            parsedGeometry = JSON.parse(decodeURIComponent(geometry));
        }
        catch (e) {
            console.error('Error parsing geometry:', e);
            throw new Error('Invalid geometry parameter');
        }
        try {
            const countySchema = yield (0, fetch_from_mongo_1.fetchCountySchema)(state, county);
            const layersDb = yield (0, database_1.connectToDatabase)('available_layers');
            const layerCollection = yield layersDb.collection('layers').findOne({}, { projection: { _id: 0 } });
            const layer = layerCollection && layerCollection['flood'] ? layerCollection['flood'] : "0";
            console.log(layer);
            const floodUrl = 'https://hazards.fema.gov/arcgis/rest/services/public/NFHLWMS/MapServer/28/query';
            let spatialReference = countySchema.spatialReference;
            if (wgs84) {
                spatialReference = '4269';
            }
            const schema = countySchema.keys;
            const features = yield (0, fetch_flood_by_post_1.fetchFloodByPost)(county, floodUrl, spatialReference, parsedGeometry, schema, layer);
            return features;
        }
        catch (e) {
            console.error('Error fetching flood data:', e);
            throw new Error('An error occurred while fetching flood data');
        }
        finally {
            //await closeClient();
        }
    });
}
/**
* @name AllController
* @description This controller is responsible for handling all requests
* @path /
* @method GET
* @param {string} pin - The pin to be used
* @returns {string} - A string response
* @example
*     "attributes": {
"spatialReference": "2273",
"LegalDescr": "BRIDGE CREEK PH II; LT 2 BL E",
"OwnerName": "N/F PETRARCA SCOTT D ",
"PIN": 44005020060,
"Address": "809 PEBBLE CREEK DR MYRTLE BEACH SC 29588",
"DeedBook": "3975",
"DeedPage": "1705",
"PlatBook": null,
"PlatPage": null,
"platRefText": "REFERENCE TO A MAP RECORDED IN PLAT BOOK: XXX, PAGE XXX AT THE HORRY COUNTY REGISTER OF DEEDS.",
"lastPropertySaleText": "LAST PROPERTY TRANSFER: 12/18/2016",
"lastPropertySaleDeedText": "DEED BOOK: 3975  PAGE: 1705"
},
* */
controller.get('/:state/:county/:pin', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
    const { state, county, pin } = req.params;
    const wgs84 = req.query.wgs84;
    let isWgs84 = false;
    if (wgs84) {
        isWgs84 = true;
    }
    console.log('isWgs84', isWgs84);
    try {
        const parcelData = yield fetchParcelData(state, county, pin, isWgs84);
        const [adjoinersData] = yield Promise.all([
            fetchAdjoinersData(state, county, pin, isWgs84),
        ]);
        if (!parcelData.features[0].geometry) {
            throw new Error('No geometry found');
        }
        const zoning = yield fetchZoningData(state, county, encodeURIComponent(JSON.stringify(parcelData.features[0].geometry)), isWgs84);
        const flood = yield fetchFloodData(state, county, encodeURIComponent(JSON.stringify(parcelData.features[0].geometry)), isWgs84);
        const finalData = [
            ...parcelData.features,
            ...adjoinersData.features,
            ...zoning.features,
            ...flood.features
        ];
        let allDeeds = [];
        let allPlats = [];
        let allFloodZones = [];
        let allZoning = [];
        const data = finalData.map((feature) => {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
            if (((_a = feature.attributes) === null || _a === void 0 ? void 0 : _a.DeedBook) && ((_b = feature.attributes) === null || _b === void 0 ? void 0 : _b.DeedPage) && ((_c = feature.attributes) === null || _c === void 0 ? void 0 : _c.DeedBook) !== 'XXX' && ((_d = feature.attributes) === null || _d === void 0 ? void 0 : _d.DeedPage) !== 'XXX') {
                allDeeds.push({
                    deedReference: `DB: ${feature.attributes.DeedBook} Pg. ${feature.attributes.DeedPage}`
                });
            }
            if (((_e = feature.attributes) === null || _e === void 0 ? void 0 : _e.PlatBook) && ((_f = feature.attributes) === null || _f === void 0 ? void 0 : _f.PlatPage) && ((_g = feature.attributes) === null || _g === void 0 ? void 0 : _g.PlatBook) !== 'XXX' && ((_h = feature.attributes) === null || _h === void 0 ? void 0 : _h.PlatPage) !== 'XXX') {
                allPlats.push({
                    platReference: `PB: ${feature.attributes.PlatBook} Pg. ${feature.attributes.PlatPage}`
                });
            }
            if ((_j = feature.attributes) === null || _j === void 0 ? void 0 : _j.FLD_ZONE) {
                allFloodZones.push({
                    floodZone: feature.attributes.FLD_ZONE,
                    firm: `${feature.attributes.DFIRM_ID} ${feature.attributes.PANEL} ${feature.attributes.SUFFIX}`,
                    effectiveDate: feature.attributes.EFF_DATE
                });
            }
            if ((_k = feature.attributes) === null || _k === void 0 ? void 0 : _k.zoning) {
                allZoning.push({
                    zoning: feature.attributes.zoning
                });
            }
            return {
                type: feature.type,
                layer: feature.layer,
                attributes: feature.attributes,
                centroid: feature.centroid,
                geometry: feature.geometry
            };
        }).flatMap((feature) => feature);
        res.send({
            features: data,
            deeds: getUniqueDeedReference(allDeeds),
            plats: getUniquePlatReference(allPlats),
            floodZones: `${getUniqueFirmPanel(allFloodZones)} ${generateFloodZoneDescription(sortFloodZones(getUniqueFloodZones(allFloodZones)))}`,
            zoning: getUniqueZoning(allZoning)
        });
    }
    catch (error) {
        console.error(error);
        res.status(500).send('An error occurred while processing the request');
    }
}));
function generateFloodZoneDescription(sortedFloodZones) {
    const count = sortedFloodZones.length;
    if (count === 0) {
        return "Parcel is not in any flood zone.";
    }
    else if (count === 1) {
        return `Parcel appears to be located in flood zone ${sortedFloodZones[0]}.`;
    }
    else if (count === 2) {
        return `Parcel appears to be located in flood zones ${sortedFloodZones[0]} & ${sortedFloodZones[1]}.`;
    }
    else {
        const lastZone = sortedFloodZones.pop(); // Get the last flood zone
        return `Parcel appears to be located in flood zones ${sortedFloodZones.join(", ")}, & ${lastZone}.`;
    }
}
function sortFloodZones(floodZones) {
    return floodZones.sort((a, b) => {
        // If "X" exists, it should come first
        if (a === "X")
            return -1;
        if (b === "X")
            return 1;
        // If "X-SHADED" exists, it should come second
        if (a === "X-SHADED")
            return -1;
        if (b === "X-SHADED")
            return 1;
        // For the rest, sort alphabetically and numerically
        const [aAlpha, aNum] = splitAlphaNum(a);
        const [bAlpha, bNum] = splitAlphaNum(b);
        if (aAlpha < bAlpha)
            return -1;
        if (aAlpha > bAlpha)
            return 1;
        return parseInt(aNum) - parseInt(bNum);
    });
}
function splitAlphaNum(floodZone) {
    const match = floodZone.match(/^([A-Za-z]+)(\d+)?$/);
    if (match) {
        return [match[1], match[2] || "0"]; // Default to "0" if no number exists
    }
    return [floodZone, "0"]; // Default to floodZone and "0" if format doesn't match
}
function getUniqueFloodZones(allFloodZones) {
    const uniqueFloodZones = new Set();
    allFloodZones.forEach(zone => {
        if (zone.floodZone) {
            uniqueFloodZones.add(zone.floodZone);
        }
    });
    return Array.from(uniqueFloodZones);
}
function getUniqueFirmPanel(allFloodZones) {
    const uniqueFirmPanels = new Set();
    allFloodZones.forEach(zone => {
        if (zone.firm) {
            uniqueFirmPanels.add(zone.firm);
        }
    });
    return Array.from(uniqueFirmPanels);
}
function getUniqueZoning(allZoning) {
    const uniqueZoning = new Set();
    allZoning.forEach(zone => {
        if (zone.zoning) {
            uniqueZoning.add(zone.zoning);
        }
    });
    return Array.from(uniqueZoning);
}
function getUniqueDeedReference(allDeeds) {
    const uniqueDeedReference = new Set();
    allDeeds.forEach(deedRef => {
        if (deedRef.deedReference) {
            uniqueDeedReference.add(deedRef.deedReference);
        }
    });
    return Array.from(uniqueDeedReference);
}
function getUniquePlatReference(allPlats) {
    const uniquePlatReference = new Set();
    allPlats.forEach(platRef => {
        if (platRef.platReference) {
            uniquePlatReference.add(platRef.platReference);
        }
    });
    return Array.from(uniquePlatReference);
}
function formatPIN(pin) {
    // Convert the number to a string and insert hyphens at the appropriate positions
    const pinStr = pin.toString();
    return `${pinStr.slice(0, 3)}-${pinStr.slice(3, 5)}-${pinStr.slice(5, 7)}-${pinStr.slice(7)}`;
}
controller.get('/research/:pins', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
    const pins = req.params.pins.split(',');
    const fetchDataForPin = (pin) => __awaiter(void 0, void 0, void 0, function* () {
        var _a, _b;
        const urls = [
            `https://www.horrycountysc.gov/parcelapp/rest/services/HorryCountyGISApp/MapServer/24/query?where=PIN=${pin}&outSR=2273&outFields=*&f=pjson`,
            `https://www.horrycounty.org/apps/LandRecords/memos/${pin}`,
            `https://www.horrycounty.org/apps/LandRecords/sales/${pin}`
        ];
        try {
            const responses = yield Promise.all(urls.map(url => axios_1.default.get(url, { httpsAgent: constants_1.agent })));
            const data = responses.map(response => response.data);
            const adjoiners = yield fetchAdjoinersData('south-carolina', 'horry', pin);
            if (!data[0] || !((_b = (_a = data[0].features) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.attributes)) {
                throw new Error(`Invalid data structure for attributes for PIN ${pin}`);
            }
            const dataAttr = Object.assign({}, data[0].features[0].attributes);
            const notes = data[1];
            const sales = data[2];
            const geometry = data[0].features[0].geometry;
            const rings = geometry.rings;
            const polyline = (0, calculate_centroid_1.convertRingsToPolyline)(rings);
            const centroid = (0, calculate_centroid_1.calculateCentroid)(polyline);
            const pbRegex = /PB\s?(\d+)[^\d]*(\d+)?/i;
            const dbRegex = /DB(\d+)-(\d+)/gi;
            const pbSet = new Set();
            const deedSet = new Set();
            const adjPbSet = new Set();
            const adjDeedSet = new Set();
            // Process notes
            notes.forEach((note) => {
                const pbMatch = note.Note.match(pbRegex);
                if (pbMatch) {
                    const bookNumber = pbMatch[1];
                    const pageNumber = pbMatch[2] ? parseInt(pbMatch[2], 10).toString() : 'N/A';
                    pbSet.add(`PB: ${bookNumber} Pg. ${pageNumber}`);
                }
                let dbMatch;
                while ((dbMatch = dbRegex.exec(note.Note)) !== null) {
                    deedSet.add(`DB: ${dbMatch[1]} Pg. ${dbMatch[2]}`);
                }
            });
            // Process sales
            sales.forEach((sale) => {
                deedSet.add(`DB: ${sale.DeedBook} Pg. ${sale.DeedPage}`);
            });
            const adjMemos = adjoiners.features.map((feature) => ({
                url: `https://www.horrycounty.org/apps/LandRecords/memos/${feature.attributes.PIN}`,
                geometry: feature.geometry,
                centroid: feature.centroid,
            }));
            if (adjMemos.length === 0) {
                throw new Error('No adjacent memos found');
            }
            // Fetch adjacent memos
            const adjRes = yield Promise.all(adjMemos.map((memo) => axios_1.default.get(memo.url, { httpsAgent: constants_1.agent }).then(response => ({
                data: response.data,
                geometry: memo.geometry,
                centroid: memo.centroid,
            }))));
            // Group adjacent data by geometry and centroid
            const groupedAdjData = adjRes.map(({ data, geometry, centroid }) => ({
                geometry,
                centroid,
                notes: data.map((note) => note.Note), // Collect all notes for this group
            }));
            // Extract PBs and DBs from grouped notes
            groupedAdjData.forEach(group => {
                group.notes.forEach((note) => {
                    const pbMatch = note.match(pbRegex);
                    if (pbMatch) {
                        const bookNumber = pbMatch[1];
                        const pageNumber = pbMatch[2] ? parseInt(pbMatch[2], 10).toString() : 'N/A';
                        adjPbSet.add(`PB: ${bookNumber} Pg. ${pageNumber}`);
                    }
                    let dbMatch;
                    while ((dbMatch = dbRegex.exec(note)) !== null) {
                        adjDeedSet.add(`DB: ${dbMatch[1]} Pg. ${dbMatch[2]}`);
                    }
                });
            });
            const pbArray = Array.from(pbSet);
            const deedArray = Array.from(deedSet);
            const adjPbArray = Array.from(adjPbSet);
            const adjDbArray = Array.from(adjDeedSet);
            return {
                LegalDescr: dataAttr.LegalDescr,
                OwnerName: `N/F ${dataAttr.OwnerName}`,
                PIN: `${formatPIN(dataAttr.PIN)}`,
                deedText: deedArray[0],
                platText: pbArray[0] || '',
                plats: pbArray,
                deeds: deedArray,
                sales,
                centroid,
                geometry,
                groupedAdjData,
                adjPbArray,
                adjDbArray,
            };
        }
        catch (error) {
            if (error instanceof Error) {
                console.error(`Error fetching data for PIN ${pin}:`, error.message);
            }
            else {
                console.error(`Error fetching data for PIN ${pin}:`, error);
            }
            return { error: `An error occurred for PIN ${pin}` };
        }
    });
    try {
        const results = yield Promise.all(pins.map(pin => fetchDataForPin(pin)));
        res.json(results);
    }
    catch (error) {
        if (error instanceof Error) {
            console.error('Error fetching data:', error.message);
        }
        else {
            console.error('Error fetching data:', error);
        }
        res.status(500).send('An error occurred while fetching data');
    }
}));
exports.default = controller;