Merge branch '270-final-cmb-fixes' of gitlab.com:jankstudio/resq into 312-cmb-highlight-selected-hub-and-rider-in-onestep-edit

This commit is contained in:
Korina Cordero 2020-01-28 03:00:42 +00:00
commit 0daf2a2813
24 changed files with 944 additions and 303 deletions

View file

@ -17,6 +17,7 @@
"predis/predis": "^1.1", "predis/predis": "^1.1",
"sensio/framework-extra-bundle": "^5.1", "sensio/framework-extra-bundle": "^5.1",
"setasign/fpdf": "^1.8", "setasign/fpdf": "^1.8",
"symfony/asset": "^4.0",
"symfony/console": "^4.0", "symfony/console": "^4.0",
"symfony/debug": "^4.0", "symfony/debug": "^4.0",
"symfony/filesystem": "^4.0", "symfony/filesystem": "^4.0",

60
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "4873ae3fd18db755bc9bf395bbbfb141", "content-hash": "b101ecfbc1f6f2270f0e8ad326035b7e",
"packages": [ "packages": [
{ {
"name": "catalyst/auth-bundle", "name": "catalyst/auth-bundle",
@ -2411,6 +2411,62 @@
], ],
"time": "2016-01-01T17:47:15+00:00" "time": "2016-01-01T17:47:15+00:00"
}, },
{
"name": "symfony/asset",
"version": "v4.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/asset.git",
"reference": "2c67c89d064bfb689ea6bc41217c87100bb94c17"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/asset/zipball/2c67c89d064bfb689ea6bc41217c87100bb94c17",
"reference": "2c67c89d064bfb689ea6bc41217c87100bb94c17",
"shasum": ""
},
"require": {
"php": "^7.1.3"
},
"require-dev": {
"symfony/http-foundation": "^3.4|^4.0|^5.0",
"symfony/http-kernel": "^3.4|^4.0|^5.0"
},
"suggest": {
"symfony/http-foundation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.4-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Asset\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Asset Component",
"homepage": "https://symfony.com",
"time": "2020-01-04T13:00:46+00:00"
},
{ {
"name": "symfony/cache", "name": "symfony/cache",
"version": "v4.3.1", "version": "v4.3.1",
@ -5344,6 +5400,7 @@
"code", "code",
"zf2" "zf2"
], ],
"abandoned": "laminas/laminas-code",
"time": "2018-08-13T20:36:59+00:00" "time": "2018-08-13T20:36:59+00:00"
}, },
{ {
@ -5398,6 +5455,7 @@
"events", "events",
"zf2" "zf2"
], ],
"abandoned": "laminas/laminas-eventmanager",
"time": "2018-04-25T15:33:34+00:00" "time": "2018-04-25T15:33:34+00:00"
} }
], ],

View file

@ -76,6 +76,7 @@ services:
App\Service\MQTTClient: App\Service\MQTTClient:
arguments: arguments:
$redis_client: "@App\\Service\\RedisClientProvider" $redis_client: "@App\\Service\\RedisClientProvider"
$key: "mqtt_events"
App\Service\APNSClient: App\Service\APNSClient:
arguments: arguments:
@ -87,7 +88,6 @@ services:
$host: "%env(REDIS_CLIENT_HOST)%" $host: "%env(REDIS_CLIENT_HOST)%"
$port: "%env(REDIS_CLIENT_PORT)%" $port: "%env(REDIS_CLIENT_PORT)%"
$password: "%env(REDIS_CLIENT_PASSWORD)%" $password: "%env(REDIS_CLIENT_PASSWORD)%"
$env_flag: "dev"
App\Service\GeofenceTracker: App\Service\GeofenceTracker:
arguments: arguments:
@ -205,3 +205,28 @@ services:
App\Service\GISManagerInterface: "@App\\Service\\GISManager\\OpenStreet" App\Service\GISManagerInterface: "@App\\Service\\GISManager\\OpenStreet"
#App\Service\GISManagerInterface: "@App\\Service\\GISManager\\Google" #App\Service\GISManagerInterface: "@App\\Service\\GISManager\\Google"
App\EventListener\JobOrderActiveCacheListener:
arguments:
$jo_cache: "@App\\Service\\JobOrderCache"
$mqtt: "@App\\Service\\MQTTClient"
tags:
- name: 'doctrine.orm.entity_listener'
event: 'postUpdate'
entity: 'App\Entity\JobOrder'
- name: 'doctrine.orm.entity_listener'
event: 'postRemove'
entity: 'App\Entity\JobOrder'
- name: 'doctrine.orm.entity_listener'
event: 'postPersist'
entity: 'App\Entity\JobOrder'
App\Service\JobOrderCache:
arguments:
$redis_prov: "@App\\Service\\RedisClientProvider"
$active_jo_key: "%env(LOCATION_JO_ACTIVE_KEY)%"
App\Service\RiderCache:
arguments:
$redis_prov: "@App\\Service\\RedisClientProvider"
$loc_key: "%env(LOCATION_RIDER_ACTIVE_KEY)%"
$status_key: "%env(STATUS_RIDER_KEY)%"

View file

@ -0,0 +1,190 @@
class DashboardMap {
constructor(options, rider_markers, cust_markers) {
this.options = options;
this.rider_markers = rider_markers;
this.cust_markers = cust_markers;
// layer groups
this.layer_groups = {
'rider_available': L.layerGroup(),
'rider_active_jo': L.layerGroup(),
'customer': L.layerGroup()
};
}
initialize() {
// main map
this.map = L.map(this.options.div_id).setView(
[this.options.center_lat, this.options.center_lng],
this.options.zoom
);
// add tile layer
var streets = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox/streets-v11',
accessToken: this.options.access_token
}).addTo(this.map);
// layer groups
this.layer_groups.rider_available.addTo(this.map);
this.layer_groups.rider_active_jo.addTo(this.map);
this.layer_groups.customer.addTo(this.map);
// base layer
var baseMaps = {
'Streets': streets
};
// overlay layer
var overlayMaps = {
'Available Riders' : this.layer_groups.rider_available,
'JO Riders' : this.layer_groups.rider_active_jo,
'Customers' : this.layer_groups.customer
}
L.control.layers(baseMaps, overlayMaps).addTo(this.map);
return this.map;
}
putMarker(id, lat, lng, markers, icon, layer_group, popup_url) {
var my = this;
// existing marker
if (markers.hasOwnProperty(id)) {
markers[id].setLatLng(L.latLng(lat, lng));
return;
}
// new marker
markers[id] = L.marker(
[lat, lng],
{ icon: icon }
).bindPopup('Loading...')
.addTo(layer_group);
// bind ajax for popup
markers[id].on('click', function(e) {
var popup = e.target.getPopup();
var url = popup_url.replace('[id]', id);
console.log(url);
$.get(url).done(function(data) {
popup.setContent(data);
popup.update();
});
});
}
putCustomerMarker(id, lat, lng) {
this.putMarker(
id,
lat,
lng,
this.cust_markers,
this.options.icons.customer,
this.layer_groups.customer,
this.options.cust_popup_url
);
}
removeCustomerMarker(id) {
console.log('removing customer marker for ' + id);
var layer_group = this.layer_groups.customer;
var markers = this.cust_markers;
// no customer marker with that id
if (!markers.hasOwnProperty(id)) {
console.log('no such marker to remove');
return;
}
layer_group.removeLayer(markers[id]);
}
putRiderAvailableMarker(id, lat, lng) {
this.putMarker(
id,
lat,
lng,
this.rider_markers,
this.options.icons.rider_available,
this.layer_groups.rider_available,
this.options.rider_popup_url
);
}
putRiderActiveJOMarker(id, lat, lng) {
this.putMarker(
id,
lat,
lng,
this.rider_markers,
this.options.icons.rider_active_jo,
this.layer_groups.rider_active_jo,
this.options.rider_popup_url
);
}
loadLocations(location_url) {
console.log(this.rider_markers);
var my = this;
$.ajax({
url: location_url,
}).done(function(response) {
// clear all markers
my.layer_groups.rider_available.clearLayers();
my.layer_groups.rider_active_jo.clearLayers();
my.layer_groups.customer.clearLayers();
// get riders and job orders
var riders = response.riders;
var jos = response.jos;
// job orders
$.each(jos, function(id, data) {
var lat = data.latitude;
var lng = data.longitude;
my.putCustomerMarker(id, lat, lng);
});
// riders
$.each(riders, function(id, data) {
var lat = data.latitude;
var lng = data.longitude;
if (data.has_jo)
my.putRiderActiveJOMarker(id, lat, lng);
else
my.putRiderAvailableMarker(id, lat, lng);
});
/*
$.each(riders, function(rider_id, rider_data) {
// rider location
var point = rider_data['loc'];
var lat = point[0];
var lng = point[1];
// customer location
var cloc = rider_data['cust_loc'];
var clat = cloc[0];
var clng = cloc[1];
// create rider markers
if (rider_data['has_jo']) {
var jo_data = rider_data['jo'];
// my.putCustomerMarker(jo_data['id'], clat, clng);
my.putRiderActiveJOMarker(rider_id, lat, lng);
} else {
my.putRiderAvailableMarker(rider_id, lat, lng);
}
});
*/
// console.log(rider_markers);
});
}
}

View file

@ -0,0 +1,110 @@
class MapEventHandler {
constructor(options, dashmap) {
this.options = options;
this.dashmap = dashmap;
}
connect(user_id, host, port) {
var d = new Date();
var client_id = "dash-" + user_id + "-" + d.getMonth() + "-" + d.getDate() + "-" + d.getHours() + "-" + d.getMinutes() + "-" + d.getSeconds() + "-" + d.getMilliseconds();
console.log(client_id);
this.mqtt = new Paho.MQTT.Client(host, port, client_id);
var options = {
// useSSL: true,
timeout: 3,
invocationContext: this,
onSuccess: this.onConnect.bind(this),
};
this.mqtt.onMessageArrived = this.onMessage.bind(this);
console.log('connecting to mqtt server...');
this.mqtt.connect(options);
}
onConnect(icontext) {
console.log('mqtt connected!');
var my = icontext.invocationContext;
// subscribe to rider locations
console.log('subscribing to ' + my.options.channels.rider_location);
my.mqtt.subscribe(my.options.channels.rider_location);
// subscribe to jo locations
console.log('subscribing to ' + my.options.channels.jo_location);
my.mqtt.subscribe(my.options.channels.jo_location);
my.mqtt.subscribe(my.options.channels.jo_status);
}
onMessage(msg) {
// console.log(msg);
console.log('received message');
var channel = msg.destinationName;
var chan_split = channel.split('/');
var payload = msg.payloadString;
// handle different channels
switch (chan_split[0]) {
case "rider":
this.handleRider(chan_split, payload);
break;
case "jo":
this.handleJobOrder(chan_split, payload);
break;
}
}
handleRider(chan_split, payload) {
console.log("rider message");
switch (chan_split[2]) {
case "location":
console.log("got location for rider " + chan_split[1] + " - " + payload);
var pl_split = payload.split(':');
console.log(pl_split);
// check for correct format
if (pl_split.length != 2)
break;
var lat = parseFloat(pl_split[0]);
var lng = parseFloat(pl_split[1]);
this.dashmap.putRiderAvailableMarker(chan_split[1], lat, lng);
break;
}
}
handleJobOrder(chan_split, payload) {
console.log("jo message");
var id = chan_split[1];
switch (chan_split[2]) {
case "location":
// var my = this;
console.log("got location for jo " + id + " - " + payload);
var pl_split = payload.split(':');
// check for correct format
if (pl_split.length != 2)
break;
var lat = parseFloat(pl_split[0]);
var lng = parseFloat(pl_split[1]);
// move marker
console.log(lat + ' - ' + lng);
this.dashmap.putCustomerMarker(id, lat, lng);
break;
case "status":
switch (payload) {
case 'cancel':
case 'fulfill':
case 'delete':
this.dashmap.removeCustomerMarker(id);
break;
}
}
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\DBAL\Connection;
use Doctrine\Common\Persistence\ObjectManager;
use App\Service\JobOrderCache;
use App\Entity\JobOrder;
use App\Ramcar\JOStatus;
use DateTime;
class RefreshJobOrderCacheCommand extends Command
{
protected $em;
protected $jo_cache;
public function __construct(ObjectManager $om, JobOrderCache $jo_cache)
{
$this->em = $om;
$this->jo_cache = $jo_cache;
parent::__construct();
}
protected function configure()
{
$this->setName('joborder:refresh_cache')
->setDescription('Refresh active job order cache from database.')
->setHelp('Refresh active job order cache from database.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$date = new DateTime();
$date->modify('-3 day');
$status_list = [
JOStatus::PENDING,
JOStatus::RIDER_ASSIGN,
JOStatus::ASSIGNED,
JOStatus::IN_TRANSIT,
JOStatus::IN_PROGRESS,
];
$qb = $this->em->getRepository(JobOrder::class)
->createQueryBuilder('jo');
$res = $qb->select('jo')
->where('jo.status IN (:statuses)')
->andWhere('jo.date_schedule >= :date')
->setParameter('statuses', $status_list, Connection::PARAM_STR_ARRAY)
->setParameter('date', $date)
->getQuery()
->execute();
// fulfill each
foreach ($res as $jo)
{
$this->jo_cache->addActiveJobOrder($jo);
}
}
}

View file

@ -9,6 +9,8 @@ use Doctrine\ORM\EntityManagerInterface;
use App\Service\RiderTracker; use App\Service\RiderTracker;
use App\Service\GISManagerInterface; use App\Service\GISManagerInterface;
use App\Service\JobOrderCache;
use App\Service\RiderCache;
use App\Entity\Rider; use App\Entity\Rider;
@ -18,8 +20,11 @@ class HomeController extends Controller
/** /**
* @Menu(selected="home") * @Menu(selected="home")
*/ */
public function index(EntityManagerInterface $em, RiderTracker $rider_tracker, public function index(
GISManagerInterface $gis_manager) EntityManagerInterface $em,
RiderTracker $rider_tracker,
GISManagerInterface $gis_manager
)
{ {
// get map // get map
$params['map_js_file'] = $gis_manager->getJSInitFile(); $params['map_js_file'] = $gis_manager->getJSInitFile();
@ -27,11 +32,40 @@ class HomeController extends Controller
return $this->render('home.html.twig', $params); return $this->render('home.html.twig', $params);
} }
public function getRiderLocations(EntityManagerInterface $em, RiderTracker $rider_tracker) public function getMapLocations(JobOrderCache $jo_cache)
{ {
// TODO: get active riders from cache $active_jos = $jo_cache->getAllActiveJobOrders();
// TODO: get active JOs from cache
// get active JOs from cache
}
public function getRiderLocations(JobOrderCache $jo_cache, RiderCache $rider_cache, EntityManagerInterface $em, RiderTracker $rider_tracker)
{
// get active JOs from cache
$active_jos = $jo_cache->getAllActiveJobOrders();
$riders = $rider_cache->getAllActiveRiders();
// TODO: optimize this
// get all riders and figure out if they have active jos
foreach ($riders as $rider_id => $rider_data)
{
$rider = $em->getRepository(Rider::class)->find($rider_id);
if ($rider == null)
{
unset($riders[$rider_id]);
continue;
}
$jo = $rider->getActiveJobOrder();
if ($jo == null)
$riders[$rider_id]['has_jo'] = false;
else
$riders[$rider_id]['has_jo'] = true;
}
// get active riders from cache
// get all riders // get all riders
/*
$riders = $em->getRepository(Rider::class)->findAll(); $riders = $em->getRepository(Rider::class)->findAll();
$locations = []; $locations = [];
@ -81,9 +115,11 @@ class HomeController extends Controller
]; ];
} }
*/
return $this->json([ return $this->json([
'riders' => $locations, 'jos' => $active_jos,
'riders' => $riders,
]); ]);
} }

View file

@ -31,6 +31,7 @@ use App\Service\InvoiceGeneratorInterface;
use App\Service\MQTTClient; use App\Service\MQTTClient;
use App\Service\WarrantyHandler; use App\Service\WarrantyHandler;
use App\Service\RedisClientProvider; use App\Service\RedisClientProvider;
use App\Service\RiderCache;
use App\Entity\RiderSession; use App\Entity\RiderSession;
use App\Entity\Customer; use App\Entity\Customer;
@ -213,7 +214,7 @@ class RAPIController extends Controller
return $res->getReturnResponse(); return $res->getReturnResponse();
} }
public function login(Request $req, EncoderFactoryInterface $ef, RedisClientProvider $redis) public function login(Request $req, EncoderFactoryInterface $ef, RedisClientProvider $redis, RiderCache $rcache)
{ {
$required_params = [ $required_params = [
'user', 'user',
@ -255,6 +256,11 @@ class RAPIController extends Controller
$rider->setAvailable(true); $rider->setAvailable(true);
$rider_id = $rider->getID();
// cache rider location (default to hub)
// TODO: figure out longitude / latitude default
$rcache->addActiveRider($rider_id, 0, 0);
// TODO: log rider logging in // TODO: log rider logging in
$em->flush(); $em->flush();
@ -293,7 +299,7 @@ class RAPIController extends Controller
return $res->getReturnResponse(); return $res->getReturnResponse();
} }
public function logout(Request $req) public function logout(Request $req, RiderCache $rcache)
{ {
$required_params = []; $required_params = [];
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
@ -305,6 +311,9 @@ class RAPIController extends Controller
$rider = $this->session->getRider(); $rider = $this->session->getRider();
$rider->setAvailable(false); $rider->setAvailable(false);
// remove from cache
$rcache->removeActiveRider($rider->getID());
// remove rider from session // remove rider from session
$this->session->setRider(null); $this->session->setRider(null);
@ -936,7 +945,6 @@ class RAPIController extends Controller
// check if new battery // check if new battery
if (($jo->getServiceType() != ServiceType::BATTERY_REPLACEMENT_NEW) || if (($jo->getServiceType() != ServiceType::BATTERY_REPLACEMENT_NEW) ||
($jo->getServiceType() != CMBServiceType::BATTERY_REPLACEMENT_NEW)) ($jo->getServiceType() != CMBServiceType::BATTERY_REPLACEMENT_NEW))
return; return;
// customer vehicle // customer vehicle

View file

@ -0,0 +1,105 @@
<?php
namespace App\EventListener;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use App\Service\JobOrderCache;
use App\Ramcar\JOStatus;
use App\Entity\JobOrder;
class JobOrderActiveCacheListener
{
protected $key;
protected $mqtt;
public function __construct(JobOrderCache $jo_cache, $mqtt)
{
$this->jo_cache = $jo_cache;
$this->mqtt = $mqtt;
}
// when a new job order comes in
public function postPersist(JobOrder $jo, LifecycleEventArgs $args)
{
$status = $jo->getStatus();
switch ($status)
{
// active
case JOStatus::PENDING:
case JOStatus::RIDER_ASSIGN:
case JOStatus::ASSIGNED:
case JOStatus::IN_TRANSIT:
case JOStatus::IN_PROGRESS:
$this->processActiveJO($jo);
break;
// inactive
case JOStatus::CANCELLED:
$this->processInactiveJO($jo, 'cancel');
break;
case JOStatus::FULFILLED:
$this->processInactiveJO($jo, 'fulfill');
break;
}
}
// when a job order is updated
public function postUpdate(JobOrder $jo, LifecycleEventArgs $args)
{
$status = $jo->getStatus();
switch ($status)
{
// active
case JOStatus::PENDING:
case JOStatus::RIDER_ASSIGN:
case JOStatus::ASSIGNED:
case JOStatus::IN_TRANSIT:
case JOStatus::IN_PROGRESS:
$this->processActiveJO($jo);
break;
// inactive
case JOStatus::CANCELLED:
$this->processInactiveJO($jo, 'cancel');
break;
case JOStatus::FULFILLED:
$this->processInactiveJO($jo, 'fulfill');
break;
}
}
// when a job order is deleted
public function postRemove(JobOrder $jo, LifecycleEventArgs $args)
{
$this->processInactiveJO($jo, 'delete');
}
protected function processActiveJO($jo)
{
// save in cache
$this->jo_cache->addActiveJobOrder($jo);
// publish to mqtt
$coords = $jo->getCoordinates();
// TODO: do we put the key in config?
$this->mqtt->publish(
'jo/' . $jo->getID() . '/location',
$coords->getLatitude() . ':' . $coords->getLongitude()
);
}
protected function processInactiveJO($jo, $status = 'cancel')
{
// remove from redis cache
$this->jo_cache->removeActiveJobOrder($jo);
// TODO: publich to mqtt
$this->mqtt->publish(
'jo/' . $jo->getID() . '/status',
$status
);
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Service;
use App\Service\RedisClientProvider;
use App\Entity\JobOrder;
class JobOrderCache
{
protected $redis;
protected $active_jo_key;
public function __construct(RedisClientProvider $redis_prov, $active_jo_key)
{
$this->redis = $redis_prov->getRedisClient();
$this->active_jo_key = $active_jo_key;
}
public function addActiveJobOrder(JobOrder $jo)
{
$coords = $jo->getCoordinates();
$this->redis->geoadd(
$this->active_jo_key,
$coords->getLongitude(),
$coords->getLatitude(),
$jo->getID()
);
}
public function getAllActiveJobOrders()
{
$all_jo = $this->redis->georadius(
$this->active_jo_key,
0,
0,
41000,
'km',
['WITHCOORD' => true]
);
$jo_locs = [];
foreach ($all_jo as $jo_data)
{
$id = $jo_data[0];
$lng = $jo_data[1][0];
$lat = $jo_data[1][1];
$jo_locs[$id] = [
'longitude' => $lng,
'latitude' => $lat,
];
}
// error_log(print_r($all_jo, true));
return $jo_locs;
}
public function removeActiveJobOrder(JobOrder $jo)
{
$this->redis->zrem(
$this->active_jo_key,
$jo->getID()
);
}
}

View file

@ -164,7 +164,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
$row['delivery_address'] = $orow->getDeliveryAddress(); $row['delivery_address'] = $orow->getDeliveryAddress();
$row['date_schedule'] = $orow->getDateSchedule()->format("d M Y g:i A"); $row['date_schedule'] = $orow->getDateSchedule()->format("d M Y g:i A");
$row['type'] = $orow->isAdvanceOrder() ? 'Advanced Order' : 'Immediate'; $row['type'] = $orow->isAdvanceOrder() ? 'Advanced Order' : 'Immediate';
$row['service_type'] = $service_types[$orow->getServiceType()]; $row['service_type'] = $service_types[$orow->getServiceType()] ?? 'Unknown';
$row['status'] = $statuses[$orow->getStatus()]; $row['status'] = $statuses[$orow->getStatus()];
$row['flag_advance'] = $orow->isAdvanceOrder(); $row['flag_advance'] = $orow->isAdvanceOrder();
$row['plate_number'] = $orow->getCustomerVehicle()->getPlateNumber(); $row['plate_number'] = $orow->getCustomerVehicle()->getPlateNumber();

View file

@ -9,14 +9,15 @@ class MQTTClient
{ {
const PREFIX = 'motolite.control.'; const PREFIX = 'motolite.control.';
const RIDER_PREFIX = 'motorider_'; const RIDER_PREFIX = 'motorider_';
const REDIS_KEY = 'events';
// protected $mclient; // protected $mclient;
protected $redis; protected $redis;
protected $key;
public function __construct(RedisClientProvider $redis_client) public function __construct(RedisClientProvider $redis_client, $key)
{ {
$this->redis = $redis_client->getRedisClient(); $this->redis = $redis_client->getRedisClient();
$this->key = $key;
} }
public function __destruct() public function __destruct()
@ -29,7 +30,7 @@ class MQTTClient
// $this->mclient->publish($channel, $message); // $this->mclient->publish($channel, $message);
$data = $channel . '|' . $message; $data = $channel . '|' . $message;
$this->redis->lpush(self::REDIS_KEY, $data); $this->redis->lpush($this->key, $data);
} }
public function sendEvent(JobOrder $job_order, $payload) public function sendEvent(JobOrder $job_order, $payload)

View file

@ -11,34 +11,46 @@ class RedisClientProvider
protected $host; protected $host;
protected $port; protected $port;
protected $password; protected $password;
protected $env_flag;
public function __construct($scheme, $host, $port, $password, $env_flag) public function __construct($scheme, $host, $port, $password)
{ {
$this->scheme = $scheme; $this->scheme = $scheme;
$this->host = $host; $this->host = $host;
$this->port = $port; $this->port = $port;
$this->password = $password; $this->password = $password;
$this->env_flag = $env_flag; $this->redis = null;
$this->connect();
} }
public function getRedisClient() protected function connect()
{ {
if ($this->env_flag == 'dev') // already connected
{ if ($this->redis != null)
$this->redis = new PredisClient([ return $this->redis;
"scheme"=>$this->scheme,
"host"=>$this->host, // if password is specified attempt connection
"port"=>$this->port]); if (strlen($this->password) > 0)
}
else
{ {
$this->redis = new PredisClient([ $this->redis = new PredisClient([
"scheme" => $this->scheme, "scheme" => $this->scheme,
"host" => $this->host, "host" => $this->host,
"port" => $this->port, "port" => $this->port,
"password" => $this->password]); "password" => $this->password]);
return $this->redis;
} }
$this->redis = new PredisClient([
"scheme" => $this->scheme,
"host" => $this->host,
"port" => $this->port]);
return $this->redis;
}
public function getRedisClient()
{
return $this->redis; return $this->redis;
} }
} }

View file

@ -0,0 +1,76 @@
<?php
namespace App\Service;
use App\Service\RedisClientProvider;
use App\Entity\Rider;
class RiderCache
{
protected $redis;
protected $loc_key;
protected $status_key;
public function __construct(RedisClientProvider $redis_prov, $loc_key, $status_key)
{
$this->redis = $redis_prov->getRedisClient();
$this->loc_key = $loc_key;
$this->status_key = $status_key;
}
public function addActiveRider($id, $lat, $lng)
{
$this->redis->geoadd(
$this->loc_key,
$lng,
$lat,
$id
);
}
public function getAllActiveRiders()
{
$all_riders = $this->redis->georadius(
$this->loc_key,
0,
0,
41000,
'km',
['WITHCOORD' => true]
);
$locs = [];
foreach ($all_riders as $data)
{
$id = $data[0];
$lng = $data[1][0];
$lat = $data[1][1];
$locs[$id] = [
'longitude' => $lng,
'latitude' => $lat,
];
}
// error_log(print_r($all_riders, true));
return $locs;
}
public function removeActiveRider($id)
{
$this->redis->zrem(
$this->loc_key,
$id
);
}
public function incJobOrderCount($id, $status)
{
$this->redis->hincrby($this->status_key, $id, 1);
}
public function decJobOrderCount($id, $status)
{
$this->redis->hincrby($this->status_key, $id, -1);
}
}

View file

@ -152,6 +152,9 @@
"setasign/fpdf": { "setasign/fpdf": {
"version": "1.8.1" "version": "1.8.1"
}, },
"symfony/asset": {
"version": "v4.4.3"
},
"symfony/cache": { "symfony/cache": {
"version": "v4.0.2" "version": "v4.0.2"
}, },

View file

@ -13,111 +13,74 @@
{% block scripts %} {% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
<script src="{{ asset('assets/js/dashboard_map.js') }}"></script>
<script src="{{ asset('assets/js/map_mqtt.js') }}"></script>
{{ include('map/' ~ map_js_file) }}
<script> <script>
var map; function initMap(r_markers, c_markers, icons) {
function initMap() {
var default_lat = {% trans %}default_lat{% endtrans %}; var default_lat = {% trans %}default_lat{% endtrans %};
var default_lng = {% trans %}default_long{% endtrans %}; var default_lng = {% trans %}default_long{% endtrans %};
var rider_popup_url = '/riders/[id]/popup'; var options = {
var cust_popup_url = '/job-order/[id]/popup'; 'access_token': 'pk.eyJ1Ijoia2NvcmRlcm8iLCJhIjoiY2szbzA3ZHdsMDZxdTNsbGl4ZGNnN2VxaSJ9.LRzAe3RlV8sIP1N1x0chdw',
map = mapCreate('dashboard_map', default_lat, default_lng, 'road', 13, rider_popup_url, cust_popup_url); 'div_id': 'dashboard_map',
'center_lat': default_lat,
'center_lng': default_lng,
'map_type': 'road',
'zoom': 13,
'rider_popup_url': '/riders/[id]/popup',
'cust_popup_url': '/job-order/[id]/popup',
'icons': icons
};
var dashmap = new DashboardMap(options, r_markers, c_markers);
dashmap.initialize();
dashmap.loadLocations('{{ path('rider_locations') }}');
return dashmap;
} }
</script>
{{ include('map/' ~ map_js_file) }} function initEventHandler(dashmap) {
var options = {
'channels': {
'rider_location': 'rider/+/location',
'jo_location': 'jo/+/location',
'jo_status': 'jo/+/status'
},
};
var event_handler = new MapEventHandler(options, dashmap);
event_handler.connect('{{ app.user.getID }}', '{{ mqtt_host }}', {{ mqtt_port }});
}
<script> // create icons
// TODO: put this in .env var icons = {
var mqtt; 'rider_active_jo': L.divIcon({
var timeout = 2000; className: 'map-div-icon',
var host = '{{ mqtt_host }}'; html: "<div style='background-color:#FF0000;' class='marker-pin'></div><i class='fa fa-bolt awesome'>",
var port = {{ mqtt_port }}; iconSize: [39, 42],
iconAnchor: [15, 42]
var icon_rider_available = L.divIcon({ }),
'rider_available': L.divIcon({
className: 'map-div-icon', className: 'map-div-icon',
html: "<div style='background-color:#00FF00;' class='marker-pin'></div><i class='fa fa-bolt awesome'>", html: "<div style='background-color:#00FF00;' class='marker-pin'></div><i class='fa fa-bolt awesome'>",
iconSize: [39, 42], iconSize: [39, 42],
iconAnchor: [15, 42] iconAnchor: [15, 42]
}); }),
'customer': L.divIcon({
function onConnect() { className: 'map-div-icon',
console.log('connected!'); html: "<div style='background-color:#0055FF;' class='marker-pin'></div><i class='fa fa-user awesome'>",
iconSize: [39, 42],
mqtt.subscribe('rider/+/location'); iconAnchor: [15, 42]
} })
function onMessage(msg) {
// console.log(msg);
console.log('received message');
var channel = msg.destinationName;
var chan_split = channel.split('/');
var payload = msg.payloadString;
// handle different channels
switch (chan_split[0]) {
case "rider":
handleRider(chan_split, payload);
break;
}
}
function handleRider(chan_split, payload) {
console.log("rider message");
switch (chan_split[2]) {
case "location":
console.log("got location for rider " + chan_split[1] + " - " + payload);
pl_split = payload.split(':');
console.log(pl_split);
// check for correct format
if (pl_split.length != 2)
break;
var lat = parseFloat(pl_split[0]);
var lng = parseFloat(pl_split[1]);
// move marker
console.log(rider_markers[chan_split[1]]);
// check if marker exists
if (rider_markers.hasOwnProperty(chan_split[1])) {
// marker's there, move it
rider_markers[chan_split[1]].setLatLng(L.latLng(lat, lng));
} else {
// no marker, make one
console.log('creating marker');
// TODO: make it add to the correct map layer
rider_markers[chan_split[1]]= L.marker([lat, lng], { icon: icon_rider_available }).addTo(map);;
}
break;
}
}
function mqttConnect() {
var d = new Date();
var client_id = "dash-{{ app.user.getID }}-" + d.getMonth() + "-" + d.getDate() + "-" + d.getHours() + "-" + d.getMinutes() + "-" + d.getSeconds() + "-" + d.getMilliseconds();
console.log(client_id);
mqtt = new Paho.MQTT.Client(host, port, client_id);
var options = {
useSSL: true,
timeout: 3,
onSuccess: onConnect,
}; };
mqtt.onMessageArrived = onMessage; var r_markers = {};
var c_markers = {};
console.log('connecting to mqtt server...'); var dashmap = initMap(r_markers, c_markers, icons);
mqtt.connect(options); initEventHandler(dashmap, icons);
}
mqttConnect();
</script> </script>
{% endblock %} {% endblock %}

View file

@ -2,142 +2,3 @@
integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="
crossorigin=""> crossorigin="">
</script> </script>
<script>
var rider_markers = {};
var cust_markers = {};
function mapCreate(div_id, center_lat, center_lng, map_type, zoom, rider_popup_url, cust_popup_url) {
var map = L.map(div_id).setView(
[center_lat, center_lng],
zoom
);
// add tile layer
// TODO: put access token in .env
var streets = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox/streets-v11',
accessToken: 'pk.eyJ1Ijoia2NvcmRlcm8iLCJhIjoiY2szbzA3ZHdsMDZxdTNsbGl4ZGNnN2VxaSJ9.LRzAe3RlV8sIP1N1x0chdw'
}).addTo(map);
// layer groups
// .addTo(map) --> this will display your riders by default
var lg_avail_rider = L.layerGroup().addTo(map);
var lg_jo_rider = L.layerGroup().addTo(map);
var lg_cust = L.layerGroup().addTo(map);
// this little snippet will not display your riders by default.
// Instead, a toggle button will display in the map, with a checkbox with text Riders.
// Check that to display the riders
//var ridersLayerGroup = L.layerGroup();
// create icons
var icon_rider_active_jo = L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#FF0000;' class='marker-pin'></div><i class='fa fa-bolt awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
});
var icon_rider_available = L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#00FF00;' class='marker-pin'></div><i class='fa fa-bolt awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
});
var icon_customer = L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#0055FF;' class='marker-pin'></div><i class='fa fa-user awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
});
$.ajax({
url: '{{ path('rider_locations') }}',
}).done(function(response) {
// clear all markers
lg_avail_rider.clearLayers();
lg_jo_rider.clearLayers();
lg_cust.clearLayers();
// get riders and mark
var riders = response.riders;
$.each(riders, function(rider_id, rider_data) {
// rider location
var point = rider_data['loc'];
var lat = point[0];
var long = point[1];
// customer location
var cloc = rider_data['cust_loc'];
var clat = cloc[0];
var clong = cloc[1];
// rider popup content
rider_popup = '<strong>' + rider_data['label'] + '</strong>';
// create rider markers
if (rider_data['has_jo']) {
var jo_data = rider_data['jo'];
// rider_markers[rider_id] = L.marker([lat, long], { icon: icon_rider_active_jo }).bindPopup(rider_popup);
rider_markers[rider_id] = L.marker([lat, long], { icon: icon_rider_active_jo }).bindPopup('Loading...');
// var cust_marker = L.marker([clat, clong], { icon: icon_customer }).bindPopup('Loading...');
cust_markers[jo_data['id']] = L.marker([clat, clong], { icon: icon_customer }).bindPopup('Loading...');
lg_cust.addLayer(cust_markers[jo_data['id']]);
lg_jo_rider.addLayer(rider_markers[rider_id]);
// customer popup ajax
cust_markers[jo_data['id']].on('click', function(e) {
var popup = e.target.getPopup();
var url = cust_popup_url.replace('[id]', jo_data['id']);
console.log(url);
$.get(url).done(function(data) {
popup.setContent(data);
popup.update();
});
});
} else {
// rider_markers[rider_id]= L.marker([lat, long], { icon: icon_rider_available }).bindPopup(rider_popup);
rider_markers[rider_id]= L.marker([lat, long], { icon: icon_rider_available }).bindPopup('Loading...');
lg_avail_rider.addLayer(rider_markers[rider_id]);
}
// ajax loading of rider popup
rider_markers[rider_id].on('click', function(e) {
var popup = e.target.getPopup();
var url = rider_popup_url.replace('[id]', rider_id);
console.log(url);
$.get(url).done(function(data) {
popup.setContent(data);
popup.update();
});
});
});
// console.log(rider_markers);
});
// base layer
var baseMaps = {
'Streets': streets
};
// overlay layer
var overlayMaps = {
'Available Riders' : lg_avail_rider,
'JO Riders' : lg_jo_rider,
'Customers' : lg_cust
}
L.control.layers(baseMaps, overlayMaps).addTo(map);
return map;
}
initMap();
</script>

View file

@ -26,4 +26,4 @@ default_lat: 14.6091
default_long: 121.0223 default_long: 121.0223
#default_lat: 3.084216 #default_lat: 3.084216
#default_long: 101.6129996 #default_long: 101.6129996
default_region: my default_region: ph

View file

@ -0,0 +1,41 @@
import paho.mqtt.client as mqtt
import ssl
import redis
import time
import signal
import sys
import os
import json
class RiderLocationCache(object):
def run(self, client):
print "running loop..."
client.loop_forever()
# TODO: fix this and put these guys back under the class
def init_subscriptions(client):
print "subscribing to rider/+/location"
client.subscribe('rider/+/location')
def on_connect(client, userdata, flags, rc):
init_subscriptions(client)
#print("Connected with result code "+str(rc))
# client.subscribe("$SYS/#")
def on_publish(client, userdata, mid):
pass
def on_message(client, userdata, message):
redis_conn = userdata['redis']
topic_split = message.topic.split('/')
if topic_split[0] != 'rider':
return;
payload_split = message.payload.split(':')
rider_long = str(payload_split[1])
rider_lat = str(payload_split[0])
# set the location
redis_conn.geoadd('loc_rider_active', rider_long, rider_lat, topic_split[1])

Binary file not shown.

View file

@ -0,0 +1,26 @@
import paho.mqtt.client as mqtt
import rider_location_cache as rlc
import ssl
import redis
import logging
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)
userdata = {'redis': redis_client}
client = mqtt.Client("", True, userdata)
client.on_connect = rlc.on_connect
# client.on_publish = on_publish
client.on_message = rlc.on_message
#client.tls_set(
# "/etc/letsencrypt/live/resqaws.jankstudio.com/fullchain.pem", cert_reqs=ssl.CERT_NONE,
# tls_version=ssl.PROTOCOL_TLSv1)
#client.tls_set(
# "/root/aws_ssl_keys/fullchain.pem", cert_reqs=ssl.CERT_NONE,
# tls_version=ssl.PROTOCOL_TLSv1)
#client.connect("resqaws.jankstudio.com", 8883, 60)
client.connect("localhost", 1883, 60)
rider_location = rlc.RiderLocationCache()
rider_location.run(client)

View file

@ -0,0 +1,12 @@
[Unit]
Description=Rider Location Cache Service
After=mosquitto.service redis.service
[Service]
Type=simple
ExecStart=/usr/bin/python /root/www/resq/utils/rider_location_cache/riderloc.py
StandardInput=tty-force
Restart=always
[Install]
WantedBy=multi-user.target

View file

@ -1,7 +1,6 @@
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
import yaml
import ssl import ssl
from threading import Thread
from daemonize import Daemonize
import redis import redis
import time import time
import signal import signal
@ -10,29 +9,23 @@ import os
import logging import logging
# TODO: yaml configuration file for redis and mqtt settings
def sigint_handler(signal, frame):
#logging.warning('Interrupted')
sys.exit(0)
os._exit(0)
def on_connect(client, userdata, flags, rc): def on_connect(client, userdata, flags, rc):
#logging.info("Connected with result code "+str(rc)) logging.info("Connected with result code "+str(rc))
client.subscribe("$SYS/#") #client.subscribe("$SYS/#")
def on_publish(client, userdata, mid): def on_publish(client, userdata, mid):
pass pass
def getRedis(i, client, logger): def redis_listen(client, logger):
logger.info("Listening in redis events") logger.info("Listening in redis events")
r = redis.StrictRedis(host='localhost', port=6379, db=0) r = redis.StrictRedis(host='localhost', port=6379, db=0)
while 1: while 1:
time.sleep(0) time.sleep(0)
data = r.brpop("events", 10) data = r.brpop("mqtt_events", 10)
if data: if data:
info = data[1].split('|') info = data[1].split('|')
logger.info("Channel: " + info[0] + " message: " + info[1]) logger.info("Channel: " + info[0] + " message: " + info[1])
@ -40,9 +33,6 @@ def getRedis(i, client, logger):
def sigint_handler(signal, frame):
sys.exit(0)
def get_logger(): def get_logger():
logger = logging.getLogger("mqtt_logger") logger = logging.getLogger("mqtt_logger")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
@ -64,25 +54,14 @@ def main():
client.on_connect = on_connect client.on_connect = on_connect
client.on_publish = on_publish client.on_publish = on_publish
client.tls_set( # configure mqtt broker to accept localhost
"/etc/letsencrypt/live/resqaws.jankstudio.com/fullchain.pem", cert_reqs=ssl.CERT_NONE, client.connect("localhost", 1883, 60)
tls_version=ssl.PROTOCOL_TLSv1)
client.connect("resqaws.jankstudio.com", 8883, 60) client.loop_start()
redis_listen(client, logger)
client.loop_end()
logger.info("Starting redis thread") #client.loop_forever()
t = Thread(target=getRedis, args=(1, client, logger))
t.start()
signal.signal(signal.SIGINT, sigint_handler)
client.loop_forever()
#logging.basicConfig(filename='/tmp/mqtt_sender.log', level=logging.INFO)
#logging.info('Started mqtt_sender')
#pid = "/tmp/mqtt_sender.pid"
#daemon = Daemonize(app="mqtt_sender", pid=pid, action=main)
#daemon.start()
main() main()

View file

@ -12,9 +12,9 @@ client.on_message = rlc.on_message
#client.tls_set( #client.tls_set(
# "/etc/letsencrypt/live/resqaws.jankstudio.com/fullchain.pem", cert_reqs=ssl.CERT_NONE, # "/etc/letsencrypt/live/resqaws.jankstudio.com/fullchain.pem", cert_reqs=ssl.CERT_NONE,
# tls_version=ssl.PROTOCOL_TLSv1) # tls_version=ssl.PROTOCOL_TLSv1)
client.tls_set( #client.tls_set(
"/root/aws_ssl_keys/fullchain.pem", cert_reqs=ssl.CERT_NONE, # "/root/aws_ssl_keys/fullchain.pem", cert_reqs=ssl.CERT_NONE,
tls_version=ssl.PROTOCOL_TLSv1) # tls_version=ssl.PROTOCOL_TLSv1)
#client.connect("resqaws.jankstudio.com", 8883, 60) #client.connect("resqaws.jankstudio.com", 8883, 60)
client.connect("localhost", 8883, 60) client.connect("localhost", 8883, 60)