🎯 What You'll Learn

📋 Before You Begin

Table of Contents

Four High-Impact Projects

Building on the Desire Path concept, here are four projects using similar logic for different architectural problems.

1. The "Ghost Plaza" Audit (Underutilization Mapping)

Focus: Dead zones where people don't go.

Data: Areas with zero photo activity over time.

Utility: Identifies "Poche Space" for tactical urbanism (pop-up cafes, art galleries).

2. Viewshed & "Instagrammability" Indexing

Focus: Camera orientation as aesthetic value.

Data: Use GPSImgDirection to map "Visual Magnets."

Utility: Ensure new construction doesn't block valued views.

3. Kinetic Energy & "Friction" Mapping

Focus: How people move, not just where.

Data: Velocity (Δd/Δt) between consecutive points.

Utility: Identify circulation conflicts in narrow hallways.

4. Semantic Environment Mapping

Focus: Image content + spatial location.

Data: Use CLIP to tag images ("Greenery," "Crowded," "Damaged").

Utility: Create sentiment maps for maintenance and wayfinding.

Project Focus Primary Metric Outcome
Desire Path Efficiency Path Deviation Pave new shortcuts
Ghost Plaza Waste Activity Absence Repurpose dead space
Viewshed Aesthetics Camera Heading Protect visual landmarks
Kinetic Mapping Comfort Velocity/Stay-time Resolve bottlenecks

Velocity & Friction Mapping

Calculate velocity using the Haversine formula for spherical coordinates.

python
import math

def haversine(coord1, coord2):
    R = 6371000  # Radius of earth in meters
    lat1, lon1 = math.radians(coord1[0]), math.radians(coord1[1])
    lat2, lon2 = math.radians(coord2[0]), math.radians(coord2[1])
    
    dlat = lat2 - lat1
    dlon = lon2 - lon1
    
    a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    return R * c

def calculate_kinetics(df):
    df = df.sort_values('time').reset_index(drop=True)
    df['velocity_kmh'] = 0.0
    
    for i in range(1, len(df)):
        dist = haversine((df.iloc[i-1]['lat'], df.iloc[i-1]['lon']), 
                         (df.iloc[i]['lat'], df.iloc[i]['lon']))
        time_diff = (df.iloc[i]['time'] - df.iloc[i-1]['time']).total_seconds() / 3600
        
        if time_diff > 0:
            df.at[i, 'velocity_kmh'] = (dist / 1000) / time_diff
            
    return df
Velocity Thresholds
High Velocity (>5 km/h): Transit zones (walking/running)
Low Velocity (<1 km/h): "Sticky" zones (lingering, talking)

Visualizing Friction vs. Flow

python
def create_kinetic_map(df, output="kinetic_analysis.html"):
    m = folium.Map(location=[df['lat'].mean(), df['lon'].mean()], zoom_start=18)
    
    for i in range(1, len(df)):
        color = "red" if df.iloc[i]['velocity_kmh'] < 1.5 else "green"
        line_coords = [
            [df.iloc[i-1]['lat'], df.iloc[i-1]['lon']], 
            [df.iloc[i]['lat'], df.iloc[i]['lon']]
        ]
        
        folium.PolyLine(
            line_coords, 
            color=color, 
            weight=5, 
            opacity=0.8,
            tooltip=f"Speed: {df.iloc[i]['velocity_kmh']:.2f} km/h"
        ).add_to(m)
        
    m.save(output)

Automated Conflict Detection

Use DBSCAN clustering to automatically identify problem zones.

python
from sklearn.cluster import DBSCAN
import numpy as np

def identify_conflict_zones(df, velocity_threshold=1.5):
    friction_points = df[df['velocity_kmh'] < velocity_threshold].copy()
    
    if friction_points.empty:
        return []

    coords = friction_points[['lat', 'lon']].values
    clustering = DBSCAN(eps=0.0001, min_samples=3).fit(coords)
    friction_points['cluster'] = clustering.labels_

    report = []
    for cluster_id in set(clustering.labels_):
        if cluster_id == -1: continue
        
        cluster_data = friction_points[friction_points['cluster'] == cluster_id]
        center_lat = cluster_data['lat'].mean()
        center_lon = cluster_data['lon'].mean()
        occurrence_count = len(cluster_data)
        
        report.append({
            'zone_id': cluster_id,
            'lat': center_lat,
            'lon': center_lon,
            'intensity': occurrence_count,
            'recommendation': "Add seating/shade" if occurrence_count > 10 else "Widening required"
        })
        
    return sorted(report, key=lambda x: x['intensity'], reverse=True)

Generating the Architect's Report

python
def generate_architect_summary(report_data, filename="architect_report.md"):
    with open(filename, 'w') as f:
        f.write("# Campus Spatial Audit: Conflict Zones Report\n")
        f.write(f"Generated on: {datetime.now().strftime('%Y-%m-%d')}\n\n")
        f.write("| Zone ID | Location | Intensity | Suggested Action |\n")
        f.write("| :--- | :--- | :--- | :--- |\n")
        
        for zone in report_data:
            f.write(f"| {zone['zone_id']} | {zone['lat']:.5f}, {zone['lon']:.5f} | {zone['intensity']} | {zone['recommendation']} |\n")
    
    print(f"Summary report generated: {filename}")
Output
# Campus Spatial Audit: Conflict Zones Report
Generated on: 2026-03-17

| Zone ID | Location | Intensity | Suggested Action |
| :--- | :--- | :--- | :--- |
| 0 | 19.12345, 72.54321 | 15 | Add seating/shade |

SpatialIntelligenceEngine Class

A modular, enterprise-grade class that encapsulates the entire ETL and analysis pipeline.

python
import os
import exifread
import pandas as pd
import folium
import geopandas as gpd
from datetime import datetime
from sklearn.cluster import DBSCAN
from folium.plugins import HeatMap, AntPath, TimestampedGeoJson

class SpatialIntelligenceEngine:
    def __init__(self, source_dir):
        self.source_dir = source_dir
        self.df = pd.DataFrame()
        self.conflict_report = []

    def run_pipeline(self, official_map_path=None):
        print("--- Starting Spatial Audit ---")
        self._extract_metadata()
        self._process_kinetics()
        self._detect_conflicts()
        self.generate_map(official_map_path)
        print("--- Audit Complete ---")

    def _extract_metadata(self):
        pass  # Internal logic from previous steps

    def _process_kinetics(self):
        pass  # Haversine + velocity calculation

    def _detect_conflicts(self):
        pass  # DBSCAN clustering logic

    def generate_map(self, official_map_path=None, output="final_audit.html"):
        m = folium.Map(location=[self.df['lat'].mean(), self.df['lon'].mean()], zoom_start=18)
        
        if official_map_path and os.path.exists(official_map_path):
            official_gdf = gpd.read_file(official_map_path)
            folium.GeoJson(
                official_gdf,
                name="Official Pathways",
                style_function=lambda x: {'color': 'black', 'weight': 2, 'dashArray': '5, 5'}
            ).add_to(m)

        HeatMap(self.df[['lat', 'lon']].values.tolist(), name="Human Density").add_to(m)
        
        fg_kinetic = folium.FeatureGroup(name="Kinetic Flow").add_to(m)
        
        folium.LayerControl().add_to(m)
        m.save(output)

Blueprint Gap Analysis

Overlay a Shapefile (official campus map) on human-generated data to calculate the "Blueprint Gap."

If the "Official Pathway" and "AntPath" layers have a high Hausdorff Distance, the architecture is failing.
Every "Desire Path" on grass not on the Shapefile represents "Infrastructure Debt" — a place needing paving or lighting investment.

Final Utility Summary

  • Objectivity: Replace "I think" with "I know" using data.
  • Agility: Run audits every semester to track construction impact.
  • Resilience: Reveal the "invisible city" — shortcuts not on any map.

Key Takeaways