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