├── FieldtypeMapMarker.module ├── InputfieldMapMarker.css ├── InputfieldMapMarker.js ├── InputfieldMapMarker.module ├── MapMarker.php ├── MarkupGoogleMap.js ├── MarkupGoogleMap.module └── README.md /FieldtypeMapMarker.module: -------------------------------------------------------------------------------- 1 | 'Map Marker', 28 | 'version' => 300, 29 | 'summary' => 'Field that stores an address with latitude and longitude coordinates and has built-in geocoding capability with Google Maps API.', 30 | 'installs' => 'InputfieldMapMarker', 31 | 'icon' => 'map-marker', 32 | ); 33 | } 34 | 35 | /** 36 | * Include our MapMarker class, which serves as the value for fields of type FieldtypeMapMarker 37 | * 38 | */ 39 | public function __construct() { 40 | parent::__construct(); 41 | require_once(dirname(__FILE__) . '/MapMarker.php'); 42 | } 43 | 44 | public function set($key, $value) { 45 | /* 46 | if($key === 'googleApiKey' && strpos($this->wire('config')->httpHost, 'localhost') !== false) { 47 | // disable API key when in localhost environment 48 | if($this->wire('page')->template == 'admin') $value = ''; 49 | } 50 | */ 51 | return parent::set($key, $value); 52 | } 53 | 54 | public function getGoogleMapsURL() { 55 | $url = 'https://maps.google.com/maps/api/js'; 56 | $key = $this->get('googleApiKey'); 57 | if($key) $url .= "?key=$key"; 58 | return $url; 59 | } 60 | 61 | /** 62 | * Return the Inputfield required by this Fieldtype 63 | * 64 | * @param Page $page 65 | * @param Field $field 66 | * @return InputfieldMapMarker 67 | * 68 | */ 69 | public function getInputfield(Page $page, Field $field) { 70 | /** @var InputfieldMapMarker $inputfield */ 71 | $inputfield = $this->wire()->modules->get('InputfieldMapMarker'); 72 | $inputfield->set('googleApiKey', $this->get('googleApiKey')); 73 | return $inputfield; 74 | } 75 | 76 | /** 77 | * Return all compatible Fieldtypes 78 | * 79 | * @param Field $field 80 | * @return null 81 | * 82 | */ 83 | public function ___getCompatibleFieldtypes(Field $field) { 84 | // there are no other fieldtypes compatible with this one 85 | return null; 86 | } 87 | 88 | /** 89 | * Sanitize value for runtime 90 | * 91 | * @param Page $page 92 | * @param Field $field 93 | * @param MapMarker $value 94 | * @return MapMarker 95 | * 96 | */ 97 | public function sanitizeValue(Page $page, Field $field, $value) { 98 | 99 | // if it's not a MapMarker, then just return a blank MapMarker 100 | if(!$value instanceof MapMarker) $value = $this->getBlankValue($page, $field); 101 | 102 | // if the address changed, tell the $page that this field changed 103 | if($value->isChanged('address')) $page->trackChange($field->name); 104 | 105 | return $value; 106 | } 107 | 108 | /** 109 | * Get a blank value used by this fieldtype 110 | * 111 | * @param Page $page 112 | * @param Field $field 113 | * @return MapMarker 114 | * 115 | */ 116 | public function getBlankValue(Page $page, Field $field) { 117 | $value = new MapMarker(); 118 | $this->wire($value); 119 | return $value; 120 | } 121 | 122 | /** 123 | * Given a raw value (value as stored in DB), return the value as it would appear in a Page object 124 | * 125 | * @param Page $page 126 | * @param Field $field 127 | * @param string|int|array $value 128 | * @return MapMarker 129 | * 130 | */ 131 | public function ___wakeupValue(Page $page, Field $field, $value) { 132 | 133 | // get a blank MapMarker instance 134 | $marker = $this->getBlankValue($page, $field); 135 | 136 | if("$value[lat]" === "0") $value['lat'] = ''; 137 | if("$value[lng]" === "0") $value['lng'] = ''; 138 | 139 | // populate the marker 140 | $marker->address = $value['data']; 141 | $marker->lat = $value['lat']; 142 | $marker->lng = $value['lng']; 143 | $marker->status = $value['status']; 144 | $marker->zoom = $value['zoom']; 145 | $marker->setTrackChanges(true); 146 | 147 | return $marker; 148 | } 149 | 150 | /** 151 | * Given an 'awake' value, as set by wakeupValue, convert the value back to a basic type for storage in DB. 152 | * 153 | * @param Page $page 154 | * @param Field $field 155 | * @param string|int|array|object $value 156 | * @return array 157 | * @throws WireException 158 | * 159 | */ 160 | public function ___sleepValue(Page $page, Field $field, $value) { 161 | 162 | $marker = $value; 163 | 164 | if(!$marker instanceof MapMarker) { 165 | throw new WireException("Expecting an instance of MapMarker"); 166 | } 167 | 168 | // if the address was changed, then force it to geocode the new address 169 | if($marker->isChanged('address') && $marker->address && $marker->status != MapMarker::statusNoGeocode) { 170 | $marker->geocode(); 171 | } 172 | 173 | $sleepValue = array( 174 | 'data' => $marker->address, 175 | 'lat' => strlen($marker->lat) ? $marker->lat : 0, 176 | 'lng' => strlen($marker->lng) ? $marker->lng : 0, 177 | 'status' => $marker->status, 178 | 'zoom' => $marker->zoom 179 | ); 180 | 181 | return $sleepValue; 182 | } 183 | 184 | 185 | /** 186 | * Return the database schema in specified format 187 | * 188 | * @param Field $field 189 | * @return array 190 | * 191 | */ 192 | public function getDatabaseSchema(Field $field) { 193 | 194 | // get the default schema 195 | $schema = parent::getDatabaseSchema($field); 196 | 197 | $schema['data'] = "VARCHAR(255) NOT NULL DEFAULT ''"; // address (reusing the 'data' field from default schema) 198 | $schema['lat'] = "FLOAT(10,6) NOT NULL DEFAULT 0"; // latitude 199 | $schema['lng'] = "FLOAT(10,6) NOT NULL DEFAULT 0"; // longitude 200 | $schema['status'] = "TINYINT NOT NULL DEFAULT 0"; // geocode status 201 | $schema['zoom'] = "TINYINT NOT NULL DEFAULT 0"; // zoom level (schema v1) 202 | 203 | $schema['keys']['latlng'] = "KEY latlng (lat, lng)"; // keep an index of lat/lng 204 | $schema['keys']['data'] = 'FULLTEXT KEY `data` (`data`)'; 205 | $schema['keys']['zoom'] = "KEY zoom (zoom)"; 206 | 207 | if($field->id) $this->updateDatabaseSchema($field, $schema); 208 | 209 | return $schema; 210 | } 211 | 212 | /** 213 | * Update the DB schema, if necessary 214 | * 215 | * @param Field $field 216 | * @param array $schema 217 | * 218 | */ 219 | protected function updateDatabaseSchema(Field $field, array $schema) { 220 | 221 | $requiredVersion = 1; 222 | $schemaVersion = (int) $field->get('schemaVersion'); 223 | 224 | if($schemaVersion >= $requiredVersion) { 225 | // already up-to-date 226 | return; 227 | } 228 | 229 | if($schemaVersion == 0) { 230 | // update schema to v1: add 'zoom' column 231 | $schemaVersion = 1; 232 | $database = $this->wire()->database; 233 | $table = $database->escapeTable($field->getTable()); 234 | $query = $database->prepare("SHOW TABLES LIKE '$table'"); 235 | $query->execute(); 236 | $row = $query->fetch(\PDO::FETCH_NUM); 237 | $query->closeCursor(); 238 | if(!empty($row)) { 239 | $query = $database->prepare("SHOW COLUMNS FROM `$table` WHERE field='zoom'"); 240 | $query->execute(); 241 | if(!$query->rowCount()) try { 242 | $database->exec("ALTER TABLE `$table` ADD zoom $schema[zoom] AFTER status"); 243 | $this->message("Added 'zoom' column to '$field->table'"); 244 | } catch(\Exception $e) { 245 | $this->error($e->getMessage()); 246 | } 247 | } 248 | } 249 | 250 | $field->set('schemaVersion', $schemaVersion); 251 | $field->save(); 252 | } 253 | 254 | /** 255 | * Match values for PageFinder 256 | * 257 | * @param PageFinderDatabaseQuerySelect|DatabaseQuerySelect $query 258 | * @param string $table 259 | * @param string $subfield 260 | * @param string $operator 261 | * @param string $value 262 | * @return DatabaseQuerySelect 263 | * 264 | */ 265 | public function getMatchQuery($query, $table, $subfield, $operator, $value) { 266 | if(!$subfield || $subfield == 'address') $subfield = 'data'; 267 | if($subfield != 'data' || $this->wire()->database->isOperator($operator)) { 268 | // if dealing with something other than address, or operator is native to SQL, 269 | // then let Fieldtype::getMatchQuery handle it instead 270 | return parent::getMatchQuery($query, $table, $subfield, $operator, $value); 271 | } 272 | // if we get here, then we're performing either %= (LIKE and variations) or *= (FULLTEXT and variations) 273 | $ft = new DatabaseQuerySelectFulltext($query); 274 | $ft->match($table, $subfield, $operator, $value); 275 | return $query; 276 | } 277 | 278 | /** 279 | * Module configuration 280 | * 281 | * @param array $data 282 | * @return InputfieldWrapper 283 | * 284 | */ 285 | public static function getModuleConfigInputfields(array $data) { 286 | $inputfields = new InputfieldWrapper(); 287 | if(wire()->config->demo) $data['googleApiKey'] = 'Not shown in demo mode'; 288 | /** @var InputfieldText $f */ 289 | $f = wire()->modules->get('InputfieldText'); 290 | $f->attr('name', 'googleApiKey'); 291 | $f->label = __('Google Maps API Key'); 292 | $f->icon = 'map'; 293 | $f->description = sprintf(__('[Click here](%s) for instructions from Google on how to obtain an API key.'), 294 | 'https://developers.google.com/maps/documentation/javascript/get-api-key'); 295 | $f->attr('value', isset($data['googleApiKey']) ? $data['googleApiKey'] : ''); 296 | $inputfields->add($f); 297 | return $inputfields; 298 | } 299 | } -------------------------------------------------------------------------------- /InputfieldMapMarker.css: -------------------------------------------------------------------------------- 1 | .InputfieldMapMarker input[type=number], 2 | .InputfieldMapMarker input[type=text] { 3 | width: 99.5%; 4 | } 5 | 6 | .InputfieldMapMarkerToggle span { 7 | display: none; 8 | } 9 | 10 | .InputfieldMapMarkerAddress { 11 | float: left; 12 | width: 70%; 13 | padding-right: 2%; 14 | } 15 | 16 | .InputfieldMapMarkerToggle { 17 | float: left; 18 | width: 28%; 19 | } 20 | 21 | .InputfieldMapMarkerLat, 22 | .InputfieldMapMarkerLng { 23 | width: 42%; 24 | float: left; 25 | padding-right: 2%; 26 | } 27 | 28 | .InputfieldMapMarkerZoom { 29 | float: left; 30 | width: 10%; 31 | } 32 | 33 | .InputfieldMapMarker .notes { 34 | clear: both; 35 | } 36 | 37 | .InputfieldMapMarkerMap { 38 | width: 100%; 39 | height: 300px; 40 | clear: left; 41 | } 42 | 43 | @media only screen and (min-width: 768px) { 44 | 45 | .InputfieldMapMarkerAddress { 46 | width: 38%; 47 | padding-right: 1%; 48 | } 49 | 50 | .InputfieldMapMarkerToggle { 51 | width: 2%; 52 | padding-right: 0.5%; 53 | position: relative; 54 | } 55 | 56 | .InputfieldMapMarkerToggle strong { 57 | /* hide geocode label */ 58 | display: none; 59 | } 60 | 61 | .InputfieldMapMarkerLat, 62 | .InputfieldMapMarkerLng { 63 | width: 23%; 64 | padding-right: 1%; 65 | } 66 | 67 | .InputfieldMapMarkerZoom { 68 | float: left; 69 | width: 9.5%; 70 | } 71 | 72 | } 73 | 74 | -------------------------------------------------------------------------------- /InputfieldMapMarker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Display a Google Map and pinpoint a location for InputfieldMapMarker 3 | * 4 | */ 5 | 6 | var InputfieldMapMarker = { 7 | 8 | options: { 9 | zoom: 12, // mats, previously 5 10 | draggable: true, // +mats 11 | center: null, 12 | //key: config.InputfieldMapMarker.googleApiKey, 13 | mapTypeId: google.maps.MapTypeId.HYBRID, 14 | scrollwheel: false, 15 | mapTypeControlOptions: { 16 | style: google.maps.MapTypeControlStyle.DROPDOWN_MENU 17 | }, 18 | scaleControl: false 19 | }, 20 | 21 | init: function(mapId, lat, lng, zoom, mapType) { 22 | 23 | var options = InputfieldMapMarker.options; 24 | 25 | if(zoom < 1) zoom = 12; 26 | options.center = new google.maps.LatLng(lat, lng); 27 | options.zoom = parseInt(zoom); 28 | 29 | if(mapType == 'SATELLITE') options.mapTypeId = google.maps.MapTypeId.SATELLITE; 30 | else if(mapType == 'ROADMAP') options.mapTypeId = google.maps.MapTypeId.ROADMAP; 31 | 32 | var map = new google.maps.Map(document.getElementById(mapId), options); 33 | 34 | var marker = new google.maps.Marker({ 35 | position: options.center, 36 | map: map, 37 | draggable: options.draggable 38 | }); 39 | 40 | var $map = $('#' + mapId); 41 | var $lat = $map.siblings(".InputfieldMapMarkerLat").find("input[type=text]"); 42 | var $lng = $map.siblings(".InputfieldMapMarkerLng").find("input[type=text]"); 43 | var $addr = $map.siblings(".InputfieldMapMarkerAddress").find("input[type=text]"); 44 | var $addrJS = $map.siblings(".InputfieldMapMarkerAddress").find("input[type=hidden]"); 45 | var $toggle = $map.siblings(".InputfieldMapMarkerToggle").find("input[type=checkbox]"); 46 | var $zoom = $map.siblings(".InputfieldMapMarkerZoom").find("input[type=number]"); 47 | var $notes = $map.siblings(".notes"); 48 | 49 | $lat.val(marker.getPosition().lat()); 50 | $lng.val(marker.getPosition().lng()); 51 | $zoom.val(map.getZoom()); 52 | 53 | google.maps.event.addListener(marker, 'dragend', function(event) { 54 | var geocoder = new google.maps.Geocoder(); 55 | var position = this.getPosition(); 56 | $lat.val(position.lat()); 57 | $lng.val(position.lng()); 58 | if($toggle.is(":checked")) { 59 | geocoder.geocode({ 'latLng': position }, function(results, status) { 60 | if(status == google.maps.GeocoderStatus.OK && results[0]) { 61 | $addr.val(results[0].formatted_address); 62 | $addrJS.val($addr.val()); 63 | } 64 | $notes.text(status); 65 | }); 66 | } 67 | }); 68 | 69 | google.maps.event.addListener(map, 'zoom_changed', function() { 70 | $zoom.val(map.getZoom()); 71 | }); 72 | 73 | $addr.blur(function() { 74 | if(!$toggle.is(":checked")) return true; 75 | var geocoder = new google.maps.Geocoder(); 76 | geocoder.geocode({ 'address': $(this).val()}, function(results, status) { 77 | if(status == google.maps.GeocoderStatus.OK && results[0]) { 78 | var position = results[0].geometry.location; 79 | map.setCenter(position); 80 | marker.setPosition(position); 81 | $lat.val(position.lat()); 82 | $lng.val(position.lng()); 83 | $addrJS.val($addr.val()); 84 | } 85 | $notes.text(status); 86 | }); 87 | return true; 88 | }); 89 | 90 | $zoom.change(function() { 91 | map.setZoom(parseInt($(this).val())); 92 | }); 93 | 94 | $toggle.click(function() { 95 | if($(this).is(":checked")) { 96 | $notes.text('Geocode ON'); 97 | // google.maps.event.trigger(marker, 'dragend'); 98 | $addr.trigger('blur'); 99 | } else { 100 | $notes.text('Geocode OFF'); 101 | } 102 | return true; 103 | }); 104 | 105 | // added by diogo to solve the problem of maps not rendering correctly in hidden elements 106 | // trigger a resize on the map when either the tab button or the toggle field bar are pressed 107 | 108 | // get the tab element where this map is integrated 109 | var $map = $('#' + mapId); 110 | var $tab = $('#_' + $map.closest('.InputfieldFieldsetTabOpen').attr('id')); 111 | // get the inputfield where this map is integrated and add the tab to the stack 112 | var $inputFields = $map.closest('.Inputfield').find('.InputfieldStateToggle').add($tab); 113 | 114 | $inputFields.on('click',function(){ 115 | // give it time to open 116 | window.setTimeout(function(){ 117 | google.maps.event.trigger(map,'resize'); 118 | map.setCenter(options.center); 119 | }, 200); 120 | }); 121 | 122 | } 123 | }; 124 | 125 | $(document).ready(function() { 126 | $(".InputfieldMapMarkerMap").each(function() { 127 | var $t = $(this); 128 | InputfieldMapMarker.init($t.attr('id'), $t.attr('data-lat'), $t.attr('data-lng'), $t.attr('data-zoom'), $t.attr('data-type')); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /InputfieldMapMarker.module: -------------------------------------------------------------------------------- 1 | 'Map Marker', 29 | 'version' => 300, 30 | 'summary' => "Provides input for the MapMarker Fieldtype", 31 | 'requires' => 'FieldtypeMapMarker', 32 | 'icon' => 'map-marker', 33 | ); 34 | } 35 | 36 | const defaultAddr = 'Castaway Cay'; 37 | 38 | /** 39 | * Just in case this Inputfield is being used separately from FieldtypeMapmarker, we include the MapMarker class 40 | * 41 | */ 42 | public function __construct() { 43 | require_once(dirname(__FILE__) . '/MapMarker.php'); 44 | $this->set('defaultAddr', self::defaultAddr); 45 | $this->set('defaultZoom', 12); 46 | $this->set('defaultType', 'HYBRID'); 47 | $this->set('defaultLat', ''); 48 | $this->set('defaultLng', ''); 49 | $this->set('height', 300); 50 | $this->set('googleApiKey', ''); 51 | parent::__construct(); 52 | } 53 | 54 | /** 55 | * Set an attribute to this Inputfield 56 | * 57 | * In this case, we just capture the 'value' attribute and make sure it's something valid 58 | * 59 | * @param string $key 60 | * @param mixed $value 61 | * @return $this 62 | * @throws WireException 63 | * 64 | */ 65 | public function setAttribute($key, $value) { 66 | 67 | if($key == 'value' && !$value instanceof MapMarker && !is_null($value)) { 68 | throw new WireException("This input only accepts a MapMarker for its value"); 69 | } 70 | 71 | return parent::setAttribute($key, $value); 72 | } 73 | 74 | /** 75 | * Is the value empty? 76 | * 77 | * @return bool 78 | * 79 | */ 80 | public function isEmpty() { 81 | return (!$this->value || ((float) $this->value->lat) === 0.0); 82 | } 83 | 84 | /** 85 | * @return FieldtypeMapMarker 86 | * 87 | */ 88 | public function fieldtype() { 89 | /** @var FieldtypeMapMarker $fieldtype */ 90 | $fieldtype = $this->wire()->modules->get('FieldtypeMapMarker'); 91 | return $fieldtype; 92 | } 93 | 94 | public function renderReady(Inputfield $parent = null, $renderValueMode = false) { 95 | /* 96 | $url = 'https://maps.google.com/maps/api/js'; 97 | $key = $this->get('googleApiKey'); 98 | if($key) $url .= "?key=$key"; 99 | */ 100 | $url = $this->fieldtype()->getGoogleMapsURL(); 101 | $this->wire()->config->scripts->add($url); 102 | return parent::renderReady($parent, $renderValueMode); 103 | } 104 | 105 | /** 106 | * Render the markup needed to draw the Inputfield 107 | * 108 | * @return string 109 | * 110 | */ 111 | public function ___render() { 112 | 113 | $sanitizer = $this->wire()->sanitizer; 114 | $adminTheme = $this->wire()->adminTheme; 115 | 116 | $name = $this->attr('name'); 117 | $id = $this->attr('id'); 118 | $marker = $this->attr('value'); 119 | 120 | if($marker->lat == 0.0) $marker->lat = $this->defaultLat; 121 | if($marker->lng == 0.0) $marker->lng = $this->defaultLng; 122 | if(!$marker->zoom) $marker->zoom = $this->defaultZoom; 123 | 124 | $address = $sanitizer->entities($marker->address); 125 | $toggleChecked = $marker->status != MapMarker::statusNoGeocode ? " checked='checked'" : ''; 126 | $status = $marker->status == MapMarker::statusNoGeocode ? 0 : $marker->status; 127 | $mapType = $this->defaultType; 128 | $height = $this->height ? (int) $this->height : 300; 129 | $classes = array('input' => '', 'checkbox' => ''); 130 | 131 | if($adminTheme && method_exists($adminTheme, 'getClass')) { 132 | foreach(array_keys($classes) as $key) { 133 | $classes[$key] = $adminTheme->getClass($key); 134 | } 135 | } 136 | 137 | $labels = array( 138 | 'addr' => $this->_('Address'), 139 | 'lat' => $this->_('Latitude'), 140 | 'lng' => $this->_('Longitude'), 141 | 'geo' => $this->_('Geocode?'), 142 | 'zoom' => $this->_('Zoom') 143 | ); 144 | 145 | foreach($labels as $key => $label) { 146 | $labels[$key] = $sanitizer->entities1($label); 147 | } 148 | 149 | $out = <<< _OUT 150 | 151 | 152 | 153 |
154 | 159 | 160 |
161 | 162 |163 | 168 |
169 | 170 |171 | 175 |
176 | 177 |178 | 182 |
183 | 184 |185 | 189 |
190 | 191 | 192 | _OUT; 193 | 194 | $out .= 195 | "" . 366 | "\$page->{$this->name}->address\n" . 367 | "\$page->{$this->name}->lat\n" . 368 | "\$page->{$this->name}->lng\n" . 369 | "\$page->{$this->name}->zoom" . 370 | ""; 371 | 372 | $inputfields->add($field); 373 | 374 | return $inputfields; 375 | } 376 | 377 | } -------------------------------------------------------------------------------- /MapMarker.php: -------------------------------------------------------------------------------- 1 | 'N/A', 21 | 1 => 'OK', 22 | 2 => 'OK_ROOFTOP', 23 | 3 => 'OK_RANGE_INTERPOLATED', 24 | 4 => 'OK_GEOMETRIC_CENTER', 25 | 5 => 'OK_APPROXIMATE', 26 | 27 | -1 => 'UNKNOWN', 28 | -2 => 'ZERO_RESULTS', 29 | -3 => 'OVER_QUERY_LIMIT', 30 | -4 => 'REQUEST_DENIED', 31 | -5 => 'INVALID_REQUEST', 32 | 33 | -100 => 'Geocode OFF', // RCD 34 | ); 35 | 36 | protected $geocodedAddress = ''; 37 | 38 | public function __construct() { 39 | parent::__construct(); 40 | $this->set('lat', ''); 41 | $this->set('lng', ''); 42 | $this->set('address', ''); 43 | $this->set('status', 0); 44 | $this->set('zoom', 0); 45 | // temporary runtime property to indicate the geocode should be skipped 46 | $this->set('skipGeocode', false); 47 | $this->set('statusString', ''); 48 | } 49 | 50 | public function set($key, $value) { 51 | 52 | if($key == 'lat' || $key == 'lng') { 53 | // if value isn't numeric, then it's not valid: make it blank 54 | if(strpos($value, ',') !== false) $value = str_replace(',', '.', $value); // convert 123,456 to 123.456 55 | if(!is_numeric($value)) $value = ''; 56 | 57 | } else if($key == 'address') { 58 | $value = $this->wire()->sanitizer->text($value); 59 | 60 | } else if($key == 'status') { 61 | $value = (int) $value; 62 | if(!isset($this->geocodeStatuses[$value])) $value = -1; // -1 = unknown 63 | } else if($key == 'zoom') { 64 | $value = (int) $value; 65 | } 66 | 67 | return parent::set($key, $value); 68 | } 69 | 70 | public function get($key) { 71 | if($key == 'statusString') return str_replace('_', ' ', $this->geocodeStatuses[$this->status]); 72 | return parent::get($key); 73 | } 74 | 75 | public function geocode() { 76 | if($this->skipGeocode) return -100; 77 | 78 | // check if address was already geocoded 79 | if($this->geocodedAddress == $this->address) return $this->status; 80 | $this->geocodedAddress = $this->address; 81 | 82 | $url = "https://maps.googleapis.com/maps/api/geocode/json?address=" . urlencode($this->address); 83 | 84 | /** @var FieldtypeMapMarker $fieldtype */ 85 | $fieldtype = $this->modules->get('FieldtypeMapMarker'); 86 | $apiKey = $fieldtype->get('googleApiKey'); 87 | if($apiKey) $url .= "&key=$apiKey"; 88 | $http = new WireHttp(); 89 | $json = $http->getJSON($url, true); 90 | if(empty($json['status'])) $json['status'] = 'No response'; 91 | 92 | if($json['status'] != 'OK') { 93 | $this->error("Error geocoding address: $json[status] ($url)"); 94 | if(isset($json['status'])) $this->status = (int) array_search($json['status'], $this->geocodeStatuses); 95 | else $this->status = -1; 96 | $this->lat = 0; 97 | $this->lng = 0; 98 | return $this->status; 99 | } 100 | 101 | $geometry = $json['results'][0]['geometry']; 102 | $location = $geometry['location']; 103 | $locationType = $geometry['location_type']; 104 | 105 | $this->lat = $location['lat']; 106 | $this->lng = $location['lng']; 107 | 108 | $statusString = $json['status'] . '_' . $locationType; 109 | $status = array_search($statusString, $this->geocodeStatuses); 110 | if($status === false) $status = 1; // OK 111 | 112 | $this->status = $status; 113 | $this->message("Geocode {$this->statusString}: '{$this->address}'"); 114 | 115 | return $this->status; 116 | } 117 | 118 | /** 119 | * If accessed as a string, then just output the lat, lng coordinates 120 | * 121 | */ 122 | public function __toString() { 123 | return "$this->address ($this->lat, $this->lng, $this->zoom) [$this->statusString]"; 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /MarkupGoogleMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ProcessWire Map Markup (JS) 3 | * 4 | * Renders maps for the FieldtypeMapMarker module 5 | * 6 | * ProcessWire 3.x 7 | * Copyright (C) 2023 by Ryan Cramer 8 | * Licensed under MPL 2.0 9 | * 10 | * http://processwire.com 11 | * 12 | * Javascript Usage: 13 | * ================= 14 | * var map = new MarkupGoogleMap(); 15 | * map.setOption('any-google-maps-option', 'value'); 16 | * map.setOption('zoom', 12); // example 17 | * 18 | * // init(container ID, latitude, longitude): 19 | * map.init('#map-div', 26.0936823, -77.5332796); 20 | * 21 | * // addMarker(latitude, longitude, optional URL, optional URL to icon file): 22 | * map.addMarker(26.0936823, -77.5332796, 'en.wikipedia.org/wiki/Castaway_Cay', ''); 23 | * map.addMarker(...you may have as many of these as you want...); 24 | * 25 | * // optionally fit the map to the bounds of the markers you added 26 | * map.fitToMarkers(); 27 | * 28 | */ 29 | 30 | function MarkupGoogleMap() { 31 | 32 | this.map = null; 33 | this.markers = []; 34 | this.numMarkers = 0; 35 | this.icon = ''; 36 | this.iconHover = ''; 37 | this.shadow = ''; 38 | 39 | this.hoverBox = null; 40 | this.hoverBoxOffsetTop = 0; 41 | this.hoverBoxOffsetLeft = 0; 42 | 43 | this.options = { 44 | zoom: 10, 45 | center: null, 46 | mapTypeId: google.maps.MapTypeId.ROADMAP, 47 | scrollwheel: false, 48 | mapTypeControlOptions: { 49 | style: google.maps.MapTypeControlStyle.DROPDOWN_MENU 50 | }, 51 | scaleControl: false, 52 | 53 | // disable points of interest 54 | styles: [{ 55 | featureType: "poi", 56 | stylers: [ { visibility: "off" } ] 57 | }] 58 | }; 59 | 60 | 61 | this._currentURL = ''; 62 | 63 | this.init = function(mapID, lat, lng) { 64 | if(lat != 0) this.options.center = new google.maps.LatLng(lat, lng); 65 | this.map = new google.maps.Map(document.getElementById(mapID), this.options); 66 | } 67 | 68 | this.setOption = function(key, value) { 69 | this.options[key] = value; 70 | } 71 | 72 | this.setIcon = function(url) { 73 | this.icon = url; 74 | } 75 | 76 | this.setIconHover = function(url) { 77 | this.iconHover = url; 78 | } 79 | 80 | this.setShadow = function(url) { 81 | this.shadow = url; 82 | } 83 | 84 | this.setHoverBox = function(markup) { 85 | 86 | if(!markup.length) { 87 | this.hoverBox = null; 88 | return; 89 | } 90 | 91 | this.hoverBox = $(markup); 92 | var $hoverBox = this.hoverBox; 93 | 94 | this.hoverBoxOffsetTop = parseInt($hoverBox.attr('data-top')); 95 | this.hoverBoxOffsetLeft = parseInt($hoverBox.attr('data-left')); 96 | 97 | $("body").append($hoverBox); 98 | 99 | // keep it hidden/out of the way until needed 100 | $hoverBox.css({ 101 | position: 'absolute', 102 | left: 0, 103 | top: '-100px' 104 | }); 105 | 106 | $hoverBox.mouseout(function() { 107 | $hoverBox.hide(); 108 | }).click(function() { 109 | if(this._currentURL.length > 0) window.location.href = this._currentURL; 110 | }); 111 | } 112 | 113 | this.addMarker = function(lat, lng, url, title, icon, shadow) { 114 | if(lat == 0.0) return; 115 | 116 | var latLng = new google.maps.LatLng(lat, lng); 117 | var zIndex = 99999 + this.numMarkers; 118 | 119 | var markerOptions = { 120 | position: latLng, 121 | map: this.map, 122 | linkURL: '', 123 | zIndex: zIndex 124 | }; 125 | 126 | if(typeof icon !== "undefined" && icon.length > 0) markerOptions.icon = icon; 127 | else if(this.icon) markerOptions.icon = this.icon; 128 | 129 | // console.log(markerOptions); 130 | 131 | if(typeof shadow !== "undefined" && shadow.length > 0) markerOptions.shadow = shadow; 132 | else if(this.shadow.length > 0) markerOptions.shadow = this.shadow; 133 | 134 | var marker = new google.maps.Marker(markerOptions); 135 | 136 | if(url.length > 0) marker.linkURL = url; 137 | if(this.hoverBox) marker.hoverBoxTitle = title; 138 | else marker.setTitle(title); 139 | 140 | this.markers[this.numMarkers] = marker; 141 | this.numMarkers++; 142 | 143 | if(marker.linkURL.length > 0) { 144 | google.maps.event.addListener(marker, 'click', function(e) { 145 | window.location.href = marker.linkURL; 146 | }); 147 | } 148 | 149 | if(markerOptions.icon !== "undefined" && this.iconHover) { 150 | var iconHover = this.iconHover; 151 | google.maps.event.addListener(marker, 'mouseover', function(e) { 152 | marker.setIcon(iconHover); 153 | }); 154 | google.maps.event.addListener(marker, 'mouseout', function(e) { 155 | marker.setIcon(markerOptions.icon); 156 | }); 157 | } 158 | 159 | if(this.hoverBox) { 160 | 161 | var $hoverBox = this.hoverBox; 162 | var offsetTop = this.hoverBoxOffsetTop; 163 | var offsetLeft = this.hoverBoxOffsetLeft; 164 | 165 | var mouseMove = function(e) { 166 | $hoverBox.css({ 167 | 'top': e.pageY + offsetTop, 168 | 'left': e.pageX + offsetLeft 169 | }); 170 | }; 171 | 172 | // console.log($hoverBox); 173 | 174 | google.maps.event.addListener(marker, 'mouseover', function(e) { 175 | this._currentURL = url; 176 | $hoverBox.html("" + marker.hoverBoxTitle + "") 177 | .css('top', '0px') 178 | .css('left', '0px') 179 | .css('display', 'block') 180 | .css('width', 'auto') 181 | .css('z-index', 9999); 182 | $hoverBox.show(); 183 | 184 | $(document).mousemove(mouseMove); 185 | }); 186 | 187 | google.maps.event.addListener(marker, 'mouseout', function(e) { 188 | $hoverBox.hide(); 189 | $(document).unbind("mousemove", mouseMove); 190 | }); 191 | 192 | } 193 | } 194 | 195 | this.fitToMarkers = function() { 196 | 197 | var bounds = new google.maps.LatLngBounds(); 198 | var map = this.map; 199 | 200 | for(var i = 0; i < this.numMarkers; i++) { 201 | var latLng = this.markers[i].position; 202 | bounds.extend(latLng); 203 | } 204 | 205 | map.fitBounds(bounds); 206 | 207 | 208 | var listener = google.maps.event.addListener(map, "idle", function() { 209 | if(map.getZoom() < 2) map.setZoom(2); 210 | google.maps.event.removeListener(listener); 211 | }); 212 | } 213 | } -------------------------------------------------------------------------------- /MarkupGoogleMap.module: -------------------------------------------------------------------------------- 1 | tag: 18 | * 19 | * 20 | * 21 | * In the location where you want to output your map, do the following in your template file: 22 | * 23 | * $map = $modules->get('MarkupGoogleMap'); 24 | * echo $map->render($page, 'map'); // replace 'map' with the name of your FieldtypeMap field 25 | * 26 | * To render a map with multiple markers on it, specify a PageArray rather than a single $page: 27 | * 28 | * $items = $pages->find("template=something, map!='', sort=title"); 29 | * $map = $modules->get('MarkupGoogleMap'); 30 | * echo $map->render($items, 'map'); 31 | * 32 | * To specify options, provide a 3rd argument with an options array: 33 | * 34 | * $map = $modules->get('MarkupGoogleMap'); 35 | * echo $map->render($items, 'map', array('height' => '500px')); 36 | * 37 | * 38 | * OPTIONS 39 | * ======= 40 | * Here is a list of all possible options (with defaults shown): 41 | * 42 | * // default width of the map 43 | * 'width' => '100%' 44 | * 45 | * // default height of the map 46 | * 'height' => '300px' 47 | * 48 | * // zoom level 49 | * 'zoom' => 12 (or $field->defaultZoom) 50 | * 51 | * // map type: ROADMAP, HYBRID or SATELLITE 52 | * 'type' => 'HYBRID' or $field->defaultType 53 | * 54 | * // map ID attribute 55 | * 'id' => "mgmap" 56 | * 57 | * // map class attribute 58 | * 'class' => "MarkupGoogleMap" 59 | * 60 | * // center latitude 61 | * 'lat' => $field->defaultLat 62 | * 63 | * // center longitude 64 | * 'lng' => $field->defaultLng 65 | * 66 | * // set to false only if you will style the map