"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;