| |
|
|
| 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] |
| rba?: number |
| 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 }> |
| } |
|
|
| |
| 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[] = [] |
| |
| |
| 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) |
| } |
| } |
| |
| |
| 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 |
| } |
| |
| |
| const totalUnits = parseInt(getValue('Number Of Units')) || 0 |
| const stories = parseInt(getValue('Number Of Stories')) || 0 |
| |
| |
| 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, |
| } |
| |
| |
| 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], |
| 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) |
| |
| |
| const developerCounts = new Map<string, { count: number; units: number }>() |
| 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) |
| |
| |
| const submarketCounts = new Map<string, { existing: number; proposed: number }>() |
| 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)) |
| |
| |
| const timelineCounts = new Map<number, { count: number; units: number }>() |
| 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) |
| |
| |
| const typeCounts = new Map<string, number>() |
| 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 |
| } |
| } |
|
|
| |
| 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 |
| }) |
| } |
|
|
| |
| export function getMarketDensity(buildings: VancouverBuilding[]): Array<{ |
| submarket: string |
| density: number |
| avgUnitsPerBuilding: number |
| totalBuildings: number |
| totalUnits: number |
| proposedShare: number |
| }> { |
| const submarketStats = new Map<string, { |
| buildings: VancouverBuilding[] |
| totalUnits: number |
| proposedCount: number |
| }>() |
| |
| 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, |
| 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 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 |
| } |
| } |
|
|