Как определить, находится ли адрес в пределах МКАД, и рассчитать расстояние

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

На самом деле я бы хотел осветить множество разных вопросов и подробно остановиться на каждом из них:

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

Если же с JavaScript вы не знакомы, или не уверены в своих знаниях, то рекомендую срочно пройти мой видеокурс по JavaScript, где мы изучаем его на готовых примерах.

Ну и думаю, что полезно будет отметить, что весь код из этого урока будет прекрасно работать и для КАД, единственное только нужно будет поменять координаты МКАД на координаты КАД.

Координаты точек МКАДа

Прежде всего, нам конечно же понадобится массив из точек МКАД в формате [ широта, долгота ]. В некоторых случаях бывает удобнее разделить точки на два массива, в одном хранить широту, в другом долготу, но намного нагляднее именно тот способ, который используется здесь:

const mkadPoints =  [
	[ 55.774558, 37.842762 ],
	[ 55.76522, 37.842789 ],
	[ 55.755723, 37.842627 ],
	[ 55.747399, 37.841828 ],
	[ 55.739103, 37.841217 ],
	[ 55.730482, 37.840175 ],
	[ 55.721939, 37.83916 ],
	[ 55.712203, 37.837121 ],
	[ 55.703048, 37.83262 ],
	[ 55.694287, 37.829512 ],
	[ 55.68529, 37.831353 ],
	[ 55.675945, 37.834605 ],
	[ 55.667752, 37.837597 ],
	[ 55.658667, 37.839348 ],
	[ 55.650053, 37.833842 ],
	[ 55.643713, 37.824787 ],
	[ 55.637347, 37.814564 ],
	[ 55.62913, 37.802473 ],
	[ 55.623758, 37.794235 ],
	[ 55.617713, 37.781928 ],
	[ 55.611755, 37.771139 ],
	[ 55.604956, 37.758725 ],
	[ 55.599677, 37.747945 ],
	[ 55.594143, 37.734785 ],
	[ 55.589234, 37.723062 ],
	[ 55.583983, 37.709425 ],
	[ 55.578834, 37.696256 ],
	[ 55.574019, 37.683167 ],
	[ 55.571999, 37.668911 ],
	[ 55.573093, 37.647765 ],
	[ 55.573928, 37.633419 ],
	[ 55.574732, 37.616719 ],
	[ 55.575816, 37.60107 ],
	[ 55.5778, 37.586536 ],
	[ 55.581271, 37.571938 ],
	[ 55.585143, 37.555732 ],
	[ 55.587509, 37.545132 ],
	[ 55.5922, 37.526366 ],
	[ 55.594728, 37.516108 ],
	[ 55.60249, 37.502274 ],
	[ 55.609685, 37.49391 ],
	[ 55.617424, 37.484846 ],
	[ 55.625801, 37.474668 ],
	[ 55.630207, 37.469925 ],
	[ 55.641041, 37.456864 ],
	[ 55.648794, 37.448195 ],
	[ 55.654675, 37.441125 ],
	[ 55.660424, 37.434424 ],
	[ 55.670701, 37.42598 ],
	[ 55.67994, 37.418712 ],
	[ 55.686873, 37.414868 ],
	[ 55.695697, 37.407528 ],
	[ 55.702805, 37.397952 ],
	[ 55.709657, 37.388969 ],
	[ 55.718273, 37.383283 ],
	[ 55.728581, 37.378369 ],
	[ 55.735201, 37.374991 ],
	[ 55.744789, 37.370248 ],
	[ 55.75435, 37.369188 ],
	[ 55.762936, 37.369053 ],
	[ 55.771444, 37.369619 ],
	[ 55.779722, 37.369853 ],
	[ 55.789542, 37.372943 ],
	[ 55.79723, 37.379824 ],
	[ 55.805796, 37.386876 ],
	[ 55.814629, 37.390397 ],
	[ 55.823606, 37.393236 ],
	[ 55.83251, 37.395275 ],
	[ 55.840376, 37.394709 ],
	[ 55.850141, 37.393056 ],
	[ 55.858801, 37.397314 ],
	[ 55.867051, 37.405588 ],
	[ 55.872703, 37.416601 ],
	[ 55.877041, 37.429429 ],
	[ 55.881091, 37.443596 ],
	[ 55.882828, 37.459065 ],
	[ 55.884625, 37.473096 ],
	[ 55.888897, 37.48861 ],
	[ 55.894232, 37.5016 ],
	[ 55.899578, 37.513206 ],
	[ 55.90526, 37.527597 ],
	[ 55.907687, 37.543443 ],
	[ 55.909388, 37.559577 ],
	[ 55.910907, 37.575531 ],
	[ 55.909257, 37.590344 ],
	[ 55.905472, 37.604637 ],
	[ 55.901637, 37.619603 ],
	[ 55.898533, 37.635961 ],
	[ 55.896973, 37.647648 ],
	[ 55.895449, 37.667878 ],
	[ 55.894868, 37.681721 ],
	[ 55.893884, 37.698807 ],
	[ 55.889094, 37.712363 ],
	[ 55.883555, 37.723636 ],
	[ 55.877501, 37.735791 ],
	[ 55.874698, 37.741261 ],
	[ 55.862464, 37.764519 ],
	[ 55.861979, 37.765992 ],
	[ 55.850257, 37.788216 ],
	[ 55.850383, 37.788522 ],
	[ 55.844167, 37.800586 ],
	[ 55.832707, 37.822819 ],
	[ 55.828789, 37.829754 ],
	[ 55.821072, 37.837148 ],
	[ 55.811599, 37.838926 ],
	[ 55.802781, 37.840004 ],
	[ 55.793991, 37.840965 ],
	[ 55.785017, 37.841576 ],
	[ 55.780825, 37.842095 ]
];

Тут безусловно важно быть внимательным, если вдруг вы случайно укажете одну некорректную точку, то вполне вероятно, что из-за этого полностью сломаются все результаты.

Кроме того, обратите пожалуйста внимание, что количество точек – произвольное, то есть, если вдруг вы заметите неточность в расчётах для определённого адреса, то вероятно, что вам поможет добавление ещё одной точки МКАДа в этот массив.

Определяем, находится ли точка внутри МКАД или за его пределами

В целом к концу этой главы я уже покажу вам готовый пример, который будет содержать пару обычных полей ввода <input type="text" />, и когда мы введём в них координаты и нажмём кнопку, выведется результат, который покажет нам, находится ли точка внутри МКАД или же она находится за его пределами.

Но для начала нам понадобится JavaScript функция, определяющая нахождение координат point внутри полигона координат poly.

/*
 * Функции определения находится ли геоточка внутри полигона геоточек или за его пределами
 * @author Миша Рудрастых
 * @url https://misha.agency/javascript/rasschet-rasstoyaniya-ot-mkad.html
 * @param point Заданная геоточка в формате [ широта, долгота ]
 * @param poly Массив из геоточек полигона (например МКАДа)
 * @return true/false (находится внутри/находится за пределами)
 */
function inPoly( point, poly ) {
 
	const x = point[0], y = point[1];
 
	let inside = false;
	for ( let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
		let xi = poly[i][0], yi = poly[i][1];
		let xj = poly[j][0], yj = poly[j][1];
 
		let intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
 
		if ( intersect ) {
			inside = !inside;
		}
	}
 
	return inside;
}

То есть работает это примерно следующим образом:

const point = [ 55.547281, 44.438658 ];
 
if( inPoly( point, mkadPoints ) ) {
	console.log( 'Да, точка внутри МКАД' );
} else {
	console.log( 'Нет, точка за пределами МКАД' );
}

Давайте теперь опробуем эту функцию на реальном примере.

Итак, сначала HTML. Я уже упоминал, что по сути нам понадобятся лишь текстовые поля для широты и долготы и кнопка.

<form id="is_in_mkad">
	<input type="text" name="lat" placeholder="Широта" />
	<input type="text" name="lng" placeholder="Долгота" />
	<button>Определить</button>
	<div id="result"></div>
</form>

И добавляем событие на отправку этой формы.

const form = document.getElementById( 'is_in_mkad' );
 
form.addEventListener("submit", function(e){
	e.preventDefault();
 
	const lat = form.querySelector( 'input[name="lat"]' );
	const lng = form.querySelector( 'input[name="lng"]' );
	const point = [ lat.value, lng.value ];
	const result = document.getElementById( 'result' );
 
	if( inPoly( point, mkadPoints ) ) {
		result.textContent = 'Да, точка внутри МКАД';
	} else {
		result.textContent = 'Нет, точка за пределами МКАД';
	}
});
определяем, находится ли точка внутри МКАД или за его пределами

Всё готово! Можете прочекать мой код с codepen.

See the Pen Untitled by Misha (@rudrastyh) on CodePen.

Определение ближайшей точки на МКАД

Прежде, чем начать определять расстояние до МКАД, нам нужно найти ближайшую точку на нём, до которой расстояние измерять и будем.

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

/*
 * Функции определения ближайшей точки на полигоне из геоточек до искомой геоточки
 * @author Миша Рудрастых
 * @url https://misha.agency/javascript/rasschet-rasstoyaniya-ot-mkad.html
 * @param point Заданная геоточка в формате [ широта, долгота ]
 * @param poly Массив из геоточек полигона (например МКАДа)
 * @return Ближайшая точка на полигоне
 */
function distSquared(pt1, pt2) { 
	return ( ( pt1[0] - pt2[0] ) * ( pt1[0] - pt2[0] ) + ( pt1[1] - pt2[1] ) * ( pt1[1] - pt2[1] ) );
}
function closestPoint( point, poly ) {
	let closestPoint = poly[0];
	let shortestDistance = distSquared( point, poly[0] );
 
	for (let i = 0; i < poly.length; i++) {
		let d = distSquared( point, poly[i] );
		if ( d < shortestDistance ) {
			closestPoint = poly[i];
			shortestDistance = d;
		}
	}
	return closestPoint;
}

После того, как вы добавили эти функции в ваш код, мы можем немного изменить наш предыдущий пример.

const form = document.getElementById( 'is_in_mkad' );
 
form.addEventListener("submit", function(e){
	e.preventDefault();
 
	const lat = form.querySelector( 'input[name="lat"]' );
	const lng = form.querySelector( 'input[name="lng"]' );
	const point = [ lat.value, lng.value ];
	const result = document.getElementById( 'result' );
	const mkadClosest = closestPoint( point, mkadPoints );
 
	if( inPoly( point, mkadPoints ) ) {
		result.innerHTML = 'Да, точка внутри МКАД';
	} else {
		result.innerHTML = 'Нет, точка за пределами МКАД';
	}
	result.innerHTML += "<br>Ближайшие координаты МКАД: " + mkadClosest[0] + ", " + mkadClosest[1];
 
});

В итоге в нашем событии произошли некоторые изменения.

  • Мы использовали функцию closestPoint() для определения ближайшей точки на МКАД до нашей точки и записали полученную геоточку в константу mkadClosest.
  • Теперь мы будем выводить не только фразу «Да, внутри МКАД» / «Нет, за пределами МКАД», а ещё и ближайшие координаты на МКАД. Поэтому мы заменили свойство textContent на innerHTML

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

Можем воспользоваться для этого API Яндекс карт например. Подключаем и инициализируем карты как обычно:

let myMap;
ymaps.ready( init );
function init(){
	myMap = new ymaps.Map( "map", { // надо создать <div id="map" style="width:100%;height:300px;"></div> где-то в HTML
		center: [55.76, 37.64], // координаты центра
		zoom: 9 // мастаб
	} );
}

Не забываем кстати при этом подключить сам скрипт API карт Яндекса и добавив в него свой ключ API.

А потом в функцию события добавляем строчку, добавляющую точки на карту:

	result.innerHTML += "<br>Ближайшие координаты МКАД: " + mkadClosest[0] + ", " + mkadClosest[1];
 
	myMap.geoObjects.removeAll()
		.add(new ymaps.Placemark( point,{ iconCaption: 'Точка' }))
		.add(new ymaps.Placemark( mkadClosest,{ iconCaption: mkadClosest[0] + ', ' + mkadClosest[1] }));
 
 
});

Если вам не до конца понятно, как использовать функции, константы, объекты в JavaScript, то рекомендую вам свой видеокурс по JS с нуля. Там мы также изучим jQuery и React.js.

Как определить ближайшую точку на МКАД

Определение расстояния от точки до МКАД

Тут есть два варианта определения кстати. В первом варианте мы будем делать это при помощи функции, а во втором – при помощи API Яндекса.

Способ 1. Функция определения расстояния в километрах между двумя координатами на карте

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

Минус этого подхода в том, что расстояние рассчитывается, как если бы вы полетели из первой точки во вторую на вертолёте или реактивном ранце.

/*
 * Функции определения расстояния в километрах между двумя геоточками
 * @author Миша Рудрастых
 * @url https://misha.agency/javascript/rasschet-rasstoyaniya-ot-mkad.html
 */
function distance( point1, point2 ) {
	// если точки совпадают, то выходим из функции, возвращаем 0
	if (( point1[0] == point2[0]) && (point1[1] == point2[1])) {
		return 0;
	} else {
		const radlat1 = Math.PI * point1[0]/180;
		const radlat2 = Math.PI * point2[0]/180;
		const theta = point1[1]-point2[1];
		const radtheta = Math.PI * theta/180;
		let dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
		if (dist > 1) {
			dist = 1;
		}
		dist = Math.acos(dist);
		dist = dist * 180/Math.PI;
		dist = dist * 60 * 1.1515;
		// чтобы получить в км
		dist = dist * 1.609344;
		return dist;
	}
}

Далее мы добавляем функцию расчёта в событие отправки формы и выводим соответствующее сообщение. Также в этом примере я округлил результат при помощи Math.round().

	const mkadClosest = closestPoint( point, mkadPoints );
	const mkadDistance = distance(point, mkadClosest);
 
	if( inPoly( point, mkadPoints ) ) {
		result.innerHTML = 'Да, точка внутри МКАД';
	} else {
		result.innerHTML = 'Нет, точка за пределами МКАД';
	}
	result.innerHTML += "<br>Ближайшие координаты МКАД: " + mkadClosest[0] + ", " + mkadClosest[1];
 
	result.innerHTML += '<br>Расстояние до ближайшей точки МКАД: ' + Math.round( mkadDistance ) + 'км';
расстояние до ближайшей точки МКАД

Калькулятор расчёта расстояния от МКАД

Небольшой бонус для вас – на основе первого способа решил написать для вас изишный калькулятор.

See the Pen Находится ли геоточка внутри МКАД by Misha (@rudrastyh) on CodePen.

Способ 2. Использование Яндекс Карт для построения маршрута и определения длины этого маршрута

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

В таком случае наш вариант – использовать API Яндекс карт. Так как API карт мы уже подключали в предыдущем шаге, то в основном все изменения будут сделаны в обработчике события отправки формы.

const form = document.getElementById( 'is_in_mkad' );
 
form.addEventListener("submit", function(e){
	e.preventDefault();
 
	const lat = form.querySelector( 'input[name="lat"]' );
	const lng = form.querySelector( 'input[name="lng"]' );
	const point = [ lat.value, lng.value ];
	const result = document.getElementById( 'result' );
	const mkadClosest = closestPoint( point, mkadPoints );
 
	if( inPoly( point, mkadPoints ) ) {
		result.innerHTML = 'Да, точка внутри МКАД';
	} else {
		result.innerHTML = 'Нет, точка за пределами МКАД';
	}
	result.innerHTML += "<br>Ближайшие координаты МКАД: " + mkadClosest[0] + ", " + mkadClosest[1];
 
	// также очищаем карту при отправке формы
	// но добавление меток на карту нам уже не понадобится, потому что они и так добавятся в маршруте
	myMap.geoObjects.removeAll();
	//	.add(new ymaps.Placemark(point,{ iconCaption: 'Точка' }))
	//	.add(new ymaps.Placemark(mkadClosest,{ iconCaption: mkadClosest[0] + ', ' + mkadClosest[1] }));
 
	// создаём машрут
	const multiRoute = new ymaps.multiRouter.MultiRoute(
		{
			referencePoints: [ point, mkadClosest ], // точка А, точка Б(МКАД)
			params: { results: 1 }
		},
		{
			boundsAutoApply: true // чтобы маршрут был виден целиком на карте
		}
	);
	// добавляем его на нашу карту
	myMap.geoObjects.add( multiRoute );
 
	// после того, как маршрут добавлен, вычислим его длину и выведем сообщение об этом
	multiRoute.model.events.add('requestsuccess', function() {
		const activeRoute = multiRoute.getActiveRoute();
		result.innerHTML += '<br>Расстояние до ближайшей точки МКАД: ' + activeRoute.properties.get("distance").text;
	});
 
});
определение длины маршрута до ближайшей точки МКАД
Возможно, что наиболее внимательные из вас заметили, что часть маршрута до точки пролегает по МКАД! То есть, в этом втором способе мы использовали API Яндекс карт для построения автомобильного маршрута до точки, которая считается ближайшей при полёте на реактивном ранце! То есть напрямую. Поэтому, если вы захотите, то можете доработать код таким образом, чтобы учитывалась первая точка на МКАД при расчёте расстояния по автомобильным дорогам. Также я и моя команда всегда рады помочь вам с кодом!

Выпадающие подсказки с выбором адреса

Об этом функционале меня ещё спрашивали в комментариях, когда пост только был опубликован. И вот наконец-то, я пишу о нём.

Вводить координаты может и прикольно, но намного прикольнее, когда можно указать сразу адрес, и уже по нему произвести все рассчёты. Вот так:

Выпадающие подсказки адресов в API Яндекс карт

Для реализации выпадающих подсказок и геокодирования адреса воспользуемся также API Яндекс карт, модулями SuggestView и geocode. Но вполне можно использовать и другие сервисы.

Итак, приступим.

Прежде всего нам нужно немного изменить HTML-форму.

<form id="is_in_mkad">
	<input type="text" id="suggest" style="width: 500px" placeholder="Начните вводить адрес..." />
	<div id="result"></div>
	<div id="map" style="width: 100%; height: 300px"></div>
</form>

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

let myMap;
ymaps.ready(init);
 
function init(){
 
	myMap = new ymaps.Map("map", {
		center: [55.76, 37.64],
		controls: [],
		zoom: 9
	});
 
	// поле с вводом адреса
	const input = document.getElementById('suggest');
	const suggestView = new ymaps.SuggestView('suggest', {
		offset: [10, 10] // чисто отступы в 10px сверху и слева
	});
 
	suggestView.events.add('select', function ( event ) {
 
		// получаем значение адреса
		const address = event.get( 'item' ).value;
 
		// получаем значение координат по адресу
		const myGeocoder = ymaps.geocode( address );
		myGeocoder.then( function (res) {
 
			// если вдруг захотите добавить точку на карту
			// myMap.geoObjects.add(result.geoObjects);
 
			// получаем координаты адреса
			const MyGeoObj = res.geoObjects.get(0);
			const point = MyGeoObj.geometry.getCoordinates();
 
			const result = document.getElementById( 'result' );
			const mkadClosest = closestPoint( point, mkadPoints );
 
			if( inPoly( point, mkadPoints ) ) {
				result.innerHTML = 'Да, точка внутри МКАД';
			} else {
				result.innerHTML = 'Нет, точка за пределами МКАД';
			}
			result.innerHTML += "<br>Ближайшие координаты МКАД: " + mkadClosest[0] + ", " + mkadClosest[1];
 
			// также очищаем карту при изменении адреса
			myMap.geoObjects.removeAll();
 
			// создаём машрут
			const multiRoute = new ymaps.multiRouter.MultiRoute(
				{
					referencePoints: [ point, mkadClosest ], // точка А, точка Б(МКАД)
					params: { results: 1 }
				},
				{
					boundsAutoApply: true // чтобы маршрут был виден целиком на карте
				}
			);
			// добавляем его на нашу карту
			myMap.geoObjects.add( multiRoute );
 
			// после того, как маршрут добавлен, вычислим его длину и выведем сообщение об этом
			multiRoute.model.events.add('requestsuccess', function() {
				// Получение ссылки на активный маршрут.
				const activeRoute = multiRoute.getActiveRoute();
				result.innerHTML += '<br>Расстояние до ближайшей точки МКАД: ' + activeRoute.properties.get("distance").text;
			});
 
		});
 
	});
 
}

Готово!

Выпадающие подсказки адресов в API Яндекс карт

И ещё раз напомню, что вы всегда можете прокачать свои знания по JavaScript в моём интенсивном видеокурсе. Нет времени даже на интенсив? Без проблем, пишите нам, мы разработаем для вас любой функционал.

Миша

Впервые познакомился с WordPress в 2009 году. Организатор и спикер на конференциях WordCamp. Преподаватель в школе Нетология.

Пишите, если нужна помощь с сайтом или разработка с нуля.

Комментарии — 9

Чтобы оставить комментарий, пожалуйста, зарегистрируйтесь или войдите.

Миша Рудрастых и WordPress

Полезности из мира WordPress и жизни студии.

Мой телеграм-канал