diff --git a/README.md b/README.md index f9bbdbc..c6433ad 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ ![Simple Map](resources/banner.jpg) # Simple Map -A beautifully simple Google Map field type for Craft CMS. Full localization support, and compatible with Matrix. +A beautifully simple Google Map field type for Craft CMS. Full localization support, compatible with Matrix, supports +searching by location and sorting by distance. ## Installation Clone this repo into `craft/plugins/simplemap`. @@ -15,15 +16,32 @@ The field type will return an array containing `lat`, `lng`, `zoom`, `address`, This contains the locations address, broken down into its constituent parts. All values are optional so you'll need to have checks on any you use to make sure they exist. A list of the available values can be found [here](https://developers.google.com/maps/documentation/geocoding/intro#Types). +**Searching and Sorting** + +You can search for elements using the location specified in your map field. When searching by your map field you also have the option to sort the results by distance. + +```twig +{% set entries = craft.entries.myMapField({ + location: 'Maidstone, Kent, UK', + radius: 100, + unit: 'mi' +}).order('distance') %} +``` + +- `location`: Can either be an address string (requires a Google Maps Geocoding API key) or a Lat Lng Array (`{ 'lat': 51.27219908, 'lng': 0.51545620 }` or `craft.simpleMap.latLng(51.27219908, 0.51545620)`). +- `radius`: The radius around the location to search. Defaults to `50`. +- `unit`: The unit of measurement for the search. Can be either `km` (kilometers) or `mi` (miles). Defaults to `km`. + ![How it looks](resources/preview.png) ## Changelog -### 1.2.0 [DEV - Work In Progress] -- Added optional Google API Server Key setting +### 1.2.0 - **Added search and sorting support.** +- Added optional Google API Server Key setting +- **_WARNING:_** This update will break any map fields that are NOT standalone (global) or in a Matrix field. -### 1.1.2 [RELEASE] +### 1.1.2 - Fix \#5 via @jripmeester - Fixed Lat / Lng being populated with function, not number. ### 1.1.1 diff --git a/SimpleMapPlugin.php b/SimpleMapPlugin.php index 285f941..d94d753 100644 --- a/SimpleMapPlugin.php +++ b/SimpleMapPlugin.php @@ -24,12 +24,12 @@ public function getDescription() public function getVersion() { - return '1.1.3'; + return '1.2.0'; } public function getSchemaVersion() { - return '0.0.2'; + return '0.0.5'; } public function getDeveloper() @@ -66,4 +66,4 @@ public function getSettingsHtml() )); } -} \ No newline at end of file +} diff --git a/fieldtypes/SimpleMap_MapFieldType.php b/fieldtypes/SimpleMap_MapFieldType.php index ee207d2..d4760a9 100644 --- a/fieldtypes/SimpleMap_MapFieldType.php +++ b/fieldtypes/SimpleMap_MapFieldType.php @@ -70,6 +70,8 @@ public function modifyElementsQuery(DbCommand $query, $value) { if ($value !== null) craft()->simpleMap->modifyQuery($query, $value); + + return $query; } } \ No newline at end of file diff --git a/migrations/m160606_162300_simpleMap_updateFieldStorage.php b/migrations/m160606_162300_simpleMap_updateFieldStorage.php new file mode 100644 index 0000000..fc85ae5 --- /dev/null +++ b/migrations/m160606_162300_simpleMap_updateFieldStorage.php @@ -0,0 +1,59 @@ +db->tableExists('simplemap_maps')) + craft()->plugins->getPlugin('simpleMap')->createTables(); + + $fields = craft()->fields->getAllFields(); + + foreach ($fields as $field) + { + if ($field->type === 'SimpleMap_Map') + $this->_transferData($field, 'content'); + + if ($field->type === 'Matrix') + foreach (craft()->matrix->getBlockTypesByFieldId($field->id) as $blockType) + foreach ($blockType->getFields() as $blockTypeField) + if ($blockTypeField->type == 'SimpleMap_Map') + $this->_transferData($blockTypeField, 'matrixcontent_' . $field->handle, $blockType->handle . '_' . $blockTypeField->handle); + } + + return true; + } + + private function _transferData(FieldModel $field, $tableName, $fieldHandle = "") + { + if (!$fieldHandle) + $fieldHandle = $field->handle; + + $tableData = craft()->db->createCommand() + ->select('elementId, locale, field_' . $fieldHandle) + ->from($tableName) + ->where('field_' . $fieldHandle . ' IS NOT NULL') + ->queryAll(); + + foreach ($tableData as $row) { + $record = new SimpleMap_MapRecord; + $record->ownerId = $row['elementId']; + $record->fieldId = $field->id; + $record->ownerLocale = $row['locale']; + + $f = json_decode($row['field_' . $fieldHandle], true); + + $record->setAttribute('lat', $f['lat']); + $record->setAttribute('lng', $f['lng']); + $record->setAttribute('zoom', $f['zoom']); + $record->setAttribute('address', $f['address']); + if ($f !== null && array_key_exists('parts', $f)) + $record->setAttribute('parts', json_encode($f['parts'])); + + $record->save(); + } + } + +} \ No newline at end of file diff --git a/records/SimpleMap_MapRecord.php b/records/SimpleMap_MapRecord.php index 0f7db8c..2e46a49 100644 --- a/records/SimpleMap_MapRecord.php +++ b/records/SimpleMap_MapRecord.php @@ -5,16 +5,16 @@ class SimpleMap_MapRecord extends BaseRecord { public static $dec = array(AttributeType::Number, 'column' => ColumnType::Decimal, 'length' => 12, 'decimals' => 8); + const TABLE_NAME = 'simplemap_maps'; public function getTableName() { - return 'simplemap_maps'; + return static::TABLE_NAME; } public function defineRelations() { return array( -// 'element' => array(static::BELONGS_TO, 'ElementRecord', 'id', 'required' => true, 'onDelete' => static::CASCADE), 'owner' => array(static::BELONGS_TO, 'ElementRecord', 'required' => true, 'onDelete' => static::CASCADE), 'ownerLocale' => array(static::BELONGS_TO, 'LocaleRecord', 'ownerLocale', 'onDelete' => static::CASCADE, 'onUpdate' => static::CASCADE), 'field' => array(static::BELONGS_TO, 'FieldRecord', 'required' => true, 'onDelete' => static::CASCADE) diff --git a/releases.json b/releases.json index 38901b7..3bd7085 100644 --- a/releases.json +++ b/releases.json @@ -50,5 +50,15 @@ "notes": [ "Fix #5 via @jripmeester - Fixed Lat / Lng being populated with function, not number." ] + }, + { + "version": "1.2.0", + "downloadUrl": "https://github.com/ethercreative/SimpleMap/archive/v1.2.0.zip", + "date": "2016-06-07T10:00:00-08:00", + "notes": [ + "**Added search and sorting support.**", + "Added optional Google API Server Key setting", + "**_WARNING:_** This update will break any map fields that are NOT standalone (global) or in a Matrix field." + ] } ] \ No newline at end of file diff --git a/services/SimpleMapService.php b/services/SimpleMapService.php index e791104..35b42a0 100644 --- a/services/SimpleMapService.php +++ b/services/SimpleMapService.php @@ -4,10 +4,32 @@ class SimpleMapService extends BaseApplicationComponent { + public $settings; + + public $searchLatLng; + public $searchEarthRad; + + /// PUBLIC /// + + /** + * Initialize + */ + public function init() + { + $this->settings = craft()->plugins->getPlugin('SimpleMap')->getSettings(); + } + + /** + * Get Map Field + * + * @param SimpleMap_MapFieldType $fieldType + * @param $value + * @return SimpleMap_MapModel + */ public function getField (SimpleMap_MapFieldType $fieldType, $value) { $owner = $fieldType->element; - $field = $fieldType->model; + $field = $fieldType->model;; $record = SimpleMap_MapRecord::model()->findByAttributes(array( 'ownerId' => $owner->id, @@ -28,11 +50,17 @@ public function getField (SimpleMap_MapFieldType $fieldType, $value) $model = new SimpleMap_MapModel; } - $model->distance = null; + $model->distance = $this->_calculateDistance($model); return $model; } + /** + * Save Map Field + * + * @param SimpleMap_MapFieldType $fieldType + * @return bool + */ public function saveField (SimpleMap_MapFieldType $fieldType) { $owner = $fieldType->element; @@ -47,8 +75,6 @@ public function saveField (SimpleMap_MapFieldType $fieldType) $data['lat'] = (double)$data['lat']; $data['lng'] = (double)$data['lng']; - SimpleMapPlugin::log(print_r(gettype($data['lat']), true)); - $record = SimpleMap_MapRecord::model()->findByAttributes(array( 'ownerId' => $owner->id, 'fieldId' => $field->id, @@ -67,15 +93,109 @@ public function saveField (SimpleMap_MapFieldType $fieldType) $save = $record->save(); if (!$save) { - SimpleMapPlugin::log(print_r($record->getErrors(), true)); + SimpleMapPlugin::log(print_r($record->getErrors(), true), LogLevel::Error); } return $save; } + /** + * Modify Query + * + * @param DbCommand $query + * @param array $params + */ public function modifyQuery (DbCommand &$query, $params = array()) { - // + $query->join(SimpleMap_MapRecord::TABLE_NAME, 'elements.id=' . craft()->db->tablePrefix . SimpleMap_MapRecord::TABLE_NAME . '.ownerId'); + + if (array_key_exists('location', $params)) { + $this->_searchLocation($query, $params); + } + } + + + /// PRIVATE /// + + /** + * Search for entries by location + * + * @param DbCommand $query + * @param array $params + */ + private function _searchLocation (DbCommand &$query, $params) + { + $location = $params['location']; + $radius = array_key_exists('radius', $params) ? $params['radius'] : 50; + $unit = array_key_exists('unit', $params) ? $params['unit'] : 'kilometers'; + + if (!is_numeric($radius)) $radius = (float)$radius; + if (!is_numeric($radius)) $radius = 50; + + if (!in_array($unit, array('km', 'mi'))) $unit = 'km'; + + if (is_string($location)) $location = $this->_getLatLngFromAddress($location); + if (is_array($location)) { + if (!array_key_exists('lat', $location) || !array_key_exists('lng', $location)) + $location = null; + } else return; + + if ($location === null) return; + + if ($unit === 'km') $earthRad = 6371; + else $earthRad = 3959; + + $this->searchLatLng = $location; + $this->searchEarthRad = $earthRad; + + $table = craft()->db->tablePrefix . SimpleMap_MapRecord::TABLE_NAME; + + $haversine = "($earthRad * acos(cos(radians($location[lat])) * cos(radians($table.lat)) * cos(radians($table.lng) - radians($location[lng])) + sin(radians($location[lat])) * sin(radians($table.lat))))"; + + $query + ->addSelect($haversine . ' AS distance') + ->having('distance <= ' . $radius); + } + + /** + * Find lat/lng from string address + * + * @param $address + * @return null|array + * + * TODO: Cache results? + */ + private function _getLatLngFromAddress ($address) + { + if (!$this->settings['serverApiKey']) return null; + + $url = 'https://maps.googleapis.com/maps/api/geocode/json?address=' . rawurlencode($address) + . '&key=' . $this->settings['serverApiKey']; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $resp = json_decode(curl_exec($ch), true); + + if (array_key_exists('error_message', $resp) && $resp['error_message']) + SimpleMapPlugin::log($resp['error_message'], LogLevel::Error); + + if (empty($resp['results'])) return null; + + return $resp['results'][0]['geometry']['location']; + } + + private function _calculateDistance (SimpleMap_MapModel $model) + { + if (!$this->searchLatLng || !$this->searchEarthRad) return null; + + $lt1 = $this->searchLatLng['lat']; + $ln1 = $this->searchLatLng['lng']; + + $lt2 = $model->lat; + $ln2 = $model->lng; + + return ($this->searchEarthRad * acos(cos(deg2rad($lt1)) * cos(deg2rad($lt2)) * cos(deg2rad($ln2) - deg2rad($ln1)) + sin(deg2rad($lt1)) * sin(deg2rad($lt2)))); } } \ No newline at end of file diff --git a/templates/plugin-settings.twig b/templates/plugin-settings.twig index 33685fc..b98a47a 100644 --- a/templates/plugin-settings.twig +++ b/templates/plugin-settings.twig @@ -2,7 +2,7 @@ {{ forms.textField({ label: "Google API Server Key"|t, - instructions: 'Optional. Used for searching and sorting by textual address / location. If left blank, only searching and sorting by Lat/Lng will be available. Get an API key.', + instructions: 'Optional. Used for searching and sorting by textual address / location. If left blank, only searching and sorting by Lat/Lng will be available. Get an API key.', id: 'serverApiKey', name: 'serverApiKey', value: settings.serverApiKey, diff --git a/variables/SimpleMapVariable.php b/variables/SimpleMapVariable.php new file mode 100644 index 0000000..a0d1a6f --- /dev/null +++ b/variables/SimpleMapVariable.php @@ -0,0 +1,15 @@ +