133 lines
5.3 KiB
JavaScript
133 lines
5.3 KiB
JavaScript
const socrataService = require('../services/socrata');
|
|
const logger = require('../utils/logger');
|
|
|
|
const EDMONTON_PARCEL_ADDRESSES_ID = 'nggt-rwac';
|
|
|
|
class ExternalDataController {
|
|
/**
|
|
* Fetches parcel addresses from the City of Edmonton open data portal.
|
|
* Uses a simple SoQL query to get points with valid locations.
|
|
*/
|
|
async getEdmontonParcelAddresses(req, res) {
|
|
try {
|
|
logger.info('Fetching Edmonton parcel addresses from Socrata API');
|
|
|
|
// Get query parameters for filtering and pagination
|
|
const {
|
|
bounds,
|
|
zoom = 10,
|
|
limit = 2000,
|
|
offset = 0,
|
|
neighborhood
|
|
} = req.query;
|
|
|
|
// Build dynamic query based on zoom level and bounds
|
|
let whereClause = 'location IS NOT NULL';
|
|
let selectFields = 'house_number, street_name, sub_address, neighbourhood_name, object_type, location, latitude, longitude';
|
|
|
|
// If bounds are provided, filter by geographic area
|
|
if (bounds) {
|
|
try {
|
|
const boundsArray = bounds.split(',').map(Number);
|
|
if (boundsArray.length === 4) {
|
|
const [south, west, north, east] = boundsArray;
|
|
whereClause += ` AND latitude BETWEEN ${south} AND ${north} AND longitude BETWEEN ${west} AND ${east}`;
|
|
logger.info(`Filtering by bounds: ${bounds}`);
|
|
}
|
|
} catch (error) {
|
|
logger.warn('Invalid bounds parameter:', bounds);
|
|
}
|
|
}
|
|
|
|
// Filter by neighborhood if specified
|
|
if (neighborhood) {
|
|
whereClause += ` AND neighbourhood_name = '${neighborhood.toUpperCase()}'`;
|
|
}
|
|
|
|
// Adjust limit based on zoom level - show fewer points when zoomed out
|
|
const dynamicLimit = Math.min(parseInt(zoom) < 12 ? 500 : parseInt(zoom) < 15 ? 1500 : 2000, parseInt(limit));
|
|
|
|
const params = {
|
|
'$select': selectFields,
|
|
'$where': whereClause,
|
|
'$limit': dynamicLimit,
|
|
'$offset': parseInt(offset),
|
|
'$order': 'house_number'
|
|
};
|
|
|
|
const data = await socrataService.get(EDMONTON_PARCEL_ADDRESSES_ID, params);
|
|
|
|
logger.info(`Successfully fetched ${data.length} Edmonton parcel addresses (zoom: ${zoom}, bounds: ${bounds})`);
|
|
|
|
// Group addresses by location to identify multi-unit buildings
|
|
const locationGroups = new Map();
|
|
|
|
data.filter(item => item.location && item.location.coordinates).forEach(item => {
|
|
const locationKey = `${item.latitude}_${item.longitude}`;
|
|
const address = `${item.house_number || ''} ${item.street_name || ''}`.trim();
|
|
|
|
if (!locationGroups.has(locationKey)) {
|
|
locationGroups.set(locationKey, {
|
|
address: address || 'No address',
|
|
location: item.location,
|
|
latitude: parseFloat(item.latitude),
|
|
longitude: parseFloat(item.longitude),
|
|
neighbourhood_name: item.neighbourhood_name || '',
|
|
suites: []
|
|
});
|
|
}
|
|
|
|
locationGroups.get(locationKey).suites.push({
|
|
suite: item.sub_address || item.suite || '',
|
|
object_type: item.object_type || 'SUITE',
|
|
record_id: item.record_id || '',
|
|
house_number: item.house_number || '',
|
|
street_name: item.street_name || ''
|
|
});
|
|
});
|
|
|
|
// Transform grouped data into GeoJSON FeatureCollection
|
|
const validFeatures = Array.from(locationGroups.values()).map(group => ({
|
|
type: 'Feature',
|
|
properties: {
|
|
address: group.address,
|
|
neighborhood: group.neighbourhood_name,
|
|
suites: group.suites,
|
|
suiteCount: group.suites.length,
|
|
isMultiUnit: group.suites.length > 3,
|
|
lat: group.latitude,
|
|
lng: group.longitude
|
|
},
|
|
geometry: group.location
|
|
}));
|
|
|
|
const geoJson = {
|
|
type: 'FeatureCollection',
|
|
features: validFeatures,
|
|
metadata: {
|
|
count: validFeatures.length,
|
|
zoom: zoom,
|
|
bounds: bounds,
|
|
hasMore: validFeatures.length === dynamicLimit // Indicates if there might be more data
|
|
}
|
|
};
|
|
|
|
logger.info(`Processed ${validFeatures.length} valid features`);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: geoJson
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Error fetching Edmonton parcel addresses:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch external map data.'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new ExternalDataController();
|