[Из песочницы] Моя первая карта на Leaflet.js

Как я делал свою первую карту на Leaflet.js.

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

Итак задание было следующее: есть черно-белый планшет (маленький кусок карты города) размером 5913x7863 пикселей в формате .bmp + .shp слои.

(изначально карты были отрисованы в формате .dwg (формат автокада), но это закрытый формат и с ним ничего не сделаешь, поэтому ребятам пришлось сохранить каждый слой отдельно в .shp + атрибутивные данные в .dbf)


Из этого всего нужно сделать онлайн карту, основной функционал которой — это вывод атрибутов при нажатии на слой и включение/отключение этих слоёв.

Выбор пал на leaflet.js, так как это оболочка с открытым кодом, на ней сделаны OSM и мой любимый 2GIS. К тому же он хорошо работает на мобильных устройствах.
Начинаем с того, что нужно порезать карту на тайлы (квадратики 256х256 px) чтобы карта быстро грузилась. Так как я мало чего в этом понимал, я не стал копаться с gdal2tiles.py, а его графическая оболочка maptiler стоит денег, я просто скачал простенькую программку LeafletPano. Она просто режет любую картинку на тайлы, достаточно задать минимальный и максимальный уровень приближения (переменную z).

Скриншот LeafletPano
2cf8bd21393e497d84c9e7cfc99a7248.png


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

Код скрипта
map = L.map('map', {                                                       // подключаем карту
     crs: L.CRS.Wall,                                                    // выбираем систему координат (об этом ниже)
        maxZoom: 5,                                                     // максимальный zoom (приближение)
         minZoom: 0                                                      // минимальный zoom (приближение)
        }).setView([1700,170], 0);                                      // точка просмотра при заходе на сайт с картой
 
        var osn = L.tileLayer('./images/map/{z}-{x}-{y}.jpg', {         // подключаем тайлы
            attribution: 'супер-карты',                               // комментарий (отображается справа внизу)
            continuousWorld: true,                                   // В документах написано, что если мы используем не настоящие координаты, то должно быть true (что-то связанное с долготой и широтой)
            noWrap: true                                                  // Не загружает до бесконечности лишние тайлы вне рамок карты
        }).addTo(map); 



После нарезки нужно привязать тайлы (квадратики 256х256 px) к координатам. И тут начинается самое интересное: дело в том, что система координат этих карт — условная местная план схема, то есть это плоская местная городская система координат, которая по сути не имеет никакого отношения к широте и долготе.

Итак, что предстояло сделать: карта размером 5913x7863 пикселей должна была находится вот в таких координатах:

42c2642f979e44bf9d235678c048bc82.png

В чем трудность? В том, что, как я уже говорил, тайлы — это квадратики 256х256px, а число пикселей по высоте 7863 на 256 без остатка не делится, не хватает 73 пикселя. Соответственно программа LeafletPano (как и любой другой нарезчик на тайлы) дополняет последние квадратики белым цветом, чтобы их размер был 256х256, а не 256х183.
Выглядит это так:

62c7cf3a662b4832b570786b4c436272.jpg

Аналогично с шириной — 5913 также не делится на 256 и не хватает 231px.

228eb6c7e0b747e6a6fe51e561b7d33f.jpg

И так как речь идёт о плоской системе координат, то заходя в документацию мы видим готовую, встроенную в Leaflet систему L.CRS.Simple (она предназначена для плоских изображений, без привязки к координатам). Как она работает? L.CRS.Simple выставляет ширину и высоту на 256 и -256 соответственно.

023316bc6f02459b98751a2406027e1d.jpg

На основе L.CRS.Simple мы и будем создавать нашу систему координат при помощи transformation = new L.Transformation(a, b, c, d). Формула в документации приведена очень простая: (x,y) трансформируются в (a*x + b, c*y + d), всего то 4 числа. Дальше немного математики:

Вычисляем высоту в нужных нам координатах:

1968.2715 - 1468.9700 = 499,3015

(Это высота нашей карты в координатах).

Делим ее на высоту в пикселях:

499,3015 / 7863 = 0,0635001271779219


Умножаем на 73 недостающих пикселя:

0,0635001271779219 * 73 = 4,6355092839883


Прибавляем высоту карты в координатах к высоте недостающих пикселов в координатах (надеюсь понятно написал):

499,3015 + 4,6355092839883 = 503,9370092839883

(Получили высоту карты + белый остаток тайла (те самые 73 пиксела в координатах)).

Теперь делим высоту в предыдущей системе координат (L.CRS.Simple) на нужную нам высоту:

256 / 503,9370092839883 = 0,5079999985786595


И вычитаем из единицы этот коэффициент:

1 - 0,5079999985786595 = 0,4920000014213405


Ура! Мы получили a и -c из формулы (a*x + b, c*y + d), осталось получить b и d

координату по Х (почему-то без минуса) умножаем на коэффициент:

11.7050 * 0,4920000014213405 = 5,758860016636791


и координату по Y умножаем на коэффициент

1968.2715 * 0,4920000014213405 = 968,389580797584

Создаем новую систему координат:

L.CRS.Wall = L.extend({}, L.CRS.Simple, {
  transformation: new L.Transformation(0.4920000014213405, 5.758860016636791, -0.4920000014213405, 968.389580797584),
});


Теперь подключаем .shp. Основная суть этих интерактивных карт в выводе атрибутивной информации при нажатии на карту. При подключении .shp она есть, но русские надписи выводятся кракозябрами. Атрибутивная информация хранится в файле .dbf и дело явно в его кодировке, но что бы вы не делали с ним — это не поможет (я испробовал огромное количество способов изменить кодировку .dbf), поэтому единственный выход — это перевести .shp в более родной для Leaflet формат .geojson с указанием кодировки utf-8. Сделать это можно при помощи программы QGIS

a2918e4ffaf3436299f54358e549d632.png

Далее мы подключаем плагин для Leaflet, который облегчает форму записи подключение слоев .geojson под названием leaflet-ajax

Подключаем .geojson слои:

Код вставки .geojson
var dorogi = new L.GeoJSON.AJAX("geoj/dorogi.geojson", {onEachFeature: function (feature, layer) {     
      if (feature.properties) {
        var info = function(k){
            var str = k + ": " + feature.properties[k];
            return str;
        }
        layer.bindPopup(Object.keys(feature.properties).map(info).join("<br />"),{maxHeight:200});
        layer.setStyle({ color: '#555', clickable: true, weight: 4, opacity: 0.8});
      }
    }});



Ну и в конце делаем отображение слоев включаемым/отключаемым, для этого вставляем L.control.layers:

Код L.control.layers
var baseMaps = {};
    var overlayMaps = {
      "Планшет": osn,
       "Дороги": dorogi,
       "Газопровод": gazoprovod,
      "Водопровод": vodoprovod,
      "Здания": zdaniz_new
        };
    L.control.layers(baseMaps, overlayMaps).addTo(map);    



Итог:

e76119b666e94b539f63ea178a9a41e8.png

P.S. Что в дальнейшем: хочется, чтобы карты адекватно выглядели, как на OSM, а не просто здания обозначеные линиями. Также я не уверен, что делаю самым адекватным способом, наверное есть способы с GeoServer или что-то вроде этого, где не надо так много делать вручную. Скорее всего нужно копать в сторону устройства карт на OSM, потому что сейчас мои карты выглядят ужасно.

Любые подсказки и замечания приветствуются.

© Habrahabr.ru