gridloc / update.cjs
lassanmonster's picture
GridLoc: Bengaluru parking congestion digital twin
db3dbe1
Raw
History Blame Contribute Delete
18.7 kB
const fs = require('fs');
let content = fs.readFileSync('src/components/landing/Sections.tsx', 'utf8');
// 1. Remove all em-dashes
content = content.replace(/β€”/g, '-');
// 2. update useCountUp to accept decimals
content = content.replace(
`function useCountUp(target: number, duration = 1400) {
const ref = useRef<HTMLSpanElement | null>(null)
const [val, setVal] = useState(0)`,
`function useCountUp(target: number, duration = 1400, decimals = 0) {
const ref = useRef<HTMLSpanElement | null>(null)
const [val, setVal] = useState(0)`);
content = content.replace(
` const eased = 1 - Math.pow(1 - p, 3)
setVal(Math.round(target * eased))`,
` const eased = 1 - Math.pow(1 - p, 3)
const currentVal = target * eased
setVal(decimals > 0 ? Number(currentVal.toFixed(decimals)) : Math.round(currentVal))`);
// 3. update Stat to accept decimals
content = content.replace(
`function Stat({ value, label }: { value: number; label: string }) {
const { ref, val } = useCountUp(value)`,
`function Stat({ value, label, decimals = 0 }: { value: number; label: string; decimals?: number }) {
const { ref, val } = useCountUp(value, 1400, decimals)`);
content = content.replace(
` {val.toLocaleString()}
</div>`,
` {decimals > 0 ? val.toFixed(decimals) : val.toLocaleString()}
</div>`);
// 4. HeroSection text modification
content = content.replace(
` Know where to
<br />
<span
className="text-shimmer"`,
` You have been enforcing where the
<br />
<span
className="text-shimmer"`);
content = content.replace(
` step next.
</span>`,
` cameras already point.
</span>`);
content = content.replace(
` A living digital twin of Bengaluru's streets - surfacing
illegal-parking hotspots before they choke the road. Built for
the officers who decide where to look first.`,
` GridLoc separates where parking is recorded from where it actually happens, then hands BTP the gap.`);
content = content.replace(
` <Stat value={2534} label="cells scored" />
<Stat value={338} label="flagged zones" />
<Stat value={11761} label="roads mapped" />`,
` <Stat value={292768} label="violations cleaned" />
<Stat value={2534} label="cells scored" />
<Stat value={5224} label="cell by time-block rows" />`);
content = content.replace(
` Know where to
<br />
<span
className="text-shimmer"`,
` You have been enforcing where the
<br />
<span
className="text-shimmer"`);
content = content.replace(
` step next.
</span>`,
` cameras already point.
</span>`);
content = content.replace(
` A living digital twin of Bengaluru's streets - surfacing illegal-parking
hotspots before they choke the road. Built for the officers who decide
where to look first.`,
` GridLoc separates where parking is recorded from where it actually happens, then hands BTP the gap.`);
content = content.replace(
` <Stat value={2534} label="cells scored" />
<Stat value={338} label="flagged zones" />
<Stat value={11761} label="roads mapped" />`,
` <Stat value={292768} label="violations cleaned" />
<Stat value={2534} label="cells scored" />
<Stat value={5224} label="cell by time-block rows" />`);
// 5. DescentSection modification
content = content.replace(
` <h2
className="reveal"
style={{
transitionDelay: '0.18s',
fontFamily: 'var(--font-display)',
fontWeight: 500,
fontSize: 'clamp(1.8rem, 3.6vw, 3rem)',
lineHeight: 1.08,
letterSpacing: '-0.015em',
color: 'var(--lp-text)',
margin: 0,
}}
>
Forty million journeys a day
<br />
converge on one city.
</h2>
<p className="reveal mt-5 text-sm leading-relaxed" style={{ color: 'var(--lp-muted)', marginLeft: 'auto', maxWidth: 380, transitionDelay: '0.32s' }}>
We cut past the noise to the hexes that matter - the corners where
parked vehicles quietly turn a moving road into a standstill.
</p>`,
` <h2
className="reveal"
style={{
transitionDelay: '0.18s',
fontFamily: 'var(--font-display)',
fontWeight: 500,
fontSize: 'clamp(1.8rem, 3.6vw, 3rem)',
lineHeight: 1.08,
letterSpacing: '-0.015em',
color: 'var(--lp-text)',
margin: 0,
}}
>
Count the busy cells,<br />
call them hotspots,<br />
and you have just redrawn the camera map.
</h2>
<div className="reveal mt-8" style={{ transitionDelay: '0.32s', marginLeft: 'auto', maxWidth: 380 }}>
<Stat value={0.66} label="correlation between violation count and number of enforcement devices" decimals={2} />
<p className="mt-4 text-sm leading-relaxed" style={{ color: 'var(--lp-muted)' }}>
Recorded volume tracks enforcement presence, not reality.
</p>
</div>`);
// 6. HOW_CARDS
content = content.replace(
`const HOW_CARDS: CardData[] = [
{
id: 'ingest',
title: 'Every street, as data',
description:
'Traffic and parking signals across Bengaluru are snapped onto an H3 hexagonal grid - one comparable cell for every neighbourhood block.',
icon: <Database className="h-5 w-5" />,
},
{
id: 'model',
title: 'Observed vs. expected',
description:
'Each cell is scored on how its real congestion compares to what the model expects for that day and time-window - isolating genuine anomalies.',
icon: <Activity className="h-5 w-5" />,
},
{
id: 'hotspots',
title: 'Ranked, not just mapped',
description:
'Cells are classified - Confirmed Hotspot, Blind Spot, Calm - and sorted so the worst zones rise to the top of the queue automatically.',
icon: <ListOrdered className="h-5 w-5" />,
},
]`,
`const HOW_CARDS: CardData[] = [
{
id: 'observed',
title: 'Observed score',
description: 'Impact weighted, percentile ranked, not raw counts.',
icon: <Activity className="h-5 w-5" />,
},
{
id: 'expected',
title: 'Expected score',
description: 'City conditions only, POI proximity 0.7 plus road hierarchy 0.3, never touches violation data.',
icon: <Database className="h-5 w-5" />,
},
{
id: 'certify',
title: 'Certify',
description: 'Getis-Ord Gi star gate, only 47 of 2534 cells survive, z up to 8.59.',
icon: <ListOrdered className="h-5 w-5" />,
},
]`);
// 7. Remove DataSection and HexBirth
content = content.replace(/\/\/ ── 03 Β· THE DATA ─────────────────────────────────────────────────────────────[\s\S]*?(?=\/\/ ── 04 Β· FINDINGS \+ CTA)/, "");
// 8. Add new components
const newComponents = `
// ── CORE IDEA ────────────────────────────────────────────────────────────────
export function CoreIdeaSection() {
const ref = useReveal<HTMLDivElement>(0.15)
return (
<section id="core-idea" ref={ref} className="relative flex min-h-screen flex-col justify-center px-8 py-32 md:px-16">
<div className="relative z-10 grid items-center gap-16 md:grid-cols-2" style={{ maxWidth: 1100, margin: '0 auto' }}>
<div className="reveal">
<div className={EYEBROW} style={{ ...eyebrowStyle, marginBottom: 14 }}>
The Core Idea
</div>
<h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2rem, 4.4vw, 3.4rem)', lineHeight: 1.02, letterSpacing: '-0.02em', color: 'var(--lp-text)', margin: 0, maxWidth: 720 }}>
Observed vs. Expected
</h2>
<p className="mt-5 text-sm leading-relaxed" style={{ color: 'var(--lp-muted)', maxWidth: 420 }}>
A low recorded count can mean a genuinely calm street, or heavy uncaught parking with no camera pointed at it. We estimate where illegal parking should occur from city conditions, and compare it against where it was recorded.
</p>
</div>
<div className="reveal" style={{ transitionDelay: '120ms', perspective: 1100 }}>
<CoreIdeaMatrix />
</div>
</div>
</section>
)
}
function CoreIdeaMatrix() {
const tiltRef = useScrollTilt<HTMLDivElement>(10)
const [hovered, setHovered] = useState<string | null>(null)
const quadrants = [
{ id: 'blindspot', col: 'Low Observed', row: 'High Expected', label: 'Blind Spot', count: 661, color: '#9a9a9e', bg: 'rgba(154,154,158,0.2)', desc: 'Conditions predict heavy parking, little recorded. Likely coverage gap. Tell BTP to look here.' },
{ id: 'confirmed', col: 'High Observed', row: 'High Expected', label: 'Confirmed', count: 95, color: '#efefef', bg: 'rgba(239,239,239,0.15)', desc: 'Busy in data and conditions agree. Enforce with high confidence.' },
{ id: 'calm', col: 'Low Observed', row: 'Low Expected', label: 'Calm', count: 4411, color: '#5c5c60', bg: 'rgba(92,92,96,0.1)', desc: 'Quiet and expected to be quiet. Do not waste patrols.' },
{ id: 'quirk', col: 'High Observed', row: 'Low Expected', label: 'Quirk', count: 57, color: '#b0b0b4', bg: 'rgba(176,176,180,0.15)', desc: 'High tickets, low demand. Possible camera trap or one-off event.' },
]
return (
<div ref={tiltRef} style={{ background: 'var(--lp-section)', border: '1px solid var(--lp-line)', borderRadius: 16, padding: 24, willChange: 'transform' }}>
<div className="grid grid-cols-2 gap-4">
{quadrants.map(q => (
<div
key={q.id}
onMouseEnter={() => setHovered(q.id)}
onMouseLeave={() => setHovered(null)}
className="p-6 rounded-xl transition-all duration-300 cursor-default"
style={{
background: q.id === 'blindspot' ? 'rgba(154,154,158,0.25)' : q.bg,
border: \`1px solid \${q.id === 'blindspot' ? q.color : 'transparent'}\`,
boxShadow: q.id === 'blindspot' ? \`0 0 20px \${q.color}40\` : 'none',
transform: hovered === q.id ? 'scale(1.02)' : 'scale(1)'
}}
>
<div className="flex justify-between items-start mb-4">
<div style={{ fontFamily: 'var(--font-display)', fontWeight: 600, color: q.color }}>{q.label}</div>
<div className="tabular-nums font-mono text-xs" style={{ color: q.color }}>{q.count}</div>
</div>
<div className="text-[10px] uppercase mb-2" style={{ fontFamily: 'var(--font-mono)', letterSpacing: '0.1em', color: 'var(--lp-muted)' }}>
{q.col} / {q.row}
</div>
<div className="text-xs" style={{ color: 'var(--lp-muted)', opacity: hovered === q.id ? 1 : 0.7, transition: 'opacity 0.2s' }}>
{q.desc}
</div>
</div>
))}
</div>
</div>
)
}
// ── BLIND SPOT RESULTS ───────────────────────────────────────────────────────
export function BlindSpotResultsSection() {
const ref = useReveal<HTMLDivElement>(0.15)
return (
<section id="results" ref={ref} className="relative px-8 py-32 md:px-16">
<div style={{ maxWidth: 800, margin: '0 auto' }}>
<div className="reveal text-center">
<div className={EYEBROW} style={{ ...eyebrowStyle, marginBottom: 14 }}>
The Output
</div>
<h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2rem, 4vw, 3.2rem)', lineHeight: 1.03, letterSpacing: '-0.02em', color: 'var(--lp-text)', margin: 0 }}>
Blind Spot Results
</h2>
<p className="mt-5 text-sm leading-relaxed mx-auto" style={{ color: 'var(--lp-muted)', maxWidth: 600 }}>
High-demand corridors where recorded enforcement is lower than conditions warrant. The output no count based method can produce.
</p>
</div>
<div className="mt-16 flex flex-col gap-4">
{[
{ name: 'Outer Ring Road', O: '0.89', E: '1.00' },
{ name: 'Whitefield Main Road', O: '0.54', E: '1.00' },
{ name: '17th Main Road', O: '0.91', E: '1.00' },
].map((row, i) => (
<div key={row.name} className="reveal flex items-center justify-between rounded-xl p-6" style={{ background: 'var(--lp-section)', border: '1px solid var(--lp-line)', transitionDelay: \`\${0.1 * i}s\` }}>
<div style={{ fontFamily: 'var(--font-display)', fontWeight: 500, color: 'var(--lp-text)', fontSize: '1.2rem' }}>{row.name}</div>
<div className="flex gap-6">
<div>
<span className="text-[10px] uppercase mr-2" style={{ fontFamily: 'var(--font-mono)', color: 'var(--lp-muted)' }}>O</span>
<span className="tabular-nums" style={{ fontFamily: 'var(--font-mono)', color: 'var(--lp-text)' }}>{row.O}</span>
</div>
<div>
<span className="text-[10px] uppercase mr-2" style={{ fontFamily: 'var(--font-mono)', color: 'var(--lp-muted)' }}>E</span>
<span className="tabular-nums" style={{ fontFamily: 'var(--font-mono)', color: 'var(--lp-text)' }}>{row.E}</span>
</div>
</div>
</div>
))}
</div>
</div>
</section>
)
}
// ── TIME AWARE ───────────────────────────────────────────────────────────────
export function TimeAwareSection() {
const ref = useReveal<HTMLDivElement>(0.15)
return (
<section id="time" ref={ref} className="relative px-8 py-24 md:px-16">
<div style={{ maxWidth: 900, margin: '0 auto', textAlign: 'center' }}>
<div className="reveal">
<div className={EYEBROW} style={{ ...eyebrowStyle, marginBottom: 14 }}>
Time-Aware
</div>
<h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2rem, 4vw, 3.2rem)', lineHeight: 1.03, letterSpacing: '-0.02em', color: 'var(--lp-text)', margin: 0 }}>
Four data-derived time blocks
</h2>
<p className="mx-auto mt-5 text-sm leading-relaxed" style={{ color: 'var(--lp-muted)', maxWidth: 600 }}>
The blocks are data-derived ranges with no real-world clock claims. Across these shifts, 36 cells dynamically change their enforcement category.
</p>
</div>
</div>
</section>
)
}
// ── TRUST ────────────────────────────────────────────────────────────────────
export function TrustSection() {
const ref = useReveal<HTMLDivElement>(0.15)
return (
<section id="trust" ref={ref} className="relative px-8 py-24 md:px-16">
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
<div className="reveal text-center">
<h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2rem, 4vw, 3.2rem)', lineHeight: 1.03, letterSpacing: '-0.02em', color: 'var(--lp-text)', margin: 0 }}>
Driven by the data, not by our weight choices.
</h2>
</div>
<div className="mt-16 grid gap-8 md:grid-cols-3">
<div className="reveal flex flex-col items-center text-center">
<div className="tabular-nums mb-4" style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: '2.5rem', color: 'var(--lp-text)' }}>β‰₯0.998</div>
<div className="text-sm" style={{ color: 'var(--lp-muted)' }}>Spearman rho correlation under weight perturbation</div>
</div>
<div className="reveal flex flex-col items-center text-center" style={{ transitionDelay: '0.1s' }}>
<div className="tabular-nums mb-4" style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: '2.5rem', color: 'var(--lp-text)' }}>97-100%</div>
<div className="text-sm" style={{ color: 'var(--lp-muted)' }}>of cells keep their category under weight perturbation</div>
</div>
<div className="reveal flex flex-col items-center text-center" style={{ transitionDelay: '0.2s' }}>
<div className="tabular-nums mb-4" style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: '2.5rem', color: 'var(--lp-text)' }}>100%</div>
<div className="text-sm" style={{ color: 'var(--lp-muted)' }}>of cells resolved to real Bengaluru street names</div>
</div>
</div>
</div>
</section>
)
}
`;
content = content.replace(/\/\/ ── FOOTER/, newComponents + "// ── FOOTER");
fs.writeFileSync('src/components/landing/Sections.tsx', content);
// LandingPage.tsx
let lpContent = fs.readFileSync('src/components/landing/LandingPage.tsx', 'utf8');
lpContent = lpContent.replace(/β€”/g, '-');
lpContent = lpContent.replace(
` ScrollExpansionHero,
DescentSection,
HowItWorksSection,
DataSection,
FindingsSection,`,
` ScrollExpansionHero,
DescentSection,
CoreIdeaSection,
HowItWorksSection,
BlindSpotResultsSection,
TimeAwareSection,
TrustSection,
FindingsSection,`);
lpContent = lpContent.replace(
` <WebGLShader />
<ScrollExpansionHero />
<DescentSection />
<HowItWorksSection />
<DataSection />
<FindingsSection />
<TeamFooter />`,
` <WebGLShader />
<ScrollExpansionHero />
<DescentSection />
<CoreIdeaSection />
<HowItWorksSection />
<BlindSpotResultsSection />
<TimeAwareSection />
<TrustSection />
<FindingsSection />
<TeamFooter />`);
fs.writeFileSync('src/components/landing/LandingPage.tsx', lpContent);
console.log("Done updating files!");