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

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。