背景#
最近接到一個項目,需要做一個網頁,內容是一個鎮的影像圖,鼠標移到每個村莊,相應村莊需要高亮,點擊後需要跳轉到該村莊的詳細頁。
技術預研#
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 獲取圖片數據,然後遍歷圖片的每個像素,找到邊界的特定顏色,就在網頁中的該位置生成一個標註點。如下圖。
我們選定某個村,然後繞著順時針方向去標註,就得到了該村的邊界。
上面代碼中這段代碼的作用是可以對顏色的判斷有一點包容性。
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;
});
然後執行這段代碼就可以得到組成某個村的邊界的所有點的數據,每個點的數據包含left
和top
,代表該點相對於影像圖的位置。
有了這些數據後,我們需要計算出每個點的經緯度。打開 邊界生成器 然後導航到需要繪製的村的大致位置。
記得切換地圖到衛星模式,這樣可以更好地辨認出具體的村莊。
使用左側工具欄裡的矩形標註功能,標註出村莊所在的位置。
點擊右側的查看 GeoJSON
就能得到四個位置的經緯度。
比如我們得到左上角和右下角的經緯度。
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 實例化地圖。
但上面有個問題是,echart 地圖並沒有和底圖完全重合。解決方式是可以使用 layoutCenter 和 layoutSize 來微調下,通過 layoutCenter 屬性定義地圖中心在容器中的位置,layoutSize 定義地圖的大小。
myChart.setOption({
geo: {
layoutCenter: ['49.5%', '51.5%'],
layoutSize: 2200,
}
});