Создание пользовательской координатной системы

API предоставляет возможности для создания пользовательских координатных систем и осуществления навигации по картам, "склеенным" вдоль вертикальной и/или горизонтальной оси.

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

  1. Задать объект-точку системы, см. Интерфейс YMaps.ICoordPoint.
  2. Задать объект-область системы, см. Интерфейс YMaps.ICoordBounds .
  3. Задать правила пересчета координат системы в пикселы на последнем масштабе карты, см. Интерфейс YMaps.ICoordSystem.

Ниже подробно описано создание галактической координатной системы, предназначенной для навигации по астрономической карте нашей Галактики.

Как задать объект-точку

Интерфейс YMaps.ICoordPoint позволяет задать объект-точку пользовательской координатной системы, когда нельзя воспользоваться стандартными классами YMaps.Point и YMaps.GeoPoint.

С помощью YMaps.ICoordPoint необходимо реализовать следующие методы:

  • методы получения первой и второй координаты точки - getX() и getY();
  • методы задания первой и второй координат точки - setX() и setY();
  • метод создания копии объекта - copy();
  • методы операций над точками: смещение в точку moveTo(), смещение на вектор и разность точек в виде вектора diff();
  • метод сравнения точек equals().

Рассмотрим использование интерфейса на примере карты Млечного Пути:

На астрономической карте Галактики используются специальные координаты: галактическая долгота (изменяется от 360 градусов до 0) и галактическая широта (изменяется от -90 до 90 градусов), поэтому стандартные классы географических точек использовать нельзя.

Кроме того, карта "склеена" вдоль горизонтальной оси, поэтому точки на ней могут, как и в случае с картой Земли, быть ограниченными и неограниченными, см. Преобразование координат. Один градус в этой системе координат приблизительно равен 500 световым годам.

Ограниченные точки в галактических координатах имеют широту от 0 до 360 и долготу от -90 до 90 градусов. Неограниченные точки могут принимать произвольные координаты и, фактически, являются вектором, соединяющим начало системы координат с определенной точкой. Для различения ограниченных и неограниченных точек используется флаг unbounded.

Чтобы задать объект-точку выполните следующие шаги:

  1. Создайте вспомогательные функции для расчета ограниченности точек.

    Например:

    // Вспомогательная функция: ограничивает значение val сверху и снизу
    function boundaryRestrict(val, min, max) {
        return Math.max(Math.min(val, max), min);
    }
            
    // Вспомогательная функция: ограничивает значение val, считая что val изменяется циклически
    function cycleRestrict(val, min, max) {
         return val - Math.floor((val - min)/(max - min)) * (max-min);    
    }
  2. Создайте класс точки координатной системы.

    Например:

    // Класс точки галактической координатной системы        
    function MyPoint (x, y, unbounded) {
       this.unbounded = !!unbounded;
       this.setX(x);
       this.setY(y);
    }  
  3. Определите методы класса точки.

    Например:

    MyPoint.prototype = {
        // Возвращает галактическую долготу
        getX: function () { 
            return this.x; 
        },
    
        // Возвращает галактическую широту
        getY: function () { 
            return this.y; 
        },
    
        // Задает долготу. Если точка ограниченная, то долгота должна лежать в диапазоне от 0 до 360 градусов
        setX: function (x) { 
            this.x = this.unbounded ? x : cycleRestrict(x, 0, 360); 
            return this; 
        },
    
        // Задает широту. Широта всегда лежит в диапазоне от -90 до 90 градусов
        setY: function (y) { 
            this.y = boundaryRestrict(y, -90, 90); 
            return this; 
        },
    
        // Создает копию
        copy: function () { 
            return new MyPoint(this.x, this.y, this.unbounded); 
        },
    
        // Передвигает точку, при этом флаг ограниченности сохраняется
        moveTo: function (point) { 
            this.setX(point.getX()); this.setY(point.getY()); 
            return this; 
        },
    
        // Рассчитывает разность точек в виде вектора
        diff: function (point) {
            return new YMaps.Point(point.getX() - this.x, point.getY() - this.y);
        },
    
        // Сдвигает точку на вектор
        moveBy: function (vector) {
            this.setX(this.x + vector.getX()); 
            this.setY(this.y + vector.getY()); 
            return this; 
        },
    
        // Сравнивает две точки
        equals: function (point) {
            return (Math.abs(this.getX() - point.getX()) < 1e-8 && Math.abs(this.getY() - point.getY()) < 1e-8);
        }
    };

Как задать объект-область

Интерфейс YMaps.ICoordBounds позволяет задавать пользовательские объекты-области координатной системы, когда нельзя воспользоваться стандартными объектами YMaps.Bounds и YMaps.GeoBounds.

С помощью YMaps.ICoordBounds необходимо реализовать следующие методы:

  • getTop, getRight, getBottom, getLeft, которые возвращают верхнюю, правую, нижнюю и левую границы области, соответственно;
  • getRightTop, getRightBottom, getLeftBottom, getLeftTop, которые возвращают (в виде точки координатной системы) правый верхний, правый нижний, левый нижний и левый верхний углы области, соответственно;
  • getCenter и getSpan возвращают центр и размеры области;
  • getMapZoom, возвращающий максимальное значение коэффициента масштабирования, при котором область видна на карте целиком;
  • equals проверяющий, совпадают ли две области;
  • contains проверяющий, содержит ли область переданную точку;
  • copy возвращающий копию области.

Например, чтобы задать объект-область на карте Млечного пути, реализуйте класс MyBounds (см. пример ниже). При этом следует учесть, что точки в галактической системе координат могут быть двух типов (ограниченные и неограниченные), соответственно и область также может принадлежать к одному из двух типов: заданная либо ограниченными, либо неограниченными точками.

// Класс "область на карте"
function MyBounds(leftBottom, rightTop) {
    this.left = leftBottom.getX();
    this.right = rightTop.getX();
    this.bottom = leftBottom.getY();
    this.top = rightTop.getY();
    // Флаг unbounded указывает какими точками задана область: ограниченными или неограниченными
    this.unbounded = leftBottom.unbounded && rightTop.unbounded;
}

MyBounds.prototype = {
    // Границы области
    getTop: function () { 
        return this.top; 
    },

    getRight: function () { 
        return this.right; 
    },

    getBottom: function () {
        return this.bottom;
    },

    getLeft: function () {
        return this.left;
    },

    // Углы области
    getRightTop: function () {
        return new MyPoint(this.right, this.top, this.unbounded); 
    },

    getRightBottom: function () { 
        return new MyPoint(this.right, this.bottom, this.unbounded); 
    },

    getLeftBottom: function () {
        return new MyPoint(this.left, this.bottom, this.unbounded);
    },

    getLeftTop: function () {
        return new MyPoint(this.left, this.top, this.unbounded);
    },

    // Центр области
    getCenter: function () {
        var x = (this.left + this.right) / 2,
            y = (this.top + this.bottom) / 2;

        // Если координата левой точки меньше координаты правой, то берется диаметрально противоположная точка
        if (this.right > this.left && !this.unbounded) {
            x += 180;
        }
        
        return new MyPoint(x, y, this.unbounded);
    },

    // Размеры области
    getSpan: function () {
        return new YMaps.Size(Math.abs(this.left - this.right), Math.abs(this.top - this.bottom));
    },

    // Вычисляет масштаб карты, при котором область видна целиком
    getMapZoom: function (map) {
        var pixelLeftBottom = map.coorSystem.toPixels(this.getLeftBottom()),
            pixelRightTop = map.coorSystem.toPixels(this.getRightTop()),

            // Вычисляет размеры области в пикселах на последнем масштабе
            pixelSpan = pixelLeftBottom.diff(pixelRightTop).apply(Math.abs),

            // Вычисляет размер HTML-элемента карты в пикселах
            mapSize = map.getContainerSize(),

            // Вычисляет отношение размеров области и карты
            scale = Math.max(pixelSpan.getX() / mapSize.getX(), pixelSpan.getX() / mapSize.getX()),

            // Вычисляет количество значений коэффициента масштабирования (не 
            // превышающих максимального), при которых область видна на карте целиком
            offset = scale < 1 ? 0 : Math.ceil(Math.log(scale)/Math.LN2);

        // Вычисляет уровень масштаба карты
        return map.coordSystem.getMaxZoom() - offset;
    },

    // Сравнивает области
    equals: function (myBounds) {
        var precision = 1e-10; // Точность сравнения объектов
        return (Math.abs(this.top - myBounds.getTop()) < precision &&
                Math.abs(this.right - myBounds.getRight()) < precision &&
                Math.abs(this.bottom - myBounds.getBottom()) < precision &&
                Math.abs(this.left - myBounds.getLeft()) < precision);
    },

    // Копирует область
    copy: function () {
        return new MyBounds(this.getLeftBottom(), this.getRightTop());
    },

    // Возвращает true, если точка попадает в область и false - если нет        
    contains: function (point) {
        // Если область задана неограниченными точками, пересчитывает координаты так, чтобы координаты левой границы всегда были больше координат правой
        var left = (this.left < this.right && !this.unbounded) ? this.left + 360 : this.right,
            right = this.right,

            // Аналогично, если точка неограниченная, пересчитывает ее координаты так, чтобы долгота была больше правой границы области
            x = (point.getX() < this.right && !point.unbounded) ? point.getX() + 360 : point.getX(),
            y = point.getY();
        return (x <= left && x >= right && y >= this.bottom && y <= this.top);
    }
}

Как задать правила пересчета координат

Интерфейс YMaps.ICoordSystem позволяет задавать пользовательские правила пересчета координат системы в пиксельные координаты на последнем масштабе карты.

С помощью YMaps.ICoordSystem необходимо реализовать следующие методы:

  • Фабричные методы создания объекта-точки и объекта-области координатной системы: методы getCoordPoint и getCoordBounds.
  • Правила пересчета координат в пикселы на последнем масштабе и обратно: методы fromCoordPoint и toCoordPoint. Правила пересчета позволяют сопоставить каждой точке пользовательской системы координат определенную точку на карте и наоборот.
  • Правила вычисления расстояния между точками (минимального и по линейке): методы distance и rulerDistance.
  • Методы вычисления размера и максимального масштаба в пользовательской координатной системе: getWorldSize и getMaxZoom.
  • Метод ограничения границ области, в которой объекты могут находиться на карте restrict.

Например:

function MyCoordSystem () {
    // Размеры мира на нулевом уровне масштаба
    this.worldSize = new YMaps.Point(32768, 1024);

    // Ограничение по широте для объектов на карте
    this.yRestriction = 5;

    // Вычисляет длину одного градуса широты и долготы в пикселах на последнем масштабе
    this.scale = this.worldSize.copy().scale(new YMaps.Point(1 / 360, 1 / (2 * this.yRestriction)));

    // Длина одного градуса дуги в световых годах
    this.lightYearsPerDegree = 500;
};

MyCoordSystem.prototype = {
    // Возвращает объект-точку
    getCoordPoint: function (x, y, unbounded) {
        return new MyPoint(x, y, unbounded);
    },

    // Возвращает объект-область
    getCoordBounds: function (leftBottom, rightTop) {
        return new MyBounds(leftBottom, rightTop);
    },

    // Преобразует координаты точки, заданные в пользовательской координатной системе в пиксельные координаты
    fromCoordPoint: function (point, anchor) {
        var x,
            y = point.getY();

        // Если точка ограниченная и задан anchor, сдвигает точку как можно ближе к anchor
        if (!point.unbounded && anchor) {
            x = cycleRestrict(point.getX(), anchor.getX() - 180, anchor.getX() + 180);
        } else {
            x = point.getX();
        }

        return new YMaps.Point(
            this.worldSize.getX() - x * this.scale.getX(),
            this.worldSize.getY() / 2 - y * this.scale.getY()
        );
    },

    // Преобразует пиксельные координаты точки в координаты, заданные в пользовательской координатной системе
    toCoordPoint: function (pixelPoint, unbounded) {
        var x = (this.worldSize.getX() - pixelPoint.getX()) / this.scale.getX(),
            y = (this.worldSize.getY() / 2 - pixelPoint.getY()) / this.scale.getY();

        return this.getCoordPoint(x, y, unbounded);
    },

    // Задает ограничения для отображения объектов на карте
    restrict: function (point) {
        return point.copy().setY(boundaryRestrict(point.getY(), - this.yRestriction, this.yRestriction));
    },

    // Возвращает кратчайшее расстояние между двумя точками координатной системы
    distance: function (point1, point2) {
        var dx = point1.getX() - point2.getX(),
            dy = point1.getY() - point2.getY();

        return this.lightYearsPerDegree * Math.sqrt(dx * dx + dy * dy);
    },

    // Возвращает максимальный коэффициент масштабирования
    getMaxZoom: function () {
        return 2;
    },

    // Возвращает размеры мира в пикселах при максимальном масштабе 
    getWorldSize: function () {
        return this.worldSize.copy();
    }
};

MyCoordSystem.prototype.rulerDistance = MyCoordSystem.prototype.distance;

Открыть пример в новом окне