Files
aramesh-backend/frontend/src/data/mapProjects.ts
T
aramesh-site deploy 14660586c4
CI / tests (push) Waiting to run
CI / images (push) Blocked by required conditions
chore: همگام‌سازی وضعیت زندهٔ سرور با مخزن
شامل کارهای چند نشست موازی که تا این‌جا فقط روی سرور بود:
- نقشهٔ دفتر مرکزی در فوتر (LazyOfficeMap) + MapViewport/LazyMaps/mapProjects
- صفحهٔ مدیریت مقالات (AdminArticlesPage) + میان‌بر ویرایش پیش‌نویس در وبلاگ
- مهاجرت ۰۰۰۴: updated_at مقالات/پروژه‌ها
- chatbot/Dockerfile: نصب دومرحله‌ای torch (کاربردی برای PyPI فیلترشده)
- seed: خالی‌کردن آمارهای دموی story
- docker-compose: اصلاح پسوند @file میان‌افزارهای Traefik
- حذف فایل موقت frontend/.tmp-style.json و اصلاحات جزئی UI

با این کامیت، آن‌چه روی سرور اجرا می‌شود Exactly همان چیزی است که در مخزن ثبت شده.
2026-10-26 12:28:28 +00:00

259 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* مختصات پروژه‌های گروه ساختمانی آرامش — برگرفته از شیت «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<MapProject, 'lat' | 'lng'>): 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<MapProject, 'lat' | 'lng'>[],
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,
}
}