Создайте движок веб-карты с нуля

внешний интерфейс GIS
Создайте движок веб-карты с нуля

Когда дело доходит до карт, вы должны быть хорошо с ними знакомы.Вы должны были использовать карты Baidu, карты AutoNavi, карты Tencent и т. д. Если речь идет о потребностях в разработке, связанных с картами, есть также много вариантов. Например, предыдущие карты предоставит наборjs API, а также есть некоторые фреймворки с открытым исходным кодом, которые можно использовать, напримерOpenLayers,LeafletЖдать.

Задумывались ли вы когда-нибудь о том, как визуализируются эти карты? Почему соответствующая карта может отображаться в зависимости от широты и долготы? Неважно, если вы этого не знаете. В этой статье вы разработаете простой картографический движок из поцарапать, чтобы помочь вам понятьGISбазовые знания иWebПринцип реализации карты.

Выберите широту и долготу

Во-первых, давайте перейдем к карте Gaode и выберем широту и долготу в качестве центральной точки нашей последующей карты, откроем ее.Пикап координат Гаодеинструмент, выберите точку наугад:

image-20220104161043710.png

Автор выбрал пагоду Лэйфэн в Ханчжоу, широта и долгота которой:[120.148732,30.231006].

анализ URL плитки

Для фрагментов карты мы используем онлайн-тайлы Gaode, а адреса следующие:

https://webrd0{1-4}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8

В настоящее время тайловые сервисы крупных производителей карт следуют другим правилам:

Спецификация Google XYZ: карта Google, OpenStreetMap, карта Gaode, geoq, карта звездного неба, начало координат находится в верхнем левом углу.

Спецификация TMS: карта Tencent, начало координат находится в левом нижнем углу.

Спецификация WMTS: начало координат в верхнем левом углу, плитка не квадратная, а прямоугольная, это должно быть официальным стандартом

Карта Baidu совершенно уникальна. Проекция, разрешение и система координат отличаются от других производителей. Начало находится в позиции, где широта и долгота равны 0, что является серединой. Правильное направление — положительное направление X, а направление вверх является положительным направлением Y.

Google иTMSНомера строк и столбцов плитки различаются следующим образом:

image.png

Хотя спецификации разные, принципы в основном одинаковые.Все они проецируют землю на огромный квадратный план мира, а затем разрезают их на слои в соответствии с деревом квадрантов.Например, первый слой имеет только один тайл для отображения информации всего мира., так что в основном вы видите только названия и границы континентов и морей.Второй слой разрезан на четыре плитки, отображающие немного больше информации, и так далее, так же как пирамида, нижний слой имеет самое высокое разрешение, и дисплей имеет наибольшее количество деталей, самое большое количество плиток, самое низкое разрешение верхнего уровня, отображает очень мало информации и имеет наименьшее количество плиток:

image-20220105134723330.png

Формула расчета количества плиток в каждом слое:

Math.pow(Math.pow(2, n), 2)// 行*列:2^n * 2^n

Требуется восемнадцатый этаж68719476736тайлы, поэтому общее количество тайлов карты очень велико.

После вырезания плитки она сохраняется по номеру строки и столбца и уровнем масштабирования, поэтому вы можете увидеть, что в адресе плитка есть три переменных:x,y,z

x:行号
y:列号
z:分辨率,一般为0-18

С помощью этих трех переменных можно найти тайл, например, следующий адрес, номер строки109280, номер столбца53979, уровень масштабирования17:

https://webrd01.is.autonavi.com/appmaptile?x=109280&y=53979&z=17&lang=zh_cn&size=1&scale=1&style=8

Соответствующие плитки:

img

Введение в системы координат

Карта Гаоде используетGCJ-02坐标系, также известная как система координат Марса, выпущенная Национальным бюро геодезии и картографии Китая в 2002 году, находится в координатах GPS (WGS-84Система координат) шифруется на основе, то есть добавляется нелинейное смещение, что делает вас неуверенным в реальном положении.В целях национальной безопасности отечественным поставщикам картографических услуг необходимо использоватьGCJ-02坐标系.

WGS-84Система координат является международным стандартом,EPSGНетEPSG:4326, обычно исходные широта и долгота, полученные GPS-устройствами и те, которые используются иностранными производителями карт,WGS-84Система координат.

Обе системы координат являются географическими системами координат, сферическими координатами, в единицах,такой вид координат удобен для позиционирования на земле,но не удобен для отображения и расчёта площади.Карты в нашем впечатлении все плоские,поэтому там другая плоская система координат.Плоская система координат проецируется из Она преобразуется в географическую систему координат, поэтому ее также называют проекционной системой координат, обычно в единицах, существует много видов проекционных систем координат в соответствии с различными методами проецирования, вWebСценарии развития обычно используются вWeb墨卡托投影,НетEPSG:3857, на основе которого墨卡托投影,ПучокWGS-84Система координат проецируется в квадрат, что достигается отбрасыванием севера и юга.85.051129纬度Вышеупомянутая область реализована, потому что она квадратная, поэтому большой квадрат можно легко разделить на меньшие квадраты.

Номер строки и столбца позиционирования долготы и широты

В предыдущем разделе мы кратко представили систему координат, согласноWebКарты стандартов, которые мы выбрали для поддержки карты двигателяEPSG:3857Проекция, но то, что мы получили с помощью инструмента Gaode, — это координаты широты и долготы системы координат Марса, поэтому первый шаг — преобразовать координаты широты и долготы вWeb墨卡托Координаты проекции, здесь для простоты, сначала непосредственно рассматривают координаты Марса какWGS-84Координаты, мы рассмотрим этот вопрос позже.

Метод преобразования можно найти в Интернете, выполнив поиск:

// 角度转弧度
const angleToRad = (angle) => {
    return angle * (Math.PI / 180)
}

// 弧度转角度
const radToAngle = (rad) => {
    return rad * (180 / Math.PI)
}

// 地球半径
const EARTH_RAD = 6378137

// 4326转3857
const lngLat2Mercator = (lng, lat) => {
    // 经度先转弧度,然后因为 弧度 = 弧长 / 半径 ,得到弧长为 弧长 = 弧度 * 半径 
    let x = angleToRad(lng) * EARTH_RAD; 
    // 纬度先转弧度
    let rad = angleToRad(lat)
    // 下面我就看不懂了,各位随意。。。
    let sin = Math.sin(rad)
    let y = EARTH_RAD / 2 * Math.log((1 + sin) / (1 - sin))
    return [x, y]
}

// 3857转4326
const mercatorTolnglat = (x, y) => {
    let lng = radToAngle(x) / EARTH_RAD
    let lat = radToAngle((2 * Math.atan(Math.exp(y / EARTH_RAD)) - (Math.PI / 2)))
    return [lng, lat]
}

3857Координаты имеют, а его единицы, а затем как преобразовать номера строк и столбцов плиток, что включает в себя分辨率Понятие , то есть фактическое количество метров, представленное пикселями на карте, если разрешение можно получить из документации производителя карты, то лучше всего, если не можете найти, то можно просто вычислить. Нам остается только обратиться к поисковикам), мы знаем, что радиус земли6378137метр,3857Система координат рассматривает землю как идеальную сферу, поэтому можно рассчитать окружность земли, а проекция близка к земному экватору:

image.png

Следовательно, длина стороны плана мира, спроецированного на квадрат, представляет собой периметр земли.Мы также знаем метод расчета количества плиток на каждом уровне, и размер плитки обычно256*256пикселей, поэтому разделите окружность земли на длину стороны расширенной карты мира, чтобы узнать, сколько метров на самом деле представляет каждый пиксель на карте:

// 地球周长
const EARTH_PERIMETER = 2 * Math.PI * EARTH_RAD
// 瓦片像素
const TILE_SIZE = 256

// 获取某一层级下的分辨率
const getResolution = (n) => {
    const tileNums = Math.pow(2, n)
    const tileTotalPx = tileNums * TILE_SIZE
    return EARTH_PERIMETER / tileTotalPx
}

Окружность Земли рассчитывается как40075016.68557849, можно увидетьOpenLayersОн рассчитывается следующим образом:

image-20220105143333164.png

3857Единицы координат, затем разделите координаты на разрешение, чтобы получить соответствующие координаты пикселей, а затем разделите на256, вы можете получить номера строк и столбцов плиток:

image-20220105185741054.png

Функция выглядит следующим образом:

// 根据3857坐标及缩放层级计算瓦片行列号
const getTileRowAndCol = (x, y, z) => {
    let resolution = getResolution(z)
    let row = Math.floor(x / resolution / TILE_SIZE)
    let col = Math.floor(y / resolution / TILE_SIZE)
    return [row, col]
}

Затем мы фиксируем иерархию как17, то разрешениеresolutionто есть1.194328566955879, широта и долгота пагоды Лэйфэн преобразуются в3857Координаты:[13374895.665697495, 3533278.205310311], используя приведенную выше функцию для вычисления номеров строк и столбцов следующим образом:[43744, 11556], подставляем эти данные в адрес тайла для доступа:

https://webrd01.is.autonavi.com/appmaptile?x=43744&y=11556&z=17&lang=zh_cn&size=1&scale=1&style=8

image-20220105150159713.png

Заготовка, зачем это, собственно, ведь происхождение другое,4326а также3857Начало системы координат находится на пересечении экватора и нулевого меридиана, в морских милях от края Африки, а начало плитки находится в верхнем левом углу:

image-20220119172036436.png

Будет легче понять, если вы посмотрите на картинку ниже:

image-20220106095034453.png

3857Начало системы координат эквивалентно середине мировой плоскости, а правееxПоложительное направление оси вверх равноyОсь находится в положительном направлении, а начало тайловой карты находится в верхнем левом углу, поэтому нам нужно рассчитать расстояние до [оранжевой сплошной линии] в соответствии с расстоянием до [зеленой пунктирной линии] на карте. Это тоже очень просто.Горизонтальная координата равна длине горизонтальной зеленой пунктирной линии плюс половина плана этажа мира, вертикальная координата равна половине плана этажа мира минус длина вертикальной зеленой пунктирной линии, половина план мира также составляет половину окружности земли, изменитьgetTileRowAndColфункция:

const getTileRowAndCol = (x, y, z) => {
  x += EARTH_PERIMETER / 2     // ++
  y = EARTH_PERIMETER / 2 - y  // ++
  let resolution = getResolution(z)
  let row = Math.floor(x / resolution / TILE_SIZE)
  let col = Math.floor(y / resolution / TILE_SIZE)
  return [row, col]
}

Номера строк и столбцов плитки, рассчитанные на этот раз, равны[109280, 53979], подставьте адрес плитки:

https://webrd01.is.autonavi.com/appmaptile?x=109280&y=53979&z=17&lang=zh_cn&size=1&scale=1&style=8

Результат выглядит следующим образом:

image-20220106095801592.png

Вы можете увидеть выход пагоды Лэйфэн.

Расчет положения отображения плитки

Теперь мы можем найти соответствующий тайл по широте и долготе, но этого недостаточно.Наша цель — отобразить его в браузере.Для этого нужно решить две задачи: сколько тайлов нужно загрузить, а другой — для расчета каждой плитки Отображаемая позиция плитки.

Для рендеринга тайлов мы используемcanvasХолст, шаблон выглядит следующим образом:

<template>
  <div class="map" ref="map">
    <canvas ref="canvas"></canvas>
  </div>
</template>

контейнер холста картыmapРазмер мы можем легко получить:

// 容器大小
let { width, height } = this.$refs.map.getBoundingClientRect()
this.width = width
this.height = height
// 设置画布大小
let canvas = this.$refs.canvas
canvas.width = width
canvas.height = height
// 获取绘图上下文
this.ctx = canvas.getContext('2d')

Мы устанавливаем центральную точку карты в середине холста, а широту и долготу другой центральной точкиcenterи уровень масштабированияzoomПоскольку все они заданы нами самими, они также известны, то мы можем вычислить тайл, соответствующий координате центра:

// 中心点对应的瓦片
let centerTile = getTileRowAndCol(
    ...lngLat2Mercator(...this.center),// 4326转3857
    this.zoom// 缩放层级
)

Уровень масштабирования по-прежнему установлен на17, центральная точка по-прежнему использует широту и долготу пагоды Лэйфэн, тогда соответствующий номер строки и столбца плитки был рассчитан ранее, как[109280, 53979].

Известны номера строки и столбца тайла, соответствующие координатам центра, тогда мы также знаем положение пикселя верхнего левого угла тайла в плане мира:

// 中心瓦片左上角对应的像素坐标
let centerTilePos = [centerTile[0] * TILE_SIZE, centerTile[1] * TILE_SIZE]

рассчитывается как[27975680, 13818624]. Как преобразовать эту координату в экранную, смотрите на следующем рисунке:

image-20220106143543672.png

Мы вычислили тайл широты и долготы центра, а так же знаем координаты пикселя левого верхнего угла тайла, а потом вычисляем координаты пикселя соответствующие широте и долготе самого центра, далее разница с левым верхним можно вычислить угол плитки, и, наконец, мы переместим начало холста в середину холста (начало холста по умолчанию — верхний левый угол, положительное направление оси X — вправо, и положительное направление оси Y вниз), то есть широта и долгота центра используются в качестве начала координат, тогда положение отображения центральной плитки является этой разницей.

Добавьте метод для преобразования широты и долготы в пиксели:

// 计算4326经纬度对应的像素坐标
const getPxFromLngLat = (lng, lat, z) => {
  let [_x, _y] = lngLat2Mercator(lng, lat)// 4326转3857
  // 转成世界平面图的坐标
  _x += EARTH_PERIMETER / 2
  _y = EARTH_PERIMETER / 2 - _y
  let resolution = resolutions[z]// 该层级的分辨率
  // 米/分辨率得到像素
  let x = Math.floor(_x / resolution)
  let y = Math.floor(_y / resolution)
  return [x, y]
}

Вычислить координаты пикселя, соответствующие широте и долготе центра:

// 中心点对应的像素坐标
let centerPos = getPxFromLngLat(...this.center, this.zoom)

Вычислите разницу:

// 中心像素坐标距中心瓦片左上角的差值
let offset = [
    centerPos[0] - centerTilePos[0],
    centerPos[1] - centerTilePos[1]
]

наконец прошлоcanvasЧтобы визуализировать центральную плитку:

// 移动画布原点到画布中间
this.ctx.translate(this.width / 2, this.height / 2)
// 加载瓦片图片
let img = new Image()
// 拼接瓦片地址
img.src = getTileUrl(...centerTile, this.zoom)
img.onload = () => {
    // 渲染到canvas
    this.ctx.drawImage(img, -offset[0], -offset[1])
}

Давайте посмотрим здесьgetTileUrlРеализация метода:

// 拼接瓦片地址
const getTileUrl = (x, y, z) => {
  let domainIndexList = [1, 2, 3, 4]
  let domainIndex =
    domainIndexList[Math.floor(Math.random() * domainIndexList.length)]
  return `https://webrd0${domainIndex}.is.autonavi.com/appmaptile?x=${x}&y=${y}&z=${z}&lang=zh_cn&size=1&scale=1&style=8`
}

Вот четыре случайных поддомена:webrd01,webrd02,webrd03,webrd04, это связано с тем, что браузер имеет ограничение на количество ресурсов, запрашиваемых одним и тем же доменным именем одновременно, и когда уровень карты становится больше, количество тайлов, которые необходимо загрузить, будет больше, поэтому запросы могут быть равномерно распределены по каждому поддомену для более быстрого рендеринга. Все плитки выпускаются для сокращения очередей и времени ожидания. По сути, адреса службы плиток всех производителей карт поддерживают несколько поддоменов.

Для того, чтобы легко увидеть положение центральной точки, мы визуализируем две дополнительные центральные вспомогательные линии, эффект будет следующим:

image-20220106150430636.png

Видно, что центральная точка действительно является пагодой Лэйфэн. Конечно, это отображает только центральную плитку. Нам нужно, чтобы плитка покрывала весь холст. Для других плиток мы можем вычислить ее на основе центральной плитки, например, левая сторона центральной плитки. , которая рассчитывается следующим образом:

// 瓦片行列号,行号减1,列号不变
let leftTile = [centerTile[0] - 1, centerTile[1]]
// 瓦片显示坐标,x轴减去一个瓦片的大小,y轴不变
let leftTilePos = [
    offset[0] - TILE_SIZE * 1,
    offset[1]
]

Таким образом, нам нужно только рассчитать, сколько плиток необходимо в каждом из четырех направлений центральной плитки, а затем использовать двойной цикл для расчета всех плиток, необходимых для холста.Рассчитать необходимое количество плиток очень просто, см. следующий рисунок:

image.png

Половину ширины и высоты холста за вычетом места, занимаемого центральной плиткой, можно получить оставшееся пространство в этом направлении, а затем разделить на размер плитки, чтобы узнать, сколько плиток необходимо:

// 计算瓦片数量
let rowMinNum = Math.ceil((this.width / 2 - offset[0]) / TILE_SIZE)// 左
let colMinNum = Math.ceil((this.height / 2 - offset[1]) / TILE_SIZE)// 上
let rowMaxNum = Math.ceil((this.width / 2 - (TILE_SIZE - offset[0])) / TILE_SIZE)// 右
let colMaxNum = Math.ceil((this.height / 2 - (TILE_SIZE - offset[1])) / TILE_SIZE)// 下

Мы берем центральную плитку за начало координат, а координаты[0, 0], сканирование с двойным циклом может отображать все плитки:

// 从上到下,从左到右,加载瓦片
for (let i = -rowMinNum; i <= rowMaxNum; i++) {
    for (let j = -colMinNum; j <= colMaxNum; j++) {
        // 加载瓦片图片
        let img = new Image()
        img.src = getTileUrl(
            centerTile[0] + i,// 行号
            centerTile[1] + j,// 列号
            this.zoom
        )
        img.onload = () => {
            // 渲染到canvas
            this.ctx.drawImage(
                img, 
                i * TILE_SIZE - offset[0], 
                j * TILE_SIZE - offset[1]
            )
        }
    }
}

Эффект следующий:

image-20220106183134954.png

превосходно.

тянуть

Перетаскивание можно рассматривать таким образом.Тайл с указанными широтой и долготой уже отрисовывался ранее.Когда мы зажимаем и перетаскиваем, мы можем узнать расстояние скольжения мыши, а затем преобразовать расстояние, то есть пиксель, в значение широты и долготы, и, наконец, мы обновляем широту и долготу текущей центральной точки, очищаем холст, вызываем предыдущий метод для повторного рендеринга и продолжаем повторный рендеринг, вызывая визуальную иллюзию движения.

Прослушивание событий, связанных с мышью:

<canvas ref="canvas" @mousedown="onMousedown"></canvas>
export default {
    data(){
        return {
            isMousedown: false
        }
    },
    mounted() {
        window.addEventListener("mousemove", this.onMousemove);
        window.addEventListener("mouseup", this.onMouseup);
    },
    methods: {
        // 鼠标按下
        onMousedown(e) {
            if (e.which === 1) {
                this.isMousedown = true;
            }
        },

        // 鼠标移动
        onMousemove(e) {
            if (!this.isMousedown) {
                return;
            }
            // ...
        },

        // 鼠标松开
        onMouseup() {
            this.isMousedown = false;
        }
    }
}

существуетonMousemoveВ методе вычислите широту и долготу центра после перетаскивания и повторного рендеринга холста:

// 计算本次拖动的距离对应的经纬度数据
let mx = e.movementX * resolutions[this.zoom];
let my = e.movementY * resolutions[this.zoom];
// 把当前中心点经纬度转成3857坐标
let [x, y] = lngLat2Mercator(...this.center);
// 更新拖动后的中心点经纬度
center = mercatorToLngLat(x - mx, my + y);

movementXа такжеmovementYСвойство может получить значение движения этого и последнего события мыши.Совместимость не очень хорошая, но также очень просто вычислить значение самостоятельно.Для деталей, пожалуйста, переместитеMDN. Умножить на текущее разрешение像素Перевести в, а затем преобразуйте широту и долготу текущей центральной точки в3857изКоординаты, компенсация расстояния этого хода и, наконец, повернуть назад4326В качестве обновленной центральной точки можно использовать координаты широты и долготы.

Зачемxминус,yДа очень просто, когда мы двигаем мышь вправо и вниз, расстояние положительное, и соответствующая карта будет двигаться вправо или вниз.4326Система координат находится в положительном направлении вправо и вверх, затем, когда карта движется вправо, центральная точка, очевидно, перемещается влево.Поскольку правое направление является положительным направлением, направление долготы центральной точки уменьшается, поэтому оно вычитается.Расстояние перемещается, и карта движется вниз, центральная точка перемещается относительно вверх, потому что вверх является положительным направлением, поэтому направление широты центральной точки увеличивается, поэтому добавьте расстояние перемещения.

После обновления широты и долготы центра очистите холст и перерисуйте:

// 清空画布
this.clear();
// 重新绘制,renderTiles方法就是上一节的代码逻辑封装
this.renderTiles();

Эффект следующий:

whbm.gif

Видно, что это беспорядок. Почему это? На самом деле это потому, что загрузка изображений является асинхронным процессом. В процессе перемещения мыши мы будем непрерывно вычислять тайлы для загрузки и загружать их, но последний партия плиток может быть загружена. Прежде чем загрузка будет завершена, мышь переместилась в новую позицию, и рассчитывается, что будет загружена новая партия плиток. В это время предыдущая партия плиток могла быть загружена и визуализирована, но некоторые из этих тайлов могли быть перемещены.Кроме канвы, отображать не нужно, некоторые могут быть еще в канве, но предыдущее положение все еще используется, и рендеринг неправильный. новая партия тайлов также может быть загружена и визуализирована, что, естественно, приводит к путанице в конечном отображении.

Причину узнать несложно.Сначала добавляем кеш-объект,потому что в процессе перетаскивания многие тайлы просто меняют свои позиции и не нуждаются в повторной загрузке.Один и тот же тайл загружается один раз,и только его положение можно обновить потом ; Установите другой объект для записи плиток, которые должны отображаться на текущем холсте, чтобы предотвратить отрисовку плиток, которые не должны отображаться:

{
    // 缓存瓦片
    tileCache: {},
    // 记录当前画布上需要的瓦片
    currentTileCache: {}
}

Поскольку нам нужно записать местоположение, статус загрузки и другую информацию о плитке, мы создаем класс плитки:

// 瓦片类
class Tile {
  constructor(opt = {}) {
    // 画布上下文
    this.ctx = ctx
    // 瓦片行列号
    this.row = row
    this.col = col
    // 瓦片层级
    this.zoom = zoom
    // 显示位置
    this.x = x
    this.y = y
    // 一个函数,判断某块瓦片是否应该渲染
    this.shouldRender = shouldRender
    // 瓦片url
    this.url = ''
    // 缓存key
    this.cacheKey = this.row + '_' + this.col + '_' + this.zoom
    // 图片
    this.img = null
    // 图片是否加载完成
    this.loaded = false

    this.createUrl()
    this.load()
  }
    
  // 生成url
  createUrl() {
    this.url = getTileUrl(this.row, this.col, this.zoom)
  }

  // 加载图片
  load() {
    this.img = new Image()
    this.img.src = this.url
    this.img.onload = () => {
      this.loaded = true
      this.render()
    }
  }

  // 将图片渲染到canvas上
  render() {
    if (!this.loaded || !this.shouldRender(this.cacheKey)) {
      return
    }
    this.ctx.drawImage(this.img, this.x, this.y)
  }
    
  // 更新位置
  updatePos(x, y) {
    this.x = x
    this.y = y
    return this
  }
}

Затем измените логику предыдущих плиток рендеринга с двойным циклом:

this.currentTileCache = {}// 清空缓存对象
for (let i = -rowMinNum; i <= rowMaxNum; i++) {
    for (let j = -colMinNum; j <= colMaxNum; j++) {
        // 当前瓦片的行列号
        let row = centerTile[0] + i
        let col = centerTile[1] + j
        // 当前瓦片的显示位置
        let x = i * TILE_SIZE - offset[0]
        let y = j * TILE_SIZE - offset[1]
        // 缓存key
        let cacheKey = row + '_' + col + '_' + this.zoom
        // 记录画布当前需要的瓦片
        this.currentTileCache[cacheKey] = true
        // 该瓦片已加载过
        if (this.tileCache[cacheKey]) {
            // 更新到当前位置
            this.tileCache[cacheKey].updatePos(x, y).render()
        } else {
            // 未加载过
            this.tileCache[cacheKey] = new Tile({
                ctx: this.ctx,
                row,
                col,
                zoom: this.zoom,
                x,
                y,
                // 判断瓦片是否在当前画布缓存对象上,是的话则代表需要渲染
                shouldRender: (key) => {
                    return this.currentTileCache[key]
                },
            })
        }
    }
}

Эффект следующий:

whbm.gif

Видно, что перетаскивание нормальное.Конечно, приведенная выше реализация все еще очень грубая, и есть много мест, которые нужно оптимизировать, например:

1. Как правило, сначала сортируется порядок, а центральная плитка загружается первой.

2. Все больше и больше кэшированных тайлов определенно повлияют на производительность, поэтому необходимы некоторые стратегии очистки.

Желающие могут подумать сами.

зум

Перетаскивание предназначено для обновления широты и долготы центральной точки в режиме реального времени, затем масштабирование естественным образом обновляет уровень масштабирования:

export default {
    data() {
        return {
            // 缩放层级范围
            minZoom: 3,
            maxZoom: 18,
            // 防抖定时器
            zoomTimer: null
        }
    },
    mounted() {
        window.addEventListener('wheel', this.onMousewheel)
    },
    methods: {
        // 鼠标滚动
        onMousewheel(e) {
            if (e.deltaY > 0) {
                // 层级变小
                if (this.zoom > this.minZoom) this.zoom--
            } else {
                // 层级变大
                if (this.zoom < this.maxZoom) this.zoom++
            }
            // 加个防抖,防止快速滚动加载中间过程的瓦片
            this.zoomTimer = setTimeout(() => {
                this.clear()
                this.renderTiles()
            }, 300)
        }
    }
}

Эффект следующий:

whbm.gif

Функция есть, но эффект очень общий, потому что масштабирование карты, которое мы обычно используем, имеет анимацию перехода увеличения или уменьшения масштаба, и это непосредственно пусто, а затем повторно отображается.Если вы не посмотрите внимательно, вы не знаете, увеличить или уменьшить масштаб.

Таким образом, мы могли бы также добавить эффект перехода: когда мы прокручиваем мышь, мы сначала увеличиваем или уменьшаем масштаб на холсте, а после завершения анимации мы визуализируем необходимые плитки в соответствии с окончательным значением масштаба.

Значение масштабирования холста по умолчанию равно1, увеличение умножается на2раз, разделить на2, а затем выполните анимацию до целевого значения, задайте значение масштабирования холста и очистите холст во время анимации, перерисуйте существующие плитки на холсте, чтобы добиться визуального эффекта увеличения или уменьшения масштаба, и вызовите его после окончания анимации.renderTilesПовторно визуализируйте плитки, необходимые для окончательного значения масштаба.

// 动画使用popmotion库,https://popmotion.io/
import { animate } from 'popmotion'

export default {
    data() {
        return {
            lastZoom: 0,
            scale: 1,
            scaleTmp: 1,
            playback: null,
        }
    },
    methods: {
        // 鼠标滚动
        onMousewheel(e) {
            if (e.deltaY > 0) {
                // 层级变小
                if (this.zoom > this.minZoom) this.zoom--
            } else {
                // 层级变大
                if (this.zoom < this.maxZoom) this.zoom++
            }
            // 层级未发生改变
            if (this.lastZoom === this.zoom) {
                return
            }
            this.lastZoom = this.zoom
            // 更新缩放比例,也就是目标缩放值
            this.scale *= e.deltaY > 0 ? 0.5 : 2
            // 停止上一次动画
            if (this.playback) {
                this.playback.stop()
            }
            // 开启动画
            this.playback = animate({
                from: this.scaleTmp,// 当前缩放值
                to: this.scale,// 目标缩放值
                onUpdate: (latest) => {
                    // 实时更新当前缩放值
                    this.scaleTmp = latest
                    // 保存画布之前状态,原因有二:
                    // 1.scale方法是会在之前的状态上叠加的,比如初始是1,第一次执行scale(2,2),第二次执行scale(3,3),最终缩放值不是3,而是6,所以每次缩放完就恢复状态,那么就相当于每次都是从初始值1开始缩放,效果就对了
                    // 2.保证缩放效果只对重新渲染已有瓦片生效,不会对最后的renderTiles()造成影响
                    this.ctx.save()
                    this.clear()
                    this.ctx.scale(latest, latest)
                    // 刷新当前画布上的瓦片
                    Object.keys(this.currentTileCache).forEach((tile) => {
                        this.tileCache[tile].render()
                    })
                    // 恢复到画布之前状态
                    this.ctx.restore()
                },
                onComplete: () => {
                    // 动画完成后将缩放值重置为1
                    this.scale = 1
                    this.scaleTmp = 1
                    // 根据最终缩放值重新计算需要的瓦片并渲染
                    this.renderTiles()
                },
            })
        }
    }
}

Эффект следующий:

2022-01-13-16-23-27.gif

Хотя эффект все еще общий, но, по крайней мере, видно, что он усиливается или сужается.

Преобразование системы координат

Спереди также осталась небольшая проблема, то есть мы напрямую рассматриваем широту и долготу, выбранные на инструменте Gaode, как4326Долгота и широта, как упоминалось ранее, между ними есть смещение, например, мобильные телефоныGPSПолученные долгота и широта обычно84Координаты, отображаемые непосредственно на карте Gaode, будут отличаться от вашего фактического местоположения, поэтому вам необходимо выполнить преобразование.Существуют некоторые инструменты, которые могут вам помочь, такие какGcoord,coordtransformЖдать.

Суммировать

Вышеприведенный эффект выглядит относительно общим. На самом деле, если на основе вышеизложенного добавить небольшую анимацию затухания плитки, эффект будет намного лучше. В настоящее время он обычно используется.canvasоказывать2DКарта, если не удобно реализовывать анимацию самостоятельно, есть и мощныеcanvasБиблиотеку можно выбрать, автор ваще используетKonva.jsRedo The Library Edition, присоединившись к плиточной исчезновению анимации, окончательные результаты следующие:

whbm.gif

Кроме того, если вы понимаете правила тайлов каждой карты, вы можете немного изменить их, чтобы поддерживать больше тайлов карты:

whbm.gif

Конкретная реализация не будет расширяться из-за нехватки места, кому интересно, можно прочитать исходный код этой статьи.

В этой статье подробно описан простойwebПроцесс разработки карты, вышеизложенные принципы реализации являются только личными идеями автора, не представляютopenlayersПринцип других фреймворков, т.к. автор тожеGISНовички, так что неизбежно будут проблемы или лучшая реализация, пожалуйста, укажите.

онлайнdemo:Ван Линь 2.GitHub.IO/Web_flattering_?…

Полный исходный код:GitHub.com/Ван Линь2/Башня…