// Vancouver Multifamily Building Data Processing Utilities export interface VancouverBuilding { id: string address: string buildingName?: string status: 'existing' | 'proposed' yearBuilt?: number buildingStatus: string units: number stories: number type: string submarket: string latitude: number longitude: number coordinates: [number, number] // [longitude, latitude] for mapping rba?: number // Rentable Building Area developer?: string propertyManager?: string zoning?: string amenities?: string[] unitMix?: { bachelors?: number oneBed?: number twoBed?: number threeBed?: number fourBed?: number } constructionBegin?: string constructionMaterial?: string averageRent?: number occupancy?: number vacancyRate?: number } export interface VancouverDataSummary { totalExisting: number totalProposed: number totalUnits: number proposedUnits: number existingUnits: number avgUnitsPerBuilding: number topDevelopers: Array<{ name: string; count: number; units: number }> submarkets: Array<{ name: string; existing: number; proposed: number }> constructionTimeline: Array<{ year: number; count: number; units: number }> buildingTypes: Array<{ type: string; count: number; percentage: number }> } // Parse CSV data from public data files export function parseCSVLine(line: string): string[] { const result = [] let current = '' let inQuotes = false for (let i = 0; i < line.length; i++) { const char = line[i] if (char === '"') { inQuotes = !inQuotes } else if (char === ',' && !inQuotes) { result.push(current.trim()) current = '' } else { current += char } } result.push(current.trim()) return result } export function processVancouverData(existingCSV: string, proposedCSV: string): { buildings: VancouverBuilding[] summary: VancouverDataSummary } { const buildings: VancouverBuilding[] = [] // Process existing buildings const existingLines = existingCSV.split('\n') const existingHeaders = parseCSVLine(existingLines[0]) for (let i = 1; i < existingLines.length; i++) { if (!existingLines[i].trim()) continue const values = parseCSVLine(existingLines[i]) try { const building = parseBuilding(values, existingHeaders, 'existing') if (building) buildings.push(building) } catch (error) { console.warn(`Error parsing existing building row ${i}:`, error) } } // Process proposed buildings const proposedLines = proposedCSV.split('\n') const proposedHeaders = parseCSVLine(proposedLines[0]) for (let i = 1; i < proposedLines.length; i++) { if (!proposedLines[i].trim()) continue const values = parseCSVLine(proposedLines[i]) try { const building = parseBuilding(values, proposedHeaders, 'proposed') if (building) buildings.push(building) } catch (error) { console.warn(`Error parsing proposed building row ${i}:`, error) } } const summary = generateSummary(buildings) return { buildings, summary } } function parseBuilding(values: string[], headers: string[], status: 'existing' | 'proposed'): VancouverBuilding | null { const getValue = (header: string): string => { const index = headers.findIndex(h => h.toLowerCase().includes(header.toLowerCase())) return index >= 0 ? values[index] || '' : '' } const address = getValue('Property Address') || getValue('Address') if (!address) return null const latitudeStr = getValue('Latitude') const longitudeStr = getValue('Longitude') const latitude = parseFloat(latitudeStr) const longitude = parseFloat(longitudeStr) if (!latitude || !longitude || isNaN(latitude) || isNaN(longitude)) { console.warn('parseBuilding: Invalid coordinates for', address, 'lat:', latitude, 'lng:', longitude) return null } // Parse unit counts const totalUnits = parseInt(getValue('Number Of Units')) || 0 const stories = parseInt(getValue('Number Of Stories')) || 0 // Parse unit mix const unitMix = { bachelors: parseInt(getValue('Number Of Bachelors')) || 0, oneBed: parseInt(getValue('Number Of 1 Bedrooms Units')) || 0, twoBed: parseInt(getValue('Number Of 2 Bedrooms Units')) || 0, threeBed: parseInt(getValue('Number Of 3 Bedrooms Units')) || 0, fourBed: parseInt(getValue('Number Of 4 Bedrooms Units')) || 0, } // Parse amenities const amenitiesText = getValue('Amenities') || '' const amenities = amenitiesText ? amenitiesText.split(',').map(a => a.trim()).filter(a => a) : [] const building: VancouverBuilding = { id: `${status}-${address.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`, address, buildingName: getValue('Property Name') || getValue('Name') || undefined, status, yearBuilt: parseInt(getValue('Year Built')) || undefined, buildingStatus: getValue('Building Status') || (status === 'existing' ? 'Existing' : 'Under Construction'), units: totalUnits, stories, type: getValue('Secondary Type') || 'Apartments', submarket: getValue('Submarket Name') || getValue('City') || 'Vancouver', latitude, longitude, coordinates: [longitude, latitude], // [lng, lat] format for mapping rba: parseInt(getValue('RBA')) || undefined, developer: getValue('Developer Name') || getValue('Owner Name') || undefined, propertyManager: getValue('Property Manager Name') || undefined, zoning: getValue('Zoning') || undefined, amenities, unitMix, constructionBegin: getValue('Construction Begin') || undefined, constructionMaterial: getValue('Construction Material') || undefined, averageRent: parseFloat(getValue('Average Weighted Rent')) || undefined, occupancy: parseFloat(getValue('Percent Leased')) || undefined, vacancyRate: parseFloat(getValue('Vacancy %')) || undefined, } return building } function generateSummary(buildings: VancouverBuilding[]): VancouverDataSummary { const existing = buildings.filter(b => b.status === 'existing') const proposed = buildings.filter(b => b.status === 'proposed') const totalUnits = buildings.reduce((sum, b) => sum + b.units, 0) const existingUnits = existing.reduce((sum, b) => sum + b.units, 0) const proposedUnits = proposed.reduce((sum, b) => sum + b.units, 0) // Top developers const developerCounts = new Map() buildings.forEach(b => { if (b.developer) { const current = developerCounts.get(b.developer) || { count: 0, units: 0 } current.count += 1 current.units += b.units developerCounts.set(b.developer, current) } }) const topDevelopers = Array.from(developerCounts.entries()) .map(([name, data]) => ({ name, ...data })) .sort((a, b) => b.units - a.units) .slice(0, 10) // Submarkets const submarketCounts = new Map() buildings.forEach(b => { const current = submarketCounts.get(b.submarket) || { existing: 0, proposed: 0 } if (b.status === 'existing') current.existing += 1 else current.proposed += 1 submarketCounts.set(b.submarket, current) }) const submarkets = Array.from(submarketCounts.entries()) .map(([name, data]) => ({ name, ...data })) .sort((a, b) => (b.existing + b.proposed) - (a.existing + a.proposed)) // Construction timeline (for proposed buildings) const timelineCounts = new Map() proposed.forEach(b => { if (b.yearBuilt) { const current = timelineCounts.get(b.yearBuilt) || { count: 0, units: 0 } current.count += 1 current.units += b.units timelineCounts.set(b.yearBuilt, current) } }) const constructionTimeline = Array.from(timelineCounts.entries()) .map(([year, data]) => ({ year, ...data })) .sort((a, b) => a.year - b.year) // Building types const typeCounts = new Map() buildings.forEach(b => { typeCounts.set(b.type, (typeCounts.get(b.type) || 0) + 1) }) const buildingTypes = Array.from(typeCounts.entries()) .map(([type, count]) => ({ type, count, percentage: Math.round((count / buildings.length) * 100 * 100) / 100 })) .sort((a, b) => b.count - a.count) return { totalExisting: existing.length, totalProposed: proposed.length, totalUnits, existingUnits, proposedUnits, avgUnitsPerBuilding: Math.round(totalUnits / buildings.length), topDevelopers, submarkets, constructionTimeline, buildingTypes } } // Filter buildings by various criteria export function filterBuildings( buildings: VancouverBuilding[], filters: { status?: 'existing' | 'proposed' | 'both' submarket?: string developer?: string minUnits?: number maxUnits?: number yearBuilt?: number buildingType?: string } ): VancouverBuilding[] { return buildings.filter(building => { if (filters.status && filters.status !== 'both' && building.status !== filters.status) { return false } if (filters.submarket && building.submarket !== filters.submarket) { return false } if (filters.developer && building.developer !== filters.developer) { return false } if (filters.minUnits && building.units < filters.minUnits) { return false } if (filters.maxUnits && building.units > filters.maxUnits) { return false } if (filters.yearBuilt && building.yearBuilt !== filters.yearBuilt) { return false } if (filters.buildingType && building.type !== filters.buildingType) { return false } return true }) } // Get market density analysis export function getMarketDensity(buildings: VancouverBuilding[]): Array<{ submarket: string density: number avgUnitsPerBuilding: number totalBuildings: number totalUnits: number proposedShare: number }> { const submarketStats = new Map() buildings.forEach(building => { if (!submarketStats.has(building.submarket)) { submarketStats.set(building.submarket, { buildings: [], totalUnits: 0, proposedCount: 0 }) } const stats = submarketStats.get(building.submarket)! stats.buildings.push(building) stats.totalUnits += building.units if (building.status === 'proposed') { stats.proposedCount += 1 } }) return Array.from(submarketStats.entries()) .map(([submarket, stats]) => ({ submarket, density: stats.totalUnits, // Could be enhanced with actual area calculations avgUnitsPerBuilding: Math.round(stats.totalUnits / stats.buildings.length), totalBuildings: stats.buildings.length, totalUnits: stats.totalUnits, proposedShare: Math.round((stats.proposedCount / stats.buildings.length) * 100) })) .sort((a, b) => b.density - a.density) } // Export functions for loading data export async function loadVancouverData(): Promise<{ buildings: VancouverBuilding[]; summary: VancouverDataSummary }> { try { console.log('loadVancouverData: Starting to fetch CSV files...') const [existingResponse, proposedResponse] = await Promise.all([ fetch('/data/vancouver-existing-mf.csv'), fetch('/data/vancouver-proposed-mf.csv') ]) console.log('loadVancouverData: Response status:', { existing: existingResponse.status, proposed: proposedResponse.status }) if (!existingResponse.ok || !proposedResponse.ok) { throw new Error(`Failed to load Vancouver data files: existing=${existingResponse.status}, proposed=${proposedResponse.status}`) } const [existingCSV, proposedCSV] = await Promise.all([ existingResponse.text(), proposedResponse.text() ]) console.log('loadVancouverData: CSV loaded, sizes:', { existingLines: existingCSV.split('\n').length, proposedLines: proposedCSV.split('\n').length }) const result = processVancouverData(existingCSV, proposedCSV) console.log('loadVancouverData: Processed data:', { buildingsCount: result.buildings.length, summary: result.summary }) return result } catch (error) { console.error('Error loading Vancouver data:', error) throw error } }