Merge branch 'master' of feistymeow.org:feisty_meow
[feisty_meow.git] / production / example_apps / zippy_maps / src / Controller / LocationsController.php
1 <?php
2
3 namespace App\Controller;
4
5 use App\Controller\AppController;
6 use Avmaps\Controller\Component\SimpleMapsComponent;
7 use Cake\Log\Log;
8
9 /**
10  * Locations Controller
11  *
12  * @property \App\Model\Table\LocationsTable $Locations
13  */
14 class LocationsController extends AppController {
15         
16         // keeps track of the API key to be used for our google queries, if one is known.
17         private $api_key = null;
18         
19         /**
20          * initializer method.
21          */
22         public function initialize() {
23                 parent::initialize ();
24                 
25                 $this->loadComponent ( 'Avmaps.SimpleMaps' );
26                 $this->loadModel ( 'Categories' );
27                 
28                 $this->api_key = SimpleMapsComponent::getGoogleAPIKey ();
29         }
30         
31         /**
32          * Index method
33          *
34          * @return \Cake\Network\Response|null
35          */
36         public function index() {
37                 $locations = $this->paginate ( $this->Locations, [ 
38                                 'contain' => 'Categories' 
39                 ] );
40                 $this->set ( compact ( 'locations' ) );
41                 $this->set ( '_serialize', [ 
42                                 'locations' 
43                 ] );
44         }
45         
46         /**
47          * sets two variables for the view: 'categoriesList' with *all* the category names that exist and
48          * 'selectedList' with the categories associated with the location 'id'.
49          *
50          * @param int $id               
51          */
52         public function loadAssociatedCategories($id) {
53                 // find all of the categories available.
54                 $this->set ( 'categoriesList', $this->Categories->getAllCategories());
55                                 
56                 // turn the chosen categories into a list of category ids for the multi-select.
57                 $selectedCategories = $this->Locations->getSelectedCategories($id);
58                 $selectedList = array_keys ( $selectedCategories->toArray () );
59                 $this->set ( 'selectedList', $selectedList );
60         }
61         
62         /**
63          * calculates the set of locations within a certain range from a starting point and returns the
64          * full set.
65          */
66         public function loadLocationsInRange($lat, $long, $radius) {
67                 Log::debug ( 'into ranged locations calculator' );
68                 
69                 // compute the lat/long bounding box for our search.
70                 $bounds = SimpleMapsComponent::calculateLatLongBoundingBox ( $lat, $long, $radius );
71                 
72                 if (! $bounds) {
73                         Log::debug ( "failed to calculate the bounding box!" );
74                 } else {
75                         Log::debug ( "bounding box: " . var_export ( $bounds, true ) );
76                 }
77                 
78                 // use the boundaries to restrict the lookup so we aren't crushed.
79                 // order: min_lat, min_long, max_lat, max_long.
80                 $locationsInRange = $this->Locations->getLocationsInBox($bounds [0], $bounds [1], $bounds [2], $bounds [3]);
81                 
82                 // heavy!
83                 // Log::debug('got a list of locations: ' + var_export($locationsInRange->toArray(), true));
84                 
85                 $this->set ( 'locationsInRange', $locationsInRange );
86         }
87         
88         /**
89          * View method
90          *
91          * @param string|null $id
92          *              Location id.
93          * @return \Cake\Network\Response|null
94          * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
95          */
96         public function view($id = null) {
97                 $location = $this->Locations->get ( $id, [ 
98                                 'contain' => [ 
99                                                 'Categories' 
100                                 ] 
101                 ] );
102                 
103                 $this->loadAssociatedCategories ( $id );
104                 
105                 $this->set ( 'api_key', $this->api_key );
106                 
107                 $this->set ( 'location', $location );
108                 $this->set ( '_serialize', [ 
109                                 'location' 
110                 ] );
111         }
112         
113         /**
114          * Add method
115          *
116          * @return \Cake\Network\Response|null Redirects on successful add, renders view otherwise.
117          */
118         public function add() {
119                 $location = $this->Locations->newEntity ();
120                 
121                 $categoriesList = $this->Categories->find ( 'list', [ 
122                                 'keyField' => 'id',
123                                 'valueField' => 'name' 
124                 ] );
125                 $this->set ( 'categoriesList', $categoriesList );
126                 
127                 if ($this->request->is ( 'post' )) {
128                         $location = $this->Locations->patchEntity ( $location, $this->request->getData () );
129                         
130                         Log::debug ("patching with " .  var_export($location, true) );
131                         
132                         $location = $this->SimpleMaps->fillInGeoPosition ( $location, [ 
133                                         'key' => $this->api_key 
134                         ] );
135                         
136                         if ($location !== false && $this->Locations->save ( $location )) {
137                                 $this->Flash->success ( __ ( 'The location has been saved.' ) );
138                                 
139                                 return $this->redirect ( [ 
140                                                 'action' => 'index' 
141                                 ] );
142                         }
143                         $this->Flash->error ( __ ( 'The location could not be saved. Please, try again.' ) );
144                 }
145                 
146                 $this->set ( compact ( 'location' ) );
147                 $this->set ( '_serialize', [ 
148                                 'location' 
149                 ] );
150         }
151         
152         /**
153          * Edit method
154          *
155          * @param string|null $id
156          *              Location id.
157          * @return \Cake\Network\Response|null Redirects on successful edit, renders view otherwise.
158          * @throws \Cake\Network\Exception\NotFoundException When record not found.
159          */
160         public function edit($id = null) {
161                 $location = $this->Locations->get ( $id, [ 
162                                 'contain' => [ 
163                                                 'Categories' 
164                                 ] 
165                 ] );
166                 
167                 $this->loadAssociatedCategories ( $id );
168                 
169                 if ($this->request->is ( [ 
170                                 'patch',
171                                 'post',
172                                 'put' 
173                 ] )) {
174                         $location = $this->Locations->patchEntity ( $location, $this->request->getData () );
175                         
176                         $new_location = $this->SimpleMaps->fillInGeoPosition ( $location, [ 
177                                         'key' => $this->api_key 
178                         ] );
179                         if ($new_location === false) {
180                                 $this->Flash->error ( __ ( 'The location could not be geocoded. Please, try again.' ) );
181                         } else {
182                                 $location = $new_location;
183                                 if ($this->Locations->save ( $location )) {
184                                         $this->Flash->success ( __ ( 'The location has been saved.' ) );
185                                         return $this->redirect ( [ 
186                                                         'action' => 'index' 
187                                         ] );
188                                 }
189                                 $this->Flash->error ( __ ( 'The location could not be saved. Please, try again.' ) );
190                         }
191                 }
192                 $this->set ( compact ( 'location' ) );
193                 $this->set ( '_serialize', [ 
194                                 'location' 
195                 ] );
196         }
197         
198         /**
199          * Delete method
200          *
201          * @param string|null $id
202          *              Location id.
203          * @return \Cake\Network\Response|null Redirects to index.
204          * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
205          */
206         public function delete($id = null) {
207                 $this->request->allowMethod ( [ 
208                                 'post',
209                                 'delete' 
210                 ] );
211                 $location = $this->Locations->get ( $id );
212                 if ($this->Locations->delete ( $location )) {
213                         $this->Flash->success ( __ ( 'The location has been deleted.' ) );
214                 } else {
215                         $this->Flash->error ( __ ( 'The location could not be deleted. Please, try again.' ) );
216                 }
217                 
218                 return $this->redirect ( [ 
219                                 'action' => 'index' 
220                 ] );
221         }
222         
223         
224         // global locations list, loaded once per object creation.
225         private $locationsListGlobal = null;
226         
227         /**
228          * generates a random list of locations with a limited number of items.
229          */
230         public function grabLocationsList() 
231         {
232                 if ($this->locationsListGlobal)
233                         return $this->locationsListGlobal;
234                 
235                 /*
236                  * load up a list of randomly chosen locations for the selection lists. we will keep this
237                  * around if possible, rather than reloading per page view.
238                  */
239                 $this->locationsListGlobal = $this->Locations->find ( 'list', [ 
240                                 'keyField' => 'id',
241                                 'valueField' => 'name' 
242                 ] )->limit ( 1000 )->order ( 'rand()' )->toArray ();
243                 
244                 // $Log::debug('got a result array: ' . var_export($this->locationsListGlobal, true));
245                 
246                 return $this->locationsListGlobal;
247         }
248         
249         /**
250          * adds an item to the location list to ensure a user will not see their previous choice disappear.
251          */
252         public function addLocationToHeldList($id, $entry) {
253                 $this->locationsListGlobal [$id1] = $entry;
254         }
255         
256         
257         /**
258          * calculate the distance between two locations in the db.
259          * will allow picking if one or both
260          * location ids are missing.
261          */
262         public function distance($id1 = null, $id2 = null) {
263                 // pretty kludgy approach here; don't yet know how to make the form refresh
264                 // without reloading it, but reloading it clears the selections. so we're redirecting
265                 // the form to itself but with the id parameters filled in.
266                 
267                 // process the parameters, if any were provided.
268                 $this->set ( 'fromId', $id1 );
269                 $this->set ( 'toId', $id2 );
270                 
271                 // load the actual location info if they specified the ids already.
272                 if ($id1 !== null) {
273                         $this->set ( 'fromAddress', $this->Locations->get ( $id1 ) ['location'] );
274                         $fromGeoCoord = $this->Locations->get ( $id1 ) ['lat'] . ',' . $this->Locations->get ( $id1 ) ['lng'];
275                         // ensure it's in our global list also, or it won't get selected.
276                         $this->addLocationToHeldList($id1, $this->Locations->get ( $id1 ) ['name']);
277                 } else {
278                         $this->set ( 'fromAddress', null );
279                 }
280                 if ($id2 !== null) {
281                         $this->set ( 'toAddress', $this->Locations->get ( $id2 ) ['location'] );
282                         $toGeoCoord = $this->Locations->get ( $id2 ) ['lat'] . ',' . $this->Locations->get ( $id2 ) ['lng'];
283                         ;
284                         // add to our global list for selection.
285                         $this->addLocationToHeldList($id2, $this->Locations->get ( $id2 ) ['name']);
286                 } else {
287                         $this->set ( 'toAddress', null );
288                 }
289                 
290                 if ($id1 === null || $id2 === null) {
291                         // set default value for distance.
292                         $distance = 'unknown';
293                 } else {
294                         // calculate distance between locations.
295                         $distance = $this->SimpleMaps->calculateDrivingDistance ( $fromGeoCoord, $toGeoCoord, [ 
296                                         'key' => $this->api_key 
297                         ] );
298                         // $this->Flash->log ( 'distance calculated is ' . $distance );
299                         if ($distance === false) {
300                                 // failed to calculate this, so we let the user know.
301                                 $distance = "Unable to calculate a route using Google Maps Distance Matrix";
302                         }
303                 }
304                 // store in distance calculated variable.
305                 $this->set ( 'distanceCalculated', $distance );
306                 
307                 // load up the selection lists for from and to addresses.
308                 $this->set ( 'locationsFrom', $this->grabLocationsList());
309                 $this->set ( 'locationsTo', $this->grabLocationsList() );
310                 
311                 if ($this->request->is ( 'post' )) {
312                         $datapack = $this->request->getData ();
313                         
314                         $fromId = $datapack ['from'] ['_ids'];
315                         $this->Flash->log ( h ( 'from id is ' . $fromId ) );
316                         $toId = $datapack ['to'] ['_ids'];
317                         $this->Flash->log ( h ( 'to id is ' . $toId ) );
318                         
319                         $fromGeoCoord = $this->Locations->get ( $fromId ) ['lat'] . ',' . $this->Locations->get ( $fromId ) ['lng'];
320                         
321                         $this->Flash->log ( 'from coord is ' . $fromGeoCoord );
322                         $toGeoCoord = $this->Locations->get ( $toId ) ['lat'] . ',' . $this->Locations->get ( $toId ) ['lng'];
323                         ;
324                         $this->Flash->log ( 'to coord is ' . $toGeoCoord );
325                         
326                         // how to make the form show the same data but with updated distance?
327                         // currently kludged...
328                         return $this->redirect ( [ 
329                                         'action' => 'distance',
330                                         $fromId,
331                                         $toId 
332                         ] );
333                 }
334         }
335         
336         /**
337          * finds all the locations within a given radius (in miles) from the location with 'id'.
338          */
339         public function radius($id = null, $radius = 20) {
340                 Log::debug ( 'into the radius method in controller...' );
341                 
342                 $this->set ( 'id', $id );
343                 if ($id != null) {
344                         $this->set ( 'fromAddress', $this->Locations->get ( $id ) ['location'] );
345                         $fromLat = $this->Locations->get ( $id ) ['lat'];
346                         $this->set ( 'fromLat', $fromLat );
347                         $fromLong = $this->Locations->get ( $id ) ['lng'];
348                         $this->set ( 'fromLong', $fromLong );
349                 }
350                 
351                 if ($this->request->is ( 'post' )) {
352                         
353                         $datapack = $this->request->getData ();
354                         
355                         // look for approximate array index.
356                         Log::debug ( 'got datapack: ' . var_export ( $datapack, true ) );
357                         foreach ( $datapack as $key => $value ) {
358                                 if ("radius" == substr ( $key, 0, 6 )) {
359                                         $radius = $value;
360                                 }
361                         }
362                 }
363                 
364                 $this->set ( 'radius', $radius );
365                 
366                 $this->set('dbProcessor', $this);
367                 $this->set('preloader', '$this->MapDisplay->addMarkers($dbProcessor, $locationsInRange, null, $radius, $fromLat . \',\'. $fromLong);');
368                 
369                 $this->loadLocationsInRange ( $fromLat, $fromLong, $radius );
370         }
371         
372 //hmmm: the below should be listed in an interface.
373         /**
374          * processes a location row by extracting the important information into an array.
375          * separates the db structure from the pieces needed for google maps.
376          */
377         public function processRow(& $location_row)
378         {               
379                 return [
380                                 'lat' => $location_row  ['lat'],
381                                 'lng' => $location_row ['lng'],
382                                 'title' => $location_row ['name'],
383                                 // kludge below adds extra space on content, since someone is not left justifying these.
384                                 'content' => h ( $location_row ['location'] ) . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;',
385                 ];
386         }
387         
388         public function extractCategoryImage(& $category)
389         {
390                 return $category->image;
391         }
392         
393         
394         /**
395          * jumps to a particular location as the center of the map and shows locations nearby.
396          */
397         public function jump($id) {
398                 Log::debug ( 'into the jump method in locations controller...' );
399                 
400                 $this->set ( 'id', $id );
401                 if ($id != null) {
402                         $fromLat = $this->Locations->get ( $id ) ['lat'];
403                         $this->set ( 'fromLat', $fromLat );
404                         $fromLong = $this->Locations->get ( $id ) ['lng'];
405                         $this->set ( 'fromLong', $fromLong );
406                 }
407                 
408                 $locationsInRange = $this->Locations->find ( 'all', [ 
409                                 'conditions' => [ 
410                                                 'id' => $id 
411                                 ] 
412                 ] );
413                 
414                 // we were handed a list of locations that match our query, so we can now add them as markers.
415                 $this->set('dbProcessor', $this);               
416                 $this->set('preloader', '$this->MapDisplay->addMarkers($dbProcessor, $locationsInRange, null, 1000, $fromLat . \',\'. $fromLong);');
417                 
418                 
419                 // heavy!
420                 // Log::debug('got a list of locations: ' + var_export($locationsInRange->toArray(), true));
421                 
422                 $this->set ( 'locationsInRange', $locationsInRange );
423         }
424         
425         /**
426          * provides restful api for looking up locations within two lat/long boundaries.
427          */
428         public function lookupajax() {
429                 Log::debug ( 'into lookupajax method in locations controller...' );
430                 
431                 $reqData = $this->request->getData ();
432                 
433                 Log::Debug ( 'got to locations lookup with data: ' . var_export ( $reqData, true ) );
434                 
435                 // check that this is a post method, since we don't support anything else.
436                 if (! $this->request->is ( [ 
437                                 'post' 
438                 ] )) {
439                         die ( 'this is a post method' );
440                 }
441                 
442                 
443                 if (array_key_exists('action', $reqData)) {
444                         $action = $reqData['action'];
445                         
446                         if (strcasecmp($action, 'lookupBox') == 0) {
447                                 $this->findLocationsWithinBounds($reqData);
448                         } else if (strcasecmp($action, 'getInfo') == 0) {
449                                 $this->getInfoOnLocation($reqData);
450                         } else {
451                                 die('lookupajax call was given unknown action: ' . $action);
452                         }
453                         
454                 } else {
455                         die('lookupajax call has no action specified');
456                 }
457         }
458         
459         public function findLocationsWithinBounds($reqData)
460         {               
461                 if (array_key_exists ( 'sw_lat', $reqData )) {
462                         $sw_lat = $reqData ['sw_lat'];
463                 }
464                 if (array_key_exists ( 'sw_lng', $reqData )) {
465                         $sw_lng = $reqData ['sw_lng'];
466                 }
467                 if (array_key_exists ( 'ne_lat', $reqData )) {
468                         $ne_lat = $reqData ['ne_lat'];
469                 }
470                 if (array_key_exists ( 'ne_lng', $reqData )) {
471                         $ne_lng = $reqData ['ne_lng'];
472                 }
473
474                 if (array_key_exists ( 'radius', $reqData )) {
475                         $radius = $reqData ['radius'];
476                 }
477                 
478                 $start = null;
479                 if (array_key_exists('start', $reqData)) {
480                         $start = $reqData['start']; 
481                 }
482                 $end = null;
483                 if (array_key_exists('end', $reqData)) {
484                         $end = $reqData['end'];
485                 }
486                 
487                 // temp! fails over to using whole range.
488                 if ($sw_lat === null) {
489                         $sw_lat = - 90;
490                 }
491                 if ($sw_lng === null) {
492                         $sw_lng = - 180;
493                 }
494                 if ($ne_lat === null) {
495                         $ne_lat = 90;
496                 }
497                 if ($ne_lng === null) {
498                         $ne_lng = 180;
499                 }
500                 
501                 // lookup the locations inside that box and store for view.
502                 $locationsToSerialize = $this->Locations->getChewedLocationsInBox($sw_lat, $sw_lng, $ne_lat, $ne_lng, $start, $end);
503                 Log::debug('db found ' . sizeof($locationsToSerialize) . ' rows for query (' . $start . '-' . $end . ')');
504
505                 // simple implementation here since cakephp v3.4 was doing weird stuff instead of returning object we chose to serialize.
506                 // ajax method would consistently return the name of the variable and 'undefined' as the only value, rather than properly
507                 // serializing.
508                 $encoded = json_encode ( $locationsToSerialize );
509                 // Log::debug('encoded json is: ' . var_export($encoded, true));
510                 
511                 die ( $encoded );
512         }
513         
514         public function getInfoOnLocation($reqData)
515         {
516                 if (array_key_exists('id', $reqData)) {
517                         $id = $reqData['id'];
518                 } else {
519                         // throw an exception here?
520                         $message = 'failed to find location id in request data';
521                         Log::Debug($message);
522                         die($message);
523                 }
524                 
525                 $location = $this->Locations->get($id, [
526                 ]);
527                 return die(json_encode($location));
528         }
529         
530 }