Pull to refresh

Нахождения минимального расстояния до кривой с помощью API Яндекс.Карт

Reading time 8 min
Views 7.8K
Здравствуйте уважаемые читатели.

Если вы когда-нибудь сталкивались с задачей описанной в топике, то наверное удивлены, тем что в посте будет что-либо кроме ссылки на описание метода getClosestPoint(), потому сразу скажу, что мое решение конечно основывается именно на ней. Однако, мне хочется поделиться не столько алгоритмической красотой (ее за меня реализовывала команда Яндекса создавая API), сколько готовым решением поставленной перед мной задачей.

Наша компания занимается в том числе представлением услуг IP-телефонии и интернета, а так же километрами собственного оптоволокна. для нас весьма важен вопрос на сколько далеко от кабеля находится офис предполагаемого клиента.

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

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

Задача сводится к следующему: пользователь вводит адрес
  • На сайте и адрес с помощью GET запроса передается на страничку с картой
  • В специальной форме на самой карте

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

После чего на карте ставиться метка с выбранным адресом и расстоянием до ближайшего оптического кабеля. Для удобства отображения к кабелю рисуется перпендикуляр(спорное решение, но результат визуально мне понравился).

Привожу пример, что бы не быть голословным http://www.infotis.ru/maps.html.

Работа свелась к нескольким этапам

  1. Перенести кабели с физической карты на Яндекс.Карту
  2. Нанести кабели на карту в виде кривых (полилайнов)
  3. Создать кастомные элементы управления
  4. Написать функцию вычисляющую растояние
  5. Получить искомый адрес и по нему нужные координаты


Этап 1


На данном этапе надо было нанести несколько десятков километров кабеля на карту, причем руками коллеги не имеющего прямого отношения к IT специальности. Тут выручил редактор ymik за что ему большое человеческое спасибо. Именно с нахождения этого поста и был сделан окончательный выбор в пользу Яндекс.Карт (вместо Google Maps).

Через некоторое время работы коллеги я получил текстовый файлик следующего вида (пример для одной кривой из 2х точек, что бы не захламлять место)


{
	"name": "слой1",
	"center": {
		"lng": 37.62110402807594,
		"lat": 55.74879798509053,
		"zoom": 12
	},
	"styles": [{
		"name": "ymikEditor#1315408925757#0",
		"style": {
			"lineStyle": {
				"strokeColor": "0000ff80",
				"strokeWidth": "5"
			}
		}
	}],
	"objects": [{
		"style": "ymikEditor#1315408925757#0",
		"name": "название1",
		"description": "описание1",
		"type": "Polyline",
		"points": [{
			"lng": 37.62110402807594,
			"lat": 55.773576281653035
		}, {
			"lng": 37.6235072873533,
			"lat": 55.74957254620929
		}]
	}]
}


Этап 2



По образу и подобию работы ymik нанесем эти художества на карту.

Договоримся что наш объект лежит в переменной val, map —
Далее немного кода (он простой, но и само решение простое, так что пусть будет с комментариями)

Создадим карту и простейшие элементы управления. Как видим из кода, в процессе подгружается JQuery чем мы в будущем воспользуемся (вместо $ употребляя YMaps.jQuery).

map = new YMaps.Map(YMaps.jQuery("#YMapsID")[0]);
map.setCenter(new YMaps.GeoPoint(37.643347, 55.745478), 13);

map.addControl(new YMaps.Zoom());
map.addControl(new YMaps.ToolBar());
map.addControl(new YMaps.ScaleLine());
map.addControl(new YMaps.TypeControl());
map.setType(YMaps.MapType.SATELLITE);


// Область Москвы для геокодера
 var moscowBounds = new YMaps.GeoBounds(
 new YMaps.GeoPoint(37.389705, 55.577759), 
 new YMaps.GeoPoint(37.844264, 55.91086)
 );



Функция отрисовки линий

polylines=[];
function draw_lines() {

	k = 0; //служебная переменная

	for (var j = 0; j < val['objects'].length; j++) //побежим по объекту рассматривая что там внутри лежит
	{
		if (val['objects'][j]['type'] == 'Polyline') //если это кривая то занесем ее точки в массив Points
		{
			Points = [];
			for (var i = 0; i < val['objects'][j]['points'].length; i++)
			{				
				Points.push(new YMaps.GeoPoint(val['objects'][j]['points'][i]['lng'], val['objects'][j]['points'][i]['lat']));
			}

			//Далее создадим кривую с указанными стилями и параметрами
			pl = new YMaps.Polyline(Points, {
				style: {
					lineStyle: {
						strokeColor: val['styles'][k]['style']['lineStyle']['strokeColor'],
						strokeWidth: val['styles'][k]['style']['lineStyle']['strokeWidth']
					}
				},
				hasHint: 1
			}); 
			
			pl.name = val['objects'][j]['name'];
			pl.description = val['objects'][j]['description'];
			polylines.push(pl); //занесем кривую в массив polylines

			map.addOverlay(pl); //нанесем кривую на карту

			k++;
		}



		if (val['objects'][j]['type'] == 'Placemark') //тут наносим метки в начале кабеля
		{
			var placemark = new YMaps.Placemark(new YMaps.GeoPoint(val['objects'][j]['points']['lng'], val['objects'][j]['points']['lat']), {
				style: val['objects'][j]['style']
			});

			placemark.name = val['objects'][j]['name'];
			placemark.description = val['objects'][j]['description'];
			map.addOverlay(placemark);
		}
	}
}


Этап 3


На этом этапе мы сделаем основную работу.

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

Вот пример собственного элемента управления от яндекса: http://api.yandex.ru/maps/jsapi/examples/mapcontrolscustomizing.html

По этому образцу и будем действовать.

Привожу конструктор этого элемента управления

function nearest_search(object_address) {
	// Обработчик добавления элемента на карту
	this.onAddToMap = function (map, position) {
		this.container = YMaps.jQuery("<div class='YMaps-button'> <i class='YMaps-button-c YMaps-button-l'><i></i></i><i class='YMaps-button-m YMaps-cursor-pointer'><i></i> <form id='find_nearest_form' action='#'  class='YMaps-button-caption'>Ваш адрес <input type='text' name='object_address' value='"+object_address+"' id='search_nearest_input' style='border:1px solid green;' size='22'/></form></i><i class='YMaps-button-c YMaps-button-r'><i></i></i></div>");
		//для схожести с прочими кнопками добавил нашей форме все необходимое (дополнительные элементы и классы) что бы выглядеть как надо
		this.map = map; 
		this.position = position || new YMaps.ControlPosition(YMaps.ControlPosition.TOP_LEFT, new YMaps.Size(0, 40)); //расположили кнопку слева, немного сместив ее вниз.

		// CSS-свойства, определяющие внешний вид элемента
		this.container.css({
			position: "absolute",
			zIndex: YMaps.ZIndex.CONTROL,
			width: "280px",
		});

		this.position.apply(this.container);
		this.container.appendTo(this.map.getContainer());
	}

	// Обработчик удаления элемента с карты
	this.onRemoveFromMap = function () {
		// Ничего мы с карты не удаляем, так что тут пусто.
	};
}


Триггер по которому вместо submit формы мы будем запускать функцию поиска и расчета расстояния.

YMaps.jQuery('#find_nearest_form').submit(function () {
	showAddress(YMaps.jQuery("#search_nearest_input").val());
	return false;
});


Теперь сделаем вариант элемента «Информация» с оригинальных maps.yandex.ru
Как его делать отлично рассказывается тут http://api.yandex.ru/maps/articles/tasks/service.xml#how-to-use-create-information-tool

Геокодирование в данном месте нам не нужно, потому сократим немного


	function InformationControl() 
	{
		var geoResult, clickPlace, listener, map;
		// Вызывается при добавлении элемента управления на карту
		this.onAddToMap = function (parentMap) {
			map = parentMap;
			map.addCursor(YMaps.Cursor.HELP);
			// Создание обработчика кликов по карте
			listener = YMaps.Events.observe(map, map.Events.Click, function (map, mEvent) {

				// Координаты клика мышью
				var clickPoint = mEvent.getGeoPoint();
				// Удаляем предыдущие результаты (если они были добавлены на карту)
				if (geoResult) {
					map.removeOverlay(geoResult);
					result = null;
				}

				//если уже была метка - убираем ее
				if (clickPlace) { 
					map.removeOverlay(clickPlace);
					clickPlace = null;
				}

				// Отмечаем точку по которой щелкнул пользователь
				clickPlace = new YMaps.Placemark(clickPoint);
				clickPlace.description = clickPoint.toString();
				map.addOverlay(clickPlace);

				//посчитаем растояние и выведем все (функция описана ниже) 
				geotrack(clickPoint, clickPlace);
			}, this);
		}

		// Вызывается при удалении элемента управления с карты
		this.onRemoveFromMap = function () 
		{
			map.removeCursor(YMaps.Cursor.HELP);
			// Удаляем метки с карты, если они были добавлены
			if (geoResult) {map.removeOverlay(geoResult);	}
			//и линию
			if (punchline) {map.removeOverlay(punchline);	}
	
			if (clickPlace) {map.removeOverlay(clickPlace);	}
			// Удаляем обработчик кликов по карте
			listener.cleanup();
			map = geoResult = clickPlace = listener = null;
		}

	}


Этап 4


Ура, все кнопки у нас есть — приступаем к написанию основной функции определения расстояния.


function geotrack(clickPoint, clickPlace) {

		//если уже есть перпендикуляр - удаляем его 
		if (punchline) {
			map.removeOverlay(punchline);
		}

		nearest = 100000000000; //установим изначальное значение побольше. Немного больше чем до Солнца.		  
		nearest_point = new YMaps.GeoPoint(0, 0); //и просто инициализируем точку
		
		for (var i = 0; i < polylines.length; i++) //просто пройдемся по всем кривым и выберем ту точку на какой-то из них, которая ближе всех к цели
		{
			if (nearest > polylines[i].getClosestPoint(clickPoint)['point'].distance(clickPoint)) {
				nearest = polylines[i].getClosestPoint(clickPoint)['point'].distance(clickPoint);
				nearest_point = polylines[i].getClosestPoint(clickPoint)['point'];
			}
		}

		clickPlace.name = "Растояние до ближайшего оптоволокна: " + YMaps.humanDistance(nearest);
		clickPlace.description = "";

		punchline = new YMaps.Polyline([clickPoint, nearest_point], {
			style: {
				lineStyle: {
					strokeColor: "ffffffff",
					strokeWidth: 2
				}
			},
			hashint: 1
		});

		punchline.name = "Растояние от выбранного объекта до оптики составляет: " + YMaps.humanDistance(nearest);
		map.addOverlay(punchline);

		return clickPlace.name;
	}


Этап 5


Займемся поиском координат по адресу.
Снова пример от яндекса http://api.yandex.ru/maps/jsapi/examples/geocoding.html


function showAddress(value) {

	// Удаление предыдущего результата поиска
	map.removeOverlay(geoResult);

	// Запуск процесса геокодирования.  strictBounds : true - что бы искал только в Москве (moscowBounds)
	var geocoder = new YMaps.Geocoder(value, {
	 results: 1,
	 boundedBy: moscowBounds, 
	 strictBounds : true
	 });

	// Создание обработчика для успешного завершения геокодирования
	YMaps.Events.observe(geocoder, geocoder.Events.Load, function () {

		// Если объект был найден, то добавляем его на карту
		// и центрируем карту по области обзора найденного объекта
		if (this.length()) {

			geoResult = this.get(0); //ага, мы нашли координаты - берем первые
			map.addOverlay(geoResult);
			name = geotrack(geoResult['_point'], geoResult);  //считаем растояние
			geoResult['text'] = '<b>' + geoResult['text'] + '.<br> ' + name + '</b>';
			map.panTo(geoResult['_point'], {
				flying: 1
			}); //и красиво летим к метке
			
		}
		else {
			alert("Ничего не найдено"); //TODO:заменить алерт на что-то более приятное
		}
	});

	// Процесс геокодирования завершен неудачно
	YMaps.Events.observe(geocoder, geocoder.Events.Fault, function (geocoder, error) {
		alert("Произошла ошибка: " + error); //TODO:заменить алерт на что-то более приятное
	})
}


Ну вот и осталось нам проверить не пришло ли что-то в GET (по концепции сайта оттуда и должно приходить) и если пришло, то использовать запрос в качестве адреса который мы ищем.


	var get = location.search; 
	if(get != '') 
	{
		object_address = decodeURI(get.split('?object_address=')[1]);
		if(object_address=="Адрес подключения")  {object_address=""; } else {YMaps.jQuery('#find_nearest_form').submit(); }
	}
	else
	{
		object_address='';
	}



Исходники целиком можно посмотреть по ссылкам в примере.
Буду рад комментариям.

*UPDATE* Наконец разобрался как редактировать посты, так что как и хотел все исправил, все добавил.
Большое спасибо всем кто помогал.



Информация по методам и объектам:
api.yandex.ru/maps/jsapi/doc/ref/reference/map.xml
api.yandex.ru/maps/jsapi/doc/ref/reference/geopoint.xml
api.yandex.ru/maps/jsapi/doc/ref/reference/polyline.xml
Tags:
Hubs:
+22
Comments 6
Comments Comments 6

Articles