/** * مختصات پروژه‌های گروه ساختمانی آرامش — برگرفته از شیت «Sheet1.csv». * * دو نکته‌ی داده‌ای که در همان‌جا حل می‌شوند: * ۱) ستون‌های neshan_url و google_maps_url در شیت فقط برچسب بودند * («Open in Neshan») و نشانی واقعی نداشتند؛ پس پیوندها از روی مختصات * ساخته می‌شوند که الگوی تثبیت‌شده‌ی هر دو سرویس است. * ۲) ستون توضیحات خالی است؛ متن پیش‌فرض در لایه‌ی رابط کاربری جایگزین می‌شود. */ import type { Project, ProjectCardItem } from './site' export type MapProject = { id: number name: string /** سال شمسی — ممکن است خالی باشد */ year: string /** پروژه‌ی در جریان (ستون new شیت) */ isNew: boolean lat: number lng: number description: string } /** مرکز پیش‌فرض نقشه — اصفهان */ export const ISFAHAN_CENTER: [number, number] = [51.6675, 32.6539] /** * مختصات دفتر مرکزی (اصفهان، خیابان ملاصدرا شمالی) — همان نقطه‌ای که سمت * بک‌اند در اسکیمای LocalBusiness به‌عنوان geo/hasMap می‌آید (seo_pages.py: * OFFICE_GEO). دو مقدار باید همیشه یکی بمانند؛ تغییر یعنی ویرایش هر دو جا. */ export const OFFICE_LOCATION = { lat: 32.61764688216492, lng: 51.666306342927214 } type CsvRow = { name: string; year: string; isNew: boolean; lat: number; lng: number; description: string } /** * ستون‌های برچسبی شیت حذف شده‌اند (نشانی واقعی نداشتند) و ستون توضیحات به * description نگاشت شده است. ترتیب: name,year,new,lat,lng,description */ const RAW_CSV = `name,year,new,lat,lng,description پروژه سروش,1381,no,32.6662842,51.701735 چهرازی-زیتون,1383,no,32.6700428,51.69985 جعفریان,1384,no,32.6669259,51.701085 بهار,1385,no,32.6468963,51.6899119 حسن مرادی,1385,no,32.6704278,51.6985567 کوثر- هشت بهشت غربی,1386,no,32.6522044,51.6856474 صبا,1387,no,32.6541363,51.6847828 شریف واقفی - بهشت,1387,no,32.6500402,51.6919823 پروژه- فردوس,1388,no,32.6614576,51.706429 ارم,1388,no,32.6563712,51.710694 سینا,1388,no,32.6469299,51.6855403 مسجد سید,1389,no,32.6669953,51.6557085 هفده شهریور,1389,no,32.7249998,51.6543819 آرامش رفیع زاده,1390,no,32.6467107,51.6873398 باهنر- ارشاد,1391,no,32.6822102,51.6690006 بن بست شریعت,1390,no,32.6459939,51.6897556 قصر - یاسین,1389,no,32.6663549,51.7065512 پردیس,1390,no,32.6518961,51.6973621 آرامش حکیم,1392,no,32.6426783,51.6813477 آرامش- ادیب,1387,no,32.6789359,51.6771241 آرامش دخانیات,1393,no,32.6411924,51.6797555 بهشتی فرشادی,1393,no,32.6531989,51.6790984 کاکتوس- شریف واقفی,1394,no,32.6486747,51.6859003 آرامش - صیاد,1395,no,32.6486798,51.712659 کوثر- میرزاطاهر,1395,no,32.6506204,51.6344053 آرامش - بوعلی 1,1396,no,32.6455156,51.6918424 آرامش - میرزا نصیر,1396,no,32.668195,51.7173079 نیکان میرزاطاهر,1396,no,32.6504663,51.6355982 آرامش-رسا,1397,no,32.6522126,51.6857297 نگین- میرزاطاهر,1393,no,32.6507084,51.6313395 آرامش - بوعلی 2,1398,no,32.645391,51.6917828 آرامش استاد همایی,1398,no,32.6563505,51.691988 آرامش- صدر,1399,no,32.6469234,51.6826176 آرامش - سپهسالار,1399,no,32.6259658,51.7080268 آرامش - سنایی,1400,no,32.6973339,51.6194128 مغازه لباس فروشی,1400,no,32.6516965,51.7020497 آرامش - آتشگاه,1401,no,32.6547584,51.6319244 آرامش - بزرگمهر,1401,no,32.651381,51.6993575 آرامش - سعدی,1401,no,32.6970115,51.6196909 آرامش - صمدیه لباف,1401,no,32.6716585,51.6359248 آرامش میرزا طاهر 2,1401,no,32.6505704,51.6337863 آرامش 5 رمضان,1401,no,32.6674407,51.6568827 آرامش احمدآباد,1401,no,32.6626101,51.6969828 باهنر- شادی,1390,no,32.6824682,51.6670556 تجاری آتشگاه,1401,no,32.6515087,51.6033278 آرامش - شهیدان,1402,no,32.6854968,51.6171964 آرامش خواجو,1402,no,32.6419187,51.6831873 آرامش- اداری ابوالحسن,1402,no,32.6453749,51.6915519 آرامش - انقلاب اسلامی,1403,no,32.719769,51.645921 آرامش - شریف,1404,no,32.6473319,51.6955996 آرامش - کهندژ,1404,no,32.6649497,51.630077 آرامش جی,1404,no,32.6610717,51.7110184 آرامش زنیث,1405,no,32.674076,51.6368204 آرامش - شمس آبادی,1405,no,32.6560844,51.664771 آرامش الیسیان,1405,no,32.6495532,51.6178643 آرامش - وحدت,1405,no,32.6537564,51.6197007 آرامش پروین,1405,no,32.6618223,51.7098152 خلیفه سلطانی,1405,no,32.7035398,51.6223296 آرامش آوالون,1406,no,32.6517244,51.6202975 آرامش - میرزا طاهر 4,,no,32.6510328,51.631436 آرامش- میرزا طاهر 1,1396,no,32.6506236,51.6298238 آرامش لکسون,1406,no,32.60600917636113,51.67503993694544 ارغوانیه,1395,no,32.64448932441766,51.77707692162298 آرامش- ابوریحان,1405,no,32.700027968614734,51.71497771552572 ` /** * تجزیه‌ی یک خط CSV — پشتیبانی از فیلدهای گیومه‌دار و گیومه‌ی دوگانه‌ی درونی. * جداکننده کاما و نویسه‌های مخفی ابتدای فایل (BOM) هم حذف می‌شوند. */ function parseCsvLine(line: string): string[] { const fields: string[] = [] let current = '' let inQuotes = false for (let i = 0; i < line.length; i += 1) { const char = line[i] if (inQuotes) { if (char === '"') { if (line[i + 1] === '"') { current += '"' i += 1 } else inQuotes = false } else current += char } else if (char === '"') inQuotes = true else if (char === ',') { fields.push(current.trim()) current = '' } else current += char } fields.push(current.trim()) return fields } /** CSV خام → آرایه‌ی تایپ‌شده‌ی JSON */ export function parseProjectsCsv(raw: string): MapProject[] { const rows: CsvRow[] = [] const lines = raw.replace(/^\ufeff/, '').split(/\r?\n/) for (const line of lines.slice(1)) { if (!line.trim()) continue const [name, year = '', isNew = '', lat = '', lng = '', description = ''] = parseCsvLine(line) const parsedLat = Number(lat) const parsedLng = Number(lng) if (!name || !Number.isFinite(parsedLat) || !Number.isFinite(parsedLng)) continue rows.push({ name, year, isNew: isNew.toLowerCase() === 'yes', lat: parsedLat, lng: parsedLng, description }) } return rows.map((row, index) => ({ id: index + 1, ...row })) } export const MAP_PROJECTS: MapProject[] = parseProjectsCsv(RAW_CSV) /** * پیوند نقشهٔ گوگل روی مختصات پروژه — قالب رسمی Maps URLs با پارامتر query، * بی‌نیاز از کلید API. یک سنجاق دقیق روی همان نقطه می‌نشاند و روی موبایل * اپلیکیشن گوگل‌مپ را باز می‌کند. */ export function buildGoogleMapsUrl(project: Pick): string { return `https://www.google.com/maps/search/?api=1&query=${project.lat},${project.lng}` } const NAME_NOISE = ['آرامش', 'پروژه', 'مجتمع', 'گروه', 'ساختمانی', 'برج', 'مجموعه'] /** * نرمال‌سازی نام برای تطبیق شیت با داده‌ی بک‌اند: نویسه‌های نامرئی، * ی/ک عربی، نشانه‌ها و واژه‌های عمومی حذف می‌شوند تا «آرامش - سنایی» و * «پروژه سنایی» یک کلید بگیرند. */ export function normalizeProjectName(name: string): string { let result = (name ?? '') .normalize('NFC') .replace(/[\u200b-\u200f\u202a-\u202e\ufeff]/g, ' ') .replace(/[يى]/g, 'ی') .replace(/ك/g, 'ک') .replace(/[ۀة]/g, 'ه') .replace(/[^\p{L}\p{N}\s]/gu, ' ') .toLowerCase() .replace(/\s+/g, ' ') .trim() for (const noise of NAME_NOISE) result = result.replace(new RegExp(`(^|\\s)${noise}(?=\\s|$)`, 'g'), ' ') return result.replace(/\s+/g, ' ').trim() } /** پروژه‌ی شیت متناظر با یک نام (تطبیق دقیق نرمال‌شده، سپس دربرگیری) */ export function findMapProjectByName(name: string): MapProject | undefined { const key = normalizeProjectName(name) if (!key) return undefined const exact = MAP_PROJECTS.find((project) => normalizeProjectName(project.name) === key) if (exact) return exact return MAP_PROJECTS.find((project) => { const candidate = normalizeProjectName(project.name) return candidate.length >= 3 && key.length >= 3 && (candidate.includes(key) || key.includes(candidate)) }) } /** * محل جایگزین وقتی پروژه‌ای در شیت پیدا نشد — مرکز اصفهان با نام پروژه‌ی * بک‌اند، تا نمای تک‌پروژه هرگز بدون نقشه نماند. */ export function createPlaceholderProject(name: string, description = ''): MapProject { return { id: 0, name, year: '', isNew: false, lat: ISFAHAN_CENTER[1], lng: ISFAHAN_CENTER[0], description, } } /** * کادر محدودهٔ نقشه از مختصات پروژه‌ها — با حاشیه تا لبهٔ نقشه خالی نماند و * کاربر به بیرون از محدودهٔ پروژه‌ها جابه‌جا نشود. */ export function boundsForProjects( projects: Pick[], padding = 0.02, ): [[number, number], [number, number]] | undefined { if (!projects.length) return undefined const lats = projects.map((project) => project.lat) const lngs = projects.map((project) => project.lng) return [ [Math.min(...lngs) - padding, Math.min(...lats) - padding], [Math.max(...lngs) + padding, Math.max(...lats) + padding], ] } /** ردیف شیت → کارت فهرست: عنوان از شیت، وضعیت از ستون new، نشانی از مختصات */ export function csvProjectToCard(project: MapProject, detailHref: string): ProjectCardItem { return { id: project.id, title: project.name, status: project.isNew ? 'progress' : 'done', detailHref, addressUrl: buildGoogleMapsUrl(project), } } /** پروژهٔ بک‌اند → کارت فهرست؛ نشانی نشان از تطبیق نامی با شیت می‌آید */ export function apiProjectToCard(project: Project, detailHref: string): ProjectCardItem { const matched = findMapProjectByName(project.title) return { id: project.id, title: project.title, status: project.status, detailHref, location: project.location || undefined, image: project.image || undefined, area: project.area || undefined, addressUrl: matched ? buildGoogleMapsUrl(matched) : undefined, } }