Skip to main content
Dar kamtar az 100 khat, naghshe amlak ba khushe bandi besazid
Tutorials

Dar kamtar az 100 khat, naghshe amlak ba khushe bandi besazid

Ba estefadeh az MapAtlas Maps API, yek naghshe fehrest amlak ba neshangarhay barg-e gheymat, khushe bandi, jostuju-ye adres va filter bazeye gheymat dar kamtar az 100

Brent van der Heiden9 min read
#real estate map#property map#map clustering#javascript#proptech#maps api

جستجوی ملک یک مشکل فضایی است. خریداران و مستأجران بر اساس محله‌ها، زمان رفت‌وآمد و نزدیکی به مدارس فکر می‌کنند، نه کدپستی‌ها و لیست‌های خیابانی. یک رابط نقشه‌اول بهتر از یک رابط لیست‌اول تبدیل می‌کند زیرا به کاربران امکان می‌دهد قبل از فیلتر کردن بر اساس قیمت، اتاق خواب یا هر ویژگی دیگری، جستجوی خود را از نظر فضایی لنگر بیندازند.

این آموزش یک نقشه فهرست ملک آماده تولید می‌سازد: نشانگرهای خوشه‌ای در زوم کم، برچسب‌های قیمت روی نشانگرهای فردی، کلیک-برای-پاپ‌آپ با جزئیات فهرست، یک جستجوی کدپستی که نقشه را مجدداً مرکزگرا می‌کند، و یک اسلایدر محدوده قیمت که فهرست‌های قابل مشاهده را در زمان واقعی فیلتر می‌کند. کامپوننت React کامل در زیر ۱۰۰ خط است. همان منطق در JavaScript ساده هم کار می‌کند اگر فریمورکی را ترجیح ندهید.

همچنین یادداشتی درباره تعهدات GDPR خاص برای پلتفرم‌های PropTech اتحادیه اروپا پیدا خواهید کرد، زیرا داده‌های مکانی تولیدشده توسط جستجوهای ملک داده‌های شخصی هستند و باید مطابق با آن مدیریت شوند.

اگر با APIهای نقشه آشنا نیستید، آموزش نحوه افزودن نقشه‌های تعاملی به وب‌سایت خود اصول اولیه را قبل از این آموزش پوشش می‌دهد.

ساختار داده ملک

هر فهرست به مختصات، قیمت و متادیتای کافی برای پاپ‌آپ نیاز دارد. این را به عنوان یک FeatureCollection GeoJSON نگه دارید، مستقیماً بدون تبدیل به منبع نقشه متصل می‌شود.

const properties = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.8952, 52.3702] },
      properties: {
        id: "prop-001",
        price: 485000,
        bedrooms: 3,
        sqm: 112,
        address: "Keizersgracht 142, Amsterdam",
        type: "apartment",
        status: "for-sale"
      }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.9123, 52.3601] },
      properties: {
        id: "prop-002",
        price: 1250,
        bedrooms: 2,
        sqm: 78,
        address: "Sarphatistraat 58, Amsterdam",
        type: "apartment",
        status: "for-rent"
      }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.8801, 52.3780] },
      properties: {
        id: "prop-003",
        price: 720000,
        bedrooms: 4,
        sqm: 195,
        address: "Herengracht 380, Amsterdam",
        type: "house",
        status: "for-sale"
      }
    }
  ]
};

در یک پلتفرم واقعی این آرایه از API فهرست‌های شما می‌آید، یک فراخوانی fetch در بارگذاری نقشه یا تغییر پارامتر پرس‌وجو.

راه‌اندازی نقشه

نقشه را با یک نمای مرکزی روی بازار ملک خود مقداردهی کنید. MapAtlas Maps API با Mapbox GL JS سازگار است، بنابراین فراخوانی مقداردهی با آنچه برای Mapbox می‌نویسید، با یک URL کاشی MapAtlas، یکسان است.

import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

const map = new mapmetricsgl.Map({
  container: 'map',
  style: 'https://tiles.mapatlas.eu/styles/bright/style.json?key=YOUR_API_KEY',
  center: [4.9041, 52.3676],
  zoom: 13
});

سبک Bright برای نقشه‌های ملک به خوبی کار می‌کند، پایه سبک‌تر به برچسب‌های قیمت و نشانگرهای رنگی اجازه می‌دهد بدون شلوغی بصری برجسته شوند.

خوشه‌بندی با برچسب‌های قیمت

الگوی کلیدی رابط کاربری برای یک نقشه ملک نمایش برچسب‌های قیمت روی نشانگرهای فردی و حباب‌های شمارش روی دایره‌های خوشه است. گزینه منبع cluster: true نقاط نزدیک را به طور خودکار گروه‌بندی می‌کند. لایه‌های جداگانه برای خوشه‌ها و نشانگرهای فردی اضافه می‌کنید.

map.on('load', () => {
  map.addSource('properties', {
    type: 'geojson',
    data: properties,
    cluster: true,
    clusterMaxZoom: 14,
    clusterRadius: 60
  });

  // Cluster circles, size scales with listing count
  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: 'properties',
    filter: ['has', 'point_count'],
    paint: {
      'circle-color': '#2563EB',
      'circle-radius': [
        'step', ['get', 'point_count'],
        22, 5, 30, 20, 38
      ],
      'circle-stroke-width': 3,
      'circle-stroke-color': '#ffffff'
    }
  });

  // Cluster count labels
  map.addLayer({
    id: 'cluster-label',
    type: 'symbol',
    source: 'properties',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': '{point_count_abbreviated}',
      'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
      'text-size': 14
    },
    paint: { 'text-color': '#ffffff' }
  });

  // Individual property markers, white circle with price text
  map.addLayer({
    id: 'property-price',
    type: 'symbol',
    source: 'properties',
    filter: ['!', ['has', 'point_count']],
    layout: {
      'text-field': [
        'concat',
        '€',
        ['to-string', ['round', ['/', ['get', 'price'], 1000]]],
        'k'
      ],
      'text-font': ['DIN Offc Pro Bold', 'Arial Unicode MS Bold'],
      'text-size': 12
    },
    paint: {
      'text-color': '#1e293b',
      'text-halo-color': '#ffffff',
      'text-halo-width': 2
    }
  });
});

تعاملات کلیک

کلیک روی یک خوشه نقشه را زوم می‌کند تا فهرست‌های فردی آن نمایش داده شوند. کلیک روی یک ملک فردی یک پاپ‌آپ جزئیات باز می‌کند.

// Zoom into clicked cluster
map.on('click', 'clusters', (e) => {
  const [feature] = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
  map.getSource('properties').getClusterExpansionZoom(
    feature.properties.cluster_id,
    (err, zoom) => {
      if (!err) map.easeTo({ center: feature.geometry.coordinates, zoom });
    }
  );
});

// Detail popup for individual listing
map.on('click', 'property-price', (e) => {
  const { address, price, bedrooms, sqm, status, id } = e.features[0].properties;
  const coords = e.features[0].geometry.coordinates.slice();
  const formattedPrice = status === 'for-rent'
    ? `€${price.toLocaleString()}/mo`
    : `€${price.toLocaleString()}`;

  new mapmetricsgl.Popup({ offset: 10 })
    .setLngLat(coords)
    .setHTML(`
      <div style="min-width:200px">
        <strong style="font-size:15px">${formattedPrice}</strong>
        <p style="margin:4px 0">${address}</p>
        <p style="margin:4px 0;color:#64748b">${bedrooms} bed · ${sqm} m²</p>
        <a href="/listings/${id}" style="color:#2563EB;font-weight:600">View listing →</a>
      </div>
    `)
    .addTo(map);
});

// Change cursor on hover
map.on('mouseenter', 'property-price', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'property-price', () => { map.getCanvas().style.cursor = ''; });
map.on('mouseenter', 'clusters',       () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'clusters',       () => { map.getCanvas().style.cursor = ''; });

فیلتر محدوده قیمت

یک اسلایدر که فهرست‌های قابل مشاهده را فیلتر می‌کند یکی از ارزشمندترین اضافه‌های رابط کاربری برای یک نقشه ملک است. سیستم بیان Mapbox GL JS به شما اجازه می‌دهد فیلتر یک منبع را سمت کلاینت بدون یک رفت‌وبرگشت شبکه به‌روز کنید، فیلتر در مرورگر روی GeoJSON بارگذاری‌شده اتفاق می‌افتد.

function applyPriceFilter(min, max) {
  const filter = ['all',
    ['!', ['has', 'point_count']],
    ['>=', ['get', 'price'], min],
    ['<=', ['get', 'price'], max]
  ];
  map.setFilter('property-price', filter);
}

// Wire up range inputs
const minInput = document.getElementById('price-min');
const maxInput = document.getElementById('price-max');

function onRangeChange() {
  applyPriceFilter(Number(minInput.value), Number(maxInput.value));
}

minInput.addEventListener('input', onRangeChange);
maxInput.addEventListener('input', onRangeChange);

توجه داشته باشید که لایه خوشه به طور خودکار هنگامی که منبع زیرین تغییر می‌کند به‌روز می‌شود، ملک‌هایی که از لایه نشانگر فردی فیلتر می‌شوند از تعداد خوشه‌ها هم خارج می‌شوند.

جستجوی آدرس با Geocoding API

به کاربران اجازه دهید یک محله یا کدپستی را تایپ کنند تا نقشه را مجدداً مرکزگرا کنند. Geocoding API ویژگی‌های GeoJSON برمی‌گرداند، بنابراین مختصات مستقیماً در map.flyTo قرار می‌گیرند.

async function searchArea(query) {
  const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
  url.searchParams.set('text', query);
  url.searchParams.set('key', 'YOUR_API_KEY');
  url.searchParams.set('size', '1');

  const res  = await fetch(url);
  const data = await res.json();

  if (!data.features.length) return;

  const [lng, lat] = data.features[0].geometry.coordinates;
  map.flyTo({ center: [lng, lat], zoom: 13 });
}

document.getElementById('area-search').addEventListener('keydown', (e) => {
  if (e.key === 'Enter') searchArea(e.target.value.trim());
});

برای جستجوی زمان سفر، «ملک‌هایی را در ۲۰ دقیقه از این مدرسه نشان بده»، یک پوشش ایزوخرون با استفاده از MapAtlas Travel Time API اضافه کنید. مقاله توضیح نقشه‌های ایزوخرون نحوه واکشی و نمایش آن چندضلعی را نشان می‌دهد.

کامپوننت React کامل

بستن همه موارد بالا در یک کامپوننت React با مدیریت چرخه حیات مناسب:

import { useEffect, useRef, useState } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

export function PropertyMap({ listings, apiKey }) {
  const containerRef = useRef(null);
  const mapRef       = useRef(null);
  const [priceRange, setPriceRange] = useState([0, 2000000]);

  useEffect(() => {
    const map = new mapmetricsgl.Map({
      container: containerRef.current,
      style: `https://tiles.mapatlas.eu/styles/bright/style.json?key=${apiKey}`,
      center: [4.9041, 52.3676],
      zoom: 13
    });
    mapRef.current = map;

    map.on('load', () => {
      map.addSource('properties', {
        type: 'geojson',
        data: { type: 'FeatureCollection', features: listings },
        cluster: true,
        clusterMaxZoom: 14,
        clusterRadius: 60
      });
      // Add cluster, cluster-label, property-price layers (see above)
      // Add click handlers (see above)
    });

    return () => map.remove();
  }, [apiKey]);

  // Update filter when price range slider changes
  useEffect(() => {
    const map = mapRef.current;
    if (!map || !map.getLayer('property-price')) return;
    map.setFilter('property-price', [
      'all',
      ['!', ['has', 'point_count']],
      ['>=', ['get', 'price'], priceRange[0]],
      ['<=', ['get', 'price'], priceRange[1]]
    ]);
  }, [priceRange]);

  return (
    <div>
      <div style={{ padding: '12px 0', display: 'flex', gap: 12 }}>
        <label>
          Min €
          <input type="range" min={0} max={2000000} step={10000}
            value={priceRange[0]}
            onChange={e => setPriceRange([+e.target.value, priceRange[1]])} />
        </label>
        <label>
          Max €
          <input type="range" min={0} max={2000000} step={10000}
            value={priceRange[1]}
            onChange={e => setPriceRange([priceRange[0], +e.target.value])} />
        </label>
      </div>
      <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
    </div>
  );
}

این کامپوننت کامل است، کمتر از ۱۰۰ خط شامل اسلایدر محدوده قیمت و پاکسازی چرخه حیات.

یادداشت‌های GDPR برای PropTech اتحادیه اروپا

وقتی یک متقاضی ملک آدرس خانه‌اش را تایپ می‌کند یا روی «از موقعیت من استفاده کن» در نقشه شما کلیک می‌کند، داده‌های شخصی را بر اساس ماده ۴ GDPR به اشتراک می‌گذارد. این داده‌های مکانی محل زندگی آنها را نشان می‌دهد، که ممکن است سایر ویژگی‌های حساس آنها را نیز نشان دهد.

مراحل عملی برای انطباق با اتحادیه اروپا:

  • درخواست‌های geocoding را از طریق backend خود هدایت کنید نه اینکه Geocoding API را مستقیماً از مرورگر فراخوانی کنید. Backend شما می‌تواند هدرهای شناسایی‌کننده را قبل از ارسال حذف کند.
  • مختصات جستجو را فراتر از جلسه ثبت یا نگه ندارید مگر اینکه یک مبنای قانونی مستند داشته باشید و رضایت خاص جمع‌آوری کرده باشید.
  • اگر «وقتی ملکی نزدیک من با معیارهایم مطابقت داشت به من اطلاع بده» پیاده‌سازی می‌کنید، منطقه جستجوی ذخیره‌شده را با یک دوره نگهداری تعریف‌شده به عنوان داده شخصی تلقی کنید.

راهنمای توسعه‌دهندگان اتحادیه اروپا برای APIهای نقشه سازگار با GDPR این را به طور کامل پوشش می‌دهد، شامل قراردادهای پردازنده و الزامات اقامت داده. MapAtlas تمام داده‌ها را در اتحادیه اروپا پردازش می‌کند و توافقنامه پردازش داده مورد نیاز بر اساس ماده ۲۸ GDPR را فراهم می‌کند.

صفحه راه‌حل صنعت املاک زمینه بیشتری درباره نحوه استفاده پلتفرم‌های PropTech از MapAtlas در محیط تولید دارد.

مراحل بعدی

سوالات متداول

آیا می‌توانم از MapAtlas برای یک وب‌سایت فهرست ملک استفاده کنم؟

بله. MapAtlas توسط پلتفرم‌های PropTech در سراسر اتحادیه اروپا برای نقشه‌های جستجوی ملک، داشبوردهای سرمایه‌گذاری و سایت‌های فهرست اجاره استفاده می‌شود. Maps API با Mapbox GL JS سازگار است، بنابراین کد نقشه ملک مبتنی بر Mapbox موجود با تغییرات حداقلی مهاجرت می‌کند.

خوشه‌بندی نشانگر برای نقشه‌های املاک چگونه کار می‌کند؟

خوشه‌بندی نشانگرهای ملک نزدیک را در یک دایره که تعداد را نشان می‌دهد هنگامی که نقشه زوم خارج است گروه‌بندی می‌کند. با زوم کردن کاربر، خوشه‌ها به نشانگرهای فردی تقسیم می‌شوند. MapAtlas به طور بومی خوشه‌بندی منبع GeoJSON را پشتیبانی می‌کند، cluster: true را روی منبع تنظیم کنید و SDK بقیه را مدیریت می‌کند.

آیا داده‌های مکانی از جستجوگران ملک مشمول GDPR هستند؟

بله. هنگامی که کاربری با آدرس خانه‌اش یا موقعیت فعلی‌اش در یک سایت ملک جستجو می‌کند، این داده‌های شخصی بر اساس ماده ۴ GDPR محسوب می‌شود. پلتفرم‌های PropTech اتحادیه اروپا باید درخواست‌های geocoding را از طریق یک پروکسی backend هدایت کنند و سیاست‌های به حداقل رساندن و نگهداری داده مناسب اعمال کنند نه اینکه فراخوانی‌های API سمت کلاینت که موقعیت‌های کاربر را در سرورهای شخص ثالث ثبت می‌کند ایجاد کنند.

این مفید بود؟ آن را به اشتراک بگذارید.

درباره نویسنده

Brent van der Heiden

نوشته

Brent van der Heiden

Co-Founder & CEO at MapAtlas

Brent built MapAtlas out of a conviction that developers deserve location APIs with fair pricing and genuine end-user privacy. He writes about geospatial infrastructure, AI search visibility, and how location data powers the products people rely on every day.

مشاهده همه مقالات
بازگشت به وبلاگ