shuilong

shuilong的博客

自訂地圖

背景#

最近接到一個項目,需要做一個網頁,內容是一個鎮的影像圖,鼠標移到每個村莊,相應村莊需要高亮,點擊後需要跳轉到該村莊的詳細頁。

微信截圖_20230923021604

技術預研#

echart 有地圖組件支持地圖的展示,比如 美國地圖。所以把影像圖作為底圖,然後在上面使用 echart 展示地圖,就能達到效果。

問題是地圖展示需要 geo 數據。所以網上搜尋了下,比如像 阿里雲的數據可視化平台 最小支持市級(區縣邊界),而 geojson.cn 有提供支持縣級(街道邊界)的數據,卻是按需製作和收費的。但我們這裡的數據需要是鎮級別(村邊界)的,https://hxkj.vip/map/ 裡有說可以定制村邊界,但聯繫了對方 qq 後,沒有回應,所以只能自己去製作村級別的 geo 數據。

製作 geo 數據#

影像圖

由於影像圖裡有用特定顏色的線條標註出了村邊界,我們先通過識別顏色來識別邊界。

let img = new window.Image();
img.crossOrigin = `Anonymous`;
img.src = "./image-1.jpg";
img.onload = function () {
  canvas = document.getElementById("myCanvas");
  canvas.width = img.width;
  canvas.height = img.height;
  let ctx = canvas.getContext(
    "2d"
  );
  ctx.drawImage(img, 0, 0);

  const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  data = imgData.data;

  for (let i = 0; i < canvas.width; ++i) {
    for (let j = 0; j < canvas.height; ++j) {
        let cols = canvas.width;
        let c = extractPixelColor(cols, j, i);

        const key = [c.red, c.green, c.blue].join('_');

        if (c.red >= 200 || c.red <= 180
              || c.green >= 10 
              || c.blue >= 249 || c.blue <= 200) {
            continue;
        }

        appendMark({
            left: i,
            top: j
        })
    }
  }
};

function appendMark(position) {
  const mark = document.createElement('div');
  mark.style.width = '6px';
  mark.style.height = '6px';
  mark.style.borderRadius = '50%';
  mark.style.position = 'absolute';
  mark.style.top = `${position.top}px`;
  mark.style.left = `${position.left}px`;
  mark.style.background = '#000';
  mark.classList.add('mark');

  mark.onclick = function() {
      this.classList.add('active');
      mark.dataset.index = index;
      index++;
  };

  const container = document.getElementById('myCanvasContainer');
  container.appendChild(mark);
}

const extractPixelColor = (cols, x, y) => {
  let pixel = cols * x + y;
  let position = pixel * 4;
  return {
    red: data[position],
    green: data[position + 1],
    blue: data[position + 2],
    alpha: data[position + 3],
  };
};

上面的代碼通過 getImageData 獲取圖片數據,然後遍歷圖片的每個像素,找到邊界的特定顏色,就在網頁中的該位置生成一個標註點。如下圖。

1222

我們選定某個村,然後繞著順時針方向去標註,就得到了該村的邊界。

2222

上面代碼中這段代碼的作用是可以對顏色的判斷有一點包容性。

if (c.red >= 200 || c.red <= 180
                 || c.green >= 10 
                 || c.blue >= 249 || c.blue <= 200) {
                continue;
            }
const data = [...document.querySelectorAll('.active')]
    .map((item) => { return {
        left: parseInt(item.style.left), 
        top: parseInt(item.style.top),
        index: parseInt(item.dataset.index)} 
    })
    .sort((a, b) => {
        return a.index - b.index;
    });

然後執行這段代碼就可以得到組成某個村的邊界的所有點的數據,每個點的數據包含lefttop,代表該點相對於影像圖的位置。

有了這些數據後,我們需要計算出每個點的經緯度。打開 邊界生成器 然後導航到需要繪製的村的大致位置。

微信圖片_20230922211616

記得切換地圖到衛星模式,這樣可以更好地辨認出具體的村莊。

使用左側工具欄裡的矩形標註功能,標註出村莊所在的位置。

222323

點擊右側的查看 GeoJSON

微信圖片_20230922211945

微信圖片_20230922212020

就能得到四個位置的經緯度。

比如我們得到左上角和右下角的經緯度。

const containerCoordinates = [
    [
        121.16169327,
        37.50491774
      ],
    
      [
        121.22931442,
        37.43753236
      ]
    ]
const CANVAS_WIDTH = 2000;
const CANVAS_HEIGHT = 2666;
const convert = ([left, top]) => {
    const x = containerCoordinates[0][0] + (containerCoordinates[1][0] - containerCoordinates[0][0]) * (left / CANVAS_WIDTH);
    const y = containerCoordinates[0][1] - (containerCoordinates[0][1] - containerCoordinates[1][1]) * (top / CANVAS_HEIGHT);

    return [x, y];
}

data.push(data[0]);
const coordinates = data.map((item) => {
    return convert([item.left, item.top]);
});

再把之前的數據通過這樣的轉換,就能得到每個點的經緯度數據。

本質上這個方法是有問題的,因為經緯度是相對地球球體來說的,而二維平面上的位置和經緯度之間需要做投影,也就是 墨卡托投影 。echart 有 convertFromPixel 方法可以從二維平面的位置轉換成經緯度。

const myChart = echarts.init(document.getElementById('main'));
myChart.setOption({
    geo: {
        left: 0,
        top: 0,
        right: 0,
        bottom: 0,
        boundingCoords: [
            [
                121.16169327,
                37.50491774
            ],
            [
                121.22931442,
                37.43753236
            ]
        ]
    }
});
const convert = ([left, top]) => {
    return myChart.convertFromPixel({geoIndex: 0}, [left, top]);
}
data.push(data[0]);
const coordinates = data.map((item) => {
    return convert([item.left, item.top]);
});

實際上,一開始我並沒使用墨卡托投影轉換,而是直接使用第一種方式,發現也大差不差,可能是因為要展示的區域非常小,所以幾乎就等於二維平面。

地圖展示#

const data = {
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "id": "01",
            "properties": { "name": "丁村" },
            "geometry": {
                "type": "Polygon",
                "coordinates": coordinates
            }
        }
    ]
}
echarts.registerMap('village', data);

const myChart = echarts.init(document.getElementById('main'));
myChart.setOption({
    geo: {
        map: 'village',
        itemStyle: {
            areaColor: 'transparent',
            borderColor:  '#c004f7',
            borderWidth: 0
        },
        label: {
          formatter({name}) {
            return '';
          }
        }
    }
});

註冊地圖數據,並使用 echart 實例化地圖。

微信圖片_20230922220814

但上面有個問題是,echart 地圖並沒有和底圖完全重合。解決方式是可以使用 layoutCenter 和 layoutSize 來微調下,通過 layoutCenter 屬性定義地圖中心在容器中的位置,layoutSize 定義地圖的大小。

myChart.setOption({
  geo: {
    layoutCenter: ['49.5%', '51.5%'],
    layoutSize: 2200,
  }
});

微信圖片_20230922222120

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。