|
|
// Класс для доступа к данным маршрутов
|
|
|
class WaypointsData {
|
|
|
constructor() {
|
|
|
// Данные для маркеров на карте
|
|
|
this.markersData = [];
|
|
|
// Регионы
|
|
|
this.regions = [];
|
|
|
// Точки
|
|
|
this.points = [];
|
|
|
}
|
|
|
|
|
|
// Заполнение данных
|
|
|
loadData() {
|
|
|
let point_counter = 0;
|
|
|
for (const r_idx in data['regions']) {
|
|
|
const region = data['regions'][r_idx]
|
|
|
this.regions.push({
|
|
|
id: r_idx,
|
|
|
name: region.name,
|
|
|
icon: 'airplane'
|
|
|
});
|
|
|
for (const p_idx in region.points) {
|
|
|
const point = region.points[p_idx];
|
|
|
const loc = point.gps.split(',');
|
|
|
const lat = parseFloat(loc[0].trim())
|
|
|
const lon = parseFloat(loc[1].trim())
|
|
|
this.markersData.push({
|
|
|
location: [lat, lon],
|
|
|
tooltip: {
|
|
|
text: point.name,
|
|
|
}
|
|
|
});
|
|
|
this.points.push({
|
|
|
id: point_counter,
|
|
|
location: [lat, lon],
|
|
|
text: point.name,
|
|
|
description: point.description,
|
|
|
regionId: r_idx,
|
|
|
});
|
|
|
point_counter++;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Получить маркеры для карты
|
|
|
getMarkers() {
|
|
|
return this.markersData;
|
|
|
}
|
|
|
|
|
|
// Получить список регионов
|
|
|
getRegions() {
|
|
|
return this.regions;
|
|
|
}
|
|
|
|
|
|
// Получить точки которые относятся к региону
|
|
|
getRegionPoints(r_idx) {
|
|
|
return this.points.filter(p => p.regionId == r_idx);
|
|
|
}
|
|
|
|
|
|
// Получить точки по переданным индексам точек
|
|
|
getSelectedPoints(indexes) {
|
|
|
let result = [];
|
|
|
this.points.forEach((point) => {
|
|
|
if (indexes.includes(point.id)) {
|
|
|
result.push(point);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Сортировка результата в порядке переданных индексов
|
|
|
let ordered = [];
|
|
|
indexes.forEach((ind)=>{
|
|
|
const index = result.findIndex(p=>p.id == ind);
|
|
|
ordered.push(result[index]);
|
|
|
result.slice(index, 1);
|
|
|
});
|
|
|
return ordered;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Точки для карты
|
|
|
const waypoints = new WaypointsData();
|
|
|
const markerUrl = './img/map-marker.png';
|
|
|
|
|
|
let regionPointsWidget;
|
|
|
let selectedPointsWidget;
|
|
|
|
|
|
// Инициализация контрола карты
|
|
|
function initMap() {
|
|
|
const mapWidget = $('#map').dxMap({
|
|
|
provider: 'bing',
|
|
|
apiKey: {
|
|
|
bing: 'AnpoY0kAA4Kk5A045nQxyVbrlkNTgOuMVBitLxN_iLnZdtONf21HxUTzXwNIebES',
|
|
|
},
|
|
|
zoom: 11,
|
|
|
height: 440,
|
|
|
width: 760,
|
|
|
controls: true,
|
|
|
markerIconSrc: markerUrl,
|
|
|
markers: waypoints.getMarkers(),
|
|
|
type: 'roadmap'
|
|
|
}).dxMap('instance');
|
|
|
}
|
|
|
|
|
|
// Пересоздание списка точек указанного региона, для выбора мест к посещению
|
|
|
function rebuildRegionPointsList(regionId) {
|
|
|
if (regionPointsWidget) {
|
|
|
regionPointsWidget.dispose();
|
|
|
}
|
|
|
if (selectedPointsWidget) {
|
|
|
selectedPointsWidget.dispose();
|
|
|
}
|
|
|
regionPointsWidget = $('#regionPointsList').dxList({
|
|
|
dataSource: new DevExpress.data.DataSource({
|
|
|
store: new DevExpress.data.ArrayStore({
|
|
|
key: 'id',
|
|
|
data: waypoints.getRegionPoints(regionId),
|
|
|
}),
|
|
|
}),
|
|
|
height: 400,
|
|
|
showScrollbar: 'always',
|
|
|
showSelectionControls: true,
|
|
|
selectionMode: 'all',
|
|
|
selectByClick: false,
|
|
|
onSelectionChanged() {
|
|
|
rebuildSelectedPointsList(regionPointsWidget.option('selectedItemKeys'));
|
|
|
},
|
|
|
itemTemplate(data) {
|
|
|
const result = $('<div>').addClass('region-point');
|
|
|
$('<h3>').text(data.text).appendTo(result);
|
|
|
$('<i>').text(data.description).appendTo(result);
|
|
|
return result;
|
|
|
}
|
|
|
}).dxList('instance');
|
|
|
}
|
|
|
|
|
|
// Инициализация выпадающего списка с регионами
|
|
|
function initRegionDropDown() {
|
|
|
$('#one-section').dxDropDownButton({
|
|
|
text: 'Выбор региона',
|
|
|
icon: 'globe',
|
|
|
dropDownOptions: {
|
|
|
width: 230,
|
|
|
},
|
|
|
displayExpr: 'name',
|
|
|
keyExpr: 'id',
|
|
|
onItemClick(e) {
|
|
|
rebuildRegionPointsList(e.itemData.id);
|
|
|
DevExpress.ui.notify(`${e.itemData.name}`, 'success', 600);
|
|
|
},
|
|
|
items: waypoints.getRegions(),
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// Перестроение списка выбранных точек
|
|
|
function rebuildSelectedPointsList(selectedPointsIndexes) {
|
|
|
const selected = waypoints.getSelectedPoints(selectedPointsIndexes);
|
|
|
if (selectedPointsWidget) {
|
|
|
selectedPointsWidget.dispose();
|
|
|
}
|
|
|
selectedPointsWidget = $("#selectedPointsList").dxList({
|
|
|
dataSource: selected,
|
|
|
keyExpr: 'id',
|
|
|
height: 400,
|
|
|
width: '100%',
|
|
|
showScrollbar: 'always',
|
|
|
itemDragging: {
|
|
|
allowReordering: true,
|
|
|
data: selected,
|
|
|
group: 'selected',
|
|
|
onDragStart(e) {
|
|
|
e.itemData = e.fromData[e.fromIndex];
|
|
|
},
|
|
|
onAdd(e) {
|
|
|
e.toData.splice(e.toIndex, 0, e.itemData);
|
|
|
e.component.reload();
|
|
|
},
|
|
|
onRemove(e) {
|
|
|
e.fromData.splice(e.fromIndex, 1);
|
|
|
e.component.reload();
|
|
|
},
|
|
|
onReorder({
|
|
|
fromIndex,
|
|
|
toIndex,
|
|
|
fromData,
|
|
|
component,
|
|
|
}) {
|
|
|
[fromData[fromIndex], fromData[toIndex]] = [fromData[toIndex], fromData[fromIndex]];
|
|
|
component.reload();
|
|
|
},
|
|
|
},
|
|
|
}).dxList('instance');
|
|
|
}
|
|
|
|
|
|
// Инициализация кнопки для расчета маршрута
|
|
|
function initRecalculateWaypointsButton() {
|
|
|
$('#recalculateWaypointsButton').dxButton({
|
|
|
stylingMode: 'contained',
|
|
|
text: 'Рассчитать маршрут',
|
|
|
type: 'default',
|
|
|
width: 600,
|
|
|
height: 40,
|
|
|
onClick() {
|
|
|
recalculateWaypoints(selectedPointsWidget.option('items'));
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// Отображение маршрута
|
|
|
function showTravelPoints(indexes) {
|
|
|
const points = waypoints.getSelectedPoints(indexes);
|
|
|
let calculatedPoints = [];
|
|
|
let prevPoint = null;
|
|
|
let id = 0;
|
|
|
for (const idx in points) {
|
|
|
const point = points[idx];
|
|
|
if (prevPoint != null) {
|
|
|
const d = (calculateDistance(point.location[0], point.location[1], prevPoint.location[0], prevPoint.location[1]) / 1000.0).toFixed(2);
|
|
|
calculatedPoints.push({
|
|
|
ID: id,
|
|
|
Waypoint: d + ' км.'
|
|
|
});
|
|
|
id++;
|
|
|
}
|
|
|
|
|
|
calculatedPoints.push({
|
|
|
ID: id,
|
|
|
Waypoint: point.text
|
|
|
});
|
|
|
id++;
|
|
|
|
|
|
prevPoint = point;
|
|
|
}
|
|
|
$(() => {
|
|
|
$('#waypointsGrid').dxDataGrid({
|
|
|
dataSource: calculatedPoints,
|
|
|
showColumnHeaders: false,
|
|
|
showColumnLines: false,
|
|
|
showRowLines: true,
|
|
|
rowAlternationEnabled: true,
|
|
|
keyExpr: 'ID',
|
|
|
columns: ['Waypoint'],
|
|
|
showBorders: true,
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// Расчет расстояние между двумя точками заданными координатами на поверхности Земли
|
|
|
function calculateDistance(lat1, lon1, lat2, lon2) {
|
|
|
const R = 6371e3; // metres
|
|
|
const φ1 = lat1 * Math.PI / 180; // φ, λ in radians
|
|
|
const φ2 = lat2 * Math.PI / 180;
|
|
|
const Δφ = (lat2 - lat1) * Math.PI / 180;
|
|
|
const Δλ = (lon2 - lon1) * Math.PI / 180;
|
|
|
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
|
|
Math.cos(φ1) * Math.cos(φ2) *
|
|
|
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
const d = R * c; // in metres
|
|
|
return d;
|
|
|
}
|
|
|
|
|
|
function shuffle(array) {
|
|
|
let currentIndex = array.length;
|
|
|
|
|
|
// While there remain elements to shuffle...
|
|
|
while (currentIndex != 0) {
|
|
|
|
|
|
// Pick a remaining element...
|
|
|
let randomIndex = Math.floor(Math.random() * currentIndex);
|
|
|
currentIndex--;
|
|
|
|
|
|
// And swap it with the current element.
|
|
|
[array[currentIndex], array[randomIndex]] = [
|
|
|
array[randomIndex], array[currentIndex]
|
|
|
];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Факториал
|
|
|
function factorial(n) {
|
|
|
j = 1;
|
|
|
for (i = 1; i <= n; i++) {
|
|
|
j = j * i;
|
|
|
}
|
|
|
return j;
|
|
|
}
|
|
|
|
|
|
|
|
|
function stochasticPathFind(array) {
|
|
|
|
|
|
}
|
|
|
|
|
|
// Вычисление оптимального маршрута по начальной и конечной точке
|
|
|
function recalculateWaypoints(waypoints) {
|
|
|
if (!waypoints || waypoints.length < 1) {
|
|
|
DevExpress.ui.notify("Недостаточно точек для построения маршрута");
|
|
|
} else if (waypoints.length == 1) {
|
|
|
|
|
|
// Одна точка, просто ее выводим
|
|
|
showTravelPoints([waypoints[0].id]);
|
|
|
} else {
|
|
|
var start = waypoints[0];
|
|
|
var end = waypoints[waypoints.length - 1];
|
|
|
DevExpress.ui.notify("Расчет маршрута от '" + start.text + "' к '" + end.text + "'");
|
|
|
if (waypoints.length == 2) {
|
|
|
|
|
|
// Две точки, старт и стоп, выводим без расчета
|
|
|
showTravelPoints([waypoints[0].id, waypoints[1].id]);
|
|
|
} else if (waypoints.length == 3) {
|
|
|
|
|
|
// Три точки, старт, стоп и промежуточная выводим без расчета
|
|
|
showTravelPoints([waypoints[0].id, waypoints[1].id, waypoints[2].id]);
|
|
|
} else if (waypoints.length <= 9) {
|
|
|
|
|
|
// Если точек от 4 до 9, используем алгоритм коммивояжера
|
|
|
// Создаем словарь с вершинами
|
|
|
let vertexes = {};
|
|
|
for (const ind in waypoints) {
|
|
|
vertexes[ind] = new GraphVertex(ind);
|
|
|
}
|
|
|
// Формируем граф
|
|
|
const graph = new Graph();
|
|
|
const len = waypoints.length;
|
|
|
for (let i = 0; i < len - 1; i++) {
|
|
|
for (let j = i + 1; j < len; j++) {
|
|
|
const point1 = waypoints[i];
|
|
|
const point2 = waypoints[j];
|
|
|
const d = calculateDistance(point1.location[0], point1.location[1], point2.location[0], point2.location[1]);
|
|
|
graph.addEdge(new GraphEdge(vertexes[i], vertexes[j], d));
|
|
|
}
|
|
|
}
|
|
|
// Решение задачи коммивояжера
|
|
|
const salesmanPath = bfTravellingSalesman(graph);
|
|
|
let indexes = [];
|
|
|
for (var ind in salesmanPath) {
|
|
|
indexes.push(waypoints[salesmanPath[ind].value].id);
|
|
|
}
|
|
|
showTravelPoints(indexes);
|
|
|
} else {
|
|
|
// Если точек от 9, близкие к np алгоритмам расчеты слишком затратные
|
|
|
// применяем лучшие поиск из 200.000 лучших вариантов (9! = 362880, берем такого порядка число чтобы не грузить браузер расчетами)
|
|
|
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$(() => {
|
|
|
waypoints.loadData();
|
|
|
|
|
|
initMap();
|
|
|
initRegionDropDown();
|
|
|
initRecalculateWaypointsButton();
|
|
|
}); |