3 namespace App\Controller;
5 use App\Controller\AppController;
6 use Avmaps\Controller\Component\SimpleMapsComponent;
10 * Locations Controller
12 * @property \App\Model\Table\LocationsTable $Locations
14 class LocationsController extends AppController {
16 // keeps track of the API key to be used for our google queries, if one is known.
17 private $api_key = null;
22 public function initialize() {
23 parent::initialize ();
25 $this->loadComponent ( 'Avmaps.SimpleMaps' );
26 $this->loadModel ( 'Categories' );
28 $this->api_key = SimpleMapsComponent::getGoogleAPIKey ();
34 * @return \Cake\Network\Response|null
36 public function index() {
37 $locations = $this->paginate ( $this->Locations, [
38 'contain' => 'Categories'
40 $this->set ( compact ( 'locations' ) );
41 $this->set ( '_serialize', [
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'.
52 public function loadAssociatedCategories($id) {
53 // find all of the categories available.
54 $this->set ( 'categoriesList', $this->Categories->getAllCategories());
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 );
63 * calculates the set of locations within a certain range from a starting point and returns the
66 public function loadLocationsInRange($lat, $long, $radius) {
67 Log::debug ( 'into ranged locations calculator' );
69 // compute the lat/long bounding box for our search.
70 $bounds = SimpleMapsComponent::calculateLatLongBoundingBox ( $lat, $long, $radius );
73 Log::debug ( "failed to calculate the bounding box!" );
75 Log::debug ( "bounding box: " . var_export ( $bounds, true ) );
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]);
83 // Log::debug('got a list of locations: ' + var_export($locationsInRange->toArray(), true));
85 $this->set ( 'locationsInRange', $locationsInRange );
91 * @param string|null $id
93 * @return \Cake\Network\Response|null
94 * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
96 public function view($id = null) {
97 $location = $this->Locations->get ( $id, [
103 $this->loadAssociatedCategories ( $id );
105 $this->set ( 'api_key', $this->api_key );
107 $this->set ( 'location', $location );
108 $this->set ( '_serialize', [
116 * @return \Cake\Network\Response|null Redirects on successful add, renders view otherwise.
118 public function add() {
119 $location = $this->Locations->newEntity ();
121 $categoriesList = $this->Categories->find ( 'list', [
123 'valueField' => 'name'
125 $this->set ( 'categoriesList', $categoriesList );
127 if ($this->request->is ( 'post' )) {
128 $location = $this->Locations->patchEntity ( $location, $this->request->getData () );
130 Log::debug ("patching with " . var_export($location, true) );
132 $location = $this->SimpleMaps->fillInGeoPosition ( $location, [
133 'key' => $this->api_key
136 if ($location !== false && $this->Locations->save ( $location )) {
137 $this->Flash->success ( __ ( 'The location has been saved.' ) );
139 return $this->redirect ( [
143 $this->Flash->error ( __ ( 'The location could not be saved. Please, try again.' ) );
146 $this->set ( compact ( 'location' ) );
147 $this->set ( '_serialize', [
155 * @param string|null $id
157 * @return \Cake\Network\Response|null Redirects on successful edit, renders view otherwise.
158 * @throws \Cake\Network\Exception\NotFoundException When record not found.
160 public function edit($id = null) {
161 $location = $this->Locations->get ( $id, [
167 $this->loadAssociatedCategories ( $id );
169 if ($this->request->is ( [
174 $location = $this->Locations->patchEntity ( $location, $this->request->getData () );
176 $new_location = $this->SimpleMaps->fillInGeoPosition ( $location, [
177 'key' => $this->api_key
179 if ($new_location === false) {
180 $this->Flash->error ( __ ( 'The location could not be geocoded. Please, try again.' ) );
182 $location = $new_location;
183 if ($this->Locations->save ( $location )) {
184 $this->Flash->success ( __ ( 'The location has been saved.' ) );
185 return $this->redirect ( [
189 $this->Flash->error ( __ ( 'The location could not be saved. Please, try again.' ) );
192 $this->set ( compact ( 'location' ) );
193 $this->set ( '_serialize', [
201 * @param string|null $id
203 * @return \Cake\Network\Response|null Redirects to index.
204 * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
206 public function delete($id = null) {
207 $this->request->allowMethod ( [
211 $location = $this->Locations->get ( $id );
212 if ($this->Locations->delete ( $location )) {
213 $this->Flash->success ( __ ( 'The location has been deleted.' ) );
215 $this->Flash->error ( __ ( 'The location could not be deleted. Please, try again.' ) );
218 return $this->redirect ( [
224 // global locations list, loaded once per object creation.
225 private $locationsListGlobal = null;
228 * generates a random list of locations with a limited number of items.
230 public function grabLocationsList()
232 if ($this->locationsListGlobal)
233 return $this->locationsListGlobal;
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.
239 $this->locationsListGlobal = $this->Locations->find ( 'list', [
241 'valueField' => 'name'
242 ] )->limit ( 1000 )->order ( 'rand()' )->toArray ();
244 // $Log::debug('got a result array: ' . var_export($this->locationsListGlobal, true));
246 return $this->locationsListGlobal;
250 * adds an item to the location list to ensure a user will not see their previous choice disappear.
252 public function addLocationToHeldList($id, $entry) {
253 $this->locationsListGlobal [$id1] = $entry;
258 * calculate the distance between two locations in the db.
259 * will allow picking if one or both
260 * location ids are missing.
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.
267 // process the parameters, if any were provided.
268 $this->set ( 'fromId', $id1 );
269 $this->set ( 'toId', $id2 );
271 // load the actual location info if they specified the ids already.
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']);
278 $this->set ( 'fromAddress', null );
281 $this->set ( 'toAddress', $this->Locations->get ( $id2 ) ['location'] );
282 $toGeoCoord = $this->Locations->get ( $id2 ) ['lat'] . ',' . $this->Locations->get ( $id2 ) ['lng'];
284 // add to our global list for selection.
285 $this->addLocationToHeldList($id2, $this->Locations->get ( $id2 ) ['name']);
287 $this->set ( 'toAddress', null );
290 if ($id1 === null || $id2 === null) {
291 // set default value for distance.
292 $distance = 'unknown';
294 // calculate distance between locations.
295 $distance = $this->SimpleMaps->calculateDrivingDistance ( $fromGeoCoord, $toGeoCoord, [
296 'key' => $this->api_key
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";
304 // store in distance calculated variable.
305 $this->set ( 'distanceCalculated', $distance );
307 // load up the selection lists for from and to addresses.
308 $this->set ( 'locationsFrom', $this->grabLocationsList());
309 $this->set ( 'locationsTo', $this->grabLocationsList() );
311 if ($this->request->is ( 'post' )) {
312 $datapack = $this->request->getData ();
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 ) );
319 $fromGeoCoord = $this->Locations->get ( $fromId ) ['lat'] . ',' . $this->Locations->get ( $fromId ) ['lng'];
321 $this->Flash->log ( 'from coord is ' . $fromGeoCoord );
322 $toGeoCoord = $this->Locations->get ( $toId ) ['lat'] . ',' . $this->Locations->get ( $toId ) ['lng'];
324 $this->Flash->log ( 'to coord is ' . $toGeoCoord );
326 // how to make the form show the same data but with updated distance?
327 // currently kludged...
328 return $this->redirect ( [
329 'action' => 'distance',
337 * finds all the locations within a given radius (in miles) from the location with 'id'.
339 public function radius($id = null, $radius = 20) {
340 Log::debug ( 'into the radius method in controller...' );
342 $this->set ( 'id', $id );
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 );
351 if ($this->request->is ( 'post' )) {
353 $datapack = $this->request->getData ();
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 )) {
364 $this->set ( 'radius', $radius );
366 $this->set('dbProcessor', $this);
367 $this->set('preloader', '$this->MapDisplay->addMarkers($dbProcessor, $locationsInRange, null, $radius, $fromLat . \',\'. $fromLong);');
369 $this->loadLocationsInRange ( $fromLat, $fromLong, $radius );
372 //hmmm: the below should be listed in an interface.
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.
377 public function processRow(& $location_row)
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'] ) . ' ',
388 public function extractCategoryImage(& $category)
390 return $category->image;
395 * jumps to a particular location as the center of the map and shows locations nearby.
397 public function jump($id) {
398 Log::debug ( 'into the jump method in locations controller...' );
400 $this->set ( 'id', $id );
402 $fromLat = $this->Locations->get ( $id ) ['lat'];
403 $this->set ( 'fromLat', $fromLat );
404 $fromLong = $this->Locations->get ( $id ) ['lng'];
405 $this->set ( 'fromLong', $fromLong );
408 $locationsInRange = $this->Locations->find ( 'all', [
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);');
420 // Log::debug('got a list of locations: ' + var_export($locationsInRange->toArray(), true));
422 $this->set ( 'locationsInRange', $locationsInRange );
426 * provides restful api for looking up locations within two lat/long boundaries.
428 public function lookupajax() {
429 Log::debug ( 'into lookupajax method in locations controller...' );
431 $reqData = $this->request->getData ();
433 Log::Debug ( 'got to locations lookup with data: ' . var_export ( $reqData, true ) );
435 // check that this is a post method, since we don't support anything else.
436 if (! $this->request->is ( [
439 die ( 'this is a post method' );
443 if (array_key_exists('action', $reqData)) {
444 $action = $reqData['action'];
446 if (strcasecmp($action, 'lookupBox') == 0) {
447 $this->findLocationsWithinBounds($reqData);
448 } else if (strcasecmp($action, 'getInfo') == 0) {
449 $this->getInfoOnLocation($reqData);
451 die('lookupajax call was given unknown action: ' . $action);
455 die('lookupajax call has no action specified');
459 public function findLocationsWithinBounds($reqData)
461 if (array_key_exists ( 'sw_lat', $reqData )) {
462 $sw_lat = $reqData ['sw_lat'];
464 if (array_key_exists ( 'sw_lng', $reqData )) {
465 $sw_lng = $reqData ['sw_lng'];
467 if (array_key_exists ( 'ne_lat', $reqData )) {
468 $ne_lat = $reqData ['ne_lat'];
470 if (array_key_exists ( 'ne_lng', $reqData )) {
471 $ne_lng = $reqData ['ne_lng'];
474 if (array_key_exists ( 'radius', $reqData )) {
475 $radius = $reqData ['radius'];
479 if (array_key_exists('start', $reqData)) {
480 $start = $reqData['start'];
483 if (array_key_exists('end', $reqData)) {
484 $end = $reqData['end'];
487 // temp! fails over to using whole range.
488 if ($sw_lat === null) {
491 if ($sw_lng === null) {
494 if ($ne_lat === null) {
497 if ($ne_lng === null) {
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 . ')');
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
508 $encoded = json_encode ( $locationsToSerialize );
509 // Log::debug('encoded json is: ' . var_export($encoded, true));
514 public function getInfoOnLocation($reqData)
516 if (array_key_exists('id', $reqData)) {
517 $id = $reqData['id'];
519 // throw an exception here?
520 $message = 'failed to find location id in request data';
521 Log::Debug($message);
525 $location = $this->Locations->get($id, [
527 return die(json_encode($location));