Map api yandex: иконка кластера в виде круговой диаграммы

Задача: отобразить на карте иконки кластеров в виде диаграммы построенной на основании сумм неких значений

Решение:

Используя библиотеку PieChartGD.php создадим файл вывода картинки (PHP):

<?php
include_once '../../SamChristy/PieChart/PieChartGD.php';

use SamChristy\PieChart\PieChartGD;

$chart = new PieChartGD(300, 300);

$chart->setLegend(false);

$sausages_6=$_GET["sausages_6"];
$sausages_6_12=$_GET["sausages_6_12"];
$sausages_more_12=$_GET["sausages_more_12"];

$max=$sausages_6+$sausages_12+$sausages_more_12;
$pers_6=round($sausages_6*100/$max);
$pers_6_12=round($sausages_6_12*100/$max);
$pers_more_12=round($sausages_more_12*100/$max);

$chart->addSlice('sausages_6', $pers_6, '#0a9b0e');
$chart->addSlice('sausages_6_12',    $pers_6_12, '#f3a61f');
$chart->addSlice('sausages_more_12',$pers_more_12, '#f31f1f');

$chart->draw();
$chart->outputPNG();

Далее создадим функцию отображения кластера (JavaScript):

function SetClusterProp(ob){
    console.log("-настраиваю иконки кластера");

   ob.clusters.options.set({
        gridSize: 800,
        groupByCoordinates: true,
        clusterDisableClickZoom: true,
        clusterIcons: [{
            href: 'images/m1.png',
            size: [50, 50],            
            offset: [-25, -25]
        }]
    });
    ob.clusters.events.add('click', function (e) {
          console.log("Кликнули по кластеру");
          click_cluster = e.get('target');//.state.get('activeObject')
    });
    ob.clusters.events.add('add', function (e) { 
       var cluster = ob.clusters.getById(e.get('objectId')),
        objects = cluster.properties.geoObjects;
        if (objects.length >= 20 && objects.length < 100) {       
            dz=0;sausages=0;sausages_6=0;sausages_6_12=0;sausages_more_12=0;
            for (var i = 0; i < objects.length; i++) {
              dz=dz+objects[i].properties.dz;
              sausages=sausages+objects[i].properties.sausages;
              sausages_6=sausages_6+objects[i].properties.sausages_6;
              sausages_6_12=sausages_6_12+objects[i].properties.sausages_6_12;
              sausages_more_12=sausages_more_12+objects[i].properties.sausages_more_12;
            };            
            ob.clusters.setClusterOptions(cluster.id, {
                clusterIcons: [{
                    href: 'images/pie.php?sausages='+sausages+"&sausages_6="+sausages_6+"&sausages_6_12="+sausages_6_12+"&sausages_more_12="+sausages_more_12,
                    size: [45, 45],            
                    offset: [-22.5, -22.5]
                }]
            });
        }
        if (objects.length >= 100 && objects.length < 1000) {       
            dz=0;sausages=0;sausages_6=0;sausages_6_12=0;sausages_more_12=0;
            for (var i = 0; i < objects.length; i++) {
              dz=dz+objects[i].properties.dz;
              sausages=sausages+objects[i].properties.sausages;
              sausages_6=sausages_6+objects[i].properties.sausages_6;
              sausages_6_12=sausages_6_12+objects[i].properties.sausages_6_12;
              sausages_more_12=sausages_more_12+objects[i].properties.sausages_more_12;
            };
            ob.clusters.setClusterOptions(cluster.id, {
                clusterIcons: [{
                    href: 'images/pie.php?sausages='+sausages+"&sausages_6="+sausages_6+"&sausages_6_12="+sausages_6_12+"&sausages_more_12="+sausages_more_12,
                    size: [55, 55],            
                    offset: [-22.5, -22.5]
                }]
            });
        }
        if (objects.length >= 1000) {       
            dz=0;sausages=0;sausages_6=0;sausages_6_12=0;sausages_more_12=0;
            for (var i = 0; i < objects.length; i++) {
              dz=dz+objects[i].properties.dz;
              sausages=sausages+objects[i].properties.sausages;
              sausages_6=sausages_6+objects[i].properties.sausages_6;
              sausages_6_12=sausages_6_12+objects[i].properties.sausages_6_12;
              sausages_more_12=sausages_more_12+objects[i].properties.sausages_more_12;
            };                   
            ob.clusters.setClusterOptions(cluster.id, {
                clusterIcons: [{
                    href: 'images/pie.php?sausages='+sausages+"&sausages_6="+sausages_6+"&sausages_6_12="+sausages_6_12+"&sausages_more_12="+sausages_more_12,
                    size: [65, 65],            
                    offset: [-22.5, -22.5]
                }]
            });
        }

    });        
  return ob;
} 

И перед началом отрисовки меток, настроим кластеризацию:

 // настройки кластеризакции
    objectManager = new ymaps.ObjectManager({
        gridSize: 120,
        clusterDisableClickZoom: true,
        geoObjectOpenBalloonOnClick: true,
        minClusterSize: 2,
        showInAlphabeticalOrder: false,
        viewportMargin: 128,
        zoomMargin: 0,         
        clusterize: true
    }); //кластеризуем
          
    objectManager=SetClusterProp(objectManager);        
    myMap.geoObjects.add(objectManager);        
...
objectManager.add(массив_объектов);    

В результате получим что-то вроде:

JavaScript: перехват всех http/https запросов страницы

Прилетела задача перехватить URL всех загрузок тайлов на странице с размещенной Яндекс картой. В принципе если бы был простой случай, то всё решилось бы созданием прототипа для функции XMLHttpRequest , что-то в духе:

var open = XMLHttpRequest.prototype.open;

XMLHttpRequest.prototype.open = function(method, uri, async, user, pass) {
    this.addEventListener("readystatechange", function(event) {  
    if(this.readyState == 4){
       var self = this;
       var response = {
         method: method,
         uri: uri,
         responseText: self.responseText
      };
      console.log(response);  
    } else {
        console.log(this.readyState);
    }
    }, false);
  open.call(this, method, uri, async, user, pass);
};

Но возник один нюанс, карта располагается в iframe, а прототипы «вниз» не распространяются. Единственным способом осталось написать сервис Service Worker, который будет отлавливать все запросы..

На странице добавляем функцию загрузки сервиса:

<script>
console.log("пробуем зарегистрировать Service Worker");    
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js',{}).then(function(registration) {
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }, function(err) {
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}
</script>

Далее в sw.js добавим слушательсобытия fetch, и реализацию отправки перехваченых url на сервер:

self.addEventListener('install', (event) => {
    console.log('Установлен');
});

self.addEventListener('activate', (event) => {
    console.log('Активирован');
    self.clients.claim();
});

self.addEventListener('fetch', (event) => {    
    console.log('Запрашиваем URL: '+event.request.url);    
    postData("woodpecker.php", { "url": event.request.url}).then((data) => {
        console.log(data); 
    });
            
});


async function postData(url = "", data = {}) {
   console.log("-отправляем в "+url);
  // Default options are marked with *
  const response = await fetch(url, {
    method: "POST", 
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded', 
    },
    body: "data="+JSON.stringify(data), // body data type must match "Content-Type" header    
  });
  return await response.json(); // parses JSON response into native JavaScript objects
}

Серверная часть woodpecker.php:

<?php
$body= json_decode($_POST["data"]);

if ($body->url!=""){
    file_put_contents("urls.txt",$body->url."\n",FILE_APPEND);
}

$answer=new stdClass();
$answer->error=false;
$answer->result="ok";

echo json_encode($answer);
die();

В результате в файл url.txt на сервере пишутся все запрошенные url.

Отладка сервиса в FireFox возможна на вкладке about:debugging#/runtime/this-firefox, ищем там свой сервис, нажимаем «исследовать»

Rbot: парсер данных с госуслуг

На днях довелось попробовать в работе специализированную платформу для написания «роботов» — RPA Bot. По сути это хорошо документированная надстройка на Selenium. В базе позволяет писать «роботов» на Python, Node.js и PHP. На сайте ОЧЕНЬ хорошая документация по всему функционалу с примерами, а потому роботов писать гораздо приятнее чем при использовании «чистого» Selenium. Это плюс. Из минусов — отсутствие версии под Linux и конский ценник. Ну если первое разработчики усиленно пилят, то второе лично мне фиолетово (босс платит 😉 )

В ходе реализации очень понравилась функция save_url_to_file, аналога которой в Selenium я не нашел в своё время, а потому приходилось изобретать велосипед. Ну скорее всего конечно это то-же «велосипед» (ну логично, проект то на основе Selenium), но сделанный «штатно». К сожалению функция не отрабатывает, если закачька происходит с использованием Redirect 301. потому в случае необходимости можно воспользоваться таким вариантом закачки:

                       $browser->set_default_download($mySettings['work_path']);
                       $browser->enable_download_file_dialog(false);
                       $browser->navigate("https://www.gosuslugi.ru/api/lk/geps/file/download/$a_aid");                       
                       wait_on_file($a_name, 120);  

function wait_on_file($path, $wait=120, $pause=1)
{
    global $file_os;
	$a=0;

	while(!$file_os->is_exist($path))
	{
		sleep($pause);
		if($a>$wait)
		{
			debug_mess("ОШИБКА: не дождались появление файла по заданному пути $path!");
			return false;   
		}

		$a++;

	}

	return true;   
} 

Yandex Map: отображение меток в зависимости от масштаба карты

Работал давече с плагином HeatMap для яндекс карт. Всё хорошо, но один нюанс — невозможно навесить эвент на клик по точке карты. Ну или не нашел как. Пришлось задействовать «финт ушами» — при достижении определенного приближения, принудительно отрисовывать поверх точек heatmap свои «кликабельные» точки. В результете родилось нечто подобное:

obj = data.result; 
                heatmap = new ymaps.Heatmap(obj, {
                    // Радиус влияния.
                    radius: 15,
                    // Нужно ли уменьшать пиксельный размер точек при уменьшении зума. False - не нужно.
                    dissipating: false,
                    // Прозрачность тепловой карты.
                    opacity: 0.8,
                    // Прозрачность у медианной по весу точки.
                    intensityOfMidpoint: 0.05,
                    // JSON описание градиента.
                    gradient: {
                           0.1: 'rgba(128, 255, 0, 0.7)',
                           0.2: 'rgba(255, 255, 0, 0.8)',
                           0.7: 'rgba(234, 72, 58, 0.9)',
                           0.9: 'rgba(162, 36, 25, 1)',
                           1.0: 'rgba(0, 0, 0, 1)'
                       }
                });
                heatmap.options.events.add('click', function (e) {   
                    console.log(e);
                    var objectId = e.get('objectId');    
                    //objectManager.objects.balloon.open(objectId);
                });                    
                heatmap.setMap(myMap); 

        
       
        
                myMap.events.add('boundschange', function () {
                    size = myMap.getZoom();
                    console.log(size);
                    if (size>15){
                        console.log("-- пора бы и нарисовать метки");
                        if (all_deleted==true){
                            console.log("--- рисуем метки");
                            // настройки кластеризакции
                            objectManager = new ymaps.ObjectManager({
                                clusterDisableClickZoom: true,
                                geoObjectOpenBalloonOnClick: true,
                                clusterize: false
                            }); //кластеризуем
                            objectManager=SetClusterProp(objectManager);     
                            myMap.geoObjects.add(objectManager);   
                            objectManager.add(obj);  
                            objectManager.objects.events.add('click', function (e) {   
                                console.log(e);
                                var objectId = e.get('objectId');    
                                objectManager.objects.balloon.open(objectId);
                            });                                        
                          all_deleted=false;  
                        };
                    } else {
                        if (all_deleted==false){
                            myMap.geoObjects.remove(myMap.geoObjects.get(myMap.geoObjects.getLength()-1));                          
                            all_deleted=true;  
                        };
                    };
                });
                

PixiJS: игра «жизнь» продолжение

В ходе наблюдения за жителями, заметил, что не очень хорошо смотрится рождение нового жителя, а именно он «рождается» в случайном месте карты. Не порядок. Пусть он рождается рядом с «мамой». Для этого изменим код генерации «нового жителя», чтобы можно было за ранее передавать координаты рождения жителя:

function GenerateNewCitizen(x,y){
   resident=new People(randomIntFromInterval(0,1),1,randomIntFromInterval(1,100),professions.get(1));   
   graphics = new PIXI.Graphics();           
   graphics.beginFill(professions.get(1).color);
   graphics.lineStyle(2, professions.get(1).color, 1);
   graphics.beginFill(professions.get(1).color, 1);           
   graphics.drawCircle(0,0, 1);  
   graphics.position.set(x, y);         
   graphics.direction=randomIntFromInterval(0,360);
   graphics.endFill();        
   graphics.resident=resident;
   residents.push(graphics);
   app.stage.addChild(residents[residents.length-1]);     
}

И соотвественно «рождение» жителя:

       // проверим, есть ли совпадение точек?
       // если возраст от 18..50
       // если полы противоположные
       // то считаем что это "лябовь" и размножаемся
        for (let j = 0; j < residents.length; j++) {   
            if (Math.round(residents[i].position.y)==Math.round(residents[j].position.y) && 
                    Math.round(residents[i].position.x)==Math.round(residents[j].position.x) && i!=j){
                      if (residents[i].resident.age>=18 && residents[i].resident.age<=50){
                        if (residents[j].resident.age>=18 && residents[j].resident.age<=50){
                            if (residents[j].resident.gender==0 && residents[i].resident.gender==1){
                                console.log("-это лябофь!");  
                                x=residents[j].position.x+1;
                                y=residents[j].position.y+1;
                                GenerateNewCitizen(x,y);
                            }
                        }                          
                      }               
            };
        };

И картинка стала выглядеть уже интереснее: жители стали «кучковаться», что логично — в тех местах где жителей больше они и рождаться стали чаще

Второй момент. Нужно как-то выделить возраст жителей. Может быть сделать более «старших» чуть толще?

        if (residents[i].resident.age>0 && residents[i].resident.age<18){
         residents[i].scale._x=1;   
         residents[i].scale._y=1;   
        };
        if (residents[i].resident.age>18 && residents[i].resident.age<50){
         residents[i].scale._x=1.5;   
         residents[i].scale._y=1.5;   
        };        
        if (residents[i].resident.age>50){
         residents[i].scale._x=1.8;   
         residents[i].scale._y=1.8;   
        }; 

Еще нашел ошибку, оказывается не все жители умирали по достижении 120 лет. Поправил:

        // прибавляем всем жителям по году жизни..
        for (let i = 0; i < residents.length; i++) {         
            residents[i].resident.age++;
            if (residents[i].resident.age>120){
                residents.pop(residents[i]); // в 120 лет жизненный путь завершается..
            };
            
        };

После этого жители стали стремительно вымирать после примерно 30-50 прошедших «лет» и на «планете» остались одни трупы:

Гнетущее впечатление, планета заваленная трупами.. Всё таки сделаю чтоб они убирались при смерти, пришлось серьёзно переписать логику, избавившись от «теневого» массива

       // перебираю каждого человека и двигаем его
    for (let i = 0; i < app.stage.children.length; i++) {
        if (app.stage.children[i].resident!==undefined){
            // регулируем размер жителей
            if (app.stage.children[i].resident.age>0 && app.stage.children[i].resident.age<18){
             app.stage.children[i].scale._x=1;   
             app.stage.children[i].scale._y=1;   
            };
            if (app.stage.children[i].resident.age>18 && app.stage.children[i].resident.age<50){
             app.stage.children[i].scale._x=1.5;   
             app.stage.children[i].scale._y=1.5;   
            };        
            if (app.stage.children[i].resident.age>50){
             app.stage.children[i].scale._x=1.8;   
             app.stage.children[i].scale._y=1.8;   
            };                
           //двигаем жителя (кудато идёт) 
            step=false;
            while (step==false){
                pre_y=app.stage.children[i].position.y+Math.sin(app.stage.children[i].direction);
                pre_x=app.stage.children[i].position.x+Math.cos(app.stage.children[i].direction);
                if ((pre_x>=0)&& (pre_x<=screen_width) && (pre_y>=0) && (pre_y<=screen_height)) {
                        step=true;
                } else {
                   app.stage.children[i].direction=randomIntFromInterval(0,360); 
                };

            };
           app.stage.children[i].position.y=app.stage.children[i].position.y+Math.sin(app.stage.children[i].direction);
           app.stage.children[i].position.x=app.stage.children[i].position.x+Math.cos(app.stage.children[i].direction);
            // размножаемся
            // проверим, есть ли совпадение точек?
            // если возраст от 18..50
            // если полы противоположные
            // то считаем что это "лябовь" и размножаемся
             for (let j = 0; j < app.stage.children.length; j++) {
                 if (app.stage.children[j].resident!==undefined){
                    if (Math.round(app.stage.children[i].position.y)==Math.round(app.stage.children[j].position.y) && 
                            Math.round(app.stage.children[i].position.x)==Math.round(app.stage.children[j].position.x) && i!=j){
                              if (app.stage.children[i].resident.age>=18 && app.stage.children[i].resident.age<=50){
                                if (app.stage.children[j].resident.age>=18 && app.stage.children[j].resident.age<=50){
                                    if (app.stage.children[j].resident.gender==0 && app.stage.children[i].resident.gender==1){
                                        console.log("-это лябофь!");  
                                        x=app.stage.children[j].position.x+1;
                                        y=app.stage.children[j].position.y+1;
                                        GenerateNewCitizen(x,y);
                                    }
                                }                          
                              }               
                    };
                     
                 }
             };           
        };

    }

Осталось придумать что-то, что бы позволило не вымирать населению? Может быть повысить «рождаемость»? Пусть иногда рождаются двойни-тройни?

                                        console.log("-это лябофь!");  
                                        for (let z = 0; z < randomIntFromInterval(0,3); z++) {
                                            x=app.stage.children[j].position.x+1;
                                            y=app.stage.children[j].position.y+1;
                                            GenerateNewCitizen(x,y);                                            
                                        };

Не помогло. Нужно разрешить рожать от 16 лет до 55. Плюс не нравится мне, что умирают жители строго по дистежении 120 лет. А если сделать что умирать будут чем больше лет начиная от 70, тем чаще?

        // прибавляем всем жителям по году жизни..
        for (let i = 0; i < app.stage.children.length; i++) {         
            if (app.stage.children[i].resident!==undefined){
                app.stage.children[i].resident.age++;
                if (app.stage.children[i].resident.age>60){
                    if (randomIntFromInterval(121,app.stage.children[i].resident.age)==120){
                        console.log("- умер "+i+" по достижению "+app.stage.children[i].resident.age+" лет");
                        app.stage.removeChild(app.stage.children[i]);                        
                    };
                };
            }            
        };

Уже выглядит красивее, но всё равно жители вымирают..

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

function DiffuzeCompare(x,y,x1,y1){
    res=false;
     if (Math.abs(x-x1)<=2&&Math.abs(y-y1)<=2){
         res=true;
     }
    return res;
};

                    if (DiffuzeCompare(app.stage.children[i].position.x,app.stage.children[i].position.y,app.stage.children[j].position.x,app.stage.children[j].position.y)==true) {
                              if (app.stage.children[i].resident.age>=16 && app.stage.children[i].resident.age<=55){
                                if (app.stage.children[j].resident.age>=16 && app.stage.children[j].resident.age<=55){
                                    if (app.stage.children[j].resident.gender==0 && app.stage.children[i].resident.gender==1){
                                        console.log("-это лябофь!");  
                                        for (let z = 0; z < randomIntFromInterval(0,3); z++) {
                                            x=app.stage.children[j].position.x+1;
                                            y=app.stage.children[j].position.y+1;
                                            GenerateNewCitizen(x,y);                                            
                                        };
                                    }
                                }                          
                              }                        
                    };

Вот теперь население стабильно стало расти. Теперь сделаем так, чтоб дети рождались только в роддоме:

                                               if (buldings[bb].bulding_type.name=="Роддом"){
                                                     x=randomIntFromInterval(buldings[bb].xx,buldings[bb].xx+buldings[bb].ww);
                                                     y=randomIntFromInterval(buldings[bb].yy,buldings[bb].yy+buldings[bb].hh);
                                                    GenerateNewCitizen(x,y);                                                                                        
                                               }
                                            }  

В итоге получили:

1 6 7 8 9 10 56