Клуб API Карт

Циклическое создание полигонов по координатам из файла или вручную при их отсутствии

lolwut-lol
31 мая 2017, 01:51

Заранее прошу извинения за ОЧЕНЬ длинный пост, но считаю, что развёрнутое и подробное объяснение гораздо полезнее двух-трёх строк и страницы кода. Готов показать любые логи консоли и дать весь код целиком.

Есть некоторый JSON файл, в котором есть массив из N объектов. Каждый из объектов, помимо некоторых данных (обрабатываются в непосредственной связи с полигонами, поэтому сам код для обработки даже не начинал писать), может хранить в себе координаты в трёхмерном массиве (начальное значение ключа везде " "). В самом скрипте координаты из строк переводятся в числа и по ним вполне себе строится полигон. 

Карта области изначально строится в схематическом виде, с помощью фотошопа и небольших костылей на неё накладываются те же дома/здания из вида со спутника. Полигон служит для обводки каждого дома, цвет зависит от "типа здания" (жилой дом, школа, магазин и т. д.). Использование схематической карты и готовых зданий гарантированно лишь сейчас, потому что потом, возможно, придётся рисовать планы ещё не застроенных участков, для которых нельзя средствами API просто определить координаты зданий, но можно вручную построить полигоны по уже наложенному на карту "плановому" изображению и сохранить их координаты.

Требуется после получения JSON-данных пройтись по массиву, и если координаты уже есть, то построить полигон (или добавить в коллекцию для дальнейшего построения). Если же координат нет, то запустить редактор, в котором один из пунктов меню позволяет завершить рисование и AJAX-запросом отправить координаты нового полигона для соответствующего объекта в JSON-файл.

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

Последняя версия выглядит вот так. В ней решены все проблемы с jQuery, с самим редактором и окраской, остались лишь специфичные для API Яндекс.Карт вопросы, которые не задашь просто так на каком-нибудь StackOverflow (в гугле и блоге тоже не нашёл). Убрал не имеющие отношения к вопросу части кода и добавил несколько объяснений.

ymaps.ready(init);

function init() {

//Тут создаётся карта

//Тут немного кода для наложения картинки

//Коллекция с объектами, для которых уже заданы координаты в JSON
 var buildingsOverlay = new ymaps.GeoObjectCollection({},{});
 $.getJSON("data/strogi.json",function(objdata){
  console.log(".getJSON success");
 })
 .done(function(objdata){
//Создаю и задаю "шаблонный" полигон тут
//Пытался решить проблему с несовпадением 
//индексов рисуемого и отправляемого объекта
//Подозревал задание пункта меню в for 
//даже при единоразовом создании меню проблема остаётся
   var tempPol = new ymaps.Polygon([],{},{
    fillColor: "#BBBBBB",
    opacity: 0.3,
    strokeColor: "#BBBBBB",
    strokeOpacity: 0.5,
    strokeWidth: 1
   });
   tempPol.editor.options.set({
    drawingCursor: "crosshair",
    maxPoints: 50,
    menuManager: function(menuItems, model){
     menuItems.push({
      id:"StopAndSend",
      title: "Завершить редактирование и отправить координаты",					
//count-1 стоит, так как при тесте двух объектов
//(индексы 0 и 1) 
//данные записывались в третий (индекс 2)
//и индекс менялся именно при нажатии на кнопку
      onClick: function(){
       console.log(count-1);
       tempPol.editor.stopDrawing();					
       var tempArr = tempPol.geometry.getCoordinates();
							
//Тут перевод координат в строки для валидности JSON
//Почти такой же код есть ниже
//с переводом строк в числа
							
       objdata.buildings[count-1].coords = tempArr;
							
//Проверял вид JSON, всегда координаты приписывались не туда
       console.log(JSON.stringify(objdata));
							
//Чтобы не отправить пустоту
       if (objdata.buildings[count-1].coords != " "){
        $.ajax({
         type: "POST",
         data: {json: JSON.stringify(objdata)},
         url: "writecoords.php",
         success: function(data){
          console.log("AJAX success");
         },
         error: function(){
          console.log("failed to send POST");
          alert("error");
         }
        });	
       }
      }
     });
     return menuItems;
    }
   });
//До трёх, потому что объектов много
//а тестировать все сразу очень долго
//Также при большем числе
//с имеющимся количеством возможных тестов
//новых вариантов появиться не должно
   for(count = 0; count < 3; count++){
    console.log(count);
    console.log(objdata.buildings[count]);
//"Шаблонный" полигон добавляется
//Дальше используется для редактирования 
//при пустых координаты у текущего объекта)
    strogino.geoObjects.add(tempPol);
    if(objdata.buildings[count].coords == " ") {
     console.log(count);
//Опустошение координат стоит, так как в предыдущих попытках
//Тот же "шаблонный" полигон использовался с готовыми координатами
//После чего добавлялся в коллекцию
     tempPol.geometry.setCoordinates([]);
     tempPol.editor.startDrawing();
    }
//Если координаты уже заданы
    else {
//Перевод координат JSON-объекта из строк в числа
     var coordArr = objdata.buildings[count].coords;
     for (var j = 0; j < coordArr[0].length; j++) {
      console.log(coordArr[0][j]);
      for (var k = 0; k < coordArr[0][j].length; k++) {
       var mn = parseFloat(coordArr[0][j][k]);
       coordArr[0][j][k] = mn;	
      }
     }
			
//Создание объекта по готовым координатам
     var newObj1 = new ymaps.Polygon([coordArr[0]],{},{
      fillColor: "#999999",
      strokeColor: "#999999",
      opacity: "0.4",
      strokeOpacity: "0.6",
      strokeWidth: "2"
     });
			
//Немного кода для установки цвета 
//по данным из JSON через 
//newObj1.options.set("fillColor", "#БлаБла"); и 
//newObj1.options.set("strokeColor", "#БлаБла"); 
//через if и switch, расположенный в else
			
     strogino.geoObjects.add(newObj1);
    }
   }			
 })
//Коллбеки .fail() и .always(), роли не играют
}

Как я понял после кучи мелких правок, экспериментов и выкидывания в консоль всего возможного, даже несмотря на цикл for, тупо упирающийся в развилку, из 3 объектов редактирование всегда вызывается для последнего: в консоли появляется первый объект, затем второй (с пустыми координатами), затем третий (также с пустыми координатами) и только после этого начинается редактирование.

После завершения рисования и отправки координат вместо нового курсора-прицела, который означает начало рисования, появляется обычная "рука" и карта становится завершённой.

Сохраняется всё, естественно, для последнего объекта, а второй (первый из пустых) так и остаётся пустым. Без count-1 в менюшке даже при цикле из ДВУХ возможных объектов objdata.buildings[0] и odjdata.buildings[1] пункт меню сохранял всё в objdata.buildings[2], что я проверял и несколькими вызовами всего объекта objdata, и вызовом objdata.buildings[1].coords в пункте меню, и выводами индексов на всех стадиях кода, и чтением JSON из файла после записи. Даже непосредственно JSON-файл смотрел.

Думал об использовании коллекции для сохранения готовых объектов, которые будут рисоваться в конце (а tempPol использовать только для постоянного рисования), но и тут ничего не помогло, пробовал сразу несколько способов. Заканчивалось несколькими разными ошибками:

  1. Объект tempPol создавался в цикле for и только потом следовала развилка. В else использовалось buildingsOverlay.add(tempPol); После добавления готового объекта (сначала выводились длины до и после добавления) в консоли появлялась ошибка (указанная строка с ошибкой показывала на следующее создание tempPol) о невозможности выполнения getParent на undefined.
  2. Использовать buildingsOverlay.set(count, tempPol);. Это я пробовал и при работе с одним объектом (0 вместо count), не работало вообще.
  3. Создавать в else{} объект newObj1, приписывать ему все свойства и координаты, затем добавлять в коллекцию. Успешно увеличивалась длина, вне else{} (в конце коллбека .done() и после окончания всей функции .getJSON()) длина становилась равной нулю.
  4. После задания нужных опций и координат у tempPol, использовать что-то вроде var newObj1 = tempPol; и записывать newObj1 в коллекцию, но эффект был совсем не тот -- newObj1 не становился геообъектом (в прицнипе, ожидаемо).

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

За сегодня пришло в голову несколько новых идей:

  • Два раза делать .getJSON, в первый раз ТОЛЬКО рисовать, если есть объекты без координат, во второй раз нарисовать все объекты, т. к. наличие координат гарантированно;

Критичной проблемой является невозможность рисовать несколько полигонов подряд. Даже если else убрать и оставить только рисование при пустых координатах, в консоли сначала появляются два пустых объекта (при том, что в том же if, где стоит вывод в консоль, есть tempPol.editor.startDrawing(), т. е. вывод два раза, а рисуется один)

  • Добавлять объекты в коллекцию без присвоения объектов, сразу через buildingsOverlay.add(new ymaps.Polygon([coords[count]], {}, {});

Так можно избежать отсутствия возможности создать новый шаблонный полигон. Однако я уже не уверен в том, что можно дальше менять опции через buildingsOverlay.get(count).options.set();, как это делается для отдельных объектов, да и внезапное обнуление коллекции после else{} в коде не просто же так происходит.

  • В самом-самом начале хотел использовать ещё и коллекцию со временными объектами (в которой не было необходимости при работе с одним-двумя объектами) и дальнейшем переносе из временной коллекции в готовую, но эта идея упирается сразу в обе проблемы.

Можно, конечно, вернуться к редактированию по одному объекту, однако их число УЖЕ перевалило за пару десятков, и несмотря на возможную простоту обводки одного дома, столько раз менять везде индексы и обновлять карту довольно утомительно. Да и после отправки карты в пользование при добавлении новых объектов заказчик, очевидно, предпочтёт сразу добавить объект на карту, а не копаться в коде скрипта.

Конечные вопросы, с учётом всех нюансов:

  1. Возможно ли использование редактора в цикле при каждом его выполнении?
  2. Как правильно использовать коллекции для хранения изменяющегося числа объектов с возможностью дальнейшего изменения опций?

Также принимаю абсолютно любые советы по улучшению благовидности кода. Особенно интересует создание отдельных функций для редактирования полигона, отправки данных (если есть способ отправлять обратно не весь JSON объект, а только координаты, которые обработаются в php и будут записаны в JSON-файл, буду крайне рад услышать), изменения типа данных сохранённых координат. Последняя попытка создать обработку для всего трёхмерного массива сразу с использованием рекурсивного захода и Array.isArray() закончилась превышением максимального размера call stack, а моё желание сделать универсально и долгосрочно и без попыток всё это создать и отладить уже надолго растянуло создание скрипта.

    1 комментарий
    Вы слишком умный. Точнее пытаетесь найти сложное обьяснение проблеме и сложное решение к ней.
    У вас проблемы
    - отсутствие связи между обьектом редактирования и count, на которое вы почему-то надеетесь. Count ВСЕГДА будет равен количеству обьектов, а вот для кого вы создали tempPol - не известно
    - многократное добавление tempPol
    - общие нарушения логики.
    Если просто переформатировать код, так чтобы в нем было "меньше проблем", фактически ничего при это не меняя - возможно станет чуток лучше
    (редактор лох)
    // добавим стиль "редактируемого" обьекта. Это как css класс
    ymaps.option.presetStorage('editing', {
    fillColor: "#BBBBBB",
    opacity: 0.3,
    strokeColor: "#BBBBBB",
    strokeOpacity: 0.5,
    strokeWidth: 1
    });
    function activateEditorOn(object) {
    object.options.set('preset', 'editable');
    object.editor.options.set({
    drawingCursor: "crosshair",
    maxPoints: 50,
    menuManager: function (menuItems, model) {
    menuItems.push({
    id: "StopAndSend",
    title: "Завершить редактирование и отправить координаты",
    onClick: function () {
    console.log(count - 1);
    // тут мы выключаем editor. "появляется обычная "рука" и карта становится завершённой."
    // именно так тут и написано
    tempPol.editor.stopDrawing();
    var object = object.geometry.getCoordinates();
    if (tempArr) {
    // вот так делать не советую. Лучше пробежать по списку ВСЕХ обьектов и сохранить ВСЕ данные в ОДИН момент.
    objdata.buildings[object.properties.get('building')].coords = tempArr;
    $.ajax({
    type: "POST",
    data: {json: JSON.stringify(objdata)},
    url: "writecoords.php",
    success: function (data) {
    console.log("AJAX success");
    // наверное надо сделать что-то типа
    queryData();
    },
    error: function () {
    console.log("failed to send POST");
    alert("error");
    }
    });
    }
    }
    });
    return menuItems;
    }
    });
    object.editor.startDrawing();
    }
    function queryData() {
    $.getJSON("data/strogi.json", function (objdata) {
    console.log(".getJSON success");
    })
    .done(function (objdata) {
    objdata.buildings.forEach(function(building, buildingId) {
    //strogino.geoObjects.add(tempPol); // <-- это ОДИН И ТОТ ЖЕ обьект
    // var coordArr = objdata.buildings[count].coords; - так легко совершить ошибку
    var coordArr = building.coords; // а так нет
    var coords = [];
    for (var j = 0; j < coordArr[0].length; j++) {
    console.log(coordArr[0][j]);
    coords[j] = [];
    for (var k = 0; k < coordArr[0][j].length; k++) {
    var mn = parseFloat(coordArr[0][j][k]);
    coords[j][k] = mn;
    }
    }
    //Создание объекта по готовым координатам
    var newObj1 = new ymaps.Polygon([coords], { building: building}, {
    fillColor: "#999999",
    strokeColor: "#999999",
    opacity: "0.4",
    strokeOpacity: "0.6",
    strokeWidth: "2"
    });
    strogino.geoObjects.add(newObj1);
    // мы просто активируем редактор на обьекте. Не на "номере"
    if (coords.length == 0) {
    activateEditorOn(newObj1);
    }
    }
    });
    }