В этом подробнейшем гайде хочу поделиться с вами кодом, который сначала написал сам, а потом его допилила наша команда, когда мы работали с расчётом стоимости доставки для интернет-магазина в Москве.
На самом деле я бы хотел осветить множество разных вопросов и подробно остановиться на каждом из них:
В целом будет много 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 Яндекса.
Этот способ чуть более лёгкий, чем если бы мы делали это при помощи 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.
Вполне возможно, что для конкретно вашей задачи важно учесть не расстояние полёта от точки А до точки Б на реактивном ранце, а именно расстояние по автомобильным дорогам.
В таком случае наш вариант – использовать 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 Яндекс карт, модулями 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; }); }); }); }
Готово!
И ещё раз напомню, что вы всегда можете прокачать свои знания по JavaScript в моём интенсивном видеокурсе. Нет времени даже на интенсив? Без проблем, пишите нам, мы разработаем для вас любой функционал.
Чтобы оставить комментарий, пожалуйста, зарегистрируйтесь или войдите.
Здравствуйте, Михаил
Не могли бы вы доработать код, чтобы адрес "подсказывался" во время набора?
И еще было бы здорово чтобы после ввода адреса карта переходила на этот адрес и приближалась к нему.
Здравствуйте,
не уверен, что я сейчас смогу найти время,
но относительно подсказывания адреса могу сделать наводку: https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/SuggestView-docpage/
А почему у вас комментарий без имени и емайл?
Видимо я не вошел под логином.
Если можете - добавьте пожалуйста данный функционал. Почти не разбираюсь в js.
Это было просто. Нашел код в интернете. Добавьте и себе
Спасибо!
Существует ли способ отучить woocommerce показывать сообщение "Введите свой адрес для просмотра параметров доставки", а сразу показывать все возможные методы доставки на странице оформления заказа для незарегистрированных пользователей?
Просто на сайте регистрация не предусмотрена. Два метода доставки имеют нулевую стоимость, а один ограничен диапазонами индексов и цена там одна. Стандартное местоположение клиента - Адрес магазина. Правил файл cart/cart-shipping.php, безрезультатно.
Техподдержка Woocommerce дала вариант, все методы доставки убрать - За пределы Вашей зоны доставки и тогда будет ОК. Ну да будет, но как тогда с диапазонами индексов работать )))))))
Скорее всего способ существует!
Добрый вечер, Миша
Самое интересное, что год назад, при равных настройках все работало прекрасно! Но после обновление woocommerce и переходе с php7.0 на php7.1 и выше, появились эти приколы ((( Откат версий проблему не решит.
Несколько человек писали в техподдержку, но вразумительного ответа в течении года так и не получили. По факту, в настройках магазина, Адрес магазина - стандартное местоположение клиента и доставка - параметры доставки - Спрятать стоимость доставки, пока адрес не введен, своих функций не выполняют.
Вот и приходится долгими ночами........))))))
Добрый вечер!
Сочувствую, желаю, чтобы у вас получилось разобраться! 💪