Pull to refresh

Отрисовка карт с условными координатами

Reading time4 min
Views8.4K

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


В прошлой своей статье я рассказал про построение простых графиков с помощью библиотеки d3, с ее же помощью планировал отрисовывать и карты, но поэкспериментировав с d3, Raphael и paper.js понял что велосипедостроения избежать не удастся и переделал отрисовку на HTML Canvas, о чем и хочу рассказать в данной статье.


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


Первая мысль была — конечно же воспользоваться готовой картографической библиотекой (еще даже до плотного знакомства с d3 ставил с ними опыты). Сложностей было две, первое — условные координаты и второе — большое количество объектов (до 10 тыс. точек на карту). И если с условными координатами leaflet.js справился то отображение на нем огромного количества точек в рамках одной карты показал что нужно смотреть в другую сторону.


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


Следующим был Raphael.js — прекрасная графическая библиотека и в сравнении с той же d3 очень простая и понятная (простая в смысле простоты использования). На Рафаэле я реализовал практически все что мне было нужно, плюс сама библиотека предоставляла огромное количество плюшек и удобств, и будь у меня задачи немного другие пользовался бы Рафаэлем и радовался. Но снова уткнувшись в ограничения и мельком попробовав paper.js перешел к чистому HTML canvas. Правда к этому времени я переписал уже практически все и для того чтобы перейти с Рафаэля на канвас пришлось заменить максимум с десяток строчек в коде.


Ну хватит со вступлениями, перехожу к реализации. Первое что нам нужно для эффективного отображения карты это иметь собственный Viewport (википедия переводит этот термин как Порт просмотра, что мне кажется корявым, потому буду писать дальше как вьюпорт). Термин этот пришел пришел из 3Д, а так же активно используется в 2Д играх и означает в нашем случае отображение только той части карты, который мы хотим увидеть.


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


Вот так выглядит вьюпорт (Чтобы не загромождать статью, даю выжимку кода, который целиком можно будет посмотреть на Гитхабе по ссылке в конце статьи).


class Viewport {
  constructor(param) {
    this.updateCallback = param.update;
    this.size = param.size;
    this.map = param.map;
...
  };

  set Center(koordXY) { ... };
  get Center() { ... };
  set Zoom(zoomXY) { ... };
  get Zoom() { ... };
  set Size(sizeWH) { ... };
  get Size() { ... };

  show() {
    this.vp = {
      x1: this.vX,
      x2: this.vX + this.size.w / this.zoom.x,
      y1: this.vY,
      y2: this.vY + this.size.h / this.zoom.y,
      zX: this.zoom.x,
      zY: this.zoom.y
    };
    this.updateCallback(this.vp);
  };

  caclViewPort() { ... };
  calcCenter() { ... };
  calcMaxZoom() { ... };
};

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


Ну вот, самое главное и сложное реализовано — а в остальном уже все просто:


class XyMap {
  constructor(container) {
    this.container = (typeof container === 'string') ? document.getElementById(container) : container;
    ...
    this.objects = []; //{ id: 1, caption: 'Obj1', type: 'circe', x: 0, y: 0, r: 5, color: 'red' }
    this.viewPort = null;
  };
    //add object for draw to array
  add(obj) {  ... };
  init() {
    let id = this.container.id +'_canvas';
    this.container.innerHTML = `<canvas id="${id}" width="${this.container.offsetWidth-1}" height="${this.container.offsetHeight-1}"></canvas>`;
    this.canvas = document.getElementById(id);
    this.viewPort = new Viewport({
      update: (vp) => { this.drawViewport(vp); },
      size: { w: this.container.offsetWidth-1, h: this.container.offsetHeight-1},
      map: this.limit,
      oneZoom: true
    });
    this.handleEvent = function(ev) {
      switch(ev.type) {
        case 'mousedown':
          ...
        case 'mousemove':
          ...
        case 'mouseup':
          ...
        case 'wheel':
          ...
      }
    };
    ...
  };
  scroll(x, y) { ... };
  show() { this.viewPort.show(); };
  zoomIn(value) {
    ...
    this.viewPort.Zoom = z;
    this.viewPort.show();    
  };
  zoomOut(value) {
    ...
    this.viewPort.Zoom = z;
    this.viewPort.show();
  };
  //callback for viewport vp = { x1, x2, y1, y2, zX, zY }
  drawViewport(vp) {
    let x,y,obj,objT;
    let other = this;
    let ctx = this.canvas.getContext('2d');
    let pi2 = Math.PI*2;
    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.objects.filter(d => d.x>=vp.x1 && d.x<=vp.x2 && d.y>=vp.y1 && d.y<=vp.y2).forEach(function(d) {
      x = (d.x - vp.x1) * vp.zX; y = (d.y - vp.y1) * vp.zY;
      if (d.type === 'circe') {
        ctx.beginPath();
        ctx.arc(x,y,d.r,0,pi2);
        ctx.fillStyle = d.color;
        ctx.fill();
        ctx.lineWidth = 0.5;
        ctx.strokeStyle = 'black';        
        ctx.stroke();
        ctx.fillStyle = 'black';
        ctx.font = '8pt arial';
        ctx.fillText(d.caption, x-20, y-9);
      };
    });
  };
};

Создаем экземпляр класса XyMap, методом add передаем ему объекты для отрисовки, после чего вызываем инициализацию в процессе которой создается вьюпорт. После этого вызываем метод show — и вуаля, карта у нас на экране.


Вот собственно и все, извиняюсь за затянутое вступление и скомканную основную часть статьи — рассказал как смог. Одна надежда, что получившийся код все же читаем и говорит сам за себя.


Саму программу и пример использования можно посмотреть на Гитхабе.


UPD.: Онлайн пример в JS-песочнице.
UPD 2.: Доработал компонент: появились слои, объекты теперь можно выбирать как по клику так и извне компонента по Id, для пространственного поиска используются R-деревья (JS библиотека RBush ).

Tags:
Hubs:
Total votes 9: ↑9 and ↓0+9
Comments9

Articles