simpse / src /lib /vancouver-data.ts
canboi99's picture
Deploy Simpse real estate platform with Vancouver multifamily building interactive map
0d2425e verified
Raw
History Blame Contribute Delete
12.5 kB
// 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<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)
// Submarkets
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))
// Construction timeline (for proposed buildings)
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)
// Building types
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
}
}
// 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<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, // 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
}
}