freealberta/map/app/controllers/externalDataController.js

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();