├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── craft-3-issue.md │ └── craft-4-issue.md └── PULL_REQUEST_TEMPLATE.md ├── LICENSE ├── composer.json └── src ├── SimpleMap.php ├── acfadapters └── GoogleMap.php ├── config.php ├── controllers ├── SettingsController.php └── StaticController.php ├── enums ├── GeoService.php └── MapTiles.php ├── fields └── MapField.php ├── icon.svg ├── integrations ├── feedme │ └── FeedMeMaps.php └── graphql │ ├── MapPartsType.php │ └── MapType.php ├── jobs └── MaxMindDBDownloadJob.php ├── migrations ├── Install.php ├── m190226_143809_craft3_upgrade.php ├── m190325_130533_repair_map_elements.php ├── m190712_104805_new_data_format.php └── m190723_105637_fix_map_field_column_type.php ├── models ├── BaseLocation.php ├── EmbedOptions.php ├── Map.php ├── Marker.php ├── Parts.php ├── PartsLegacy.php ├── Point.php ├── Settings.php ├── StaticOptions.php └── UserLocation.php ├── records └── Map.php ├── resources ├── OpenSans-Bold.ttf ├── OpenSans_LICENSE.txt ├── marker.png ├── marker.svg └── markerNoLabel.png ├── services ├── EmbedService.php ├── GeoLocationService.php ├── GeoService.php ├── MapService.php ├── StaticService.php └── What3WordsService.php ├── templates ├── _feedme-mapping.twig ├── field-settings.twig └── settings.twig ├── translations └── en │ └── simplemap.php ├── utilities └── StaticMap.php └── web ├── Variable.php └── assets ├── MapAsset.php ├── imgs ├── carto-dark_all-1x.jpg ├── carto-dark_all.jpg ├── carto-light_all-1x.jpg ├── carto-light_all.jpg ├── carto-rastertiles-voyager-1x.jpg ├── carto-rastertiles-voyager.jpg ├── google-hybrid-1x.jpg ├── google-hybrid.jpg ├── google-roadmap-1x.jpg ├── google-roadmap.jpg ├── google-terrain-1x.jpg ├── google-terrain.jpg ├── here-hybrid-day-1x.jpg ├── here-hybrid-day.jpg ├── here-normal-day-1x.jpg ├── here-normal-day-grey-1x.jpg ├── here-normal-day-grey.jpg ├── here-normal-day-transit-1x.jpg ├── here-normal-day-transit.jpg ├── here-normal-day.jpg ├── here-pedestrian-day-1x.jpg ├── here-pedestrian-day.jpg ├── here-reduced-day-1x.jpg ├── here-reduced-day.jpg ├── here-satellite-day-1x.jpg ├── here-satellite-day.jpg ├── here-terrain-day-1x.jpg ├── here-terrain-day.jpg ├── mapbox-dark-1x.jpg ├── mapbox-dark.jpg ├── mapbox-light-1x.jpg ├── mapbox-light.jpg ├── mapbox-outdoors-1x.jpg ├── mapbox-outdoors.jpg ├── mapbox-streets-1x.jpg ├── mapbox-streets.jpg ├── mapkit-hybrid-1x.jpg ├── mapkit-hybrid.jpg ├── mapkit-muted-1x.jpg ├── mapkit-muted.jpg ├── mapkit-satellite-1x.jpg ├── mapkit-satellite.jpg ├── mapkit-standard-1x.jpg ├── mapkit-standard.jpg ├── openstreetmap-1x.jpg ├── openstreetmap.jpg ├── wikimedia-1x.jpg └── wikimedia.jpg ├── map ├── css │ ├── app.css │ └── chunk-vendors.css ├── index.html └── js │ ├── app.js │ ├── app.js.map │ ├── chunk-vendors.js │ └── chunk-vendors.js.map └── svgs ├── apple.svg ├── google.svg ├── here.svg ├── ipstack.svg ├── mapbox.svg ├── maxmind.svg └── w3w.svg /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Don't be a twat. -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### New Features 4 | 5 | You can request new features by submitting an issue with `[FR]` prefix in the title. 6 | 7 | ### Bugs 8 | 9 | If you find a bug, submit an issue and I promise I will get around to it. Eventually. 10 | Make sure you search around to see if the same (or similar) issue exists before 11 | submitting a new one. 12 | 13 | Know how to fix a bug and have too much spare time on your hands? Submit a Pull Request! 14 | Check out the code conventions below before hand. 15 | 16 | ## Code Conventions 17 | 18 | In addition to [Crafts Guidelines](https://github.com/craftcms/docs/blob/v3/en/coding-guidelines.md): 19 | 20 | - Use tabs not spaces 21 | - Try to keep within the 80 characters line length 22 | - If function arguments or array values can't fit on one line, break each value onto it's own line 23 | - Comment as much as possible 24 | 25 | #### JavaScript 26 | 27 | In addition to the above: 28 | 29 | - Write valid ES6 30 | - Use single quotes `'` 31 | 32 | Don't follow my example, try to stick to the above guidelines! 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [tam] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | ### Steps to reproduce 6 | 7 | 1. 8 | 2. 9 | 10 | 11 | ### Additional info 12 | 13 | - Craft version: 14 | - Maps version: 15 | - PHP version: 16 | - Database driver & version: 17 | - Other Plugins: 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/craft-3-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Craft 3 Issue 3 | about: An issue with the Craft 3 version of the plugin 4 | title: '' 5 | labels: Craft 3 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/craft-4-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Craft 4 Issue 3 | about: An issue with the Craft 4 version of the plugin 4 | title: '' 5 | labels: Craft 4 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # . 2 | 3 | Changes proposed in this pull request: 4 | 5 | - 6 | - 7 | - -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © Ether Creative 2 | 3 | Permission is hereby granted to any person obtaining a copy of this software 4 | (the “Software”) to use, copy, modify, merge, publish and/or distribute copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | 1. **Don’t plagiarize.** The above copyright notice and this license shall be 9 | included in all copies or substantial portions of the Software. 10 | 11 | 2. **Don’t use the same license on more than one project.** Each licensed copy 12 | of the Software shall be actively installed in no more than one production 13 | environment at a time. 14 | 15 | 3. **Don’t mess with the licensing features.** Software features related to 16 | licensing shall not be altered or circumvented in any way, including (but 17 | not limited to) license validation, payment prompts, feature restrictions, 18 | and update eligibility. 19 | 20 | 4. **Pay up.** Payment shall be made immediately upon receipt of any notice, 21 | prompt, reminder, or other message indicating that a payment is owed. 22 | 23 | 5. **Follow the law.** All use of the Software shall not violate any applicable 24 | law or regulation, nor infringe the rights of any other person or entity. 25 | 26 | Failure to comply with the foregoing conditions will automatically and 27 | immediately result in termination of the permission granted hereby. This 28 | license does not include any right to receive updates to the Software or 29 | technical support. Licensees bear all risk related to the quality and 30 | performance of the Software and any modifications made or obtained to it, 31 | including liability for actual and consequential harm, such as loss or 32 | corruption of data, and any necessary service, repair, or correction. 33 | 34 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 38 | LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN 39 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ether/simplemap", 3 | "description": "A beautifully simple Map field type for Craft CMS", 4 | "type": "craft-plugin", 5 | "license": "proprietary", 6 | "minimum-stability": "dev", 7 | "require": { 8 | "craftcms/cms": "^5.0.0", 9 | "mapkit/jwt": "^1.1.2", 10 | "geoip2/geoip2": "~2.0", 11 | "guzzlehttp/guzzle": "^6.3.3|^7.2.0", 12 | "what3words/w3w-php-wrapper": "3.*", 13 | "ext-openssl": "*", 14 | "ext-json": "*", 15 | "ext-zlib": "*", 16 | "php": "^8.2" 17 | }, 18 | "require-dev": { 19 | "craftcms/wp-import": "dev-main" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "ether\\simplemap\\": "src/" 24 | } 25 | }, 26 | "support": { 27 | "email": "help@ethercreative.co.uk", 28 | "docs": "https://docs.ethercreative.co.uk/maps", 29 | "source": "https://github.com/ethercreative/simplemap", 30 | "issues": "https://github.com/ethercreative/simplemap/issues" 31 | }, 32 | "extra": { 33 | "handle": "simplemap", 34 | "name": "Maps", 35 | "developer": "Ether Creative", 36 | "developerUrl": "https://ethercreative.co.uk", 37 | 38 | "class": "ether\\simplemap\\SimpleMap", 39 | "schemaVersion": "3.4.2" 40 | }, 41 | "config": { 42 | "allow-plugins": { 43 | "yiisoft/yii2-composer": true, 44 | "craftcms/plugin-installer": true 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SimpleMap.php: -------------------------------------------------------------------------------- 1 | setComponents([ 89 | 'map' => MapService::class, 90 | 'static' => StaticService::class, 91 | 'embed' => EmbedService::class, 92 | 'geolocation' => GeoLocationService::class, 93 | ]); 94 | 95 | Event::on( 96 | UrlManager::class, 97 | UrlManager::EVENT_REGISTER_CP_URL_RULES, 98 | [$this, 'onRegisterCPUrlRules'] 99 | ); 100 | 101 | Event::on( 102 | Fields::class, 103 | Fields::EVENT_REGISTER_FIELD_TYPES, 104 | [$this, 'onRegisterFieldTypes'] 105 | ); 106 | 107 | Event::on( 108 | CraftVariable::class, 109 | CraftVariable::EVENT_INIT, 110 | [$this, 'onRegisterVariable'] 111 | ); 112 | 113 | if (class_exists(Gql::class)) 114 | { 115 | Event::on( 116 | Gql::class, 117 | Gql::EVENT_REGISTER_GQL_TYPES, 118 | [$this, 'onRegisterGqlTypes'] 119 | ); 120 | } 121 | 122 | if (class_exists(\craft\feedme\Plugin::class)) 123 | { 124 | Event::on( 125 | \craft\feedme\services\Fields::class, 126 | \craft\feedme\services\Fields::EVENT_REGISTER_FEED_ME_FIELDS, 127 | [$this, 'onRegisterFeedMeFields'] 128 | ); 129 | } 130 | 131 | $request = Craft::$app->getRequest(); 132 | if ( 133 | !$request->getIsConsoleRequest() 134 | && $request->getMethod() === 'GET' 135 | && $request->getIsSiteRequest() 136 | && !$request->getIsPreview() 137 | && !$request->getIsActionRequest() 138 | ) { 139 | Event::on( 140 | Application::class, 141 | Application::EVENT_INIT, 142 | [$this, 'onApplicationInit'] 143 | ); 144 | } 145 | 146 | if (class_exists(WpImportCommand::class)) { 147 | Event::on( 148 | WpImportCommand::class, 149 | WpImportCommand::EVENT_REGISTER_ACF_ADAPTERS, 150 | static function (RegisterComponentTypesEvent $event) { 151 | $event->types[] = GoogleMapAcfAdapter::class; 152 | } 153 | ); 154 | } 155 | } 156 | 157 | protected function beforeUninstall (): void 158 | { 159 | if ($this->getSettings()->geoLocationService === GeoLocationService::MaxMindLite) 160 | GeoLocationService::purgeDb(); 161 | } 162 | 163 | // Settings 164 | // ========================================================================= 165 | 166 | protected function createSettingsModel (): Model 167 | { 168 | return new Settings(); 169 | } 170 | 171 | /** 172 | * @return Model 173 | */ 174 | public function getSettings (): Model 175 | { 176 | return parent::getSettings(); 177 | } 178 | 179 | protected function settingsHtml (): ?string 180 | { 181 | // Redirect to our settings page 182 | Craft::$app->controller->redirect( 183 | UrlHelper::cpUrl('maps/settings') 184 | ); 185 | 186 | return null; 187 | } 188 | 189 | public function afterSaveSettings (): void 190 | { 191 | parent::afterSaveSettings(); 192 | 193 | $service = $this->getSettings()->geoLocationService; 194 | 195 | if ($service !== GeoLocationService::MaxMindLite) 196 | GeoLocationService::purgeDb(); 197 | else if (!GeoLocationService::dbExists()) 198 | GeoLocationService::dbQueueDownload(); 199 | } 200 | 201 | // Events 202 | // ========================================================================= 203 | 204 | public function onRegisterCPUrlRules (RegisterUrlRulesEvent $event) 205 | { 206 | $event->rules['maps/settings'] = 'simplemap/settings'; 207 | } 208 | 209 | public function onRegisterFieldTypes (RegisterComponentTypesEvent $event) 210 | { 211 | $event->types[] = MapField::class; 212 | } 213 | 214 | /** 215 | * @param Event $event 216 | * 217 | * @throws InvalidConfigException 218 | */ 219 | public function onRegisterVariable (Event $event) 220 | { 221 | /** @var CraftVariable $variable */ 222 | $variable = $event->sender; 223 | $variable->set('simpleMap', Variable::class); 224 | $variable->set('maps', Variable::class); 225 | } 226 | 227 | public function onRegisterFeedMeFields (\craft\feedme\events\RegisterFeedMeFieldsEvent $event) 228 | { 229 | $event->fields[] = FeedMeMaps::class; 230 | } 231 | 232 | public function onRegisterGqlTypes (RegisterGqlTypesEvent $event) 233 | { 234 | $event->types[] = MapType::class; 235 | $event->types[] = MapPartsType::class; 236 | } 237 | 238 | /** 239 | * @throws Exception 240 | */ 241 | public function onApplicationInit () 242 | { 243 | if ($this->getSettings()->geoLocationAutoRedirect) 244 | $this->geolocation->redirect(); 245 | } 246 | 247 | // Helpers 248 | // ========================================================================= 249 | 250 | public static function t ($message, $params = []): string 251 | { 252 | return Craft::t('simplemap', $message, $params); 253 | } 254 | 255 | public static function v ($version, $operator = '='): bool 256 | { 257 | return SimpleMap::getInstance()->is($version, $operator); 258 | } 259 | 260 | } 261 | -------------------------------------------------------------------------------- /src/acfadapters/GoogleMap.php: -------------------------------------------------------------------------------- 1 | lat = $data['center_lat']; 34 | } 35 | if ($data['center_lng']) { 36 | $field->lng = $data['center_lng']; 37 | } 38 | if ($data['zoom']) { 39 | $field->zoom = $data['zoom']; 40 | } 41 | return $field; 42 | } 43 | 44 | public function normalizeValue(mixed $value, array $data): mixed 45 | { 46 | return [ 47 | 'address' => $value['address'], 48 | 'lat' => $value['lat'], 49 | 'lng' => $value['lng'], 50 | 'zoom' => $value['zoom'], 51 | 'parts' => [ 52 | 'number' => $value['street_number'], 53 | 'address' => $value['street_name'], 54 | 'city' => $value['city'], 55 | 'postcode' => $value['post_code'], 56 | 'state' => $value['state'], 57 | 'country' => $value['country'], 58 | 'administrative_area_level_1' => $value['state'], 59 | 'locality' => $value['city'], 60 | 'postal_code' => $value['post_code'], 61 | 'route' => $value['street_name'], 62 | 'street_number' => $value['street_number'], 63 | ], 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | MapTiles::Wikimedia, 8 | 'mapToken' => '', 9 | 10 | 'geoService' => GeoService::Nominatim, 11 | 'geoToken' => '', 12 | 13 | 'w3wToken' => '', 14 | ]; 15 | -------------------------------------------------------------------------------- /src/controllers/SettingsController.php: -------------------------------------------------------------------------------- 1 | renderTemplate( 30 | 'simplemap/settings', 31 | [ 32 | 'isLite' => SimpleMap::v(SimpleMap::EDITION_LITE), 33 | 'settings' => SimpleMap::getInstance()->getSettings(), 34 | 'mapTileOptions' => MapTiles::getSelectOptions(), 35 | 'geoServiceOptions' => GeoService::getSelectOptions(), 36 | 'geoLocationOptions' => GeoLocationService::getSelectOptions(), 37 | ] 38 | ); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/controllers/StaticController.php: -------------------------------------------------------------------------------- 1 | getRequest(); 36 | 37 | if (!$request->validateCsrfToken($request->getRequiredQueryParam('csrf'))) 38 | throw new BadRequestHttpException(Yii::t('yii', 'Unable to verify your data submission.')); 39 | 40 | return (new StaticMap( 41 | $request->getRequiredQueryParam('lat'), 42 | $request->getRequiredQueryParam('lng'), 43 | $request->getRequiredQueryParam('width'), 44 | $request->getRequiredQueryParam('height'), 45 | $request->getRequiredQueryParam('zoom'), 46 | $request->getRequiredQueryParam('scale'), 47 | $request->getQueryParam('markers') 48 | ))->render(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/enums/GeoService.php: -------------------------------------------------------------------------------- 1 | SimpleMap::t('Open Source') ], 51 | 52 | self::Nominatim => SimpleMap::t('Nominatim'), 53 | 54 | [ 'optgroup' => SimpleMap::t('Requires API Key (Token)') ], 55 | 56 | self::GoogleMaps => SimpleMap::t('Google Maps'), 57 | self::Mapbox => MapTiles::pro('Mapbox', $isLite), 58 | 59 | // MapKit lacks both separate address parts and country restriction 60 | // on the front-end, and any sort of server-side API, so it's 61 | // disabled for now. 62 | // self::AppleMapKit => MapTiles::pro('Apple MapKit', $isLite), 63 | 64 | self::Here => MapTiles::pro('Here', $isLite), 65 | ]; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/enums/MapTiles.php: -------------------------------------------------------------------------------- 1 | SimpleMap::t('Open Source')], 85 | 86 | // self::Wikimedia => SimpleMap::t('Wikimedia'), 87 | 88 | self::OpenStreetMap => SimpleMap::t('OpenStreetMap'), 89 | 90 | self::CartoVoyager => SimpleMap::t('Carto: Voyager'), 91 | self::CartoPositron => SimpleMap::t('Carto: Positron'), 92 | self::CartoDarkMatter => SimpleMap::t('Carto: Dark Matter'), 93 | 94 | ['optgroup' => SimpleMap::t('Requires API Key (Token)')], 95 | 96 | self::GoogleRoadmap => SimpleMap::t('Google Maps: Roadmap'), 97 | self::GoogleTerrain => SimpleMap::t('Google Maps: Terrain'), 98 | self::GoogleHybrid => SimpleMap::t('Google Maps: Hybrid'), 99 | 100 | self::MapboxOutdoors => self::pro('Mapbox: Outdoors', $isLite), 101 | self::MapboxStreets => self::pro('Mapbox: Streets', $isLite), 102 | self::MapboxLight => self::pro('Mapbox: Light', $isLite), 103 | self::MapboxDark => self::pro('Mapbox: Dark', $isLite), 104 | 105 | self::MapKitStandard => self::pro('Apple MapKit: Standard', $isLite), 106 | self::MapKitMutedStandard => self::pro('Apple MapKit: Muted Standard', $isLite), 107 | self::MapKitSatellite => self::pro('Apple MapKit: Satellite', $isLite), 108 | self::MapKitHybrid => self::pro('Apple MapKit: Hybrid', $isLite), 109 | 110 | self::HereNormalDay => self::pro('Here: Normal Day', $isLite), 111 | self::HereNormalDayGrey => self::pro('Here: Normal Day Grey', $isLite), 112 | self::HereNormalDayTransit => self::pro('Here: Normal Day Transit', $isLite), 113 | self::HereReduced => self::pro('Here: Reduced', $isLite), 114 | self::HerePedestrian => self::pro('Here: Pedestrian', $isLite), 115 | self::HereTerrain => self::pro('Here: Terrain', $isLite), 116 | self::HereSatellite => self::pro('Here: Satellite', $isLite), 117 | self::HereHybrid => self::pro('Here: Hybrid', $isLite), 118 | ]; 119 | } 120 | 121 | /** 122 | * Get the tiles url for the given type and scale 123 | * 124 | * @param string $type 125 | * @param int $scale 126 | * 127 | * @return array 128 | * @throws Exception 129 | */ 130 | public static function getTiles (string $type, int $scale = 1): array 131 | { 132 | $scale = $scale == 1 ? '.png' : '@2x.png'; 133 | $style = str_contains($type, '.') ? explode('.', $type, 2)[1] : ''; 134 | 135 | switch ($type) 136 | { 137 | case self::Wikimedia: 138 | return [ 139 | 'url' => 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}' . $scale, 140 | 'size' => 512, 141 | ]; 142 | case self::OpenStreetMap: 143 | return [ 144 | 'url' => 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', 145 | 'size' => 256, 146 | ]; 147 | case self::CartoVoyager: 148 | case self::CartoPositron: 149 | case self::CartoDarkMatter: 150 | return [ 151 | 'url' => 'https://a.basemaps.cartocdn.com/' . $style . '/{z}/{x}/{y}' . $scale, 152 | 'size' => 256, 153 | ]; 154 | } 155 | 156 | throw new Exception('Unknown tile type "' . $type . '"'); 157 | } 158 | 159 | // Helpers 160 | // ========================================================================= 161 | 162 | public static function pro ($label, $isLite): array 163 | { 164 | return [ 165 | 'label' => SimpleMap::t($label) . ($isLite ? ' (Pro)' : ''), 166 | 'disabled' => $isLite, 167 | ]; 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/integrations/feedme/FeedMeMaps.php: -------------------------------------------------------------------------------- 1 | fieldInfo, 'fields'); 48 | 49 | if (!$fields) 50 | return null; 51 | 52 | foreach ($fields as $subFieldHandle => $subFieldInfo) 53 | { 54 | if ($subFieldHandle === 'parts') { 55 | foreach ($subFieldInfo as $handle => $info) 56 | $preppedData[$subFieldHandle][$handle] = DataHelper::fetchValue( 57 | $this->feedData, 58 | $info 59 | ); 60 | 61 | continue; 62 | } 63 | 64 | $preppedData[$subFieldHandle] = DataHelper::fetchValue( 65 | $this->feedData, 66 | $subFieldInfo 67 | ); 68 | } 69 | 70 | if (isset($preppedData['parts'])) 71 | $preppedData['parts'] = new Parts($preppedData['parts']); 72 | 73 | if (!$preppedData) 74 | return null; 75 | 76 | return new Map($preppedData); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/integrations/graphql/MapPartsType.php: -------------------------------------------------------------------------------- 1 | [ 35 | 'name' => 'number', 36 | 'type' => Type::string(), 37 | 'description' => 'The address name / number.', 38 | ], 39 | 'address' => [ 40 | 'name' => 'address', 41 | 'type' => Type::string(), 42 | 'description' => 'The street address.', 43 | ], 44 | 'city' => [ 45 | 'name' => 'city', 46 | 'type' => Type::string(), 47 | 'description' => 'The city.', 48 | ], 49 | 'postcode' => [ 50 | 'name' => 'postcode', 51 | 'type' => Type::string(), 52 | 'description' => 'The postal code.', 53 | ], 54 | 'county' => [ 55 | 'name' => 'county', 56 | 'type' => Type::string(), 57 | 'description' => 'The county.', 58 | ], 59 | 'state' => [ 60 | 'name' => 'state', 61 | 'type' => Type::string(), 62 | 'description' => 'The state.', 63 | ], 64 | 'country' => [ 65 | 'name' => 'country', 66 | 'type' => Type::string(), 67 | 'description' => 'The country.', 68 | ], 69 | ]; 70 | } 71 | 72 | public static function getType (): Type 73 | { 74 | if ($type = GqlEntityRegistry::getEntity(static::class)) 75 | return $type; 76 | 77 | return GqlEntityRegistry::createEntity( 78 | static::class, 79 | new ObjectType( 80 | [ 81 | 'name' => static::getName(), 82 | 'fields' => static::class . '::getFieldDefinitions', 83 | ] 84 | ) 85 | ); 86 | } 87 | 88 | public static function getInputType (): InputType 89 | { 90 | $name = static::class . 'Input'; 91 | 92 | if ($type = GqlEntityRegistry::getEntity($name)) 93 | return $type; 94 | 95 | return GqlEntityRegistry::createEntity( 96 | $name, 97 | new InputObjectType([ 98 | 'name' => static::getName() . 'Input', 99 | 'fields' => static::class . '::getFieldDefinitions', 100 | ]) 101 | ); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/integrations/graphql/MapType.php: -------------------------------------------------------------------------------- 1 | [ 36 | 'name' => 'lat', 37 | 'type' => Type::float(), 38 | 'description' => 'The maps latitude.', 39 | ], 40 | 'lng' => [ 41 | 'name' => 'lng', 42 | 'type' => Type::float(), 43 | 'description' => 'The maps longitude.', 44 | ], 45 | 'zoom' => [ 46 | 'name' => 'zoom', 47 | 'type' => Type::int(), 48 | 'description' => 'The maps zoom level.', 49 | ], 50 | 'distance' => [ 51 | 'name' => 'distance', 52 | 'type' => Type::float(), 53 | 'description' => 'The distance to this location.', 54 | ], 55 | 'address' => [ 56 | 'name' => 'address', 57 | 'type' => Type::string(), 58 | 'description' => 'The full address.', 59 | ], 60 | 'parts' => [ 61 | 'name' => 'parts', 62 | 'type' => MapPartsType::getType(), 63 | 'description' => 'The maps address parts.', 64 | ], 65 | ]; 66 | } 67 | 68 | public static function getInputDefinitions (): array 69 | { 70 | $fields = static::getFieldDefinitions(); 71 | 72 | unset($fields['distance']); 73 | $fields['parts']['type'] = MapPartsType::getInputType(); 74 | 75 | return $fields; 76 | } 77 | 78 | public static function getQueryInputDefinitions (): array 79 | { 80 | return [ 81 | 'coordinate' => [ 82 | 'name' => 'coordinate', 83 | 'type' => static::getCoordsType(), 84 | ], 85 | 'location' => [ 86 | 'name' => 'location', 87 | 'type' => Type::string(), 88 | ], 89 | 'country' => [ 90 | 'name' => 'country', 91 | 'type' => Type::string(), 92 | ], 93 | 'radius' => [ 94 | 'name' => 'radius', 95 | 'type' => Type::float(), 96 | ], 97 | 'unit' => [ 98 | 'name' => 'unit', 99 | 'type' => static::getUnitType(), 100 | ], 101 | ]; 102 | } 103 | 104 | public static function getType (): Type 105 | { 106 | if ($type = GqlEntityRegistry::getEntity(static::getName())) 107 | return $type; 108 | 109 | return GqlEntityRegistry::createEntity( 110 | static::getName(), 111 | new ObjectType([ 112 | 'name' => static::getName(), 113 | 'fields' => static::class . '::getFieldDefinitions', 114 | ]) 115 | ); 116 | } 117 | 118 | public static function getInputType (): InputType 119 | { 120 | $name = static::getName() . 'Input'; 121 | 122 | if ($type = GqlEntityRegistry::getEntity($name)) 123 | return $type; 124 | 125 | return GqlEntityRegistry::createEntity( 126 | $name, 127 | new InputObjectType([ 128 | 'name' => static::getName() . 'Input', 129 | 'fields' => static::class . '::getInputDefinitions', 130 | ]) 131 | ); 132 | } 133 | 134 | public static function getQueryType (): InputType 135 | { 136 | $name = static::getName() . 'Query'; 137 | 138 | if ($type = GqlEntityRegistry::getEntity($name)) 139 | return $type; 140 | 141 | return GqlEntityRegistry::createEntity( 142 | $name, 143 | new InputObjectType([ 144 | 'name' => static::getName() . 'Query', 145 | 'fields' => static::class . '::getQueryInputDefinitions', 146 | ]) 147 | ); 148 | } 149 | 150 | public static function getCoordsType (): InputType 151 | { 152 | $name = static::getName() . 'Coords'; 153 | 154 | if ($type = GqlEntityRegistry::getEntity($name)) 155 | return $type; 156 | 157 | return GqlEntityRegistry::createEntity( 158 | $name, 159 | new InputObjectType([ 160 | 'name' => static::getName() . 'Coords', 161 | 'fields' => [ 162 | 'lat' => [ 163 | 'name' => 'lat', 164 | 'type' => Type::nonNull(Type::float()), 165 | ], 166 | 'lng' => [ 167 | 'name' => 'lng', 168 | 'type' => Type::nonNull(Type::float()), 169 | ], 170 | ], 171 | ]) 172 | ); 173 | } 174 | 175 | public static function getUnitType (): EnumType 176 | { 177 | $name = static::getName() . 'Unit'; 178 | 179 | if ($type = GqlEntityRegistry::getEntity($name)) 180 | return $type; 181 | 182 | return GqlEntityRegistry::createEntity( 183 | $name, 184 | new EnumType([ 185 | 'name' => static::getName() . 'Unit', 186 | 'values' => [ 187 | 'Miles' => 'mi', 188 | 'Kilometres' => 'km', 189 | ], 190 | ]) 191 | ); 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /src/jobs/MaxMindDBDownloadJob.php: -------------------------------------------------------------------------------- 1 | get( 49 | 'https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz', 50 | [ 51 | 'save_to' => $temp, 52 | 'progress' => function ($total, $current) use ($queue) { 53 | if ($total > 0) 54 | $queue->setProgress(($current / $total) * 100); 55 | }, 56 | ] 57 | ); 58 | 59 | $extracted = ''; 60 | $gz = gzopen($temp, 'r'); 61 | 62 | while ($data = gzread($gz, 10000000)) 63 | $extracted .= $data; 64 | 65 | gzclose($gz); 66 | 67 | if (!file_exists($target)) 68 | FileHelper::createDirectory(pathinfo($target)['dirname']); 69 | 70 | $saved = file_put_contents($target, $extracted); 71 | @unlink($temp); 72 | 73 | if (!$saved) 74 | Craft::error('Unable to save MaxMind DB!', 'maps'); 75 | 76 | Craft::$app->getCache()->delete('maps_db_updating'); 77 | } catch (Exception $e) { 78 | Craft::$app->getCache()->delete('maps_db_updating'); 79 | throw $e; 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/migrations/Install.php: -------------------------------------------------------------------------------- 1 | createTable( 29 | Map::TableName, 30 | [ 31 | 'id' => $this->primaryKey(), 32 | 'ownerId' => $this->integer()->notNull(), 33 | 'ownerSiteId' => $this->integer(), 34 | 'fieldId' => $this->integer()->notNull(), 35 | 36 | 'lat' => $this->decimal(11, 9), 37 | 'lng' => $this->decimal(12, 9), 38 | 39 | 'dateCreated' => $this->dateTime()->notNull(), 40 | 'dateUpdated' => $this->dateTime()->notNull(), 41 | 'uid' => $this->uid()->notNull(), 42 | ] 43 | ); 44 | 45 | // Indexes 46 | 47 | $this->createIndex( 48 | null, 49 | Map::TableName, 50 | ['ownerId', 'ownerSiteId', 'fieldId'], 51 | true 52 | ); 53 | 54 | $this->createIndex( 55 | null, 56 | Map::TableName, 57 | ['lat'] 58 | ); 59 | 60 | $this->createIndex( 61 | null, 62 | Map::TableName, 63 | ['lng'] 64 | ); 65 | 66 | // Relations 67 | 68 | $this->addForeignKey( 69 | null, 70 | Map::TableName, 71 | ['ownerId'], 72 | Table::ELEMENTS, 73 | ['id'], 74 | 'CASCADE' 75 | ); 76 | 77 | $this->addForeignKey( 78 | null, 79 | Map::TableName, 80 | ['ownerSiteId'], 81 | Table::SITES, 82 | ['id'], 83 | 'CASCADE', 84 | 'CASCADE' 85 | ); 86 | 87 | $this->addForeignKey( 88 | null, 89 | Map::TableName, 90 | ['fieldId'], 91 | Table::FIELDS, 92 | ['id'], 93 | 'CASCADE' 94 | ); 95 | 96 | // Upgrade from Craft 2 97 | if ($this->db->tableExists('{{%simplemap_maps}}')) 98 | (new m190226_143809_craft3_upgrade())->safeUp(); 99 | } 100 | 101 | public function safeUpPre34 () 102 | { 103 | // Create 104 | 105 | $this->createTable( 106 | Map::TableName, 107 | [ 108 | 'id' => $this->primaryKey(), 109 | 'ownerId' => $this->integer()->notNull(), 110 | 'ownerSiteId' => $this->integer(), 111 | 'fieldId' => $this->integer()->notNull(), 112 | 113 | 'lat' => $this->decimal(11, 9), 114 | 'lng' => $this->decimal(12, 9), 115 | 'zoom' => $this->integer(2), 116 | 'address' => $this->string(255), 117 | 'parts' => $this->text(), 118 | 119 | 'dateCreated' => $this->dateTime()->notNull(), 120 | 'dateUpdated' => $this->dateTime()->notNull(), 121 | 'uid' => $this->uid()->notNull(), 122 | ] 123 | ); 124 | 125 | // Indexes 126 | 127 | $this->createIndex( 128 | null, 129 | Map::TableName, 130 | ['ownerId', 'ownerSiteId', 'fieldId'], 131 | true 132 | ); 133 | 134 | $this->createIndex( 135 | null, 136 | Map::TableName, 137 | ['lat'] 138 | ); 139 | 140 | $this->createIndex( 141 | null, 142 | Map::TableName, 143 | ['lng'] 144 | ); 145 | 146 | } 147 | 148 | public function safeDown () 149 | { 150 | $this->dropTableIfExists(Map::TableName); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/migrations/m190226_143809_craft3_upgrade.php: -------------------------------------------------------------------------------- 1 | db->tableExists(MapRecord::OldTableName) && !$this->db->tableExists(MapRecord::TableName)) 44 | (new Install())->safeUp(); 45 | 46 | // 2. Upgrade the data 47 | if ($this->db->tableExists('{{%simplemap_maps}}')) 48 | $this->_upgrade2(); 49 | else 50 | $this->_upgrade3(); 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function safeDown() 57 | { 58 | echo "m190226_143809_craft3_upgrade cannot be reverted.\n"; 59 | return false; 60 | } 61 | 62 | /** 63 | * Upgrade from Craft 2 64 | * 65 | * @throws \Throwable 66 | * @throws \yii\base\Exception 67 | * @throws \yii\db\Exception 68 | */ 69 | private function _upgrade2 () 70 | { 71 | $mapService = SimpleMap::getInstance()->map; 72 | $fieldsService = Craft::$app->getFields(); 73 | 74 | // Delete the old plugin row 75 | $this->delete(Table::PLUGINS, ['handle' => 'simple-map']); 76 | 77 | // Update the old data 78 | echo ' > Start map data upgrade' . PHP_EOL; 79 | 80 | $rows = (new Query()) 81 | ->select('*') 82 | ->from('{{%simplemap_maps}}') 83 | ->all(); 84 | 85 | foreach ($rows as $row) 86 | { 87 | echo ' > Upgrade map value ' . $row['address'] . PHP_EOL; 88 | 89 | $site = $this->getSiteByLocale($row['ownerLocale']); 90 | 91 | $map = new Map(); 92 | $map->ownerId = $row['ownerId']; 93 | $map->ownerSiteId = $site->id; 94 | $map->fieldId = $row['fieldId']; 95 | $map->lat = $row['lat']; 96 | $map->lng = $row['lng']; 97 | 98 | $mapService->saveRecord($map, true); 99 | } 100 | 101 | $this->dropTable('{{%simplemap_maps}}'); 102 | 103 | // Update old field types 104 | echo ' > Upgrade map field type upgrade' . PHP_EOL; 105 | 106 | $fieldContexts = []; 107 | $fieldContextsData = (new \craft\db\Query()) 108 | ->select(['context']) 109 | ->from(['{{%fields}}']) 110 | ->all(); 111 | foreach ($fieldContextsData as $fieldData) { 112 | $fieldContexts[] = $fieldData['context']; 113 | } 114 | $fieldContexts = array_unique($fieldContexts); 115 | 116 | $fields = $fieldsService->getAllFields($fieldContexts); 117 | foreach ($fields as $field) 118 | { 119 | if ($field instanceof \craft\fields\MissingField && $field->expectedType === 'SimpleMap_Map') { 120 | echo ' > Upgrade map field ' . $field->handle . PHP_EOL; 121 | 122 | $oldSettings = Json::decodeIfJson($field->settings); 123 | 124 | $newField = new MapField([ 125 | 'id' => $field->id, 126 | 'groupId' => $field->groupId, 127 | 'name' => $field->name, 128 | 'handle' => $field->handle, 129 | 'instructions' => $field->instructions, 130 | 'searchable' => $field->searchable, 131 | 'translationMethod' => $field->translationMethod, 132 | 'translationKeyFormat' => $field->translationKeyFormat, 133 | 134 | 'lat' => $oldSettings['lat'], 135 | 'lng' => $oldSettings['lng'], 136 | 'zoom' => $oldSettings['zoom'] ?? 15, 137 | 'country' => strtoupper($oldSettings['countryRestriction'] ?? '') ?: null, 138 | 'hideMap' => $oldSettings['hideMap'], 139 | ]); 140 | 141 | $fieldsService->saveField($newField); 142 | } 143 | } 144 | 145 | // Update the plugin settings 146 | $this->updatePluginSettings(); 147 | } 148 | 149 | /** 150 | * Upgrade from SimpleMap (3.3.x) 151 | * 152 | * @throws \Throwable 153 | * @throws \yii\base\Exception 154 | */ 155 | private function _upgrade3 () 156 | { 157 | $mapService = SimpleMap::getInstance()->map; 158 | 159 | // 1. Store the old data 160 | echo ' > Start map data upgrade' . PHP_EOL; 161 | 162 | $rows = (new Query()) 163 | ->select([ 164 | 'ownerId', 165 | 'ownerSiteId', 166 | 'fieldId', 167 | 'lat', 168 | 'lng', 169 | 'zoom', 170 | 'address', 171 | 'parts', 172 | ]) 173 | ->from(MapRecord::OldTableName) 174 | ->all(); 175 | 176 | // 2. Re-create the table 177 | $this->dropTable(MapRecord::OldTableName); 178 | 179 | if (!$this->db->tableExists(MapRecord::TableName)) 180 | (new Install())->safeUpPre34(); 181 | 182 | // 3. Store the old data as new 183 | $dupeKeys = []; 184 | foreach ($rows as $row) 185 | { 186 | $key = $row['ownerId'] . '_' . $row['ownerSiteId'] . '_' . $row['fieldId']; 187 | 188 | if (in_array($key, $dupeKeys)) 189 | continue; 190 | 191 | $dupeKeys[] = $key; 192 | 193 | echo ' > Upgrade map value ' . $row['address'] . PHP_EOL; 194 | 195 | $map = new Map($row); 196 | $map->ownerId = $row['ownerId']; 197 | $map->ownerSiteId = $row['ownerSiteId']; 198 | $map->fieldId = $row['fieldId']; 199 | 200 | if (!$map->zoom) 201 | $map->zoom = 15; 202 | 203 | $mapService->saveRecord($map, true); 204 | } 205 | 206 | // 4. Update field settings 207 | echo ' > Upgrade map field type upgrade' . PHP_EOL; 208 | 209 | $rows = (new Query()) 210 | ->select(['id', 'settings', 'handle']) 211 | ->from(Table::FIELDS) 212 | ->where(['type' => MapField::class]) 213 | ->all(); 214 | 215 | foreach ($rows as $row) 216 | { 217 | echo ' > Upgrade map field ' . $row['handle'] . PHP_EOL; 218 | 219 | $id = $row['id']; 220 | $oldSettings = Json::decodeIfJson($row['settings']); 221 | 222 | $newSettings = [ 223 | 'lat' => $oldSettings['lat'], 224 | 'lng' => $oldSettings['lng'], 225 | 'zoom' => $oldSettings['zoom'] ?? 15, 226 | 'country' => strtoupper($oldSettings['countryRestriction']), 227 | 'hideMap' => $oldSettings['hideMap'], 228 | ]; 229 | 230 | $this->db->createCommand() 231 | ->update( 232 | Table::FIELDS, 233 | [ 'settings' => Json::encode($newSettings) ], 234 | compact('id') 235 | ) 236 | ->execute(); 237 | } 238 | 239 | $this->updatePluginSettings(); 240 | } 241 | 242 | // Helpers 243 | // ========================================================================= 244 | 245 | /** 246 | * Returns a site handle based on a given locale. 247 | * 248 | * @param string $locale 249 | * 250 | * @return string 251 | */ 252 | private function locale2handle (string $locale): string 253 | { 254 | if ( 255 | !preg_match('/^' . HandleValidator::$handlePattern . '$/', $locale) || 256 | in_array(strtolower($locale), HandleValidator::$baseReservedWords, true) 257 | ) { 258 | $localeParts = array_filter(preg_split('/[^a-zA-Z0-9]/', $locale)); 259 | 260 | return $localeParts ? '_' . implode('_', $localeParts) : ''; 261 | } 262 | 263 | return $locale; 264 | } 265 | 266 | /** 267 | * Gets the new site based off the old locale 268 | * 269 | * @param string $locale 270 | * 271 | * @return \craft\models\Site 272 | */ 273 | private function getSiteByLocale ($locale) 274 | { 275 | $sites = \Craft::$app->sites; 276 | 277 | if ($locale === null) 278 | return static::$sitesByOldLocale[$locale] = $sites->primarySite; 279 | 280 | if (array_key_exists($locale, static::$sitesByOldLocale)) 281 | return static::$sitesByOldLocale[$locale]; 282 | 283 | $handle = $this->locale2handle($locale); 284 | 285 | $siteId = (new Query()) 286 | ->select('id') 287 | ->from(Table::SITES) 288 | ->where(['like', 'handle', '%' . $handle]) 289 | ->column(); 290 | 291 | if (!empty($siteId)) 292 | return static::$sitesByOldLocale[$locale] = $sites->getSiteById($siteId[0]); 293 | 294 | return static::$sitesByOldLocale[$locale] = $sites->primarySite; 295 | } 296 | 297 | /** 298 | * Updates the plugins settings 299 | * @throws \craft\errors\InvalidPluginException 300 | */ 301 | private function updatePluginSettings () 302 | { 303 | echo ' > Upgrade Maps settings' . PHP_EOL; 304 | 305 | /** @var Settings $settings */ 306 | $settings = SimpleMap::getInstance()->getSettings()->toArray(); 307 | $newSettings = SimpleMap::getInstance()->getSettings()->toArray(); 308 | 309 | $craft2Settings = \Craft::$app->projectConfig->get( 310 | Plugins::CONFIG_PLUGINS_KEY . '.simple-map.settings' 311 | ); 312 | 313 | if (is_array($craft2Settings) && !empty($craft2Settings)) 314 | { 315 | $settings = [ 316 | 'apiKey' => @$craft2Settings['browserApiKey'] ?: '', 317 | 'unrestrictedApiKey' => @$craft2Settings['serverApiKey'] ?: '', 318 | ]; 319 | } 320 | 321 | if ($settings['unrestrictedApiKey']) 322 | { 323 | $newSettings['geoService'] = GeoService::GoogleMaps; 324 | $newSettings['geoToken'] = $settings['unrestrictedApiKey']; 325 | } 326 | 327 | if ($settings['apiKey']) 328 | { 329 | $newSettings['mapTiles'] = MapTiles::GoogleRoadmap; 330 | $newSettings['mapToken'] = $settings['apiKey']; 331 | 332 | if (!$settings['unrestrictedApiKey']) 333 | { 334 | $newSettings['geoService'] = GeoService::GoogleMaps; 335 | $newSettings['geoToken'] = $settings['apiKey']; 336 | } 337 | } 338 | 339 | \Craft::$app->plugins->savePluginSettings( 340 | SimpleMap::getInstance(), 341 | $newSettings 342 | ); 343 | 344 | \Craft::$app->plugins->enablePlugin(SimpleMap::getInstance()->handle); 345 | } 346 | 347 | } 348 | -------------------------------------------------------------------------------- /src/migrations/m190325_130533_repair_map_elements.php: -------------------------------------------------------------------------------- 1 | db->columnExists(Map::TableName, 'elementId')) 25 | return true; 26 | 27 | echo ' > Start map data fix' . PHP_EOL; 28 | 29 | $rows = (new Query()) 30 | ->select('*') 31 | ->from(Map::TableName) 32 | ->orderBy('dateUpdated DESC') 33 | ->all(); 34 | 35 | $validMapElementIds = (new Query()) 36 | ->select('id') 37 | ->from(Table::ELEMENTS) 38 | ->where(['=', 'type', MapElement::class]) 39 | ->column(); 40 | 41 | $this->dropTable(Map::TableName); 42 | (new Install())->safeUp(); 43 | 44 | $updatedElementIds = []; 45 | 46 | foreach ($rows as $row) 47 | { 48 | // Skip any rows that don't have a matching element 49 | if (!in_array($row['elementId'], $validMapElementIds)) 50 | continue; 51 | 52 | // Skip and duplicate elements 53 | if (in_array($row['elementId'], $updatedElementIds)) 54 | continue; 55 | 56 | echo ' > Fix map value ' . $row['address'] . PHP_EOL; 57 | 58 | $record = new Map(); 59 | $record->id = $row['elementId']; 60 | $record->ownerId = $row['ownerId']; 61 | $record->ownerSiteId = $row['ownerSiteId']; 62 | $record->fieldId = $row['fieldId']; 63 | 64 | $record->lat = $row['lat']; 65 | $record->lng = $row['lng']; 66 | $record->zoom = $row['zoom']; 67 | $record->address = $row['address']; 68 | $record->parts = $row['parts']; 69 | 70 | $record->save(false); 71 | 72 | $updatedElementIds[] = $record->id; 73 | } 74 | 75 | return true; 76 | } 77 | 78 | /** 79 | * @inheritdoc 80 | */ 81 | public function safeDown() 82 | { 83 | echo "m190325_130533_repair_map_elements cannot be reverted.\n"; 84 | return false; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/migrations/m190712_104805_new_data_format.php: -------------------------------------------------------------------------------- 1 | getDb(); 31 | $mapService = SimpleMap::getInstance()->map; 32 | $matrixService = Craft::$app->getMatrix(); 33 | $superTableService = null; 34 | $hasSuperTable = class_exists(SuperTableField::class); 35 | 36 | // 1. Add content columns 37 | // --------------------------------------------------------------------- 38 | 39 | echo '1. Creating Maps content columns' . PHP_EOL; 40 | 41 | $matrixFields = []; 42 | $superTableFields = []; 43 | 44 | $fields = array_reduce( 45 | Craft::$app->getFields()->getAllFields(), 46 | function ($carry, FieldInterface $field) use ($hasSuperTable, &$matrixFields, &$superTableFields) { 47 | if ($field instanceof MapField) 48 | $carry[$field->id] = $field; 49 | 50 | elseif ($field instanceof Matrix) 51 | $matrixFields[] = $field; 52 | 53 | elseif ($hasSuperTable && $field instanceof SuperTableField) 54 | $superTableFields[] = $field; 55 | 56 | return $carry; 57 | }, 58 | [] 59 | ); 60 | 61 | $matrixMapFields = []; 62 | $superTableMapFields = []; 63 | 64 | $matrixMapFields = array_merge( 65 | $matrixMapFields, 66 | $this->_reduceMatrixFields( 67 | $matrixFields, 68 | $hasSuperTable, 69 | $matrixMapFields, 70 | $superTableMapFields 71 | ) 72 | ); 73 | 74 | $superTableMapFields = array_merge( 75 | $superTableMapFields, 76 | $this->_reduceSuperTableFields( 77 | $superTableFields, 78 | $matrixMapFields, 79 | $superTableMapFields 80 | ) 81 | ); 82 | 83 | $fieldIdToMatrixBlockHandle = []; 84 | 85 | if (!empty($matrixMapFields)) 86 | { 87 | foreach ($matrixFields as $field) 88 | { 89 | $blockTypes = $matrixService->getBlockTypesByFieldId($field->id); 90 | 91 | foreach ($blockTypes as $blockType) 92 | foreach ($blockType->getFields() as $field) 93 | $fieldIdToMatrixBlockHandle[$field->id] = $blockType->handle; 94 | } 95 | } 96 | 97 | $columnType = (new MapField())->getContentColumnType(); 98 | $contentTable = Craft::$app->getContent()->contentTable; 99 | $fieldColumnPrefix = Craft::$app->getContent()->fieldColumnPrefix; 100 | 101 | /** @var MapField $field */ 102 | foreach ($fields as $field) 103 | { 104 | echo '- Create content column for ' . $field->name . ' in content table' . PHP_EOL; 105 | 106 | $exists = $this->db->columnExists( 107 | $contentTable, 108 | $fieldColumnPrefix . $field->handle 109 | ); 110 | 111 | if ($exists) 112 | { 113 | $this->alterColumn( 114 | $contentTable, 115 | $fieldColumnPrefix . $field->handle, 116 | $columnType 117 | ); 118 | continue; 119 | } 120 | 121 | $this->addColumn( 122 | $contentTable, 123 | $fieldColumnPrefix . $field->handle, 124 | $columnType 125 | ); 126 | } 127 | 128 | foreach ($matrixMapFields as $table => $mmFields) 129 | { 130 | foreach ($mmFields as $field) 131 | { 132 | if (!$blockTypeHandle = @$fieldIdToMatrixBlockHandle[$field->id]) 133 | continue; 134 | 135 | echo '- Create content column for ' . $field->name . ' in matrix ' . $blockTypeHandle . PHP_EOL; 136 | 137 | $handle = 138 | $fieldColumnPrefix . $blockTypeHandle . '_' . $field->handle; 139 | 140 | $exists = $this->db->columnExists( 141 | $table, 142 | $handle 143 | ); 144 | 145 | if ($exists) 146 | { 147 | $this->alterColumn( 148 | $table, 149 | $handle, 150 | $columnType 151 | ); 152 | continue; 153 | } 154 | 155 | $this->addColumn( 156 | $table, 157 | $handle, 158 | $columnType 159 | ); 160 | } 161 | } 162 | 163 | foreach ($superTableMapFields as $table => $stFields) 164 | { 165 | foreach ($stFields as $field) 166 | { 167 | echo '- Create content column for ' . $field->name . ' in super table' . PHP_EOL; 168 | 169 | $exists = $this->db->columnExists( 170 | $table, 171 | $fieldColumnPrefix . $field->handle 172 | ); 173 | 174 | if ($exists) 175 | { 176 | $this->alterColumn( 177 | $table, 178 | $fieldColumnPrefix . $field->handle, 179 | $columnType 180 | ); 181 | continue; 182 | } 183 | 184 | $this->addColumn( 185 | $table, 186 | $fieldColumnPrefix . $field->handle, 187 | $columnType 188 | ); 189 | } 190 | } 191 | 192 | // 2. Create new maps table 193 | // --------------------------------------------------------------------- 194 | 195 | echo '2. Creating new Maps table' . PHP_EOL; 196 | 197 | if ($this->db->tableExists(MapRecord::TableName)) 198 | { 199 | $rawTableName = $this->getDb()->getSchema()->getRawTableName( 200 | MapRecord::TableName 201 | ); 202 | 203 | if ($this->getDb()->getDriverName() === 'pgsql') 204 | { 205 | $indexNames = $this->getDb()->createCommand( 206 | 'SELECT indexname FROM pg_indexes WHERE [[tablename]]=:tablename', 207 | ['tablename' => $rawTableName] 208 | )->queryColumn(); 209 | 210 | foreach ($indexNames as $name) 211 | $this->getDb()->createCommand( 212 | 'ALTER INDEX "' . $name . '" RENAME TO "' . $name . '_old"' 213 | )->execute(); 214 | } 215 | else 216 | { 217 | $indexNames = $this->getDb()->createCommand( 218 | 'SHOW INDEX FROM ' . $rawTableName 219 | )->queryAll(); 220 | 221 | $indexNames = array_unique(array_reduce( 222 | $indexNames, 223 | function ($carry, $row) { 224 | if ($row['Key_name'] === 'PRIMARY') 225 | return $carry; 226 | 227 | $carry[] = $row['Key_name']; 228 | return $carry; 229 | }, 230 | [] 231 | )); 232 | 233 | // Look, I know this is hacky but whatever who still uses MySQL 234 | // anyway? You know Postgres exists, right? 235 | $this->getDb()->createCommand('SET foreign_key_checks = 0;')->execute(); 236 | 237 | foreach ($indexNames as $name) 238 | $this->dropIndex($name, MapRecord::TableName); 239 | 240 | $this->getDb()->createCommand('SET foreign_key_checks = 1;')->execute(); 241 | } 242 | 243 | $this->renameTable(MapRecord::TableName, MapRecord::OldTableName); 244 | } 245 | 246 | (new Install())->safeUp(); 247 | 248 | // 3. Move content to table 249 | // --------------------------------------------------------------------- 250 | 251 | echo '3. Moving existing maps content' . PHP_EOL; 252 | 253 | $contentRows = (new Query()) 254 | ->select('id, elementId, siteId') 255 | ->from($contentTable); 256 | 257 | foreach ($contentRows->each() as $row) 258 | { 259 | $mapContent = $this->_getMapContent( 260 | $row['elementId'], 261 | $row['siteId'] 262 | ); 263 | 264 | if (empty($mapContent)) 265 | continue; 266 | 267 | foreach ($mapContent as $mapData) 268 | { 269 | $map = new Map($mapData); 270 | 271 | echo '- Moving ' . $map->address . ' (' . $mapData['id'] . ') to ' . $contentTable . PHP_EOL; 272 | 273 | $map->ownerId = $row['elementId']; 274 | $map->ownerSiteId = $row['siteId']; 275 | $map->fieldId = $mapData['fieldId']; 276 | 277 | $field = @$fields[$mapData['fieldId']]; 278 | 279 | // Skip if the field no longer exists 280 | if (!$field) 281 | { 282 | echo '- Skipping ' . $map->address . ' (' . $mapData['id'] . ') - Field no longer exists' . PHP_EOL; 283 | continue; 284 | } 285 | 286 | $col = $fieldColumnPrefix . $field->handle; 287 | 288 | $db->createCommand() 289 | ->update( 290 | $contentTable, 291 | [$col => Json::encode($map)], 292 | ['id' => $row['id']] 293 | ) 294 | ->execute(); 295 | 296 | $mapService->saveRecord($map, true); 297 | } 298 | } 299 | 300 | foreach ($matrixMapFields as $contentTable => $fields) 301 | { 302 | $contentRows = (new Query()) 303 | ->select('id, elementId, siteId') 304 | ->from($contentTable); 305 | 306 | foreach ($contentRows->each() as $row) 307 | { 308 | $mapContent = $this->_getMapContent( 309 | $row['elementId'], 310 | $row['siteId'] 311 | ); 312 | 313 | if (empty($mapContent)) 314 | continue; 315 | 316 | foreach ($mapContent as $mapData) 317 | { 318 | if (!$blockHandle = @$fieldIdToMatrixBlockHandle[$mapData['fieldId']]) 319 | continue; 320 | 321 | $map = new Map($mapData); 322 | 323 | $map->ownerId = $row['elementId']; 324 | $map->ownerSiteId = $row['siteId']; 325 | $map->fieldId = $mapData['fieldId']; 326 | 327 | $field = $fields[$mapData['fieldId']]; 328 | $col = $fieldColumnPrefix . $blockHandle . '_' . $field->handle; 329 | 330 | echo '- Moving ' . $map->address . ' (' . $mapData['id'] . ') to ' . $contentTable . ', ' . $col . PHP_EOL; 331 | 332 | $db->createCommand() 333 | ->update( 334 | $contentTable, 335 | [$col => Json::encode($map)], 336 | ['id' => $row['id']] 337 | ) 338 | ->execute(); 339 | 340 | $mapService->saveRecord($map, true); 341 | } 342 | } 343 | } 344 | 345 | foreach ($superTableMapFields as $contentTable => $fields) 346 | { 347 | $contentRows = (new Query()) 348 | ->select('id, elementId, siteId') 349 | ->from($contentTable); 350 | 351 | foreach ($contentRows->each() as $row) 352 | { 353 | $mapContent = $this->_getMapContent( 354 | $row['elementId'], 355 | $row['siteId'] 356 | ); 357 | 358 | if (empty($mapContent)) 359 | continue; 360 | 361 | foreach ($mapContent as $mapData) 362 | { 363 | $map = new Map($mapData); 364 | 365 | $map->ownerId = $row['elementId']; 366 | $map->ownerSiteId = $row['siteId']; 367 | $map->fieldId = $mapData['fieldId']; 368 | 369 | $field = $fields[$mapData['fieldId']]; 370 | $col = $fieldColumnPrefix . $field->handle; 371 | 372 | echo '- Moving ' . $map->address . ' (' . $mapData['id'] . ') to ' . $contentTable . ', ' . $col . PHP_EOL; 373 | 374 | $db->createCommand() 375 | ->update( 376 | $contentTable, 377 | [$col => Json::encode($map)], 378 | ['id' => $row['id']] 379 | ) 380 | ->execute(); 381 | 382 | $mapService->saveRecord($map, true); 383 | } 384 | } 385 | } 386 | 387 | // 4. Drop old data table 388 | // --------------------------------------------------------------------- 389 | 390 | $this->dropTableIfExists(MapRecord::OldTableName); 391 | 392 | } 393 | 394 | /** 395 | * @inheritdoc 396 | */ 397 | public function safeDown() 398 | { 399 | echo "m190712_104805_new_data_format cannot be reverted.\n"; 400 | return false; 401 | } 402 | 403 | // Helpers 404 | // ========================================================================= 405 | 406 | private function _reduceMatrixFields ($matrixFields, $hasSuperTable, &$matrixMapFields, &$superTableMapFields) 407 | { 408 | return array_reduce( 409 | $matrixFields, 410 | function ($carry, Matrix $matrix) use ( 411 | $hasSuperTable, &$matrixMapFields, &$superTableMapFields 412 | ) { 413 | $fields = []; 414 | 415 | foreach ($matrix->getBlockTypeFields() as $field) 416 | { 417 | if ($field instanceof MapField) 418 | $fields[$field->id] = $field; 419 | 420 | elseif ($hasSuperTable && $field instanceof SuperTableField) 421 | $superTableMapFields = array_merge( 422 | $superTableMapFields, 423 | $this->_reduceSuperTableFields( 424 | [$field], 425 | $matrixMapFields, 426 | $superTableMapFields 427 | ) 428 | ); 429 | } 430 | 431 | if (!empty($fields)) 432 | $carry[$matrix->contentTable] = $fields; 433 | 434 | return $carry; 435 | }, 436 | [] 437 | ); 438 | } 439 | 440 | private function _reduceSuperTableFields ($superTableFields, &$matrixMapFields, &$superTableMapFields) 441 | { 442 | return array_reduce( 443 | $superTableFields, 444 | function ($carry, SuperTableField $superTable) use ( 445 | &$matrixMapFields, &$superTableMapFields 446 | ) { 447 | $fields = []; 448 | 449 | foreach ($superTable->getBlockTypeFields() as $field) 450 | { 451 | if ($field instanceof MapField) 452 | $fields[$field->id] = $field; 453 | 454 | elseif ($field instanceof Matrix) 455 | $matrixMapFields = array_merge( 456 | $matrixMapFields, 457 | $this->_reduceMatrixFields( 458 | [$field], 459 | true, 460 | $matrixMapFields, 461 | $superTableMapFields 462 | ) 463 | ); 464 | } 465 | 466 | if (!empty($fields)) 467 | $carry[$superTable->contentTable] = $fields; 468 | 469 | return $carry; 470 | }, 471 | [] 472 | ); 473 | } 474 | 475 | private function _getMapContent ($elementId, $siteId) 476 | { 477 | return (new Query()) 478 | ->select( 479 | 'id, ownerId, ownerSiteId, fieldId, lat, lng, zoom, address, parts' 480 | ) 481 | ->from(MapRecord::OldTableName) 482 | ->where([ 483 | 'ownerId' => $elementId, 484 | 'ownerSiteId' => $siteId, 485 | ]) 486 | ->groupBy('id, fieldId') 487 | ->orderBy('dateUpdated') 488 | ->all(); 489 | } 490 | 491 | } 492 | -------------------------------------------------------------------------------- /src/migrations/m190723_105637_fix_map_field_column_type.php: -------------------------------------------------------------------------------- 1 | getProjectConfig(); 29 | 30 | $fields = $pc->get('fields', true) ?? []; 31 | $matrixBlockTypes = $pc->get('matrixBlockTypes', true) ?? []; 32 | $superTableBlockTypes = $pc->get('superTableBlockTypes', true) ?? []; 33 | $updates = []; 34 | 35 | foreach ($fields as $f => $field) 36 | if (!empty($field['type']) && $field['type'] === MapField::class && $field['contentColumnType'] !== 'text') 37 | $updates["fields.$f.contentColumnType"] = 'text'; 38 | 39 | foreach ($matrixBlockTypes as $b => $blockType) 40 | if (array_key_exists('fields', $blockType) && is_array($blockType['fields'])) 41 | foreach ($blockType['fields'] as $f => $field) 42 | if ($field['type'] === MapField::class && $field['contentColumnType'] !== 'text') 43 | $updates["matrixBlockTypes.$b.fields.$f.contentColumnType"] = 'text'; 44 | 45 | foreach ($superTableBlockTypes as $b => $blockType) 46 | if (array_key_exists('fields', $blockType) && is_array($blockType['fields'])) 47 | foreach ($blockType['fields'] as $f => $field) 48 | if ($field['type'] === MapField::class && $field['contentColumnType'] !== 'text') 49 | $updates["superTableBlockTypes.$b.fields.$f.contentColumnType"] = 'text'; 50 | 51 | foreach ($updates as $path => $value) 52 | $pc->set($path, $value); 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function safeDown () 59 | { 60 | echo "m190723_105637_fix_map_field_column_type cannot be reverted.\n"; 61 | 62 | return false; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/models/BaseLocation.php: -------------------------------------------------------------------------------- 1 | address === null) 53 | $this->address = ''; 54 | 55 | if ($this->parts === null) 56 | { 57 | $this->parts = new Parts(); 58 | } 59 | else if (!($this->parts instanceof Parts)) 60 | { 61 | if ($this->parts && !is_array($this->parts)) 62 | $this->parts = Json::decodeIfJson($this->parts, true); 63 | 64 | if (Parts::isLegacy($this->parts)) 65 | $this->parts = new PartsLegacy($this->parts); 66 | else 67 | $this->parts = new Parts($this->parts); 68 | } 69 | } 70 | 71 | // Methods 72 | // ========================================================================= 73 | 74 | /** 75 | * Output the address in an easily formatted way 76 | * 77 | * @param array $exclude - An array of parts to exclude from the output 78 | * @param string $glue - The glue to join the parts together 79 | * 80 | * @return Markup 81 | */ 82 | public function address (array $exclude = [], string $glue = '
'): Markup 83 | { 84 | $addr = []; 85 | 86 | if (!is_array($exclude)) 87 | $exclude = [$exclude]; 88 | 89 | foreach ([['number', 'address'], 'city', 'county', 'state', 'postcode', 'country'] as $part) 90 | { 91 | if (is_array($part)) 92 | { 93 | $line = []; 94 | 95 | foreach ($part as $p) 96 | { 97 | if (in_array($p, $exclude)) 98 | continue; 99 | 100 | $line[] = $this->parts->$p; 101 | } 102 | 103 | $addr[] = implode(' ', array_filter($line)); 104 | continue; 105 | } 106 | 107 | if (in_array($part, $exclude)) 108 | continue; 109 | 110 | $addr[] = $this->parts->$part; 111 | } 112 | 113 | $addr = array_filter($addr); 114 | 115 | return new Markup(implode($glue, $addr), 'utf8'); 116 | } 117 | 118 | public function __toString(): string 119 | { 120 | return (string) $this->address([], ', '); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/models/EmbedOptions.php: -------------------------------------------------------------------------------- 1 | id) 39 | $this->id = StringHelper::appendUniqueIdentifier('map'); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/models/Map.php: -------------------------------------------------------------------------------- 1 | distance = SimpleMap::getInstance()->map->getDistance($this); 60 | } 61 | 62 | // Getters 63 | // ========================================================================= 64 | 65 | public function __get ($name) 66 | { 67 | $isPart = property_exists($this->parts, $name) || $name === 'streetAddress'; 68 | 69 | if (in_array($name, PartsLegacy::$legacyKeys) && !$isPart) 70 | return null; 71 | else if ($isPart) 72 | return $this->parts->$name; 73 | 74 | return parent::__get($name); 75 | } 76 | 77 | public function canGetProperty ($name, $checkVars = true, $checkBehaviors = true): bool 78 | { 79 | try 80 | { 81 | if ( 82 | property_exists($this->parts, $name) || 83 | $name === 'streetAddress' || 84 | in_array($name, PartsLegacy::$legacyKeys) 85 | ) return true; 86 | } catch (Exception $e) { 87 | return false; 88 | } 89 | 90 | return parent::canGetProperty($name, $checkVars, $checkBehaviors); 91 | } 92 | 93 | // Methods 94 | // ========================================================================= 95 | 96 | public function rules (): array 97 | { 98 | $rules = parent::rules(); 99 | 100 | $rules[] = [ 101 | ['zoom'], 102 | 'required', 103 | ]; 104 | $rules[] = [ 105 | ['lat'], 106 | 'double', 107 | 'min' => -90, 108 | 'max' => 90, 109 | ]; 110 | $rules[] = [ 111 | ['lng'], 112 | 'double', 113 | 'min' => -180, 114 | 'max' => 180, 115 | ]; 116 | 117 | return $rules; 118 | } 119 | 120 | public function isValueEmpty (): bool 121 | { 122 | return empty($this->lat) && empty($this->lng); 123 | } 124 | 125 | // Render Map 126 | // ========================================================================= 127 | 128 | // Render Map: Image 129 | // ------------------------------------------------------------------------- 130 | 131 | /** 132 | * Output the map field as a static image 133 | * 134 | * @param array $options 135 | * 136 | * @return string|void 137 | * @throws Exception 138 | */ 139 | public function img (array $options = []) 140 | { 141 | return SimpleMap::getInstance()->static->generate( 142 | $this->_getMapOptions($options) 143 | ); 144 | } 145 | 146 | /** 147 | * Output the map ready for srcset 148 | * 149 | * @param array $options 150 | * 151 | * @return string 152 | * @throws Exception 153 | */ 154 | public function imgSrcSet (array $options = []): string 155 | { 156 | $options = $this->_getMapOptions($options); 157 | 158 | $x1 = $this->img(array_merge($options, ['scale' => 1])); 159 | $x2 = $this->img(array_merge($options, ['scale' => 2])); 160 | 161 | return $x1 . ' 1x, ' . $x2 . ' 2x'; 162 | } 163 | 164 | /** 165 | * Output an interactive map 166 | * 167 | * @param array $options 168 | * 169 | * @return string|void 170 | * @throws InvalidConfigException 171 | */ 172 | public function embed (array $options = []) 173 | { 174 | $options = $this->_getMapOptions($options); 175 | return SimpleMap::getInstance()->embed->embed($options); 176 | } 177 | 178 | // Helpers 179 | // ========================================================================= 180 | 181 | /** 182 | * Merge options w/ map properties 183 | * 184 | * @param array $options 185 | * 186 | * @return array 187 | */ 188 | private function _getMapOptions (array $options): array 189 | { 190 | return array_merge($options, [ 191 | 'center' => [ 192 | $this->lat, 193 | $this->lng, 194 | ], 195 | 'zoom' => $this->zoom, 196 | ]); 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /src/models/Marker.php: -------------------------------------------------------------------------------- 1 | lat, 'lng' => lng] array */ 30 | public string|array $location; 31 | 32 | /** @var string The colour of the marker in Hex format */ 33 | public string $color = '#ff0000'; 34 | 35 | /** @var string|null A single character label, or null for no label */ 36 | public mixed $label = null; 37 | 38 | // Constructor 39 | // ========================================================================= 40 | 41 | public function __construct ($config = []) 42 | { 43 | if (!empty($config)) 44 | Yii::configure($this, $config); 45 | 46 | if (empty($this->location)) 47 | throw new InvalidConfigException('Marker location is missing!'); 48 | 49 | if (empty($this->color)) 50 | throw new InvalidConfigException('Marker colour is missing!'); 51 | 52 | $this->color = strtolower($this->color); 53 | if (preg_match('/^#[a-z0-9]{3}$/', $this->color)) 54 | $this->color = Marker::_expandHex($this->color); 55 | 56 | if ($this->label === '') 57 | $this->label = null; 58 | 59 | if ($this->label !== null && strlen($this->label) > 1) 60 | $this->label = $this->label[0]; 61 | } 62 | 63 | // Methods 64 | // ========================================================================= 65 | 66 | public function __toString () 67 | { 68 | return implode('|', [ 69 | Json::encode($this->location), 70 | $this->color, 71 | $this->label, 72 | ]); 73 | } 74 | 75 | public function getLocation ($toLatLng = false): array|string 76 | { 77 | if (is_string($this->location)) 78 | return $toLatLng 79 | ? implode(',', array_values(GeoService::latLngFromAddress($this->location))) 80 | : $this->location; 81 | 82 | return implode(',', array_values($this->location)); 83 | } 84 | 85 | /** 86 | * @return array|string|null 87 | * @throws Exception 88 | */ 89 | public function getCenter (): array|string|null 90 | { 91 | if (is_string($this->location)) 92 | return GeoService::latLngFromAddress($this->location); 93 | 94 | if (!array_key_exists('lat', $this->location) || !array_key_exists('lng', $this->location)) 95 | return ['lat' => (float)$this->location[0], 'lng' => (float)$this->location[1]]; 96 | 97 | return $this->location; 98 | } 99 | 100 | // Helpers 101 | // ========================================================================= 102 | 103 | private static function _expandHex ($hex): string 104 | { 105 | $r = $hex[1]; 106 | $g = $hex[2]; 107 | $b = $hex[3]; 108 | 109 | return '#' . $r . $r . $g . $g . $b . $b; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/models/Parts.php: -------------------------------------------------------------------------------- 1 | _nominatim($parts); 86 | break; 87 | case GeoService::Mapbox: 88 | $this->_mapbox($parts); 89 | break; 90 | case GeoService::GoogleMaps: 91 | $this->_google($parts); 92 | break; 93 | case GeoService::Here: 94 | $this->_here($parts); 95 | break; 96 | default: 97 | $this->_fromArray($parts); 98 | } 99 | } 100 | 101 | public function getStreetAddress (): string 102 | { 103 | return $this->address; 104 | } 105 | 106 | // Methods: Private 107 | // ------------------------------------------------------------------------- 108 | 109 | /** 110 | * Parse Nominatim parts 111 | * 112 | * @param array $parts 113 | */ 114 | private function _nominatim (array $parts) 115 | { 116 | // Add any missing values 117 | $keys = [ 118 | 'house_number', 119 | 'address29', 120 | 'type', 121 | 'pedestrian', 122 | 'footway', 123 | 'path', 124 | 'road', 125 | 'neighbourhood', 126 | 'suburb', 127 | 'village', 128 | 'town', 129 | 'city_district', 130 | 'city', 131 | 'postcode', 132 | 'county', 133 | 'state_district', 134 | 'state', 135 | 'country', 136 | ]; 137 | 138 | foreach ($keys as $key) 139 | if (!array_key_exists($key, $parts)) 140 | $parts[$key] = null; 141 | 142 | $this->number = $this->_join([ 143 | $parts['house_number'], 144 | $parts['address29'], 145 | in_array($parts['type'], [ 146 | 'pedestrian', 147 | 'footway', 148 | 'path', 149 | 'road', 150 | 'neighbourhood', 151 | 'suburb', 152 | 'village', 153 | 'town', 154 | 'city_district', 155 | 'city', 156 | ]) ? $parts[$parts['type']] : null, 157 | ]); 158 | 159 | $this->address = $this->_join([ 160 | $parts['pedestrian'], 161 | $parts['footway'], 162 | $parts['path'], 163 | $parts['road'], 164 | $parts['neighbourhood'], 165 | $parts['suburb'], 166 | ]); 167 | 168 | $this->city = $this->_join([ 169 | $parts['village'], 170 | $parts['town'], 171 | $parts['city_district'], 172 | $parts['city'], 173 | ]); 174 | 175 | $this->postcode = $parts['postcode']; 176 | $this->county = $parts['county']; 177 | 178 | $this->state = $this->_join([ 179 | $parts['state_district'], 180 | $parts['state'], 181 | ]); 182 | 183 | $this->country = $parts['country']; 184 | } 185 | 186 | /** 187 | * Parse Mapbox parts 188 | * 189 | * @param array $parts 190 | */ 191 | private function _mapbox (array $parts) 192 | { 193 | $parts = array_reduce( 194 | $parts['context'], 195 | function ($a, $part) { 196 | $key = explode('.', $part['id'])[0]; 197 | $a[$key] = $part['text']; 198 | 199 | return $a; 200 | }, 201 | [ 202 | 'number' => @$parts['address'], 203 | $parts['place_type'][0] => $parts['text'], 204 | ] 205 | ); 206 | 207 | $this->number = @$parts['number']; 208 | $this->address = @$parts['address']; 209 | $this->city = @$parts['city']; 210 | $this->postcode = @$parts['postcode']; 211 | $this->county = @$parts['county']; 212 | $this->state = @$parts['state']; 213 | $this->country = @$parts['country']; 214 | } 215 | 216 | /** 217 | * Parse Google parts 218 | * 219 | * @param $parts 220 | */ 221 | private function _google ($parts) 222 | { 223 | if (!$this->_isAssoc($parts)) 224 | { 225 | $parts = array_reduce( 226 | $parts, 227 | function ($a, $part) { 228 | $key = $part['types'][0]; 229 | $a[$key] = $part['long_name']; 230 | 231 | return $a; 232 | }, 233 | [] 234 | ); 235 | } 236 | 237 | foreach (PartsLegacy::$legacyKeys as $key) 238 | if (!array_key_exists($key, $parts)) 239 | $parts[$key] = ''; 240 | 241 | $this->number = $parts['number'] ?? $this->_join([ 242 | $parts['subpremise'], 243 | $parts['premise'], 244 | $parts['street_number'], 245 | ]); 246 | 247 | $this->address = $parts['address'] ?? $this->_join([ 248 | $parts['route'], 249 | $parts['neighborhood'], 250 | $parts['sublocality_level_5'], 251 | $parts['sublocality_level_4'], 252 | $parts['sublocality_level_3'], 253 | $parts['sublocality_level_2'], 254 | $parts['sublocality_level_1'], 255 | $parts['sublocality'], 256 | ]); 257 | 258 | $this->city = $parts['city'] ?? $this->_join([ 259 | $parts['postal_town'], 260 | $parts['locality'], 261 | ]); 262 | 263 | $this->postcode = $parts['postcode'] ?? $parts['postal_code'] ?? $parts['postal_code_prefix']; 264 | $this->county = $parts['county'] ?? $parts['administrative_area_level_2']; 265 | $this->state = $parts['state'] ?? $parts['administrative_area_level_1']; 266 | $this->country = $parts['country']; 267 | } 268 | 269 | /** 270 | * Parse Here parts 271 | * 272 | * @param $parts 273 | */ 274 | private function _here ($parts) 275 | { 276 | $parts = array_merge( 277 | $parts, 278 | array_reduce($parts['additionalData'], function ($a, $b) { 279 | $a[$b['key']] = $b['value']; 280 | return $a; 281 | }, []) 282 | ); 283 | 284 | $this->number = $parts['number']; 285 | $this->address = $this->_join([ 286 | $parts['street'], 287 | $parts['district'], 288 | ]); 289 | $this->city = $parts['city']; 290 | $this->postcode = $parts['postalCode']; 291 | $this->county = $parts['CountyName'] ?? $parts['county']; 292 | $this->state = $parts['StateName'] ?? $parts['state']; 293 | $this->country = $parts['CountryName'] ?? $parts['country']; 294 | } 295 | 296 | // Methods: Helpers 297 | // ------------------------------------------------------------------------- 298 | 299 | /** 300 | * Determines if the given array of parts contains legacy data 301 | * 302 | * @param array|null $parts 303 | * 304 | * @return bool 305 | */ 306 | public static function isLegacy (array $parts = null): bool 307 | { 308 | if ($parts === null) 309 | return false; 310 | 311 | $keys = PartsLegacy::$legacyKeys; 312 | 313 | unset($keys[array_search('country', $keys)]); 314 | 315 | foreach ($keys as $key) 316 | if (isset($parts[$key]) || array_key_exists($key, $parts)) 317 | return true; 318 | 319 | return false; 320 | } 321 | 322 | /** 323 | * Filters and joins the given array 324 | * 325 | * @param array $parts 326 | * 327 | * @return string 328 | */ 329 | private function _join (array $parts): string 330 | { 331 | return implode(', ', array_filter($parts)); 332 | } 333 | 334 | /** 335 | * Populates Parts from the given array 336 | * 337 | * @param array $parts 338 | */ 339 | private function _fromArray (array $parts) 340 | { 341 | $this->number = $parts['number'] ?? ''; 342 | $this->address = $parts['address'] ?? ''; 343 | $this->city = $parts['city'] ?? ''; 344 | $this->postcode = $parts['postcode'] ?? ''; 345 | $this->county = $parts['county'] ?? ''; 346 | $this->state = $parts['state'] ?? ''; 347 | $this->country = $parts['country'] ?? ''; 348 | } 349 | 350 | /** 351 | * Returns true if the given array is associative 352 | * 353 | * @param array $arr 354 | * 355 | * @return bool 356 | */ 357 | protected function _isAssoc (array $arr): bool 358 | { 359 | if ([] === $arr) return false; 360 | return array_keys($arr) !== range(0, count($arr) - 1); 361 | } 362 | 363 | } 364 | -------------------------------------------------------------------------------- /src/models/PartsLegacy.php: -------------------------------------------------------------------------------- 1 | _isAssoc($parts)) 156 | { 157 | $parts = array_reduce( 158 | $parts, 159 | function ($a, $part) { 160 | $key = $part['types'][0]; 161 | $a[$key] = $part['long_name']; 162 | 163 | return $a; 164 | }, 165 | [] 166 | ); 167 | } 168 | 169 | \Yii::configure($this, $parts); 170 | 171 | parent::__construct($parts, GeoService::GoogleMaps); 172 | } 173 | 174 | public function __set ($name, $value) 175 | { 176 | // Prevent setting any new parameters that we don't support 177 | if (!$this->hasProperty($name)) 178 | { 179 | $name = Json::encode($name); 180 | $value = Json::encode($value); 181 | 182 | Craft::info( 183 | 'Attempted to set unsupported legacy part: "' . $name . '" to value "' . $value . '"', 184 | 'simplemap' 185 | ); 186 | return; 187 | } 188 | 189 | parent::__set($name, $value); 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/models/Point.php: -------------------------------------------------------------------------------- 1 | x = $x; 44 | $this->y = $y; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | * 50 | * @see \Imagine\Image\PointInterface::getX() 51 | */ 52 | public function getX (): int 53 | { 54 | return $this->x; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | * 60 | * @see \Imagine\Image\PointInterface::getY() 61 | */ 62 | public function getY (): int 63 | { 64 | return $this->y; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | * 70 | * @see \Imagine\Image\PointInterface::in() 71 | */ 72 | public function in (BoxInterface $box): bool 73 | { 74 | return $this->x < $box->getWidth() && $this->y < $box->getHeight(); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | * 80 | * @see \Imagine\Image\PointInterface::move() 81 | */ 82 | public function move ($amount): Point|PointInterface 83 | { 84 | return new self($this->x + $amount, $this->y + $amount); 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | * 90 | * @see \Imagine\Image\PointInterface::__toString() 91 | */ 92 | public function __toString () 93 | { 94 | return sprintf('(%d, %d)', $this->x, $this->y); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/models/Settings.php: -------------------------------------------------------------------------------- 1 | [ 'countryCode' => 'uk' ], 96 | * 'eu' => [ 'isEU' => true ], 97 | * 'global' => '*', 98 | * ] 99 | */ 100 | public array $geoLocationRedirectMap = []; 101 | 102 | // Methods 103 | // ========================================================================= 104 | 105 | public function __construct ($config = []) 106 | { 107 | parent::__construct($config); 108 | 109 | try { 110 | $this->geoLocationCacheDuration = ConfigHelper::durationInSeconds( 111 | $this->geoLocationCacheDuration 112 | ); 113 | } catch (Exception $e) { 114 | Craft::error($e->getMessage()); 115 | } 116 | } 117 | 118 | public function isW3WEnabled (): bool 119 | { 120 | return $this->w3wEnabled && SimpleMap::v(SimpleMap::EDITION_PRO); 121 | } 122 | 123 | // Getters 124 | // ========================================================================= 125 | 126 | public function getMapToken (): bool|array|string|null 127 | { 128 | return $this->_parseEnv($this->mapToken); 129 | } 130 | 131 | public function getGeoToken (): bool|array|string|null 132 | { 133 | return $this->_parseEnv($this->geoToken); 134 | } 135 | 136 | public function getW3WToken (): bool|array|string|null 137 | { 138 | return $this->_parseEnv($this->w3wToken); 139 | } 140 | 141 | public function getGeoLocationToken (): bool|array|string|null 142 | { 143 | return $this->_parseEnv($this->geoLocationToken); 144 | } 145 | 146 | // Helpers 147 | // ========================================================================= 148 | 149 | private function _parseEnv ($value): array|bool|string|null 150 | { 151 | if (is_string($value)) 152 | return App::parseEnv($value); 153 | 154 | return array_map(function ($v) { 155 | return App::parseEnv($v); 156 | }, $value); 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/models/StaticOptions.php: -------------------------------------------------------------------------------- 1 | lat, 'lng' => lng] array */ 29 | public string|array $center = [51.272154, 0.514951]; 30 | 31 | /** @var string|array Must be [lat, lng] or ['lat' => lat, 'lng' => lng] array */ 32 | public string|array $centerFallback = [51.272154, 0.514951]; 33 | 34 | /** @var int The width of the map */ 35 | public int $width = 640; 36 | 37 | /** @var int The height of the map */ 38 | public int $height = 480; 39 | 40 | /** @var int The maps zoom level */ 41 | public int $zoom = 12; 42 | 43 | /** @var int The scale of the map image (i.e. 2 for @2x retina screens) */ 44 | public int $scale = 1; 45 | 46 | /** 47 | * @var Marker[] An array of map markers 48 | */ 49 | public array $markers = []; 50 | 51 | // Constructor 52 | // ========================================================================= 53 | 54 | /** 55 | * StaticOptions constructor. 56 | * 57 | * @param array $config 58 | * 59 | * @throws InvalidConfigException 60 | * @throws Exception 61 | */ 62 | public function __construct (array $config = []) 63 | { 64 | $center = $config['center'] ?? null; 65 | 66 | if ($center instanceof Map) 67 | $center = ['lat' => $center->lat, 'lng' => $center->lng, 'zoom' => $center->zoom]; 68 | elseif ($center instanceof UserLocation) 69 | $center = ['lat' => $center->lat, 'lng' => $center->lng]; 70 | elseif (is_string($center)) 71 | $center = GeoService::latLngFromAddress($center); 72 | 73 | if (empty($center)) 74 | $center = $config['centerFallback'] ?? $this->centerFallback; 75 | 76 | $config['center'] = $center; 77 | 78 | $markers = $config['markers'] ?? []; 79 | unset($config['markers']); 80 | 81 | if (!empty($config)) 82 | Yii::configure($this, $config); 83 | 84 | foreach (['center', 'zoom', 'scale'] as $key) 85 | if (empty($this->$key)) 86 | throw new InvalidConfigException('Map ' . $key . ' is missing!'); 87 | 88 | if (!empty($markers)) 89 | { 90 | foreach ($markers as $marker) 91 | { 92 | if (!array_key_exists('location', $marker) || empty($marker['location'])) 93 | $marker['location'] = $this->center; 94 | 95 | $this->markers[] = new Marker($marker); 96 | } 97 | } 98 | } 99 | 100 | // Getters 101 | // ========================================================================= 102 | 103 | /** 104 | * @return array|string|null 105 | * @throws Exception 106 | */ 107 | public function getCenter (): array|string|null 108 | { 109 | if (!array_key_exists('lat', $this->center) || !array_key_exists('lng', $this->center)) 110 | $this->center = ['lat' => $this->center[0], 'lng' => $this->center[1]]; 111 | 112 | $this->center['lat'] = floatval($this->center['lat']); 113 | $this->center['lng'] = floatval($this->center['lng']); 114 | 115 | return $this->center; 116 | } 117 | 118 | public function getSize (): string 119 | { 120 | return ($this->width ?? 0) . 'x' . ($this->height ?? 0); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/models/UserLocation.php: -------------------------------------------------------------------------------- 1 | lat)) * 74 | cos(deg2rad($targetLat)) * 75 | cos(deg2rad($this->lng) - deg2rad($targetLng)) + 76 | sin(deg2rad($this->lat)) * 77 | sin(deg2rad($targetLat)) 78 | ) 79 | ) 80 | ); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/records/Map.php: -------------------------------------------------------------------------------- 1 | hasOne(Element::class, ['id' => 'ownerId']); 53 | } 54 | 55 | public function getOwnerSite (): ActiveQueryInterface 56 | { 57 | return $this->hasOne(Site::class, ['id' => 'ownerSiteId']); 58 | } 59 | 60 | public function getField (): ActiveQueryInterface 61 | { 62 | return $this->hasOne(Field::class, ['id' => 'fieldId']); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/resources/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/resources/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/resources/OpenSans_LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/resources/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/resources/marker.png -------------------------------------------------------------------------------- /src/resources/marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ##LABEL## 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/resources/markerNoLabel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/resources/markerNoLabel.png -------------------------------------------------------------------------------- /src/services/GeoLocationService.php: -------------------------------------------------------------------------------- 1 | getRequest()->getUserIP(); 60 | 61 | if (!self::_isValidIp($ip)) 62 | { 63 | Craft::error('Invalid or not allowed IP address: "' . $ip . '"', 'maps'); 64 | 65 | return null; 66 | } 67 | 68 | /** @var Settings $settings */ 69 | $settings = SimpleMap::getInstance()->getSettings(); 70 | 71 | if ($cached = $this->_getUserLocationFromCache($ip, $settings)) 72 | return $cached; 73 | 74 | $userLocation = match ($settings->geoLocationService) 75 | { 76 | self::IpStack => $this->_lookup_IpStack( 77 | $settings->getGeoLocationToken(), $ip 78 | ), 79 | self::MaxMind => $this->_lookup_MaxMind( 80 | $settings->getGeoLocationToken(), $ip 81 | ), 82 | self::MaxMindLite => $this->_lookup_MaxMindLite($ip), 83 | default => null, 84 | }; 85 | 86 | if ($userLocation) 87 | $this->_cacheUserLocation($userLocation, $settings); 88 | 89 | return $userLocation; 90 | } 91 | 92 | /** 93 | * @throws Exception 94 | */ 95 | public function redirect () 96 | { 97 | $settings = SimpleMap::getInstance()->getSettings(); 98 | $siteHandle = null; 99 | $location = $this->lookup(); 100 | 101 | if (!$location || empty($settings->geoLocationAutoRedirect)) 102 | return; 103 | 104 | foreach ($settings->geoLocationRedirectMap as $handle => $props) 105 | { 106 | if ($props === '*') 107 | { 108 | $siteHandle = $handle; 109 | continue; 110 | } 111 | 112 | if (self::_validateProps($location, $props)) 113 | { 114 | $siteHandle = $handle; 115 | break; 116 | } 117 | } 118 | 119 | if ($siteHandle === null) 120 | return; 121 | 122 | $site = Craft::$app->getSites()->getSiteByHandle($siteHandle); 123 | 124 | if ($site->id === Craft::$app->getSites()->getCurrentSite()->id) 125 | return; 126 | 127 | if (!$site) 128 | { 129 | Craft::error('Unable to find site with handle "' . $siteHandle . '"', 'simplemap'); 130 | return; 131 | } 132 | 133 | if (!$site->hasUrls) 134 | { 135 | Craft::error('Selected site (' . $siteHandle . ') doesn\'t have URLs!', 'simplemap'); 136 | return; 137 | } 138 | 139 | $currentBaseUrl = Craft::$app->getSites()->getCurrentSite()->getBaseUrl(); 140 | $currentUrl = str_replace( 141 | $currentBaseUrl, 142 | '', 143 | Craft::$app->getRequest()->getAbsoluteUrl() 144 | ); 145 | 146 | if (str_contains($currentUrl, '://')) 147 | $currentUrl = str_replace( 148 | Craft::$app->getRequest()->getBaseUrl(), 149 | '', 150 | $currentUrl 151 | ); 152 | 153 | $url = rtrim($site->getBaseUrl(), '/') . '/' . $currentUrl; 154 | 155 | Craft::$app->getResponse()->redirect($url); 156 | } 157 | 158 | // Public Helpers 159 | // ========================================================================= 160 | 161 | public static function getSelectOptions (): array 162 | { 163 | return [ 164 | self::None => SimpleMap::t('None'), 165 | self::IpStack => SimpleMap::t('ipstack'), 166 | // self::MaxMindLite => SimpleMap::t('MaxMind (Lite, ~60MB download)'), 167 | self::MaxMind => SimpleMap::t('MaxMind'), 168 | ]; 169 | } 170 | 171 | // MaxMind DB 172 | // ------------------------------------------------------------------------- 173 | 174 | /** 175 | * Check if the database file exists 176 | * 177 | * @param string $filename 178 | * 179 | * @return bool 180 | */ 181 | public static function dbExists (string $filename = 'default.mmdb'): bool 182 | { 183 | return file_exists( 184 | Craft::getAlias(self::DB_STORAGE . DIRECTORY_SEPARATOR . $filename) 185 | ); 186 | } 187 | 188 | /** 189 | * Should we update the database (is it older than 1 week?) 190 | * 191 | * @param string $filename 192 | * 193 | * @return bool 194 | * @throws Exception 195 | */ 196 | public static function dbShouldUpdate (string $filename = 'default.mmdb'): bool 197 | { 198 | $updated = filemtime( 199 | Craft::getAlias(self::DB_STORAGE . DIRECTORY_SEPARATOR . $filename) 200 | ); 201 | 202 | if ($updated === false) return false; 203 | return $updated < (new DateTime())->modify('-7 days')->getTimestamp(); 204 | } 205 | 206 | /** 207 | * Start the MaxMind DB download job 208 | */ 209 | public static function dbQueueDownload () 210 | { 211 | if (Craft::$app->getCache()->get('maps_db_updating')) 212 | return; 213 | 214 | Craft::$app->getCache()->set('maps_db_updating', true); 215 | Craft::$app->getQueue()->push(new MaxMindDBDownloadJob()); 216 | } 217 | 218 | public static function purgeDb ($filename = 'default.mmdb') 219 | { 220 | $file = Craft::getAlias(self::DB_STORAGE . DIRECTORY_SEPARATOR . $filename); 221 | 222 | if (file_exists($file)) 223 | unlink($file); 224 | } 225 | 226 | // Private Helpers 227 | // ========================================================================= 228 | 229 | // Caching 230 | // ------------------------------------------------------------------------- 231 | 232 | /** 233 | * @param string $ip 234 | * @param Settings $settings 235 | * 236 | * @return UserLocation|false 237 | */ 238 | private function _getUserLocationFromCache (string $ip, Settings $settings): bool|UserLocation 239 | { 240 | if (!$settings->geoLocationCacheDuration) 241 | return false; 242 | 243 | return Craft::$app->getCache()->get( 244 | 'maps_ip_' . $ip 245 | ); 246 | } 247 | 248 | /** 249 | * @param UserLocation $userLocation 250 | * @param Settings $settings 251 | * 252 | * @return bool 253 | */ 254 | private function _cacheUserLocation (UserLocation $userLocation, Settings $settings): bool 255 | { 256 | if (!$settings->geoLocationCacheDuration) 257 | return true; 258 | 259 | return Craft::$app->getCache()->set( 260 | 'maps_ip_' . $userLocation->ip, 261 | $userLocation, 262 | $settings->geoLocationCacheDuration 263 | ); 264 | } 265 | 266 | // Lookup Services 267 | // ------------------------------------------------------------------------- 268 | 269 | private function _lookup_IpStack ($token, $ip): ?UserLocation 270 | { 271 | $url = 'http://api.ipstack.com/' . $ip; 272 | $url .= '?access_key=' . $token; 273 | $url .= '&language=' . Craft::$app->getLocale()->getLanguageID(); 274 | 275 | $data = self::_client()->get($url)->getBody(); 276 | $data = Json::decodeIfJson($data); 277 | 278 | if (array_key_exists('success', $data) && $data['success'] === false) 279 | { 280 | Craft::error($data['error']['info'], 'maps'); 281 | 282 | return null; 283 | } 284 | 285 | $parts = [ 286 | 'city' => $data['city'], 287 | 'postcode' => $data['zip'], 288 | 'state' => $data['region_name'], 289 | 'country' => $data['country_name'], 290 | ]; 291 | 292 | return new UserLocation([ 293 | 'ip' => $ip, 294 | 'lat' => $data['latitude'], 295 | 'lng' => $data['longitude'], 296 | 'address' => implode(', ', array_filter($parts)), 297 | 'countryCode' => $data['country_code'], 298 | 'isEU' => $data['location']['is_eu'], 299 | 'parts' => $parts, 300 | ]); 301 | } 302 | 303 | private function _lookup_MaxMind ($token, $ip): ?UserLocation 304 | { 305 | $client = new Client( 306 | $token['accountId'], 307 | $token['licenseKey'], 308 | [ 309 | Craft::$app->getLocale()->getLanguageID(), 310 | 'en' 311 | ] 312 | ); 313 | 314 | $record = null; 315 | 316 | try { 317 | $record = $client->city($ip); 318 | } catch (Exception $e) { 319 | Craft::error($e->getMessage(), 'maps'); 320 | 321 | return null; 322 | } 323 | 324 | return self::_populateMaxMind($ip, $record); 325 | } 326 | 327 | /** 328 | * @param $ip 329 | * 330 | * @return UserLocation|null 331 | * @throws Exception 332 | */ 333 | private function _lookup_MaxMindLite ($ip): ?UserLocation 334 | { 335 | if (!self::dbExists()) 336 | { 337 | // self::dbQueueDownload(); 338 | 339 | throw new Exception('MaxMind Lite is no longer supported'); 340 | } 341 | 342 | // if (self::dbShouldUpdate()) 343 | // self::dbQueueDownload(); 344 | Craft::warning('MaxMind Lite is no longer supported and will not receive updates'); 345 | 346 | try 347 | { 348 | $reader = new Reader( 349 | Craft::getAlias( 350 | self::DB_STORAGE . DIRECTORY_SEPARATOR . 'default.mmdb' 351 | ) 352 | ); 353 | $record = $reader->city($ip); 354 | } catch (Exception $e) 355 | { 356 | Craft::dd($e); 357 | Craft::error($e->getMessage(), 'maps'); 358 | 359 | return null; 360 | } 361 | 362 | return self::_populateMaxMind($ip, $record); 363 | } 364 | 365 | // Misc 366 | // ------------------------------------------------------------------------- 367 | 368 | private static function _client () 369 | { 370 | static $client; 371 | 372 | if (!$client) 373 | $client = Craft::createGuzzleClient(); 374 | 375 | return $client; 376 | } 377 | 378 | /** 379 | * Ensure IP is valid and not private or reserved 380 | * 381 | * @param string $ip 382 | * 383 | * @return mixed 384 | */ 385 | private static function _isValidIp (string $ip): mixed 386 | { 387 | return filter_var( 388 | $ip, 389 | FILTER_VALIDATE_IP, 390 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE 391 | ); 392 | } 393 | 394 | /** 395 | * @param $ip 396 | * @param $record 397 | * 398 | * @return UserLocation 399 | */ 400 | private static function _populateMaxMind ($ip, $record): UserLocation 401 | { 402 | $parts = [ 403 | 'city' => $record->city->name, 404 | 'postcode' => $record->postal->code, 405 | 'state' => $record->mostSpecificSubdivision->name, 406 | 'country' => $record->country->name, 407 | ]; 408 | 409 | return new UserLocation([ 410 | 'ip' => $ip, 411 | 'lat' => $record->location->latitude, 412 | 'lng' => $record->location->longitude, 413 | 'address' => implode(', ', array_filter($parts)), 414 | 'countryCode' => $record->country->isoCode, 415 | 'isEU' => $record->country->isInEuropeanUnion, 416 | 'parts' => $parts, 417 | ]); 418 | } 419 | 420 | /** 421 | * @param UserLocation $location 422 | * @param $props 423 | * 424 | * @return bool 425 | */ 426 | private static function _validateProps (UserLocation $location, $props): bool 427 | { 428 | foreach ($props as $key => $value) 429 | { 430 | if (is_array($value)) 431 | { 432 | foreach ($value as $item) 433 | if ($location->$key === $item) 434 | continue; 435 | 436 | return false; 437 | } 438 | 439 | if (is_callable($value)) 440 | return $value($location->$key); 441 | 442 | if ($location->$key !== $value) 443 | return false; 444 | } 445 | 446 | return true; 447 | } 448 | 449 | } 450 | -------------------------------------------------------------------------------- /src/services/MapService.php: -------------------------------------------------------------------------------- 1 | getFieldValue($field->handle); 55 | 56 | $valid = $map->validate(); 57 | 58 | foreach ($map->getErrors() as $error) 59 | $owner->addError($field->handle, $error[0]); 60 | 61 | return $valid; 62 | } 63 | 64 | /** 65 | * @param MapField $field 66 | * @param ElementInterface $owner 67 | * 68 | * @throws InvalidFieldException 69 | */ 70 | public function saveField (MapField $field, ElementInterface $owner) 71 | { 72 | /** @var Map $map */ 73 | $map = $owner->getFieldValue($field->handle); 74 | 75 | $map->fieldId = $field->id; 76 | $map->ownerId = $owner->id; 77 | $map->ownerSiteId = $owner->siteId; 78 | 79 | $record = MapRecord::findOne([ 80 | 'ownerId' => $map->ownerId, 81 | 'ownerSiteId' => $map->ownerSiteId, 82 | 'fieldId' => $map->fieldId, 83 | ]); 84 | 85 | if ($record) 86 | $map->id = $record->id; 87 | 88 | $this->saveRecord($map, !$map->id); 89 | } 90 | 91 | /** 92 | * @param Map $map 93 | * @param $isNew 94 | * 95 | * @throws Exception 96 | */ 97 | public function saveRecord (Map $map, $isNew) 98 | { 99 | $record = null; 100 | 101 | if (!$isNew) 102 | { 103 | $record = MapRecord::findOne($map->id); 104 | 105 | if (!$record) 106 | throw new Exception('Invalid map ID: ' . $map->id); 107 | } 108 | 109 | if ($record === null) 110 | { 111 | $record = new MapRecord(); 112 | 113 | if ($map->id) 114 | $record->id = $map->id; 115 | 116 | $record->ownerId = $map->ownerId; 117 | $record->ownerSiteId = $map->ownerSiteId; 118 | $record->fieldId = $map->fieldId; 119 | } 120 | 121 | $record->lat = $map->lat; 122 | $record->lng = $map->lng; 123 | 124 | $record->save(false); 125 | } 126 | 127 | /** 128 | * Returns the distance from the search origin (if one exists) 129 | * 130 | * @param Map $map 131 | * 132 | * @return float|int|null 133 | */ 134 | public function getDistance (Map $map): float|int|null 135 | { 136 | if (!$this->_location || !$this->_distance) 137 | return null; 138 | 139 | $originLat = (float) $this->_location['lat']; 140 | $originLng = (float) $this->_location['lng']; 141 | 142 | $targetLat = (float) $map->lat; 143 | $targetLng = (float) $map->lng; 144 | 145 | return ( 146 | $this->_distance * 147 | rad2deg( 148 | acos( 149 | cos(deg2rad($originLat)) * 150 | cos(deg2rad($targetLat)) * 151 | cos(deg2rad($originLng) - deg2rad($targetLng)) + 152 | sin(deg2rad($originLat)) * 153 | sin(deg2rad($targetLat)) 154 | ) 155 | ) 156 | ); 157 | } 158 | 159 | /** 160 | * @param ElementQueryInterface $query 161 | * @param mixed $value 162 | * @param MapField $field 163 | * 164 | * @throws Exception 165 | */ 166 | public function modifyElementsQuery (ElementQueryInterface $query, mixed $value, MapField $field) 167 | { 168 | if (is_null($value)) 169 | return; 170 | 171 | // Work-around for Craft built-in GraphQL not supporting custom 172 | // arguments for fields: 173 | 174 | // If it's an array with a 0 key, that means it's likely to be a `[QueryArgument]` 175 | if (is_array($value) && array_key_exists(0, $value)) 176 | $value = $value[0]; 177 | 178 | // If it's a string, check to see if it's JSON and decode it. 179 | if (is_string($value)) 180 | $value = Json::decodeIfJson($value); 181 | 182 | // End work-around 183 | 184 | /** @var ElementQuery $query */ 185 | 186 | $table = MapRecord::TableName; 187 | $alias = MapRecord::TableNameClean . '_' . $field->handle; 188 | $on = [ 189 | 'and', 190 | '[[elements.id]] = [[' . $alias . '.ownerId]]', 191 | '[[elements.dateDeleted]] IS NULL', 192 | '[[elements_sites.siteId]] = [[' . $alias . '.ownerSiteId]]', 193 | '[[' . $alias . '.fieldId]] = ' . $field->id, 194 | ]; 195 | 196 | $query->subQuery->join('LEFT JOIN', $table . ' ' . $alias, $on); 197 | 198 | if ($value === ':empty:') 199 | { 200 | $query->subQuery->andWhere([ 201 | '[[' . $alias . '.lat]]' => null, 202 | ]); 203 | 204 | return; 205 | } 206 | else if ($value === ':notempty:' || $value === 'not :empty:') 207 | { 208 | $query->subQuery->andWhere([ 209 | 'not', 210 | ['[[' . $alias . '.lat]]' => null], 211 | ]); 212 | 213 | return; 214 | } 215 | 216 | $oldOrderBy = null; 217 | $search = false; 218 | 219 | if (!is_array($query->orderBy)) 220 | { 221 | $oldOrderBy = $query->orderBy; 222 | $query->orderBy = []; 223 | } 224 | 225 | // Coordinate CraftQL support 226 | if (array_key_exists('coordinate', $value)) 227 | $value['location'] = $value['coordinate']; 228 | 229 | if (array_key_exists('location', $value)) 230 | $search = $this->_searchLocation($query, $value, $alias); 231 | 232 | if (array_key_exists('distance', $query->orderBy)) 233 | $this->_replaceOrderBy($query, $search); 234 | 235 | if (empty($query->orderBy)) 236 | $query->orderBy = $oldOrderBy; 237 | } 238 | 239 | /** 240 | * Populates any missing location data 241 | * 242 | * @param Map $map 243 | * @param MapField $field 244 | * 245 | * @throws Exception 246 | */ 247 | public function populateMissingData (Map $map, MapField $field) 248 | { 249 | $settings = SimpleMap::getInstance()->getSettings(); 250 | 251 | // Missing zoom 252 | if (!$map->zoom) 253 | $map->zoom = $field->zoom; 254 | 255 | // Skip the rest if populate missing is disabled 256 | if ($settings->disablePopulateMissingFieldData) 257 | return; 258 | 259 | $postcode = is_array($map->parts) 260 | ? @$map->parts['postcode'] 261 | : $map->parts->postcode; 262 | 263 | // Missing Lat / Lng 264 | if (!($map->lat && $map->lng) && !empty($map->address ?: $postcode)) 265 | { 266 | $latLng = GeoService::latLngFromAddress($map->address ?: $postcode); 267 | if ($latLng) 268 | { 269 | $map->lat = $latLng['lat']; 270 | $map->lng = $latLng['lng']; 271 | } 272 | } 273 | 274 | // Missing address / parts 275 | if ((!$map->address || $map->address === $postcode) && ($map->lat && $map->lng)) 276 | { 277 | $loc = GeoService::addressFromLatLng($map->lat, $map->lng); 278 | if ($loc) 279 | { 280 | $map->address = $loc['address']; 281 | $map->parts = array_merge( 282 | array_filter((array) $loc['parts']), 283 | array_filter((array) $map->parts) 284 | ); 285 | } 286 | } 287 | 288 | // Missing what3words 289 | if ($settings->isW3WEnabled() && empty($map->what3words)) 290 | $map->what3words = What3WordsService::convertLatLngToW3W($map->lat, $map->lng); 291 | } 292 | 293 | // Private Methods 294 | // ========================================================================= 295 | 296 | /** 297 | * Filters the query by location. 298 | * 299 | * Returns either `false` if we can't filter by location, or the location 300 | * search string if we can. 301 | * 302 | * @param ElementQuery $query 303 | * @param mixed $value 304 | * @param string $table 305 | * 306 | * @return bool|string 307 | * @throws Exception 308 | */ 309 | private function _searchLocation (ElementQuery $query, mixed $value, string $table): bool|string 310 | { 311 | $location = $value['location']; 312 | $country = $value['country'] ?? null; 313 | $radius = $value['radius'] ?? 50.0; 314 | $unit = $value['unit'] ?? 'km'; 315 | 316 | // Normalize location 317 | $location = GeoService::normalizeLocation($location, $country); 318 | 319 | // If we don't have a location, reduce the search radius to 0 320 | if (empty($location)) 321 | { 322 | $location = ['lat' => 0, 'lng' => 0]; 323 | $radius = 0; 324 | } 325 | 326 | $lat = $location['lat']; 327 | $lng = $location['lng']; 328 | 329 | // Normalize radius 330 | if (!is_numeric($radius)) 331 | $radius = (float) $radius; 332 | 333 | if (!is_numeric($radius)) 334 | $radius = 50.0; 335 | 336 | // Normalize unit 337 | $unit = GeoService::normalizeDistance($unit); 338 | 339 | // Base Distance 340 | $distance = $unit === 'km' ? '111.045' : '69.0'; 341 | 342 | // Store for populating search result distance 343 | $this->_location = $location; 344 | $this->_distance = (float) $distance; 345 | 346 | // Search Query 347 | $search = str_replace(["\r", "\n", "\t"], '', "( 348 | $distance * 349 | DEGREES( 350 | ACOS( 351 | COS(RADIANS($lat)) * 352 | COS(RADIANS([[$table.lat]])) * 353 | COS(RADIANS($lng) - RADIANS([[$table.lng]])) + 354 | SIN(RADIANS($lat)) * 355 | SIN(RADIANS([[$table.lat]])) 356 | ) 357 | ) 358 | )"); 359 | 360 | // Restrict the results 361 | $restrict = [ 362 | 'and', 363 | [ 364 | 'and', 365 | "[[$table.lat]] >= $lat - ($radius / $distance)", 366 | "[[$table.lat]] <= $lat + ($radius / $distance)", 367 | ], 368 | [ 369 | 'and', 370 | "[[$table.lng]] >= $lng - ($radius / ($distance * COS(RADIANS($lat))))", 371 | "[[$table.lng]] <= $lng + ($radius / ($distance * COS(RADIANS($lat))))", 372 | ] 373 | ]; 374 | 375 | // Filter the query 376 | $query 377 | ->subQuery 378 | ->addSelect($search . ' as [[distance]]') 379 | ->andWhere($restrict) 380 | ->andWhere([ 381 | 'not', 382 | ['[[' . $table . '.lat]]' => null], 383 | ]); 384 | 385 | if (Craft::$app->getDb()->driverName === 'pgsql') 386 | $query->subQuery->andWhere($search . ' <= ' . $radius); 387 | else 388 | $query->subQuery->andHaving('[[distance]] <= ' . $radius); 389 | 390 | return '[[distance]]'; 391 | } 392 | 393 | /** 394 | * Will replace the distance search with the correct query if available, 395 | * or otherwise remove it. 396 | * 397 | * @param ElementQuery $query 398 | * @param bool $search 399 | */ 400 | private function _replaceOrderBy (ElementQuery $query, string $search = "") 401 | { 402 | $nextOrder = []; 403 | 404 | foreach ((array) $query->orderBy as $order => $sort) 405 | { 406 | if ($order === 'distance' && !empty($search)) $nextOrder[$search] = $sort; 407 | elseif ($order !== 'distance') $nextOrder[$order] = $sort; 408 | } 409 | 410 | $query->subQuery->orderBy($nextOrder); 411 | $query->orderBy($nextOrder); 412 | } 413 | 414 | } 415 | -------------------------------------------------------------------------------- /src/services/StaticService.php: -------------------------------------------------------------------------------- 1 | getSettings(); 47 | 48 | switch ($settings->mapTiles) { 49 | case MapTiles::GoogleHybrid: 50 | case MapTiles::GoogleRoadmap: 51 | case MapTiles::GoogleTerrain: 52 | return $this->_generateGoogle($options, $settings); 53 | case MapTiles::MapKitHybrid: 54 | case MapTiles::MapKitMutedStandard: 55 | case MapTiles::MapKitSatellite: 56 | case MapTiles::MapKitStandard: 57 | return $this->_generateApple($options, $settings); 58 | case MapTiles::MapboxDark: 59 | case MapTiles::MapboxLight: 60 | case MapTiles::MapboxOutdoors: 61 | case MapTiles::MapboxStreets: 62 | return $this->_generateMapbox($options, $settings); 63 | case MapTiles::HereHybrid: 64 | case MapTiles::HereNormalDay: 65 | case MapTiles::HereNormalDayGrey: 66 | case MapTiles::HereNormalDayTransit: 67 | case MapTiles::HerePedestrian: 68 | case MapTiles::HereReduced: 69 | case MapTiles::HereSatellite: 70 | case MapTiles::HereTerrain: 71 | return $this->_generateHere($options, $settings); 72 | default: 73 | return $this->_generateDefault($options); 74 | } 75 | } 76 | 77 | // Generators 78 | // ========================================================================= 79 | 80 | /** 81 | * @param StaticOptions $options 82 | * @param Settings $settings 83 | * 84 | * @return string 85 | * @throws Exception 86 | */ 87 | private function _generateGoogle (StaticOptions $options, Settings $settings): string 88 | { 89 | $params = [ 90 | 'center' => implode(',', $options->getCenter()), 91 | 'zoom' => $options->zoom, 92 | 'size' => $options->getSize(), 93 | 'scale' => $options->scale, 94 | 'language' => Craft::$app->getLocale()->getLanguageID(), 95 | 'region' => $this->_getTld(), 96 | 'key' => GeoService::getToken( 97 | $settings->getMapToken(), 98 | $settings->mapTiles 99 | ), 100 | ]; 101 | 102 | $markersString = ''; 103 | if (!empty($options->markers)) 104 | { 105 | $markers = []; 106 | 107 | /** @var Marker $marker */ 108 | foreach ($options->markers as $marker) 109 | { 110 | $m = [ 111 | 'color:' . str_replace('#', '0x', $marker->color), 112 | ]; 113 | 114 | if ($marker->label !== null) 115 | $m[] = 'label:' . strtoupper($marker->label); 116 | 117 | $m[] = $marker->getLocation(); 118 | 119 | $markers[] = implode('|', $m); 120 | } 121 | 122 | $markersString = '&markers=' . implode('&markers=', $markers); 123 | } 124 | 125 | $params['maptype'] = match ($settings->mapTiles) 126 | { 127 | MapTiles::GoogleTerrain => 'terrain', 128 | MapTiles::GoogleRoadmap => 'roadmap', 129 | MapTiles::GoogleHybrid => 'hybrid', 130 | }; 131 | 132 | return 'https://maps.googleapis.com/maps/api/staticmap?' . http_build_query($params) . $markersString; 133 | } 134 | 135 | /** 136 | * @param StaticOptions $options 137 | * @param Settings $settings 138 | * 139 | * @return string 140 | * @throws Exception 141 | */ 142 | private function _generateApple (StaticOptions $options, Settings $settings): string 143 | { 144 | $params = [ 145 | 'center' => implode(',', $options->getCenter()), 146 | 'z' => $options->zoom, 147 | 'size' => $options->getSize(), 148 | 'scale' => $options->scale, 149 | 'lang' => Craft::$app->getLocale()->getLanguageID(), 150 | 'teamId' => $settings->getMapToken()['teamId'], 151 | 'keyId' => $settings->getMapToken()['keyId'], 152 | ]; 153 | 154 | if (!empty($options->markers)) 155 | { 156 | $params['annotations'] = []; 157 | 158 | foreach ($options->markers as $marker) 159 | { 160 | $params['annotations'][] = [ 161 | 'color' => str_replace('#', '', $marker->color), 162 | 'glyphText' => $marker->label, 163 | 'point' => $marker->getLocation(true), 164 | ]; 165 | } 166 | 167 | $params['annotations'] = Json::encode($params['annotations']); 168 | } 169 | 170 | switch ($settings->mapTiles) 171 | { 172 | case MapTiles::MapKitStandard: 173 | $params['type'] = 'standard'; 174 | break; 175 | case MapTiles::MapKitSatellite: 176 | $params['type'] = 'satellite'; 177 | break; 178 | case MapTiles::MapKitMutedStandard: 179 | $params['type'] = 'mutedStandard'; 180 | break; 181 | case MapTiles::MapKitHybrid: 182 | $params['type'] = 'hybrid'; 183 | break; 184 | } 185 | 186 | $path = '/api/v1/snapshot?' . http_build_query($params); 187 | openssl_sign($path, $signature, $settings->getMapToken()['privateKey'], OPENSSL_ALGO_SHA256); 188 | $signature = $this->_encode($signature); 189 | 190 | return 'https://snapshot.apple-mapkit.com' . $path . '&signature=' . $signature; 191 | } 192 | 193 | /** 194 | * @param StaticOptions $options 195 | * @param Settings $settings 196 | * 197 | * @return string 198 | * @throws Exception 199 | */ 200 | private function _generateMapbox (StaticOptions $options, Settings $settings): string 201 | { 202 | $url = 'https://api.mapbox.com/styles/v1/mapbox/'; 203 | 204 | switch ($settings->mapTiles) 205 | { 206 | case MapTiles::MapboxStreets: 207 | $url .= 'streets-v11'; 208 | break; 209 | case MapTiles::MapboxOutdoors: 210 | $url .= 'outdoors-v11'; 211 | break; 212 | case MapTiles::MapboxLight: 213 | $url .= 'light-v10'; 214 | break; 215 | case MapTiles::MapboxDark: 216 | $url .= 'dark-v10'; 217 | break; 218 | } 219 | 220 | $url .= '/static/'; 221 | 222 | if (!empty($options->markers)) 223 | { 224 | $markers = []; 225 | $i = 0; 226 | 227 | foreach ($options->markers as $marker) 228 | { 229 | $m = 'pin-l-'; 230 | $m .= strtolower($marker->label ?: ++$i); 231 | $m .= '+' . str_replace('#', '', $marker->color); 232 | $m .= '(' . implode(',', array_reverse(explode(',', $marker->getLocation(true)))) . ')'; 233 | 234 | $markers[] = $m; 235 | } 236 | 237 | $url .= implode(',', $markers) . '/'; 238 | } 239 | 240 | $center = $options->getCenter(); 241 | $url .= $center['lng'] . ','; 242 | $url .= $center['lat'] . ','; 243 | $url .= $options->zoom . ',0,0'; 244 | $url .= '/' . $options->getSize(); 245 | 246 | if ($options->scale > 1) 247 | $url .= '@2x'; 248 | 249 | return $url . '?access_token=' . $settings->getMapToken(); 250 | } 251 | 252 | /** 253 | * @param StaticOptions $options 254 | * @param Settings $settings 255 | * 256 | * @return string 257 | * @throws Exception 258 | */ 259 | private function _generateHere (StaticOptions $options, Settings $settings): string 260 | { 261 | $params = [ 262 | 'app_id' => $settings->getMapToken()['appId'], 263 | 'app_code' => $settings->getMapToken()['appCode'], 264 | 'nodot' => true, 265 | 'c' => implode(',', $options->getCenter()), 266 | 'z' => $options->zoom, 267 | 'w' => $options->width * $options->scale, 268 | 'h' => $options->height * $options->scale, 269 | ]; 270 | 271 | switch ($settings->mapTiles) 272 | { 273 | case MapTiles::HereHybrid: 274 | $params['t'] = 3; 275 | break; 276 | case MapTiles::HereNormalDay: 277 | $params['t'] = 0; 278 | break; 279 | case MapTiles::HereNormalDayGrey: 280 | $params['t'] = 5; 281 | break; 282 | case MapTiles::HereNormalDayTransit: 283 | $params['t'] = 4; 284 | break; 285 | case MapTiles::HereReduced: 286 | $params['t'] = 6; 287 | break; 288 | case MapTiles::HerePedestrian: 289 | $params['t'] = 13; 290 | break; 291 | case MapTiles::HereSatellite: 292 | $params['t'] = 1; 293 | break; 294 | case MapTiles::HereTerrain: 295 | $params['t'] = 2; 296 | break; 297 | } 298 | 299 | if (!empty($options->markers)) 300 | { 301 | $i = 0; 302 | 303 | foreach ($options->markers as $marker) 304 | { 305 | $m = [$marker->getLocation(true)]; 306 | $m[] = str_replace('#', '', $marker->color); 307 | $m[] = StaticMap::getLabelColour($marker->color); 308 | $m[] = 18; 309 | $m[] = $marker->label ?: ++$i; 310 | 311 | $params['poix' . ($i - 1)] = implode(';', $m); 312 | } 313 | } 314 | 315 | return 'https://image.maps.api.here.com/mia/1.6/mapview?' . http_build_query($params); 316 | } 317 | 318 | /** 319 | * @param StaticOptions $options 320 | * 321 | * @return string 322 | * @throws Exception 323 | */ 324 | private function _generateDefault (StaticOptions $options): string 325 | { 326 | $center = $options->getCenter(); 327 | 328 | return UrlHelper::actionUrl( 329 | 'simplemap/static', 330 | [ 331 | 'lat' => $center['lat'], 332 | 'lng' => $center['lng'], 333 | 'zoom' => $options->zoom, 334 | 'width' => $options->width, 335 | 'height' => $options->height, 336 | 'scale' => $options->scale, 337 | 'markers' => urlencode(implode(';', $options->markers)), 338 | 'csrf' => Craft::$app->getRequest()->getCsrfToken(), 339 | ] 340 | ); 341 | } 342 | 343 | // Helpers 344 | // ========================================================================= 345 | 346 | private function _getTld (): array 347 | { 348 | $url = 'http://' . $_SERVER['SERVER_NAME']; 349 | 350 | return explode(".", parse_url($url, PHP_URL_HOST)); 351 | } 352 | 353 | private function _encode ($data): string 354 | { 355 | $encoded = strtr(base64_encode($data), '+/', '-_'); 356 | 357 | return rtrim($encoded, '='); 358 | } 359 | 360 | } 361 | -------------------------------------------------------------------------------- /src/services/What3WordsService.php: -------------------------------------------------------------------------------- 1 | convertTo3wa($lat, $lng)['words'] ?? false; 26 | } 27 | 28 | private static function _geocoder () 29 | { 30 | static $geocoder; 31 | 32 | if ($geocoder) 33 | return $geocoder; 34 | 35 | return $geocoder = new Geocoder(SimpleMap::getInstance()->getSettings()->getW3WToken()); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/templates/_feedme-mapping.twig: -------------------------------------------------------------------------------- 1 | {# ------------------------ #} 2 | {# Available Variables #} 3 | {# ------------------------ #} 4 | {# Attributes: #} 5 | {# type, name, handle, instructions, attribute, default, feed, feedData #} 6 | {# ------------------------ #} 7 | {# Fields: #} 8 | {# name, handle, instructions, feed, feedData, field, fieldClass #} 9 | {# ------------------------ #} 10 | 11 | {% import 'feed-me/_macros' as feedMeMacro %} 12 | {% import '_includes/forms' as forms %} 13 | 14 | {# Special case when inside another complex field (Matrix) #} 15 | {% if parentPath is defined %} 16 | {% set prefixPath = parentPath %} 17 | {% else %} 18 | {% set prefixPath = [handle] %} 19 | {% endif %} 20 | 21 | {% set classes = ['complex-field'] %} 22 | 23 | 24 | 25 |
26 |
27 | 28 |
29 | 30 |
31 | {% namespace 'fieldMapping[' ~ prefixPath | join('][') ~ ']' %} 32 | 33 | {% endnamespace %} 34 |
35 |
36 | 37 | 38 | 39 | {% set simpleMapSubfields = { 40 | lat: 'Latitude'|t('simplemap'), 41 | lng: 'Longitude'|t('simplemap'), 42 | zoom: 'Zoom'|t('simplemap'), 43 | address: 'Full Address'|t('simplemap'), 44 | 'parts.number': 'Number'|t('simplemap'), 45 | 'parts.address': 'Address'|t('simplemap'), 46 | 'parts.city': 'City'|t('simplemap'), 47 | 'parts.postcode': 'Postcode'|t('simplemap'), 48 | 'parts.county': 'County'|t('simplemap'), 49 | 'parts.state': 'State'|t('simplemap'), 50 | 'parts.country': 'Country'|t('simplemap'), 51 | } %} 52 | 53 | {% for key, col in simpleMapSubfields %} 54 | {% set splitKey = '.' in key %} 55 | {% set subKey = null %} 56 | 57 | {% if splitKey %} 58 | {% set key = key|split('.') %} 59 | {% set subKey = key[1] %} 60 | {% set key = key[0] %} 61 | {% endif %} 62 | 63 | {% set nameLabel = col %} 64 | {% set instructionsHandle = handle ~ '[' ~ key ~ ']' ~ (subKey ? '[' ~ subKey ~ ']') %} 65 | 66 | {% set path = prefixPath | merge ([ 'fields', key, subKey ]|filter) %} 67 | 68 | {% set default = default ?? { 69 | type: 'text', 70 | } %} 71 | 72 | {% embed 'feed-me/_includes/fields/_base' %} 73 | {% block additionalFieldSettings %} 74 | 75 | {% endblock %} 76 | 77 | {% block fieldSettings %} 78 | 79 | {% endblock %} 80 | {% endembed %} 81 | {% endfor %} 82 | -------------------------------------------------------------------------------- /src/templates/field-settings.twig: -------------------------------------------------------------------------------- 1 | {% import '_includes/forms' as forms %} 2 | 3 | {{ forms.field({ 4 | label: 'Initial Location'|t('simplemap'), 5 | instructions: 'The initial location and zoom that will show in the map field'|t('simplemap'), 6 | }, map) }} 7 | 8 | {{ forms.lightswitchField({ 9 | label: 'Hide Search'|t('simplemap'), 10 | instructions: 'Hide the location search field'|t('simplemap'), 11 | id: 'hideSearch', 12 | name: 'hideSearch', 13 | on: field.hideSearch 14 | }) }} 15 | 16 | {{ forms.lightswitchField({ 17 | label: 'Hide Map'|t('simplemap'), 18 | instructions: 'Hide the map'|t('simplemap'), 19 | id: 'hideMap', 20 | name: 'hideMap', 21 | on: field.hideMap 22 | }) }} 23 | 24 | {{ forms.lightswitchField({ 25 | label: 'Hide Address'|t('simplemap'), 26 | instructions: 'Hide the address fields'|t('simplemap'), 27 | id: 'hideAddress', 28 | name: 'hideAddress', 29 | on: field.hideAddress 30 | }) }} 31 | 32 | {{ forms.lightswitchField({ 33 | label: 'Show Latitude / Longitude'|t('simplemap'), 34 | instructions: 'Show the latitude / longitude fields'|t('simplemap'), 35 | id: 'showLatLng', 36 | name: 'showLatLng', 37 | on: field.showLatLng 38 | }) }} 39 | 40 | {{ forms.lightswitchField({ 41 | label: 'Show Current Location'|t('simplemap'), 42 | instructions: 'Show a button to centre the map on the users current location'|t('simplemap'), 43 | id: 'showCurrentLocation', 44 | name: 'showCurrentLocation', 45 | on: field.showCurrentLocation 46 | }) }} 47 | 48 | {{ forms.selectField({ 49 | label: 'Field Size'|t('simplemap'), 50 | instructions: 'Choose the size of the field to display'|t('simplemap'), 51 | id: 'size', 52 | name: 'size', 53 | options: { 54 | 'normal': 'Normal'|t('simplemap'), 55 | 'mini': 'Mini'|t('simplemap'), 56 | }, 57 | value: field.size, 58 | }) }} 59 | 60 | {{ forms.selectField({ 61 | label: 'Preferred Country'|t('simplemap'), 62 | instructions: 'When searching for a location, results in this country will take precedence. Be aware that some services will show results ONLY within this country.'|t('simplemap'), 63 | id: 'country', 64 | name: 'country', 65 | options: countries, 66 | value: field.country, 67 | }) }} 68 | 69 | {{ forms.textField({ 70 | label: 'Min Zoom'|t('simplemap'), 71 | instructions: 'The minimum level the user can zoom the map out to.'|t('simplemap'), 72 | id: 'minZoom', 73 | name: 'minZoom', 74 | type: 'number', 75 | value: field.minZoom, 76 | min: 0, 77 | max: 18, 78 | step: 0.01, 79 | }) }} 80 | 81 | {{ forms.textField({ 82 | label: 'Max Zoom'|t('simplemap'), 83 | instructions: 'The maximum level the user can zoom the map in to.'|t('simplemap'), 84 | id: 'maxZoom', 85 | name: 'maxZoom', 86 | type: 'number', 87 | value: field.maxZoom, 88 | min: 0, 89 | max: 18, 90 | step: 0.01, 91 | }) }} 92 | 93 | {% if settings.isW3WEnabled() %} 94 | {{ forms.lightswitchField({ 95 | label: 'Enable what3words grid'|t('simplemap'), 96 | instructions: 'Will enable the what3words grid overlay when zoomed in above 17'|t('simplemap'), 97 | id: 'showW3WGrid', 98 | name: 'showW3WGrid', 99 | on: field.showW3WGrid 100 | }) }} 101 | 102 | {{ forms.lightswitchField({ 103 | label: 'Show what3words field'|t('simplemap'), 104 | instructions: 'Show the what3words field for the selected location'|t('simplemap'), 105 | id: 'showW3WField', 106 | name: 'showW3WField', 107 | on: field.showW3WField 108 | }) }} 109 | {% endif %} 110 | -------------------------------------------------------------------------------- /src/translations/en/simplemap.php: -------------------------------------------------------------------------------- 1 | 'Map', 15 | 'Geo-Coding' => 'Geo-Coding', 16 | 17 | 'Search for a location' => 'Search for a location', 18 | 'Clear address' => 'Clear address', 19 | 'No address selected' => 'No address selected', 20 | 21 | 'Full Address' => 'Full Address', 22 | 'Name / Number' => 'Name / Number', 23 | 'Street Address' => 'Street Address', 24 | 'Town / City' => 'Town / City', 25 | 'Postcode' => 'Postcode', 26 | 'County' => 'County', 27 | 'State' => 'State', 28 | 'Country' => 'Country', 29 | 30 | 'Latitude' => 'Latitude', 31 | 'Longitude' => 'Longitude', 32 | 33 | 'Zoom In' => 'Zoom In', 34 | 'Zoom Out' => 'Zoom Out', 35 | 'Center on Marker' => 'Center on Marker', 36 | 'Current Location' => 'Current Location', 37 | 38 | // Field: Settings 39 | // ------------------------------------------------------------------------- 40 | 41 | 'Initial Location' => 'Initial Location', 42 | 'The initial location and zoom that will show in the map field' => 43 | 'The initial location and zoom that will show in the map field', 44 | 45 | 'Hide Search' => 'Hide Search', 46 | 'Hide the location search field' => 'Hide the location search field', 47 | 48 | 'Hide Map' => 'Hide Map', 49 | 'Hide the map' => 'Hide the map', 50 | 51 | 'Hide Address' => 'Hide Address', 52 | 'Hide the address fields' => 'Hide the address fields', 53 | 54 | 'Show Latitude / Longitude' => 'Show Latitude / Longitude', 55 | 'Show the latitude / longitude fields' => 'Show the latitude / longitude fields', 56 | 57 | 'Show Current Location' => 'Show Current Location', 58 | 'Show a button to centre the map on the users current location' => 59 | 'Show a button to centre the map on the users current location', 60 | 61 | 'Field Size' => 'Field Size', 62 | 'Choose the size of the field to display' => 'Choose the size of the field to display', 63 | 'Normal' => 'Normal', 64 | 'Mini' => 'Mini', 65 | 66 | 'All Countries' => 'All Countries', 67 | 'Preferred Country' => 'Preferred Country', 68 | 'When searching for a location, results in this country will take precedence. Be aware that some services will show results ONLY within this country.' => 69 | 'When searching for a location, results in this country will take precedence. Be aware that some services will show results ONLY within this country.', 70 | 71 | 'Enable what3words grid' => 'Enable what3words grid', 72 | 'Will enable the what3words grid overlay when zoomed in above 17' => 'Will enable the what3words grid overlay when zoomed in above 17', 73 | 74 | 'Show what3words field' => 'Show what3words field', 75 | 'Show the what3words field for the selected location' => 'Show the what3words field for the selected location', 76 | 77 | // Settings 78 | // ========================================================================= 79 | 80 | 'Select your map style' => 'Select your map style', 81 | 'Map Tiles' => 'Map Tiles', 82 | 'Select the style of map tiles.' => 'Select the style of map tiles.', 83 | 84 | 'Map Token' => 'Map Token', 85 | 'Add the API key for map tiles service you are using.' => 86 | 'Add the API key for map tiles service you are using.', 87 | 88 | 'Geo Service' => 'Geo Service', 89 | 'Select the service to be used for Geocoding.' => 90 | 'Select the service to be used for Geocoding.', 91 | 92 | 'Geo Token' => 'Geo Token', 93 | 'Add the API key for the geocoding service.' => 94 | 'Add the API key for the geocoding service.', 95 | 96 | 'Private Key' => 'Private Key', 97 | 'Paste the contents of your private key files below (supports env variables).' => 'Paste the contents of your private key files below (supports env variables).', 98 | 99 | 'Key ID' => 'Key ID', 100 | 'The ID of the key associated with your private key.' => 'The ID of the key associated with your private key.', 101 | 102 | 'Team ID' => 'Team ID', 103 | 'The team ID that created the key ID and private key.' => 'The team ID that created the key ID and private key.', 104 | 105 | 'Notice' => 'Notice', 106 | 'MapKit does not support individual address parts.' => 'MapKit does not support individual address parts.', 107 | 108 | 'App ID' => 'App ID', 109 | 'Your Here app ID.' => 'Your Here app ID.', 110 | 111 | 'App Code' => 'App Code', 112 | 'Your Here app code.' => 'Your Here app code.', 113 | 114 | 'API Key' => 'API Key', 115 | 'Your Here API Key (only required for front-end embed maps).' => 'Your Here API Key (only required for front-end embed maps).', 116 | 117 | 'Geolocation Service' => 'Geolocation Service', 118 | 'Select the service to be used for Geolocating users.' => 'Select the service to be used for Geolocating users.', 119 | 120 | 'Geolocation Token' => 'Geolocation Token', 121 | 'Add the API key for the geolocation service.' => 'Add the API key for the geolocation service.', 122 | 123 | 'Account ID' => 'Account ID', 124 | 'Your MaxMind account ID.' => 'Your MaxMind account ID.', 125 | 126 | 'License Key' => 'License Key', 127 | 'Your MaxMind license key.' => 'Your MaxMind license key.', 128 | 129 | 'what3words' => 'what3words', 130 | 'Enable what3words Integration' => 'Enable what3words Integration', 131 | 'what3words Token' => 'what3words Token', 132 | 'Your what3words API key.' => 'Your what3words API key.', 133 | 134 | 'Getting a what3words API Key' => 'Getting a what3words API Key', 135 | 'what3words offer a free public API key.' => 'what3words offer a free public API key.', 136 | 137 | // Settings: Map Tiles Options 138 | // ------------------------------------------------------------------------- 139 | 140 | 'Open Source' => 'Open Source', 141 | 142 | 'Wikimedia' => 'Wikimedia', 143 | 144 | 'OpenStreetMap' => 'OpenStreetMap', 145 | 146 | 'Carto: Voyager' => 'Carto: Voyager', 147 | 'Carto: Positron' => 'Carto: Positron', 148 | 'Carto: Dark Matter' => 'Carto: Dark Matter', 149 | 150 | 'Requires API Key (Token)' => 'Requires API Key (Token)', 151 | 152 | 'Mapbox: Outdoors' => 'Mapbox: Outdoors', 153 | 'Mapbox: Streets' => 'Mapbox: Streets', 154 | 'Mapbox: Light' => 'Mapbox: Light', 155 | 'Mapbox: Dark' => 'Mapbox: Dark', 156 | 157 | 'Google Maps: Roadmap' => 'Google Maps: Roadmap', 158 | 'Google Maps: Terrain' => 'Google Maps: Terrain', 159 | 'Google Maps: Hybrid' => 'Google Maps: Hybrid', 160 | 161 | 'Apple MapKit: Standard' => 'Apple MapKit: Standard', 162 | 'Apple MapKit: Muted Standard' => 'Apple MapKit: Muted Standard', 163 | 'Apple MapKit: Satellite' => 'Apple MapKit: Satellite', 164 | 'Apple MapKit: Hybrid' => 'Apple MapKit: Hybrid', 165 | 166 | 'Here: Normal Day' => 'Here: Normal Day', 167 | 'Here: Normal Day Grey' => 'Here: Normal Day Grey', 168 | 'Here: Normal Day Transit' => 'Here: Normal Day Transit', 169 | 'Here: Reduced' => 'Here: Reduced', 170 | 'Here: Pedestrian' => 'Here: Pedestrian', 171 | 'Here: Terrain' => 'Here: Terrain', 172 | 'Here: Satellite' => 'Here: Satellite', 173 | 'Here: Hybrid' => 'Here: Hybrid', 174 | 175 | // Settings: Geo Service Options 176 | // ------------------------------------------------------------------------- 177 | 178 | 'Nominatim' => 'Nominatim', 179 | 'Mapbox' => 'Mapbox', 180 | 'Google Maps' => 'Google Maps', 181 | 'Apple MapKit' => 'Apple MapKit', 182 | 'Here' => 'Here', 183 | 184 | // Settings: Geo Location Services 185 | // ------------------------------------------------------------------------- 186 | 187 | 'None' => 'None', 188 | 'ipstack' => 'ipstack', 189 | 'MaxMind (Lite, ~60MB download)' => 'MaxMind (Lite, ~60MB download)', 190 | 'MaxMind' => 'MaxMind', 191 | 192 | // Settings: Info 193 | // ------------------------------------------------------------------------- 194 | 195 | 'Getting API Keys' => 'Getting API Keys', 196 | 'You will need to enable the **Maps JavaScript API** and **Places API** for if using Google for the map tiles, and the **Places API** and **Geocoding API** if you are using it for the Geo service.' => 197 | 'You will need to enable the **Maps JavaScript API** and **Places API** for if using Google for the map tiles, and the **Places API** and **Geocoding API** if you are using it for the Geo service.', 198 | 'You can use the same key for both map tiles and geo service, no configuration needed!' => 199 | 'You can use the same key for both map tiles and geo service, no configuration needed!', 200 | 'We currently only support Apple MapKit for map tiles only.' => 201 | 'We currently only support Apple MapKit for map tiles only.', 202 | 203 | 'Getting Geolocation API Keys' => 'Getting Geolocation API Keys', 204 | 'ipstack offer free and paid-for versions of their API.' => 'ipstack offer free and paid-for versions of their API.', 205 | 'MaxMind offer free lookup database that must be stored locally, and a paid-for version of their API.' => 206 | 'MaxMind offer free lookup database that must be stored locally, and a paid-for version of their API.', 207 | 208 | ]; 209 | -------------------------------------------------------------------------------- /src/utilities/StaticMap.php: -------------------------------------------------------------------------------- 1 | lat = $lat; 93 | $this->lng = $lng; 94 | $this->width = $width; 95 | $this->height = $height; 96 | $this->zoom = $zoom; 97 | $this->scale = $scale; 98 | 99 | if (empty($markers)) $this->markers = []; 100 | else 101 | { 102 | $this->markers = array_map(function ($m) { 103 | $m = explode('|', $m); 104 | 105 | return new Marker([ 106 | 'location' => Json::decode($m[0]), 107 | 'color' => $m[1], 108 | 'label' => $m[2], 109 | ]); 110 | }, explode(';', urldecode($markers))); 111 | } 112 | 113 | /** @var Settings $settings */ 114 | $settings = SimpleMap::getInstance()->getSettings(); 115 | $tiles = MapTiles::getTiles($settings->mapTiles, $scale); 116 | $this->tiles = $tiles['url']; 117 | $this->tileSize = $tiles['size']; 118 | $this->mapTiles = $settings->mapTiles; 119 | } 120 | 121 | // Public Methods 122 | // ========================================================================= 123 | 124 | public function render () 125 | { 126 | $filename = $this->_mapCacheIdToFilename(); 127 | 128 | if ($this->_checkMapCache()) 129 | return $this->_send(file_get_contents($filename)); 130 | 131 | $this->_initCoords(); 132 | $this->_createBaseMap(); 133 | $this->_placeMarkers(); 134 | 135 | self::_mkdirRecursive(dirname($filename), 0777); 136 | $this->image->save($filename); 137 | 138 | if (file_exists($filename)) 139 | return $this->_send(file_get_contents($filename)); 140 | 141 | return $this->_send($this->image->show('png')); 142 | } 143 | 144 | // Private Methods 145 | // ========================================================================= 146 | 147 | private function _initCoords (): void 148 | { 149 | $this->centerX = $this->_lngToTile($this->lng); 150 | $this->centerY = $this->_latToTile($this->lat); 151 | } 152 | 153 | private function _createBaseMap (): void 154 | { 155 | $imagine = $this->_getImagine(); 156 | $palette = new RGB(); 157 | 158 | $w = $this->width * $this->scale; 159 | $h = $this->height * $this->scale; 160 | $_ts = $this->tileSize * $this->scale; 161 | 162 | $this->image = $imagine->create(new Box($w, $h)); 163 | 164 | $startX = floor($this->centerX - ($w / $_ts) / 2); 165 | $startY = floor($this->centerY - ($h / $_ts) / 2); 166 | 167 | $endX = ceil($this->centerX + ($w / $_ts) / 2); 168 | $endY = ceil($this->centerY + ($h / $_ts) / 2); 169 | 170 | $this->offsetX = -floor(($this->centerX - floor($this->centerX)) * $_ts); 171 | $this->offsetY = -floor(($this->centerY - floor($this->centerY)) * $_ts); 172 | 173 | $this->offsetX += floor($w / 2); 174 | $this->offsetY += floor($h / 2); 175 | 176 | $this->offsetX += floor($startX - floor($this->centerX)) * $_ts; 177 | $this->offsetY += floor($startY - floor($this->centerY)) * $_ts; 178 | 179 | for ($x = $startX; $x <= $endX; $x++) 180 | { 181 | for ($y = $startY; $y <= $endY; $y++) 182 | { 183 | $url = str_replace( 184 | ['{z}', '{x}', '{y}'], 185 | [$this->zoom, $x, $y], 186 | $this->tiles 187 | ); 188 | 189 | $tileData = $this->_fetchTile($url); 190 | 191 | if ($tileData) { 192 | $tileImg = $imagine->load($tileData); 193 | } else { 194 | $tileImg = $imagine->create(new Box($_ts, $_ts)); 195 | $tileImg->draw()->text( 196 | 'err', 197 | null, 198 | new Point($_ts / 2, $_ts / 2), 199 | $palette->color('#fff', 100) 200 | ); 201 | } 202 | 203 | $destX = ($x - $startX) * $_ts + $this->offsetX; 204 | $destY = ($y - $startY) * $_ts + $this->offsetY; 205 | 206 | $this->image->paste( 207 | $tileImg, 208 | new Point($destX, $destY) 209 | ); 210 | } 211 | } 212 | } 213 | 214 | private function _placeMarkers (): void 215 | { 216 | $w = $this->width * $this->scale; 217 | $h = $this->height * $this->scale; 218 | $_ts = $this->tileSize * $this->scale; 219 | 220 | /** @var Marker $marker */ 221 | foreach ($this->markers as $marker) 222 | { 223 | $img = $this->_renderMarker( 224 | $marker->color, 225 | $marker->label 226 | ); 227 | 228 | $pos = explode(',', $marker->getLocation(true)); 229 | 230 | $x = floor(($w / 2) - $_ts * ($this->centerX - $this->_lngToTile($pos[1]))); 231 | $y = floor(($h / 2) - $_ts * ($this->centerY - $this->_latToTile($pos[0]))); 232 | 233 | $x -= $img->getSize()->getWidth() / 2; 234 | $y -= $img->getSize()->getHeight(); 235 | 236 | $this->image->paste( 237 | $img, 238 | new Point($x, $y) 239 | ); 240 | } 241 | } 242 | 243 | private function _send ($file) 244 | { 245 | $response = Craft::$app->getResponse(); 246 | $response->format = Response::FORMAT_RAW; 247 | 248 | $expires = 60 * 60 * 24 * 14; 249 | $headers = $response->getHeaders(); 250 | $headers->set('content-type', 'image/png'); 251 | $headers->set('cache-control', 'maxage=' . $expires); 252 | $headers->set('expires', gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT'); 253 | 254 | return $file; 255 | } 256 | 257 | // Helpers 258 | // ========================================================================= 259 | 260 | public static function getLabelColour ($color): string 261 | { 262 | $r = hexdec($color[1] . $color[2]); 263 | $g = hexdec($color[3] . $color[4]); 264 | $b = hexdec($color[5] . $color[6]); 265 | 266 | return (($r * 299 + $g * 587 + $b * 114) / 1000 > 130) ? '000' : 'fff'; 267 | } 268 | 269 | private static function _join (): string 270 | { 271 | $paths = func_get_args(); 272 | $paths = array_map(function ($p) { 273 | return rtrim($p, '/'); 274 | }, $paths); 275 | $paths = array_filter($paths); 276 | 277 | return join('/', $paths); 278 | } 279 | 280 | private static function _mkdirRecursive ($pathname, $mode): bool 281 | { 282 | is_dir(dirname($pathname)) || self::_mkdirRecursive(dirname($pathname), $mode); 283 | return is_dir($pathname) || mkdir($pathname, $mode); 284 | } 285 | 286 | // Imagine 287 | // ------------------------------------------------------------------------- 288 | 289 | private function _getImageDriver () 290 | { 291 | static $driver; 292 | 293 | if ($driver) 294 | return $driver; 295 | 296 | $generalConfig = Craft::$app->getConfig()->getGeneral(); 297 | $extension = strtolower($generalConfig->imageDriver); 298 | 299 | if ($extension === 'gd' || Craft::$app->getImages()->getIsGd()) 300 | $driver = 'gd'; 301 | else 302 | $driver = 'imagick'; 303 | 304 | return $driver; 305 | } 306 | 307 | private function _getImagine () 308 | { 309 | static $imagine; 310 | 311 | if ($imagine) 312 | return $imagine; 313 | 314 | if ($this->_getImageDriver() === 'gd') { 315 | $imagine = new \Imagine\Gd\Imagine(); 316 | } else { 317 | $imagine = new \Imagine\Imagick\Imagine(); 318 | } 319 | 320 | return $imagine; 321 | } 322 | 323 | private function _getFont (ColorInterface $colour, $size = 10): \Imagine\Imagick\Font|FontInterface|\Imagine\Gd\Font 324 | { 325 | $key = ((string) $colour) . '-' . $size; 326 | 327 | /** @var FontInterface[] $fonts */ 328 | static $fonts = []; 329 | 330 | if (array_key_exists($key, $fonts)) 331 | return $fonts[$key]; 332 | 333 | $file = Craft::getAlias('@simplemap/resources/OpenSans-Bold.ttf'); 334 | 335 | if ($this->_getImageDriver() === 'gd') 336 | $fonts[$key] = new \Imagine\Gd\Font($file, $size, $colour); 337 | else 338 | $fonts[$key] = new \Imagine\Imagick\Font(new Imagick(), $file, $size, $colour); 339 | 340 | return $fonts[$key]; 341 | } 342 | 343 | private function _renderMarker ($colour, $label = null): ImageInterface 344 | { 345 | $resizeMultiplier = 0.1 * $this->scale; 346 | $fontSize = 12 * $this->scale; 347 | $fontOffset = 4 * $this->scale; 348 | 349 | $svg = $label === null ? 'markerNoLabel.png' : 'marker.png'; 350 | 351 | $img = $this->_getImagine()->open( 352 | Craft::getAlias('@simplemap/resources/' . $svg) 353 | ); 354 | $img->resize(new Box( 355 | $img->getSize()->getWidth() * $resizeMultiplier, 356 | $img->getSize()->getHeight() * $resizeMultiplier 357 | ), ImageInterface::FILTER_MITCHELL); 358 | $img->effects()->colorize($img->palette()->color($colour)); 359 | 360 | if ($label !== null) 361 | { 362 | $textColour = $img->palette()->color(self::getLabelColour($colour)); 363 | $imgCenter = new Center($img->getSize()); 364 | $font = $this->_getFont($textColour, $fontSize); 365 | $textCenter = new Center($font->box($label)); 366 | 367 | $img->draw()->text( 368 | $label, 369 | $font, 370 | new Point( 371 | $imgCenter->getX() - $textCenter->getX(), 372 | $fontOffset 373 | ) 374 | ); 375 | } 376 | 377 | return $img; 378 | } 379 | 380 | // Tiles 381 | // ------------------------------------------------------------------------- 382 | 383 | private function _latToTile ($lat): float|int 384 | { 385 | return (1 - log(tan($lat * pi() / 180) + 1 / cos($lat * pi() / 180)) / pi()) / 2 * pow(2, $this->zoom); 386 | } 387 | 388 | private function _lngToTile ($lng): float|int 389 | { 390 | return (($lng + 180) / 360) * pow(2, $this->zoom); 391 | } 392 | 393 | /** 394 | * @throws GuzzleException 395 | */ 396 | private function _fetchTile ($url): StreamInterface|string 397 | { 398 | if ($cached = $this->_checkTileCache($url)) 399 | return $cached; 400 | 401 | $client = new Client(); 402 | $res = $client->get($url); 403 | $tile = $res->getBody(); 404 | $this->_writeTileToCache($url, $tile); 405 | 406 | return $tile; 407 | } 408 | 409 | // Map 410 | // ------------------------------------------------------------------------- 411 | 412 | private function _getMapId (): string 413 | { 414 | return md5( 415 | http_build_query([ 416 | 'lat' => $this->lat, 417 | 'lng' => $this->lng, 418 | 'width' => $this->width, 419 | 'height' => $this->height, 420 | 'zoom' => $this->zoom, 421 | 'scale' => $this->scale, 422 | 'tiles' => $this->mapTiles, 423 | 'markers' => $this->markers, 424 | ]) 425 | ); 426 | } 427 | 428 | // Cache 429 | // ------------------------------------------------------------------------- 430 | 431 | private static function _tileCache (): bool|string 432 | { 433 | return Craft::getAlias(self::TILE_CACHE_DIR); 434 | } 435 | 436 | private static function _mapCache (): bool|string 437 | { 438 | return Craft::getAlias(self::MAP_CACHE_DIR); 439 | } 440 | 441 | private function _tileUrlToFilename ($url): string 442 | { 443 | return self::_join( 444 | self::_tileCache(), 445 | str_replace(['http://', 'https://'], '', $url) 446 | ); 447 | } 448 | 449 | private function _mapCacheIdToFilename (): string 450 | { 451 | $id = $this->_getMapId(); 452 | 453 | return self::_join( 454 | self::_mapCache(), 455 | substr($id, 0, 2), 456 | substr($id, 2, 2), 457 | substr($id, 4) 458 | ) . '.png'; 459 | } 460 | 461 | private function _checkTileCache ($url): bool|string|null 462 | { 463 | $filename = $this->_tileUrlToFilename($url); 464 | 465 | return file_exists($filename) ? file_get_contents($filename) : null; 466 | } 467 | 468 | private function _checkMapCache (): bool 469 | { 470 | return file_exists($this->_mapCacheIdToFilename()); 471 | } 472 | 473 | private function _writeTileToCache ($url, $data): void 474 | { 475 | $filename = $this->_tileUrlToFilename($url); 476 | self::_mkdirRecursive(dirname($filename), 0777); 477 | file_put_contents($filename, $data); 478 | } 479 | 480 | } 481 | -------------------------------------------------------------------------------- /src/web/Variable.php: -------------------------------------------------------------------------------- 1 | getSettings(); 38 | 39 | return GeoService::getToken( 40 | $settings->getMapToken(), 41 | $settings->mapTiles 42 | ); 43 | } 44 | 45 | /** 46 | * Returns the map token 47 | * 48 | * @deprecated as of 3.4.0 49 | * @return string 50 | * @throws DeprecationException 51 | */ 52 | public function getApiKey (): string 53 | { 54 | Craft::$app->getDeprecator()->log( 55 | 'Variable::getApiKey()', 56 | 'ether\simplemap\web\Variable::getApiKey() has been deprecated. Use `getMapToken()` instead.' 57 | ); 58 | 59 | return $this->getMapToken(); 60 | } 61 | 62 | /** 63 | * Returns the current users approximate location 64 | * 65 | * @param string|null $ip - Override the lookup IP 66 | * 67 | * @return UserLocation|null 68 | * @throws Exception 69 | */ 70 | public function getUserLocation (string $ip = null): ?UserLocation 71 | { 72 | return SimpleMap::getInstance()->geolocation->lookup($ip); 73 | } 74 | 75 | /** 76 | * Converts the given address to lat/lng 77 | * 78 | * @param string $address The address to search 79 | * @param string|null $country The ISO 3166-1 alpha-2 country code to 80 | * restrict the search to 81 | * 82 | * @return array|null 83 | */ 84 | public function getLatLngFromAddress (string $address, string $country = null): ?array 85 | { 86 | try 87 | { 88 | return GeoService::latLngFromAddress($address, $country); 89 | } 90 | catch (Exception $e) 91 | { 92 | Craft::error($e->getMessage(), 'simplemap'); 93 | 94 | return [ 95 | 'lat' => '', 96 | 'lng' => '', 97 | ]; 98 | } 99 | } 100 | 101 | /** 102 | * Will return a static map image using the given options. 103 | * 104 | * @param $options - See StaticOptions for the available options 105 | * 106 | * @return string 107 | * @throws Exception 108 | */ 109 | public function getImg ($options): string 110 | { 111 | return SimpleMap::getInstance()->static->generate($options); 112 | } 113 | 114 | /** 115 | * Will return a static map image ready for srcset 116 | * 117 | * @param $options - See StaticOptions for the available options 118 | * 119 | * @return string 120 | * @throws Exception 121 | */ 122 | public function getImgSrcSet ($options): string 123 | { 124 | $x1 = $this->getImg(array_merge($options, ['scale' => 1])); 125 | $x2 = $this->getImg(array_merge($options, ['scale' => 2])); 126 | 127 | return $x1 . ' 1x, ' . $x2 . ' 2x'; 128 | } 129 | 130 | /** 131 | * Will return markup for a dynamic map embed 132 | * 133 | * @param $options - See EmbedOptions for the available options 134 | * 135 | * @return string|void 136 | * @throws InvalidConfigException 137 | */ 138 | public function getEmbed ($options) 139 | { 140 | return SimpleMap::getInstance()->embed->embed($options); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/web/assets/MapAsset.php: -------------------------------------------------------------------------------- 1 | sourcePath = __DIR__ . '/map'; 27 | 28 | $this->depends = [ 29 | CpAsset::class, 30 | VueAsset::class, 31 | ]; 32 | 33 | if (getenv('ETHER_ENVIRONMENT') === 'true') 34 | { 35 | $this->js = [ 36 | 'https://localhost:8080/app.js', 37 | ]; 38 | } 39 | else 40 | { 41 | $this->css = [ 42 | 'css/app.css', 43 | 'css/chunk-vendors.css', 44 | ]; 45 | 46 | $this->js = [ 47 | 'js/app.js', 48 | 'js/chunk-vendors.js', 49 | ]; 50 | } 51 | 52 | parent::init(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/web/assets/imgs/carto-dark_all-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/carto-dark_all-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/carto-dark_all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/carto-dark_all.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/carto-light_all-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/carto-light_all-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/carto-light_all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/carto-light_all.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/carto-rastertiles-voyager-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/carto-rastertiles-voyager-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/carto-rastertiles-voyager.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/carto-rastertiles-voyager.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/google-hybrid-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/google-hybrid-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/google-hybrid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/google-hybrid.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/google-roadmap-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/google-roadmap-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/google-roadmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/google-roadmap.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/google-terrain-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/google-terrain-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/google-terrain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/google-terrain.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-hybrid-day-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-hybrid-day-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-hybrid-day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-hybrid-day.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-normal-day-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-normal-day-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-normal-day-grey-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-normal-day-grey-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-normal-day-grey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-normal-day-grey.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-normal-day-transit-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-normal-day-transit-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-normal-day-transit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-normal-day-transit.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-normal-day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-normal-day.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-pedestrian-day-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-pedestrian-day-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-pedestrian-day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-pedestrian-day.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-reduced-day-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-reduced-day-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-reduced-day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-reduced-day.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-satellite-day-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-satellite-day-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-satellite-day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-satellite-day.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-terrain-day-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-terrain-day-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/here-terrain-day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/here-terrain-day.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapbox-dark-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapbox-dark-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapbox-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapbox-dark.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapbox-light-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapbox-light-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapbox-light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapbox-light.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapbox-outdoors-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapbox-outdoors-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapbox-outdoors.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapbox-outdoors.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapbox-streets-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapbox-streets-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapbox-streets.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapbox-streets.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapkit-hybrid-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapkit-hybrid-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapkit-hybrid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapkit-hybrid.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapkit-muted-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapkit-muted-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapkit-muted.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapkit-muted.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapkit-satellite-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapkit-satellite-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapkit-satellite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapkit-satellite.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapkit-standard-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapkit-standard-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/mapkit-standard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/mapkit-standard.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/openstreetmap-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/openstreetmap-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/openstreetmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/openstreetmap.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/wikimedia-1x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/wikimedia-1x.jpg -------------------------------------------------------------------------------- /src/web/assets/imgs/wikimedia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethercreative/simplemap/45da58e7fb472989efcdf2f2471ca41aa761e7df/src/web/assets/imgs/wikimedia.jpg -------------------------------------------------------------------------------- /src/web/assets/map/css/app.css: -------------------------------------------------------------------------------- 1 | .Search_wrap_rNtoB{position:relative;display:block}@media only screen and (max-width:767px){.Search_wrap_rNtoB{margin-right:44px}}.Search_wrap_rNtoB,.Search_wrap_rNtoB *{-webkit-box-sizing:border-box;box-sizing:border-box}.Search_wrap_rNtoB>svg{position:absolute;top:17px;left:16px;pointer-events:none}.Search_wrap_rNtoB.Search_no-map_1K6JR{margin-right:0}.Search_input_3tpBT{width:100%;padding:16px 0 15px 48px;font-size:16px;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border:none;border-bottom:1px solid transparent;border-radius:5px;-webkit-box-shadow:0 2px 15px 0 rgba(0,0,0,.2);box-shadow:0 2px 15px 0 rgba(0,0,0,.2)}.Search_input_3tpBT.Search_no-map_1K6JR{-webkit-box-shadow:none;box-shadow:none;border:1px solid #dce4ea}.Search_open_a4lCm .Search_input_3tpBT{border-radius:5px 5px 0 0;border-bottom-color:#d2dbe1}.Search_resultsWrap_1ufuc{position:absolute;top:100%;left:-20px;width:calc(100% + 40px);height:210px;padding:0 20px 20px;overflow:hidden;pointer-events:none}.Search_results_nREcw{position:relative;z-index:2;width:100%;padding:7px 0;background-color:#fff;-webkit-box-shadow:0 5px 15px 0 rgba(0,0,0,.1);box-shadow:0 5px 15px 0 rgba(0,0,0,.1);border-radius:0 0 5px 5px;-webkit-transform:translateY(calc(-100% - 15px));transform:translateY(calc(-100% - 15px));-webkit-transition:-webkit-transform .3s ease;transition:-webkit-transform .3s ease;transition:transform .3s ease;transition:transform .3s ease,-webkit-transform .3s ease;pointer-events:all}.Search_open_a4lCm .Search_results_nREcw{-webkit-transform:translateY(0);transform:translateY(0)}.Search_results_nREcw li{padding:7px 14px;-webkit-transition:background-color .15s ease;transition:background-color .15s ease}.Search_results_nREcw li[class*=highlighted]{background-color:#e4edf3;cursor:pointer}.Label_label_GHq68{position:relative;display:block;border-right:1px solid #dce4ea}.Label_label_GHq68:not(:last-child){border-bottom:1px solid #dce4ea}.Label_name_3Feow{position:absolute;top:9px;left:12px;display:block;color:rgba(41,50,61,.41);font-size:12px;font-weight:500;letter-spacing:0}.Input_input_1qTm5{width:100%;padding:29px 0 10px;color:#29323d;font-size:15px;letter-spacing:0;text-indent:12px;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:none;border-radius:0;-webkit-transition:color .15s ease;transition:color .15s ease}.Input_input_1qTm5:disabled{opacity:.5}.Address_grid_3Vxrg{display:grid;grid-template-columns:1fr 1fr;background-color:#fff;border-radius:5px;-webkit-box-shadow:0 2px 15px 0 rgba(0,0,0,.2);box-shadow:0 2px 15px 0 rgba(0,0,0,.2);overflow:hidden;-webkit-transition:opacity .3s ease,-webkit-transform .3s ease;transition:opacity .3s ease,-webkit-transform .3s ease;transition:transform .3s ease,opacity .3s ease;transition:transform .3s ease,opacity .3s ease,-webkit-transform .3s ease}.Address_grid_3Vxrg:not(:first-child){margin-top:24px}.Address_grid_3Vxrg.Address_fade_1Khjf{opacity:.8}.Address_grid_3Vxrg.Address_no-map_kB9Ag{-webkit-box-shadow:none;box-shadow:none;border:1px solid #dce4ea}@media only screen and (max-width:1199px){.Address_grid_3Vxrg{grid-template-columns:1fr}.Address_grid_3Vxrg label{border-right:none}}@media only screen and (max-width:767px){.Address_grid_3Vxrg:not(.Address_no-map_kB9Ag):not(.Address_no-search_1JpdT){margin-top:200px!important}.Address_grid_3Vxrg:not(.Address_no-map_kB9Ag).Address_no-search_1JpdT{margin-top:260px!important}}.Address_grid_3Vxrg,.Address_grid_3Vxrg *{-webkit-box-sizing:border-box;box-sizing:border-box}.Address_grid_3Vxrg label.Address_right_3dCTx{border-right:none}.Address_grid_3Vxrg .Address_full_2Hoxx,.Address_grid_3Vxrg label:last-child{grid-column:span 2;border-right:none}@media only screen and (max-width:1199px){.Address_grid_3Vxrg .Address_full_2Hoxx,.Address_grid_3Vxrg label:last-child{grid-column:span 1}}.Address_row_3kuz9{display:-webkit-box;display:-ms-flexbox;display:flex}.Address_row_3kuz9:last-child>*{border-bottom:none}.Address_row_3kuz9 label{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;border-right:none}.Address_btn_2BSW6{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:none;border-bottom:1px solid #dce4ea;border-radius:0 5px 0 0;cursor:pointer;pointer-events:none}.Address_btn_2BSW6 svg{opacity:.5;-webkit-transform:translateX(200%);transform:translateX(200%);-webkit-transition:opacity .15s ease,-webkit-transform .3s ease;transition:opacity .15s ease,-webkit-transform .3s ease;transition:transform .3s ease,opacity .15s ease;transition:transform .3s ease,opacity .15s ease,-webkit-transform .3s ease}.Address_btn_2BSW6 path{-webkit-transition:fill .15s ease;transition:fill .15s ease}.Address_show-clear_rAelt .Address_btn_2BSW6{pointer-events:auto}.Address_show-clear_rAelt .Address_btn_2BSW6 svg{-webkit-transform:translateX(0);transform:translateX(0)}.Address_btn_2BSW6:hover svg{opacity:1}.Address_btn_2BSW6:hover path{fill:#f22c26}.Address_delete_gHZG0 input{color:#f22c26}.Map_map_2NpD9{position:absolute;z-index:0;top:0;left:0;right:0;bottom:0;-webkit-box-sizing:border-box;box-sizing:border-box}@media only screen and (max-width:767px){.Map_map_2NpD9{height:300px;bottom:auto}}.Map_control_3-EIS{margin:24px!important;font-size:0;border-radius:5px;-webkit-box-shadow:0 2px 15px 0 rgba(0,0,0,.2);box-shadow:0 2px 15px 0 rgba(0,0,0,.2)}@media only screen and (max-width:767px){.Map_control_3-EIS{margin:14px!important}}.Map_control_3-EIS button{display:block;width:100%;padding:2px 10px 3px;color:rgba(41,50,61,.75);font-size:20px;font-weight:700;line-height:normal;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border:none;cursor:pointer;-webkit-transition:background-color .15s ease;transition:background-color .15s ease}.Map_control_3-EIS button:not(:last-child){border-bottom:1px solid #dce4ea}.Map_control_3-EIS button:first-child{padding-bottom:4px;border-radius:5px 5px 0 0}.Map_control_3-EIS button:last-child{padding-top:4px;border-radius:0 0 5px 5px}.Map_control_3-EIS button:hover{background-color:#f0f9ff}.App_wrap_2esTH{position:relative;margin:0 -24px;min-height:284px;overflow:hidden}@media only screen and (max-width:767px){.App_wrap_2esTH{margin:0 -12px}}.hud .App_wrap_2esTH{margin:-24px!important}.matrixblock .App_wrap_2esTH{margin:0 -14px!important}.superTable-layout-row .App_wrap_2esTH,.superTable-layout-table .App_wrap_2esTH{margin:-4px -10px!important}.App_wrap_2esTH.App_no-map_2mvF1{min-height:0;margin:0;overflow:visible}.App_wrap_2esTH.App_no-map_2mvF1 .App_content_pIJBB{padding:0}.hud .App_wrap_2esTH.App_no-map_2mvF1 .App_content_pIJBB{width:100%;padding:24px}.App_mini_RpxNW{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.App_mini_RpxNW .btn{margin-left:24px;font-size:14px}.App_empty_1A4ND{opacity:.5}.App_content_pIJBB{position:relative;z-index:2;-webkit-box-sizing:border-box;box-sizing:border-box;width:50%;padding:24px;pointer-events:none}.App_content_pIJBB.App_noMap_Mu8tf{width:100%}.matrixblock .App_content_pIJBB,.superTable-layout-row .App_content_pIJBB,.superTable-layout-table .App_content_pIJBB{padding:14px}@media only screen and (max-width:767px){.App_content_pIJBB{width:100%;padding:12px}}.App_content_pIJBB>*{pointer-events:all} -------------------------------------------------------------------------------- /src/web/assets/map/index.html: -------------------------------------------------------------------------------- 1 | Vue App
-------------------------------------------------------------------------------- /src/web/assets/svgs/apple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/web/assets/svgs/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/web/assets/svgs/here.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/web/assets/svgs/ipstack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/web/assets/svgs/mapbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/web/assets/svgs/maxmind.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/web/assets/svgs/w3w.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------