├── .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 |
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 |
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 |