Skip to main content
用 JavaScript 构建带聚类功能的房产地图
Tutorials

用 JavaScript 构建带聚类功能的房产地图

使用MapAtlas Maps API,以原生 JavaScript 或 React 构建一个包含价格标签标记、聚类、地址搜索和价格范围筛选器的房产列表地图。

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

房产搜索本质上是一个空间问题。买家和租客考虑的是街区、通勤时间和离学校有多近,而不是邮编和街道列表。以地图为主的界面更符合用户的找房方式,因为它让用户先在空间上确定搜索范围,再按价格、卧室数量或其他属性进行筛选。

本教程将构建一个可用于生产环境的房源地图:低缩放级别下的聚类标记、单个标记上的价格标签、点击后显示房源详情的弹窗、可重新定位地图中心的邮编搜索,以及实时筛选可见房源的价格范围滑块。React 组件本身约 65 行,加上图层和点击处理程序的完整实现约 150 行。如果你不想使用框架,同样的逻辑也适用于原生 JavaScript。

文中还会说明欧盟 PropTech 平台需要履行的 GDPR 义务,因为房产搜索产生的位置数据属于个人数据,必须相应处理。

如果你刚接触地图 API,可以先阅读 如何在网站中添加交互式地图 教程,它介绍了本教程所需的基础知识。

房源数据结构

每条房源需要坐标、价格,以及足够填充弹窗的元数据。把数据保存为 GeoJSON FeatureCollection,它可以直接接入地图数据源,无需转换。

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 的写法相同,只需换成 MapAtlas 的瓦片 URL。

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

const map = new mapmetricsgl.Map({
  container: 'map',
  style: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=YOUR_API_KEY',
  center: [4.9041, 52.3676],
  zoom: 13
});

明亮、低对比度的样式特别适合房产地图,浅色底图能让价格标签和彩色标记更加醒目,又不会显得杂乱。

带价格标签的聚类

房产地图最关键的界面模式,是在单个标记上显示价格标签,在聚类圆圈上显示数量。数据源选项 cluster: true 会自动把相邻的点分组。你需要分别为聚类和单个标记添加图层。

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

  // 聚类圆圈,大小随房源数量变化
  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'
    }
  });

  // 聚类数量标签
  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' }
  });

  // 单个房源标记,白色描边的价格文字
  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
    }
  });
});

点击交互

点击聚类时,地图会放大并展开其中的单个房源。点击单个房源时,会打开详情弹窗。

// 放大到被点击的聚类
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 });
    }
  );
});

// 单个房源的详情弹窗
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);
});

// 悬停时切换鼠标指针
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);
}

// 绑定范围输入框
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);

注意,聚类是根据数据源中的数据计算的,并不参考图层过滤条件,因此聚类计数仍会包含被滑块隐藏的房源。要让两者保持一致,请在 JavaScript 中筛选房源,再把结果传给 map.getSource('properties').setData()。

使用 Geocoding API 进行地址搜索

让用户输入街区名称或邮编来重新定位地图。Geocoding API 返回 GeoJSON 要素,坐标可以直接传给 map.flyTo。

async function searchArea(query) {
  const url = new URL('https://gateway.mapmetrics-atlas.net/forward-geocode/');
  url.searchParams.set('text', query);
  url.searchParams.set('token', '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());
});

如果要实现按通行时间搜索,比如「显示离这所学校 20 分钟以内的房源」,可以使用 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://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=${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
      });
      // 添加 cluster、cluster-label、property-price 图层(见上文)
      // 添加点击事件处理(见上文)
    });

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

  // 价格范围滑块变化时更新过滤条件
  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>
  );
}

这是组件的基本框架,包括价格范围滑块和生命周期清理在内约 65 行;再加上前文各节中的图层和点击处理代码,即可完成整个组件。

欧盟 PropTech 的 GDPR 注意事项

当找房用户在你的地图上输入自己的住址,或点击「使用我的位置」时,他们分享的是 GDPR 第 4 条第 (1) 项所定义的个人数据,该条款明确把位置数据列为标识符。这些位置数据会暴露用户的居住地,并可能由此推断出其他敏感属性。

满足欧盟合规要求的实操步骤:

  • 通过后端转发地理编码请求,而不是在浏览器中直接调用 Geocoding API。后端可以在转发前去掉可识别身份的请求头。
  • 除非具备 GDPR 第 6 条规定的、有书面记录的合法依据(例如用户同意),否则不要在会话结束后记录或保存搜索坐标。
  • 如果你实现了「附近有符合条件的房源时通知我」这类功能,应把保存的搜索区域视为个人数据,并设定明确的保留期限。

欧盟开发者 GDPR 合规地图 API 指南 对此有完整介绍,包括数据处理协议和数据驻留要求。MapAtlas 在欧盟境内处理数据,并提供 GDPR 第 28 条第 3 款要求控制者与处理者之间签订的数据处理协议(Data Processing Agreement)。

房地产行业解决方案页面 进一步介绍了 PropTech 平台如何在生产环境中使用 MapAtlas。

后续步骤

常见问题

可以把 MapAtlas 用于房源网站吗?

可以。MapAtlas 覆盖了房产网站所需的功能:房产搜索地图、投资仪表板和租赁房源地图。Maps API 与 Mapbox GL JS 兼容,因此基于 Mapbox 的现有房产地图代码只需少量修改即可迁移。

房产地图中的标记聚类是如何工作的?

地图缩小时,聚类会把相邻的房源标记合并成一个显示数量的圆圈。用户放大时,聚类会拆分为单个标记。MapAtlas 原生支持 GeoJSON 数据源聚类,只需在数据源上设置 cluster: true,其余工作由 SDK 完成。

找房用户的位置数据受 GDPR 约束吗?

受约束。当用户在房产网站上按住址或当前位置搜索时,这些信息属于 GDPR 第 4 条所定义的个人数据。欧盟 PropTech 平台应通过后端代理转发地理编码请求,并执行相应的数据最小化和保留策略,而不是在客户端直接调用 API,导致用户位置被记录到第三方服务器上。

常见问题

可以把 MapAtlas 用于房源网站吗?

可以。MapAtlas 覆盖了房产网站所需的功能:房产搜索地图、投资仪表板和租赁房源地图。Maps API 与 Mapbox GL JS 兼容,因此基于 Mapbox 的现有房产地图代码只需少量修改即可迁移。

房产地图中的标记聚类是如何工作的?

地图缩小时,聚类会把相邻的房源标记合并成一个显示数量的圆圈。用户放大时,聚类会拆分为单个标记。MapAtlas 原生支持 GeoJSON 数据源聚类,只需在数据源上设置 cluster: true,其余工作由 SDK 完成。

找房用户的位置数据受 GDPR 约束吗?

受约束。当用户在房产网站上按住址或当前位置搜索时,这些信息属于 GDPR 第 4 条所定义的个人数据。欧盟 PropTech 平台应通过后端代理转发地理编码请求,并执行相应的数据最小化和保留策略,而不是在客户端直接调用 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. Before that he co-founded MapMetrics, a community mapping and navigation app. He writes about geospatial infrastructure, map data, AI search visibility, and how location data powers the products people rely on every day.

查看所有文章 →
返回博客