Showing preview only (667K chars total). Download the full file or copy to clipboard to get everything.
Repository: ethercreative/simplemap
Branch: v5
Commit: 0f99bcd14cfd
Files: 95
Total size: 634.2 KB
Directory structure:
gitextract_o1ic_4j1/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── craft-3-issue.md
│ │ └── craft-4-issue.md
│ ├── ISSUE_TEMPLATE.md
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── docs/
│ ├── .docs.json
│ ├── geolocation/
│ │ ├── get.md
│ │ └── redirect.md
│ ├── getting-started/
│ │ ├── config.md
│ │ ├── installation.md
│ │ └── usage.md
│ ├── how-to/
│ │ ├── graphql.md
│ │ └── search.md
│ ├── index.md
│ └── rendering/
│ ├── embed.md
│ └── static.md
├── resources/
│ ├── .editorconfig
│ ├── .gitignore
│ ├── README.md
│ ├── babel.config.js
│ ├── package.json
│ ├── src/
│ │ ├── App.vue
│ │ ├── common/
│ │ │ └── Geo.js
│ │ ├── components/
│ │ │ ├── Address.vue
│ │ │ ├── Fragment.vue
│ │ │ ├── Input.vue
│ │ │ ├── Label.vue
│ │ │ ├── Map.vue
│ │ │ └── Search.vue
│ │ ├── enums/
│ │ │ ├── GeoService.js
│ │ │ └── MapTiles.js
│ │ ├── filters/
│ │ │ └── craft.js
│ │ ├── helpers/
│ │ │ ├── createW3WGrid.js
│ │ │ ├── debounce.js
│ │ │ └── waitForGlobal.js
│ │ ├── main.js
│ │ └── models/
│ │ ├── Parts.js
│ │ └── PartsLegacy.js
│ └── vue.config.js
└── src/
├── SimpleMap.php
├── acfadapters/
│ └── GoogleMap.php
├── config.php
├── controllers/
│ ├── SettingsController.php
│ └── StaticController.php
├── enums/
│ ├── GeoService.php
│ └── MapTiles.php
├── fields/
│ └── MapField.php
├── 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_LICENSE.txt
├── 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
└── map/
├── css/
│ ├── app.css
│ └── chunk-vendors.css
├── index.html
└── js/
├── app.js
└── chunk-vendors.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
indent_size = 4
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{yml, json}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .gitattributes
================================================
README.md export-ignore
CHANGELOG.md export-ignore
/resources export-ignore
.gitignore export-ignore
.gitattributes export-ignore
/.idea export-ignore
.editorconfig export-ignore
/docs export-ignore
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Code of Conduct
Don't be a twat.
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing
### New Features
You can request new features by submitting an issue with `[FR]` prefix in the title.
### Bugs
If you find a bug, submit an issue and I promise I will get around to it. Eventually.
Make sure you search around to see if the same (or similar) issue exists before
submitting a new one.
Know how to fix a bug and have too much spare time on your hands? Submit a Pull Request!
Check out the code conventions below before hand.
## Code Conventions
In addition to [Crafts Guidelines](https://github.com/craftcms/docs/blob/v3/en/coding-guidelines.md):
- Use tabs not spaces
- Try to keep within the 80 characters line length
- If function arguments or array values can't fit on one line, break each value onto it's own line
- Comment as much as possible
#### JavaScript
In addition to the above:
- Write valid ES6
- Use single quotes `'`
Don't follow my example, try to stick to the above guidelines!
================================================
FILE: .github/FUNDING.yml
================================================
github: [tam]
================================================
FILE: .github/ISSUE_TEMPLATE/craft-3-issue.md
================================================
---
name: Craft 3 Issue
about: An issue with the Craft 3 version of the plugin
title: ''
labels: Craft 3
assignees: ''
---
================================================
FILE: .github/ISSUE_TEMPLATE/craft-4-issue.md
================================================
---
name: Craft 4 Issue
about: An issue with the Craft 4 version of the plugin
title: ''
labels: Craft 4
assignees: ''
---
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
### Description
### Steps to reproduce
1.
2.
### Additional info
- Craft version:
- Maps version:
- PHP version:
- Database driver & version:
- Other Plugins:
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Fixes # .
Changes proposed in this pull request:
-
-
-
================================================
FILE: .gitignore
================================================
### OSX
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
vendor
node_modules
hot
mix-manifest.json
/.idea
================================================
FILE: CHANGELOG.md
================================================
## 5.0.4 - 2024-11-11
### Added
- Add support for WordPress ACF -> Craft import (via @brandonkelly)
## 5.0.3 - 2024-09-16
### Fixed
- Fixed location filter affecting unrelated queries (Fixes #405)
## 5.0.2 - 2024-09-10
### Fixed
- Fixes location search (Fixes #394, #393, #392)
## 5.0.1 - 2024-06-27 [CRITICAL]
### Security
- Removed Polyfill.io (https://sansec.io/research/polyfill-supply-chain-attack)
## 5.0.0 - 2024-04-04
### Fixed
- Fix referred on Mapbox geo requests (Fixes #338, via @maxdmyers)
- Fix type errors in front-end usage (Fixes #379, via @samhibberd)
- Fix error when normalizing invalid location (Fixes #368, #380, via @Decyphr)
- Fix intermittent issues w/ Google Maps API loading (Fixes #294, via @davidwebca)
- Allow nullable zoom value (Fixes #381)
- Pass site language to embedded Google map (Fixes #373)
- Support casting map to string (Fixes #362)
- Remove reference to MaxMind Lite from docs (Fixes #358)
## 5.0.0-rc1 - 2024-03-01
### Changed
- Add support for Craft 5
## 4.0.4 - 2023-06-26
### Fixed
- Update settings autosuggest input (Fixes #374)
## 4.0.3 - 2022-07-11
### Fixed
- Fixed $id embed issue (Fixes #353)
- Allow all Parts to be null as well as an empty string (Fixes #349)
- Fix issue placing marker along coastline when using Mapbox (Fixes #322)
### Changed
- Removed WikiMedia tiles, falling back to OpenStreetMap (Fixes #307)
## 4.0.2 - 2022-06-08
### Fixed
- Misc fixes via @davidwebca & @jamesedmonston
## 4.0.1 - 2022-05-18
### Fixed
- Fix Geo Location Token only allowing string (Fixes #343)
## 4.0.0 - 2022-05-10
### Fixed
- Mostly functional, only slightly buggy, Craft 4 Support
## 3.9.3 - 2022-01-17
### Fixed
- Fix Mapbox Forbidden issue (Fixes #218, via @Saboteur777)
## 3.9.2 - 2021-08-26
### Changed
- Change default map tileset to Carto Voyager
### Fixed
- Fix some geolocation services conversion failing when the given location is not a valid address (Fixes #322)
- Fix error when upgrading due to missing `type` field (via @cornernote)
## 3.9.1 - 2021-04-30
### Added
- Add Guzzle 7 support (via @dwheeldo)
## 3.9.0.2 - 2020-12-01
### Improved
- Improved GQL docs
### Fixed
- Fix GQL Coords type missing `lng`
## 3.9.0.1 - 2020-11-27
### Fixed
- Removed distance field from GQL mutation input
## 3.9.0 - 2020-11-27
> {warning} **BREAKING**: This release changes how GraphQL querying works for
the map field. You should now pass the search query as an input rather than a
JSON string.
### Added
- Added GraphQL support for query filtering and mutations
### Improved
- Address inputs will span full width when map is disabled (Fixes #282)
## 3.8.5 - 2020-10-08
### Added
- Add `centerFallback` option to map embeds (Fixes #263)
### Changed
- Prefix name of `Map` graphql elements
### Fixed
- Reduce search radius to 0 if location is not valid (Fixes #277)
- Fix mapbox geocode error when country is not valid ISO code (Fixes #276)
### Removed
- Remove MaxMind Lite option
## 3.8.4.1 - 2020-07-03
### Fixed
- Fix url encode issue for markers in static map images
## 3.8.4 - 2020-06-12
### Changed
- Allow zoom override on Google/Mapbox embeds
- Allow style override on Mapbox embeds (Fixes #256)
### Fixed
- Cast embed center coordinates to floats
- Upgraded Mapbox to use new Static Tiles API
## 3.8.3 - 2020-04-09
### Added
- Added JSON support when filtering by a map field. Very useful for Crafts current GQL implementation (Fixes #248)
### Fixed
- Markers coodinates are now cast to floats (via [@Sekonda](https://github.com/Sekonda))
- Exclude empty map fields when sorting by distance (Fixes #245)
## 3.8.2 - 2020-03-04
### Fixed
- Fix issue where elements failed to save when searching for a location via Google or Here (Fixes #242)
- Fix what3words not updating when searching for a location
## 3.8.1 - 2020-02-27
### Added
- Add env support to settings (Closes #241)
### Improved
- Improve settings page appearance during load
## 3.8.0 - 2020-02-25
### Added
- Add What3Words support (Closes #236)
### Fixed
- Fix issue when trying to render a static map without markers (Fixes #225)
## 3.7.7 - 2020-01-17
### Fixes
- Fix error when attempts to populate missing data return null (Fixes #233)
## 3.7.6 - 2019-12-18
### Fixed
- Fix issue with getting distance when lat/lng was stored as string (Fixes #230)
- Fix issue when upgrading maps for fields that no longer exist (Fixes #227)
## 3.7.5 - 2019-12-04
### Added
- Add config option to disable missing field data population (Fixes #226)
- Add Current Location button to Map (Closes #219)
## 3.7.4 - 2019-12-03
### Improved
- 🔥 Improved location searching and distance sorting by up to 1800%! ⚡️
## 3.7.3 - 2019-11-29
### Fixed
- Fix issue with Mapbox parts lookup when address property is missing
- Fix issue with count when doing a location search
## 3.7.2 - 2019-11-19
### Changed
- Embed map width and height can now be set to `null` (Fixes #221)
### Fixed
- Fix map embed / static options not converting center string correctly
- Fix multiple leaflet maps not rendering correctly
- Fix map not being draggable on mobile (Fixes #220)
- Fix mini-map going off screen on small laptops (Fixes #222)
## 3.7.1 - 2019-10-24
### Added
- Add support for `:empty:` and `:notempty:` (Fixes #214)
### Fixed
- Fix `embed` and `imgSrcSet` not setting options correctly when outputting from a map field (Fixes #215)
## 3.7.0 - 2019-10-15
### Added
- Add docs
- Add Craft GraphQL support
- Add Pro edition
- Add static map image support
- Add new map field size options
- Add IP based user location lookup
- Add ability to redirect to a specific site based off user location
- Add `coordinate` query argument to CraftQL (Closes #205)
- Add "mini" size for a tiny field footprint (Closes #203)
- Add `address()` method to map value for easy address formatting
- Add galactic address parts
### Changed
- 🍆 New, sexier UI! 💦
- Mapbox, Apple Maps, and Here are now only available in Maps Pro
- Now requires Craft 3.2.1 or newer
### Improved
- Remove Vue from JS bundle to reduce file size
- Removed fly animation when updating map location for snappier UI (Closes #202)
### Fixed
- Fix map not showing when other Vue based plugins interfere (Fixes #196)
- Fix issue when migrating from an older version of Maps (Fixes #195)
- Fix project config migration issue (Fixes #207)
- Fix issues upgrading Maps from Craft 2 to 3 (via [@roelvanhintum](https://github.com/roelvanhintum))
## 3.6.4.3 - 2019-08-30
### Fixed
- Fix issue when trying to save map field on initial draft entry
## 3.6.4.2 - 2019-08-01
### Changed
- Don't update project config unnecessarily during migration (Closes #194)
### Fixed
- Fix migration error when upgrading from 3.3.4 or lower (Fixes #192)
## 3.6.4.1 - 2019-07-30
### Fixed
- Fix error when populating legacy parts server-side from lat/lng
- Fix error when logging invalid legacy part
## 3.6.4 - 2019-07-30
### Fixed
- Remove errant debug code causing migration to run every request (Fixes #190)
- Fix migration trying to change a column type to a table (Fixes #189, #188)
## 3.6.3 - 2019-07-25
### Added
- Add min / max zoom settings to map field (Closes #186)
### Fixed
- Fix migration from Craft 2 (Fixes #153)
- Fix issue when column already exists during migration (Fixes #187)
## 3.6.2.2 - 2019-07-24
### Fixed
- Fix migration issue when matrix / super table blocks don't have any fields (Fixes #184)
## 3.6.2.1 - 2019-07-23
### Fixed
- Fix migration issue when no matrix or SuperTable blocks exist (Fixes #182)
- Fix issue with Google trying to set legacy parts that aren’t supported (Fixes #183)
- Fix getting top-level map value part if no parts exist (Fixes #181)
## 3.6.2 - 2019-07-23
### Added
- Add `postal_code_suffix` to `PartsLegacy` (Fixes #179)
### Fixed
- Fix migration error when upgrading from 3.4.x to 3.6.x (Fixes #178)
- Fix project config content column type being string instead of text (Fixes #180)
## 3.6.1 - 2019-07-19
### Added
- Add support for getting parts without having to go via the `parts` property.
(i.e. `myMap.parts.number` can be simply `myMap.number`). This _doesn't_ work
for the `address` part, which is already in use and returns the full address
as a string (alternatively, use the `streetAddress` alias). (Closes #154)
- Add `streetAddress` alias of `address` to Parts.
### Changed
- `PartsLegacy` will be used when Google is the chosen Geo service, giving
access to additional Google specific parts (Fixes #167)
### Fixed
- Fix error when normalizing value without an element (Fixes #174)
- Fix JS error when using two different API keys for Google maps services (Fixes #165)
- Fix parts being lost when moving from new to legacy (any other geo service to google)
- Fix issue with Mapbox geo service when country was unrestricted
- Fix JS issues when using Apple or Google Maps in an element edit HUD (Fixes #175)
## 3.6.0 - 2019-07-12
> {warning} This update changes how map data is stored, moving away from an
element type. This means if you are eager loading the a map field, you'll want
to remove the `with` from your query and `[0]` when outputting the map (if you
have it). We also **strongly** recommend taking a backup before updating.
> {tip} If you get a `Column not found` error when upgrading, try running `./craft migrate/all`.
### Changed
- Reformat data structure to remove map element type and need for eager loading
### Fixed
- Fix missing postcode warning (Fixes #169)
- Fix map save DB issue in Craft 3.2 (Fixes #170)
- Fix map not retrieving saved values in Craft 3.2 (Fixes #171)
- Fix DB error on duplicate import via FeedMe (Fixes #168)
- Fix maps not propagating across sites (Fixes #141)
## 3.5.2 - 2019-06-20
### Improved
- FeedMe can now import the individual map parts
## 3.5.1 - 2019-06-20
### Added
- Maps can now populate address and lat/lng data based off only a postcode
## 3.5.0 - 2019-06-13
### Added
- Added FeedMe support!
### Fixed
- Fixed results being duplicated when searching by location when an entry has
multiple map fields within the search catchment.
- Account for missing Craft 2 API keys
- Fix HERE search not working when no country restriction was set
### Changed
- Add default zoom to map element
- Update preferred country instructions to be clearer
- Support rendering a map field without a value
- Use field handle as table alias suffix, instead of random bytes
## 3.4.11 - 2019-04-05
### Fixed
- Map records are no longer double saved when upgrading to from Craft 2 to 3
## 3.4.10 - 2019-04-04
### Fixed
- Map records are no longer double saved when upgrading to >3.4.x
## 3.4.9 - 2019-04-01
### Added
- Added option to show lat / lng fields
### Fixed
- Fixed map not validating correctly
- Fixed wrong map value being shown on element index with multiple sites
- Fixed missing table prefix in map element query
- Fixed migration issue when upgrading due to duplicate element IDs
### Improved
- Scrolling to zoom disabled on map
- Clearing the map will no longer store the default data
## 3.4.8 - 2019-03-27
### Fixed
- Fix error when migrating a field from Craft 2 when `countryRestriction` isn't set
- Location search excludes elements that have been soft-deleted
- Fixed issue restoring trashed elements that have a map field
- Map field elements are trashed and deleted correctly
- Fixed syntax issue on PHP <7.1.0
- Fixed error during repair migration when element doesn't exist
## 3.4.7 - 2019-03-25
### Fixed
- Fixed JS error when clearing field
- Fixed missing parts when using Google maps for geo-coding
### Improved
- Clear button now translatable
## 3.4.6 - 2019-03-25
### Added
- Added "Clear" button
- Always show full address field even if address block is hidden
### Fixed
- The really shitty element stuff. Is good now. I think.
## 3.4.5 - 2019-03-25
### Fixed
- Fixed maps failing to get value after save
### Changed
- Using Google Maps geo service will result in legacy parts always being used,
meaning you can access all available address components.
## 3.4.4 - 2019-03-22
### Fixed
- Fixed some issues when upgrading from older versions of Maps. We recommend
upgrading from 3.3.4 or lower directly to this release or later.
## 3.4.3 - 2019-03-20
### Changed
- You can now pass a map to the location query (fixes #99)
### Fixed
- Fixed issue when `cp-field-inspect` plugin is installed (fixes #127)
- Fixed `elementId cannot be null` error on saving new entries with map fields (fixes #126)
## 3.4.2 - 2019-03-20
### Fixed
- Fixed issue setting old field settings after upgrade.
## 3.4.1 - 2019-03-20
### Fixed
- Fixed an issue where the map field class broke after upgrading.
## 3.4.0 - 2019-03-20
> {warning} This is a major update, we strongly recommend taking a database backup before updating!
### Changed
- SimpleMap is now Maps! We've re-written the plugin from the ground-up while
keeping it backwards compatible (even back to Craft 2!)
- Maps is now powered by Vue!
- New icon yo
### Added
- OpenStreetMap Support and map tiles
- Mapbox Support and map tiles
- Apple MapKit Map Tiles
- Here Maps Support and map tiles
- Wikimedia Map Tiles
- Carto Map Tiles
- Address inputs for manually settings address parts data.
### Improved
- We've normalized the map "Parts", so you'll always know what data you have available.
- CraftQL support: you can now query and mutate Maps fields via Graph!
- Field Customization: It's now possible to hide the location search, map, and address inputs.
### Fixed
- Maps are now multi-site aware and can be translated.
### Removed
- Removed lat/lng inputs from field
- Removed restrict by type
- Removed boundary restriction
## 3.3.4 - 2018-09-05
### Fixed
- Fixed a bug where SimpleMap would not validate required fields. (via @samhibberd)
## 3.3.3 - 2018-03-13
### Fixed
- Fixed a bug where SimpleMap would cause the `ResaveElements` job to error when triggered via console.
## 3.3.2 - 2018-03-05
### Added
- Added docs for using a config file to configure the plugin.
### Fixed
- Fixed JOIN alias issue when using the Element API plugin (via @idontmessabout)
## 3.3.1 - 2018-01-30
### Fixed
- Fixed JS bug on settings page
## 3.3.0 - 2018-01-30
### Fixed
- Added a fix for those annoying `Call to a member function getMap() on null` bugs
### Improved
- Map height no longer jumps when page loads
- Vastly improved the map fields settings UI/UX
- No more nasty text fields!
- Map height and position is now set by resizing and moving a map
- Auto-complete search bounds can now be drawn directly onto a map
- Radio buttons are now drop-downs
### Changed
- Now using the plugins `afterInstall` function instead of the plugin after install event
- The "Hide Lat/Lng" option is now true by default
## 3.2.0 - 2018-01-25
### Fixed
- Fixed bug where pagination would error when querying via a map field. #70
### Improved
- Updated CraftQL support (via @markhuot)
- Removed webonyx/graphql-php dependency #71
- Improved address and lat/lng input sizing on smaller screens and in a HUD #73
- Updated Mapbox example to use latest API #74
## 3.1.3 - 2017-12-18
### Fixed
- Map fields no longer cause global sets to fail to save!
## 3.1.2 - 2017-12-18
### Fixed
- Fixed settings not translating for non-English languages
- Fixed boundary settings fields not accepting decimals
## 3.1.1 - 2017-11-30
### Fixed
- Fixed bug where maps were failing to save.
## 3.1.0 - 2017-11-30
### Added
- [CraftQL](https://github.com/markhuot/craftql) support!
- Added `craft.simpleMap.getLatLngFromAddress($addressString[, $country])`.
### Improved
- The maps `parts` now contains all available options from [here](https://developers.google.com/maps/documentation/geocoding/intro#Types) (including the `_small` variants). Any options without values are returned as empty strings.
## 3.0.4 - 2017-11-28
### Added
- Added ability to restrict location search by country
### Changed
- New icon!
## 3.0.3 - 2017-11-08
### Added
- It's now possible to save the map field with only an address! Useful for populating the field from the front-end. (Requires the Geocoding API).
### Improved
- The address and lat/lng are now validated.
## 3.0.2 - 2017-11-03
### Fixed
- Fixed a bug where location searches would error if `orderBy` was not defined
## 3.0.1 - 2017-11-03
### Fixed
- Fixed maps not rendering
## 3.0.0 - 2017-11-03
### Changed
- Initial Craft 3 Release
================================================
FILE: LICENSE
================================================
Copyright © Ether Creative
Permission is hereby granted to any person obtaining a copy of this software
(the “Software”) to use, copy, modify, merge, publish and/or distribute copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
1. **Don’t plagiarize.** The above copyright notice and this license shall be
included in all copies or substantial portions of the Software.
2. **Don’t use the same license on more than one project.** Each licensed copy
of the Software shall be actively installed in no more than one production
environment at a time.
3. **Don’t mess with the licensing features.** Software features related to
licensing shall not be altered or circumvented in any way, including (but
not limited to) license validation, payment prompts, feature restrictions,
and update eligibility.
4. **Pay up.** Payment shall be made immediately upon receipt of any notice,
prompt, reminder, or other message indicating that a payment is owed.
5. **Follow the law.** All use of the Software shall not violate any applicable
law or regulation, nor infringe the rights of any other person or entity.
Failure to comply with the foregoing conditions will automatically and
immediately result in termination of the permission granted hereby. This
license does not include any right to receive updates to the Software or
technical support. Licensees bear all risk related to the quality and
performance of the Software and any modifications made or obtained to it,
including liability for actual and consequential harm, such as loss or
corruption of data, and any necessary service, repair, or correction.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================

# Maps
A beautifully simple, yet deceptively powerful, Map field that works out of the
box with no setup or API tokens needed!
Configure the map field to only show the features you want; hide the address,
show latitude / longitude, or hide the map entirely. Choose from 4 geocoding
services, 3 geolocation services, and 24 map tilesets!
Maps offers full multi-site support, compatibility with Matrix,
[SuperTable](https://verbb.io/craft-plugins/super-table/features), and
[CraftQL](https://plugins.craftcms.com/craftql), and the ability to search by
location and sort by distance.


## Mini Map
Maps offers a mini map field that fits perfectly in a Super Table without
taking up a lot of space!

## Map Tiles and Geo
Maps supports the following map tiles:
<details>
<summary>Wikimedia</summary>
[Wikimedia](https://foundation.wikimedia.org/wiki/Maps_Terms_of_Use) | | |
--- | --- | ---
 | <img src="data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27400%27%20height%3D%27400%27%20style%3D%27background%3Atransparent%27%2F%3E" /> | <img src="data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27400%27%20height%3D%27400%27%20style%3D%27background%3Atransparent%27%2F%3E" />
Wikimedia | |
</details>
<details>
<summary>OpenStreetMap</summary>
[OpenStreetMap](https://www.openstreetmap.org) | | |
--- | --- | ---
 | <img src="data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27400%27%20height%3D%27400%27%20style%3D%27background%3Atransparent%27%2F%3E" /> | <img src="data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27400%27%20height%3D%27400%27%20style%3D%27background%3Atransparent%27%2F%3E" />
OpenStreetMap | |
</details>
<details>
<summary>Carto (Voyager, Positron, Dark Matter)</summary>
[Carto](https://carto.com/location-data-services/basemaps/) | | |
--- | --- | ---
 |  | 
Voyager | Positron | Dark Matter
</details>
<details>
<summary>Mapbox (Outdoors, Streets, Dark, Light)</summary>
[Mapbox](https://www.mapbox.com) | | |
--- | --- | ---
 |  | 
Outdoors | Streets | Dark
 | |
Light | |
</details>
<details>
<summary>Google Maps (Roadmap, Terrain, Hybrid)</summary>
[Google Maps](https://www.google.com/maps) | | |
--- | --- | ---
 |  | 
Roadmap | Terrain | Hybrid
</details>
<details>
<summary>Apple MapKit (Standard, Muted, Satellite, Hybrid)</summary>
[Apple MapKit](https://developer.apple.com/maps/mapkitjs/) | | |
--- | --- | ---
 |  | 
Standard | Muted | Satellite
 | |
Hybrid | |
</details>
<details>
<summary>Here (Day, Day Grey, Day Transit, Reduced, Pedestrian, Terrain, Satellite, Hybrid)</summary>
[Here](https://www.here.com/) | | |
--- | --- | ---
 |  | 
Day | Day Grey | Day Transit
 |  | 
Reduced | Pedestrian | Terrain
 |  |
Satellite | Hybrid |
</details>
And these geocoding services:
- [Nominatim (OpenStreetMap)](https://nominatim.openstreetmap.org/)
- [Mapbox](https://www.mapbox.com/)
- [Google Maps](https://www.google.com/maps)
- [Here](https://www.here.com/)
And these geolocation services:
- [ipstack](https://ipstack.com/)
- [MaxMind Lite](https://dev.maxmind.com/geoip/geoip2/geolite2/)
- [MaxMind](https://maxmind.com/)
## Documentation
For full documentation visit the
**[Maps Ether Docs](https://docs.ethercreative.co.uk/maps)**.
================================================
FILE: composer.json
================================================
{
"name": "ether/simplemap",
"description": "A beautifully simple Map field type for Craft CMS",
"type": "craft-plugin",
"license": "proprietary",
"minimum-stability": "dev",
"require": {
"craftcms/cms": "^5.0.0",
"mapkit/jwt": "^1.1.2",
"geoip2/geoip2": "~2.0",
"guzzlehttp/guzzle": "^6.3.3|^7.2.0",
"what3words/w3w-php-wrapper": "3.*",
"ext-openssl": "*",
"ext-json": "*",
"ext-zlib": "*",
"php": "^8.2"
},
"require-dev": {
"craftcms/wp-import": "dev-main"
},
"autoload": {
"psr-4": {
"ether\\simplemap\\": "src/"
}
},
"support": {
"email": "help@ethercreative.co.uk",
"docs": "https://docs.ethercreative.co.uk/maps",
"source": "https://github.com/ethercreative/simplemap",
"issues": "https://github.com/ethercreative/simplemap/issues"
},
"extra": {
"handle": "simplemap",
"name": "Maps",
"developer": "Ether Creative",
"developerUrl": "https://ethercreative.co.uk",
"class": "ether\\simplemap\\SimpleMap",
"schemaVersion": "3.4.2"
},
"config": {
"allow-plugins": {
"yiisoft/yii2-composer": true,
"craftcms/plugin-installer": true
}
}
}
================================================
FILE: docs/.docs.json
================================================
{
"nav": {
"index": "Introduction",
"getting-started": {
"_label": "Getting Started",
"installation": "Installation",
"config": "Configuration",
"usage": "Usage"
},
"how-to": {
"_label": "How-to Guides",
"search": "Search by Location",
"graphql": "Querying in GraphQL"
},
"rendering": {
"_label": "Rendering",
"static": "Static",
"embed": "Embed"
},
"geolocation": {
"_label": "Geo-location",
"get": "Get User Location",
"redirect": "User Location Redirecting"
}
}
}
================================================
FILE: docs/geolocation/get.md
================================================
---
title: Get User Location
---
# Get User Location
Getting the current users location based of their IP address is easy using the
handy Craft Twig method:
```twig
{% set userLocation = craft.maps.getUserLocation([ip]) %}
```
- **`ip`** is an optional parameter that expects a valid and not private or
reserved IPv4 or IPv6. If null the function will use the IP address from the
user of the current request.
This function returns a [`UserLocation`](#User-Location) which is very similar to the location
returned by a Map field, with a few added bonuses.
## User Location
The user location has the following properties:
- **`ip`** The IP address of the user (that was used to lookup the location).
- **`lat`** The latitude of the users location.
- **`lng`** The longitude of the users location.
- **`address`** The full address (see [Address](../getting-started/usage.md#address)).
- **`parts`** The separate parts of the address (see [Parts](../getting-started/usage.md#parts)).
- **`countryCode`** The ISO country code of the user locations country.
- **`isEU`** Will be true if the user is in an EU country.
### Distance
You can get the distance between the user and a given location using this method
on the User Location:
```twig
{{ userLocation.distance({ lat: 51.272154, lng: 0.514951 }, 'miles') }}
```
The method accepts two parameters:
- **`to`** A lat/lng keyed array, address string, or a Map or User location.
- **`unit`** An optional parameter specifying which unit to use for the
measurement. Either `mi` (miles) or `km` (kilometers). _Defaults to `km`._
It will return a float of the distance between the two locations in the unit
specified (or `km` if no unit is specified).
================================================
FILE: docs/geolocation/redirect.md
================================================
---
title: User Location Redirecting
---
# User Location Redirecting
With Maps it is now possible to redirect the user to a specific site based off
their physical location. You can do this by setting the
[`geoLocationRedirectMap`](../getting-started/config.md#geolocationredirectmap)
in the [`simplemap.php`](../getting-started/config.md) config file.
The value of `geoLocationRedirectMap` should be a keyed array. The key of each
item should be the handle for the site you want to redirect to. The value of
each item should be an array of properties to match against
([explained below](#location-matching)), or a string containing an asterisk `*`
which will act as a catch-all.
## Location Matching
Location matching is performed by looking at each key/value pair in an array and
checking if they all match the users location. The keys should match the
available properties in the [User Location](./get.md#user-location). The values
should be an exact match for the contents of the User Location.
The priority is top-down, first come first serve. This means that the first site
that matches the users location will be the one that is used.
See the example below for a visual explanation.
### Example
```php
<?php
return [
'geoLocationRedirectMap' => [
'uk' => [ 'countryCode' => ['uk', 'ie'] ],
'eu' => [ 'isEU' => true ],
'southern' => [ 'lat' => function ($lat) { return $lat <= 0; } ],
'global' => '*',
],
];
```
The first site, `uk`, is checking to see if the `countryCode` of the users
location matches either `'uk'` or `'ie'` because we want to bundle the Irish in
with the English. I'm sure they won't mind. If it does match, the user will be
redirected to the current page on the UK site.
The second site, `eu`, checks to see if the user is in the EU and will redirect
to the `eu` site if they are.
`southern` is using an anonymous function, or lambda, to check if the users
latitude is on or below the equator. You can use lambdas for any of the
properties. The first and only argument will be the value of that property on
the users location. All lambdas must return a boolean value.
The final site, `global`, uses an asterisk as a catch-all. This means that if
none of the previous rules match this one will be used. You should always use
the wildcard last since any subsequent rules will be ignored.
================================================
FILE: docs/getting-started/config.md
================================================
---
title: Configuration
---
# Configuration
There are two ways to configure **Maps**, via the Craft CP or using a config
file (for advanced configuration).
### Services & Tokens
Below is a list of the various services supported by Maps and links on how to
get their tokens.
#### Map Tiles & Geocoding
##### OpenStreetMap / Nominatim
No token required
##### Wikimedia
No token required
##### Carto
No token required
##### [Google](https://cloud.google.com/maps-platform/#get-started)
You will need to enable the **Maps JavaScript API** and **Places API** for if
using Google for the map tiles, and the **Places API** and **Geocoding API** if
you are using it for the Geo service.
##### [Mapbox](https://docs.mapbox.com/help/how-mapbox-works/access-tokens/)
You can use the same key for both map tiles and geo service, no configuration
needed!
##### [Apple MapKit](https://developer.apple.com/documentation/mapkitjs/setting_up_mapkit_js)
We currently only support Apple MapKit for map tiles only.
##### [Here](https://developer.here.com/)
You can use the same key for both map tiles and geo service, no configuration
needed!
#### Geolocation
##### [ipstack](https://ipstack.com/product)
ipstack offer free and paid-for versions of their API.
##### [MaxMind](https://www.maxmind.com/en/geoip2-precision-services)
MaxMind offer free lookup database that must be stored locally, and a more
accurate paid-for version of their API.
## CP
You can get to the Maps settings in the Craft CP by navigating to "Settings" ->
"Maps" in an environment where `allowAdminChanges` is set to `true`.
## Config File
For advanced configuration create a `simplemap.php` file in your `config` folder.
This file should return an array of Maps settings.
```php
<?php
return [
'mapToken' => '123abc',
];
```
### Settings
#### `mapTiles`
_Default: `MapTiles::Wikimedia`_
The map tileset to use. Must be set to one of the `MapTiles` constants.
```php
<?php
use ether\simplemap\enums\MapTiles;
return [
'mapTiles' => MapTiles::CartoVoyager,
];
```
#### `mapToken`
_Default: `''`_
The token to use with your selected map tileset. This is only required when you
are using a tileset that requires a token.
**Mapbox & Google Maps**
For these services your token should be a string containing the token.
```php
<?php
return [
'mapToken' => '',
];
```
**Apple MapKit**
Your token should be an array containing `privateKey`, `teamId`, `keyId`.
```php
<?php
return [
'mapToken' => [
'privateKey' => '',
'teamId' => '',
'keyId' => '',
],
];
```
**Here**
The token should be an array containing `appId`, `apiKey`, `appCode`.
```php
<?php
return [
'mapToken' => [
'appId' => '',
'apiKey' => '',
'appCode' => '',
],
];
```
#### `geoService`
_Default: `GeoService::Nominatim`_
The geocoding service to use. Must be set to one of the `GeoService` constants.
```php
<?php
use ether\simplemap\enums\GeoService;
return [
'geoService' => GeoService::GoogleMaps,
];
```
#### `geoToken`
_Default: `''`_
The token to use with your selected geocoding service. This is only required
when you are using a geocoding that requires a token.
**Mapbox & Google Maps**
For these services your token should be a string containing the token.
```php
<?php
return [
'geoToken' => '',
];
```
**Here**
The token should be an array containing `appId`, `appCode`.
```php
<?php
return [
'geoToken' => [
'appId' => '',
'appCode' => '',
],
];
```
#### `disablePopulateMissingFieldData`
_Default: `false`_
Will disable the automatic population of missing field data. This can be useful
in preventing API spam when importing lots of map data.
#### `geoLocationService`
_Default: `GeoLocationService::None`_
The geolocation service to use. Must be set to one of the `GeoLocationService`
constants.
```php
<?php
use ether\simplemap\services\GeoLocationService;
return [
'geoLocationService' => GeoLocationService::MaxMind,
];
```
#### `geoLocationToken`
_Default: `''`_
The token to use with your selected geolocation service. This is only required
when you are using a geolocation that requires a token.
**ipstack**
For this services your token should be a string containing the token.
```php
<?php
return [
'geoLocationToken' => '',
];
```
**MaxMind**
The token should be an array containing `accountId`, `licenseKey`.
```php
<?php
return [
'geoLocationToken' => [
'accountId' => '',
'licenseKey' => '',
],
];
```
#### `geoLocationCacheDuration`
_Default: `'P2M'`_
A string (a [duration interval](https://en.wikipedia.org/wiki/ISO_8601#Durations))
or int (in seconds) of how long we should cache IP lookups.
#### `geoLocationAutoRedirect`
_Default: `false`_
Will automatically redirect the user according to `geoLocationRedirectMap` when
set to true.
#### `geoLocationRedirectMap`
_Default: `[]`_
This dictates what site the user is redirected to based off their IPs location.
It should be a key value array where key is the handle of the site to redirect,
and value is a key value array of user location properties and their required
matches or an string to catch all.
For more details on how to setup your geolocation redirects have a look at the
[Geolocation / Redirect](../geolocation/redirect.md) docs.
```php
<?php
return [
'geoLocationRedirectMap' => [
'uk' => [ 'country' => 'uk' ],
'eu' => [ 'isEU' => true ],
'global' => '*',
],
];
```
================================================
FILE: docs/getting-started/installation.md
================================================
---
title: Installation
---
# Installation
## Plugin Store
You can install **Maps** from the [Craft Plugin Store](https://plugins.craftcms.com/simplemap)
inside your Craft admin! Simply search for "Maps" and click the
<svg xmlns="http://www.w3.org/2000/svg" style="width:20px;vertical-align:text-top" viewBox="0 0 344 345"><g fill="none" fill-rule="evenodd"><path fill="#EF2F30" d="M73.013407,2.6866064 C106.18824,0.895535467 139.183771,-3.78753344e-15 172,0 C204.832174,0 237.664282,0.896405919 270.496323,2.68921776 L270.496323,2.68921771 C308.699378,4.77531668 339.208201,35.2833237 341.295323,73.4863222 C343.098441,106.490881 344,139.495441 344,172.5 C344,205.511425 343.098066,238.52285 341.294197,271.534276 L341.294197,271.534275 C339.20649,309.740047 308.693155,340.248892 270.487077,342.330977 C237.836179,344.110326 205.007154,345 172,345 C139.007428,345 106.01479,344.111111 73.0220864,342.333334 L73.0220864,342.333335 C34.4742283,340.256224 3.79947595,309.257162 2.12787429,270.689591 C0.709291429,237.959727 1.89395185e-15,205.229864 0,172.5 C0,139.776865 0.708999838,107.053729 2.12699951,74.3305938 L2.12699924,74.3305938 C3.79813889,35.7657548 34.4685114,4.76760084 73.013407,2.6866064 Z"/><path fill="#5C0405" fill-rule="nonzero" d="M171.727273,63 C131.659862,63 99,95.5999003 99,135.578099 C99,162.959499 118.676238,184.419499 132.773492,205.617199 C146.870545,226.815099 155.716439,248.2265 163.322107,276.5547 C164.381317,280.5076 168.202458,283 171.727273,283 C175.252088,283 179.073229,280.5076 180.132439,276.5547 C188.433881,245.6352 197.223398,224.385199 211.131661,203.839799 C225.040527,183.294099 244.454545,162.889399 244.454545,135.578099 C244.454545,95.5999003 211.794683,63 171.727273,63 Z M172.181818,107.545455 C186.048118,107.545455 197.181818,118.679154 197.181818,132.545456 C197.181818,146.411755 186.048118,157.545455 172.181818,157.545455 C158.315518,157.545455 147.181818,146.411755 147.181818,132.545456 C147.181818,118.679154 158.315518,107.545455 172.181818,107.545455 Z"/></g></svg>
icon.
## Composer
To install via command-line execute the following from inside your projects root
directory:
```bash
composer require ether/simplemap
./craft install/plugin simplemap
```
================================================
FILE: docs/getting-started/usage.md
================================================
---
title: Usage
---
# Usage
## Creating a Map field
You can create a Map field in the same way you would create any other field in
Craft. Simple go to "Settings" -> "Fields" and click the "New Field" button.
Fill out the fields required and select "Map" from the "Field Type" select.
You can now configure the initial state of the map and how it appears to the
user. Use the Map to select the initial location of the map and the fields
below the map to customize the layout and any restrictions.
## Displaying the Map
When accessing the Map field in twig you will have access to the following
properties:
- **`lat`** The latitude of the selected maps location
- **`lng`** The longitude of the selected maps location
- **`zoom`** The zoom level of the map
- **`address`** The full address (see [Address](#address))
- **`parts`** The separate parts of the address (see [Parts](#parts))
- **`distance`** The distance the location is from your search (only populated when [Searching](#searching))
### Address
The `address` comes in two flavours. The first is as a string, and will output
the Full Address as it appears in the Map field.
```twig
{{ myMapField.address }}
```
The second is as a function, `address([exclude = [] [, glue = '<br/>']])`, which
will allow you to output the address in a more formatted way. Both arguments are
optional.
- **`exclude`** expects an array of [Parts](#parts) that you don't want to
appear when outputting the address. _Defaults to `[]`_.
- **`glue`** is the string that joins the parts together. _Defaults to `'<br/>'`.
```twig
{{ myMapField.address(['country'], ', ') }}
```
The example above is outputting the address as a comma-separated string,
excluding the country.
### Parts
The parts contains the, well, parts that make up the address.
- **`number`** The name or number of the location
- **`address`** The street address of the location (not the full address)
- **`city`** The city in which the location is situated
- **`postcode`** The postal or zip code of the location
- **`county`** The county of the location
- **`state`** The state or region of the location
- **`country`** The locations country
- **`planet`** The planet of the location
- **`system`** The system containing the planet of the location
- **`arm`** The galactic arm of the location
- **`galaxy`** The galaxy the arm is attached to
- **`group`** The group the locations galaxy belongs to
- **`cluster`** The galaxy cluster containing the group
- **`supercluster`** The supercluster the galaxy belongs to
```twig
{{ myMapField.parts.city }}
```
You can also access the parts directly from the map with the exception of the
`address` part, which can be accessed via `streetAddress`.
```twig
{{ myMapField.streetAddress }}
{{ myMapField.city }}
```
### Searching
When querying elements you can filter them by proximity to a given location. To
do so, simply pass an address and radius to the Map field in the
[element query](https://docs.craftcms.com/v3/dev/element-queries/).
- **`location`** An address string, map field, or `{ lat: 0, lng: 0 }` object to
search by.
- **`country`** An optional country to restrict the address string to.
- **`radius`** The radius around the location to get results from. _Defaults to `50`_.
- **`unit`** The distance unit to use. Can be either `mi` or `km`. _Defaults to `km`_.
```twig
{% set entries = craft.entries.myMapField({
location: 'Maidstone, Kent',
country: 'UK',
radius: 100,
unit: 'mi',
}).all() %}
```
If you search using this method you will have access to the `distance` property
in the resulting elements Map fields. This will return the distance that
location is from the location searched for, in the unit specified when searching.
You can also sort by `distance` when searching for an address.
```twig
{% set entries = craft.entries.myMapField({
location: { lat: 51.272154, lng: 0.514951 },
}).orderBy('distance').all() %}
```
```twig
{% set entries = craft.entries.myMapField({
location: myOtherMapField,
}).orderBy('distance desc').all() %}
```
================================================
FILE: docs/how-to/graphql.md
================================================
---
title: Querying in GraphQL
---
# Querying in GraphQL
The query input can support all the parameters that you can use in regular
[Searching](../getting-started/usage/#searching), with the exception that
`location` only supports a string value. This means if you want to search by
lat/lng you need to pass them to the `coordinate` input.
```graphql
{
entries (
map: {
unit: Kilometres
location: "Maidstone, Kent"
country: "UK"
radius: 10
coordinate: {
lat: 51.27136675686769
lng: 0.4939985275268555
}
}
section: "locations"
orderBy: "distance"
) {
title
... on locations_locations_Entry {
map {
lat
lng
distance
zoom
address
parts {
number
address
city
postcode
county
state
country
}
}
}
}
}
```
================================================
FILE: docs/how-to/search.md
================================================
---
title: Search by Location
---
# Search by Location
Being able to search your maps by a location is one of the most important parts
of using a map plugin. So here's how you do it.
When building your [element query](https://docs.craftcms.com/v3/dev/element-queries/)
(in either Twig or PHP) you can pass an object to the map field that will let
you search by your location. This object can have the following properties:
- **`location`**: An address string, map field, or `{ lat: 0, lng: 0 }` object to search by.
- **`country`**: An optional country to restrict the address string to.
- **`radius`**: The radius around the location to get results from. _Defaults to `50`_.
- **`unit`**: The distance unit to use. Can be either `mi` (miles) or `km` (kilometres). _Defaults to `km`_.
### By Address
Let's say you want to search for a location within 10 miles of a given address
(this can be a full address, or just part of an address like a town or city
name). In that case you would do the following:
```twig
{% set entries = craft.entries.myMapField({
location: 'Maidstone, Kent',
radius: 10,
unit: 'mi',
}).all() %}
```
Here we're saying that we want to find all locations with 10 (radius) miles
(unit) of Maidstone, Kent (location).
### By Coordinates
Alternatively you could have a set of coordinates that you want to search by. In
that case you would pass the coordinates to the `location` parameter instead of
an address string.
```twig
{% set entries = craft.entries.myMapField({
location: { lat: 51.272154, lng: 0.514951 },
}).all() %}
```
By excluding the other fields we're letting them fall back to their defaults (as
specified above). In this case we're searching for all locations around those
given coordinates within 50 kilometres.
================================================
FILE: docs/index.md
================================================
---
title: Maps
---

# Maps
A beautifully simple, yet deceptively powerful map field that works out of the
box with no setup or API tokens needed!
Configure the map field to show only the features you want; display
latitude/longitude, hide the address or even the entire map. Choose from 4
geocoding services, 3 geolocation services, and 24 map tilesets!
'Maps' offers full multi-site support, compatibility with Matrix,
[SuperTable](https://verbb.io/craft-plugins/super-table/features),
[CraftQL](https://plugins.craftcms.com/craftql), and the ability to search by
location and sort by distance.
**[View Maps on the Plugin Store](https://plugins.craftcms.com/simplemap)**


================================================
FILE: docs/rendering/embed.md
================================================
---
title: Embed Dynamic Maps
---
# Embed Dynamic Maps
With **Maps** you can quickly and easily output an interactive map using one of
two templating methods. The first is via a Map field, and the second is using
the global map variable.
## Options
*The available options include those from the [Static Map](./static.md#options)
as well as two additional options:*
- **`id`** - The ID to use when creating the map (using in JavaScript and on the
HTML tag).
- **`options`** - An object of options that will be passed to the JavaScript,
allowing you to customise the map according to the map library
being used.
## From a Map field
Both of the below `embed` methods will return a div tag with the given (or
generated) ID, and will include the necessary JavaScript and CSS to render the
map. They both accept an [options](#options) object as their only parameter.
### `mapField.embed([options])`
To render a dynamic map from a Map field, use the `embed` method on the fields
value. The `center` and `zoom` options will be ignored, since their values are
gathered from the Map field's value.
```twig
{{ myMapField.embed({
id: 'map',
markers: [{}],
}) }}
```
### `craft.maps.embed([options])`
```twig
{{ craft.maps.embed({
center: 'Maidstone, UK',
options: {
disableDefaultUI: true,
draggable: false,
},
}) }}
```
## Additional attributes
If you want to add additional attributes to the output div from the `embed`
methods you should do so using Craft's built-in [`|attr` filter](https://docs.craftcms.com/v3/dev/filters.html#attr).
```twig
{{ myMapField.embed()|attr({
class: 'map',
}) }}
```
## Libraries & Caveats
### Google Maps
The Google Maps service uses Google's [Maps JavaScript library](https://developers.google.com/maps/documentation/javascript/reference/).
You can view the options that you can pass to `options.options` [here](https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions).
**Caveats**
- Google Maps doesn't support coloured markers.
### Apple Maps
Apple Maps uses the [Mapkit JS library](https://developer.apple.com/documentation/mapkitjs).
You can view the options [here](https://developer.apple.com/documentation/mapkitjs/mapconstructoroptions).
### Mapbox
Mapbox uses [Mapbox](https://docs.mapbox.com/mapbox-gl-js/api/). You can view
the options [here](https://docs.mapbox.com/mapbox-gl-js/api/#map).
**Caveats**
- Mapbox doesn't support marker labels.
### Here
Here uses [Here](https://developer.here.com/documentation/maps/topics/overview.html).
You can view the options [here](https://developer.here.com/documentation/maps/topics_api/h-map-options.html).
### Others
Wikimedia, OSM, and Carto all use [Leaflet JS](https://leafletjs.com/). You can
view the options [here](https://leafletjs.com/reference-1.5.0.html#map-option).
================================================
FILE: docs/rendering/static.md
================================================
---
title: Static Map Images
---
# Static Map Images
**Maps** makes it really easy to render static map images in twig. There are two
ways of rendering a static map image. First is via a Map field, second is using
the global map variable.
## Options
Both methods support the following options, passed as a Twig object to the
method (we'll cover that later):
- **`center`** - This can be an address string (i.e. "Maidstone, UK"), a lat /
lng variable (i.e. `[51.272154, 0.514951]` or
`{ lat: 51.272154, lng: 0.514951 }`).
- **`centerFallback`** - The fallback location that will be used if the center
specified above is empty. Must be lat / lng.
- **`width`** - The width of the map image (see max image sizes below).
- **`height`** - The height of the map image (see max image sizes below).
- **`zoom`** - The zoom level of the map (must be between 0 and 18).
- **`scale`** - The scale of the image (can be either 1 or 2 (retina), defaults to 1).
- **`markers`** - An array of map [markers](#markers) (see below)
### Markers
The `markers` option accepts an array of objects with the following parameters
(all are optional):
- **`location`** - This can be an address string (i.e. "Maidstone, UK"), a lat /
lng variable (i.e. `[51.272154, 0.514951]` or
`{ lat: 51.272154, lng: 0.514951 }`). If left blank the
marker will appear at the center of the map.
- **`color`** - A valid hex colour string. Will default to `#ff0000` if blank.
- **`label`** - An option label, must be a single character A-Z0-9.
## From a Map field
To render a map from a Map field, use the `img` or `imgSrcSet` methods on the
fields value:
```twig
{% set myMapField = entry.myMapField %}
<img
src="{{ myMapField.img() }}"
srcset="{{ myMapField.imgSrcSet() }}"
alt="{{ myMapField.address }}"
/>
```
### `mapField.img([options])`
The `img` method returns the URL for the static map image. It accepts an
[options](#options) object as its only parameter. Since we already
have a location and zoom level from the Map field, the `center` and `zoom`
options will be ignored.
```twig
{{ entry.mapField.img({
width: 800,
height: 600,
}) }}
```
### `mapField.imgSrcSet([options])`
`imgSrcSet` is similar to `img` accept it returns a `srcset` ready string,
supporting @1x and @2x resolutions. As with `img` it accepts an
[options](#options) object as its only parameter. Along with the `center` and
`zoom` options being ignored (as with `img`), the `scale` option is also
ignored.
## Using the global `maps` variable
You can turn any address into a static map using the globally available `maps`
variable.
```twig
<img
src="{{ craft.maps.img() }}"
srcset="{{ craft.maps.imgSrcSet() }}"
alt=""
/>
```
### `craft.maps.img([options])`
The `img` method returns the URL for the static map image. It accepts an
[options](#options) object as its only parameter.
```twig
{{ craft.maps.img({
center: 'Maidstone, UK',
width: 800,
height: 600,
}) }}
```
### `craft.maps.imgSrcSet([options])`
`imgSrcSet` is similar to `img` accept it returns a `srcset` ready string,
supporting @1x and @2x resolutions. As with `img` it accepts an
[options](#options) object as its only parameter. The `scale` is ignored.
================================================
FILE: resources/.editorconfig
================================================
[*.{js,jsx,ts,tsx,vue}]
indent_style = tab
indent_size = 4
end_of_line = lf
insert_final_newline = true
max_line_length = 80
================================================
FILE: resources/.gitignore
================================================
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
================================================
FILE: resources/README.md
================================================
# simplemap
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn run serve
```
### Compiles and minifies for production
```
yarn run build
```
### Run your tests
```
yarn run test
```
### Lints and fixes files
```
yarn run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
================================================
FILE: resources/babel.config.js
================================================
module.exports = {
presets: [
'@vue/app',
],
};
================================================
FILE: resources/package.json
================================================
{
"name": "simplemap",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "PORT=8080 vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"leaflet": "^1.4.0",
"leaflet.gridlayer.googlemutant": "^0.8.0",
"leaflet.mapkitmutant": "^0.3.0",
"vue": "^2.6.10",
"vue-autosuggest": "^2.0.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.3.0",
"@vue/cli-plugin-eslint": "^3.3.0",
"@vue/cli-service": "^3.3.0",
"@vue/eslint-config-airbnb": "^4.0.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.8.0",
"eslint-plugin-vue": "^5.0.0",
"less": "^3.0.4",
"less-loader": "^4.1.0",
"vue-template-compiler": "^2.5.21",
"webpack-bundle-analyzer": "^3.4.1"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"@vue/airbnb"
],
"rules": {
"no-tabs": "off",
"indent": "off",
"padded-blocks": "off",
"comma-dangle": "off",
"arrow-parens": "off",
"space-before-function-paren": "off",
"no-underscore-dangle": "off",
"no-param-reassign": "off",
"nonblock-statement-body-position": "off",
"func-names": "off",
"prefer-template": "off",
"lines-between-class-members": "off",
"class-methods-use-this": "off",
"curly": "off",
"prefer-destructuring": "off",
"no-trailing-spaces": "off",
"import/extensions": "off",
"max-len": "off",
"comma-style": "off",
"import/prefer-default-export": "off",
"one-var": "off",
"operator-linebreak": "off",
"no-multi-spaces": "off",
"no-prototype-builtins": "off",
"object-curly-newline": "off",
"no-plusplus": "off",
"no-continue": "off"
},
"parserOptions": {
"parser": "babel-eslint",
"ecmaFeatures": {
"legacyDecorators": true
}
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
================================================
FILE: resources/src/App.vue
================================================
<template>
<div>
<div :class="wrapCls" ref="field">
<Map
v-if="!config.hideMap"
:tiles="config.mapTiles"
:token="config.mapToken"
:latLng="{ lat: val.lat, lng: val.lng }"
:zoom="+val.zoom"
:min-zoom="config.minZoom"
:max-zoom="config.maxZoom"
@change="onMapChange"
@zoom="onZoom"
:hide-search="config.hideSearch"
:hide-address="config.hideAddress"
:show-current-location="config.showCurrentLocation"
:w3w-enabled="config.w3wEnabled"
:show-w3w-grid="config.showW3WGrid"
/>
<div :class="[$style.content, config.hideMap && $style.noMap]">
<Search
v-if="!config.hideSearch"
:service="config.geoService"
:default-value="val.address"
:geo="geo"
@selected="onSearchSelected"
@open-offset="onResultsOpenOffset"
:has-map="!config.hideMap"
/>
<Address
v-if="!config.isSettings"
:hide="config.hideAddress"
:has-search="!config.hideSearch"
:has-map="!config.hideMap"
:showLatLng="config.showLatLng"
:value="val"
@changed="onPartChange"
@clear="onClear"
@w3w-change="onW3WChange"
:open-offset="resultsOpenOffset"
:show-w3w-field="config.showW3WField"
/>
</div>
</div>
<div v-if="config.size === 'mini'" :class="$style.mini">
<span :class="value.address === '' && $style.empty">{{ value.address || emptyLabel }}</span>
<button
@click="onEditClick"
type="button"
ref="btn"
class="btn"
>Edit</button>
</div>
<input
type="hidden"
:name="config.name"
:value="JSON.stringify(value)"
v-if="!config.isSettings"
/>
<Fragment v-if="config.isSettings">
<input
type="hidden"
:name="config.name.replace('__settings__', 'lat')"
:value="value.lat"
/>
<input
type="hidden"
:name="config.name.replace('__settings__', 'lng')"
:value="value.lng"
/>
<input
type="hidden"
:name="config.name.replace('__settings__', 'zoom')"
:value="value.zoom"
/>
</Fragment>
</div>
</template>
<script lang="js">
import Geo from './common/Geo';
import GeoService from './enums/GeoService';
import Parts from './models/Parts';
import Fragment from './components/Fragment';
import PartsLegacy from './models/PartsLegacy';
import Search from './components/Search';
import Address from './components/Address';
import Map from './components/Map';
import { t } from './filters/craft';
const MapHud = window.Garnish.HUD.extend();
export default {
props: {
options: String,
},
components: {
Search,
Address,
Map,
Fragment,
},
data () {
return {
config: {
isSettings: false,
name: '',
hideSearch: false,
hideMap: false,
hideAddress: false,
showLatLng: false,
showCurrentLocation: false,
minZoom: 3,
maxZoom: 20,
mapTiles: 'wikimedia',
mapToken: '',
geoService: 'nominatim',
geoToken: '',
w3wEnabled: false,
showW3WGrid: false,
showW3WField: false,
locale: 'en',
size: 'large',
},
value: {
address: '',
zoom: 15,
lat: null,
lng: null,
parts: new Parts(),
what3words: '',
},
geo: null,
fullAddressDirty: false,
defaultValue: null,
emptyLabel: t('No address selected'),
hud: null,
resultsOpenOffset: 0,
};
},
created () {
const { config, value, defaultValue } = JSON.parse(this.options);
const isGoogle = config.geoService === GeoService.GoogleMaps;
this.config = config;
this.value = value;
this.value.parts = isGoogle
? new PartsLegacy(value.parts)
: Parts.from(value.parts);
this.defaultValue = defaultValue;
this.defaultValue.parts = isGoogle
? new PartsLegacy()
: new Parts();
this.geo = new Geo(config);
},
mounted () {
if (this.config.size === 'mini')
this.$refs.field.style.display = 'none';
},
computed: {
wrapCls () {
const cls = [this.$style.wrap];
if (this.config.hideMap)
cls.push(this.$style['no-map']);
return cls;
},
val () {
return this.value.lat === null ? this.defaultValue : this.value;
},
},
methods: {
onEditClick () {
if (this.hud) {
this.hud.show();
return;
}
let minBodyWidth;
if (this.config.hideMap) {
minBodyWidth = Math.min(626, window.innerWidth * 0.9);
} else {
minBodyWidth = Math.min(834, window.innerWidth * 0.9);
}
this.$refs.field.style.display = 'block';
this.hud = new MapHud(this.$refs.btn, this.$refs.field, {
minBodyWidth,
});
},
onResultsOpenOffset (value) {
this.resultsOpenOffset = value;
},
async onSearchSelected (item) {
this.value = {
...this.value,
...item,
};
await this.updateW3WByLatLng({ lat: item.lat, lng: item.lng });
},
async onMapChange (latLng) {
const zoom = this.value.zoom;
await this.onLatLngChange(latLng);
await this.updateW3WByLatLng(latLng);
this.value.zoom = zoom;
this.fullAddressDirty = false;
},
async updateW3WByLatLng (latLng) {
if (this.config.w3wEnabled) {
try {
const res = await window.what3words.api.convertTo3wa(latLng);
this.value.what3words = res.words;
} catch (e) {
this.value.what3words = null;
}
} else {
this.value.what3words = null;
}
},
async onW3WChange (words) {
if (this.config.w3wEnabled && !/([a-z]+\.){2}[a-z]+/g.test(words))
return;
try {
const res = await window.what3words.api.convertToCoordinates(words);
this.onLatLngChange(res.coordinates);
this.value.what3words = words;
} catch (e) {
// do nothing
}
},
async onLatLngChange (latLng) {
switch (this.config.geoService) {
case GeoService.Nominatim:
this.value = await this.geo.reverseNominatim(latLng, this.value);
break;
case GeoService.Mapbox:
this.value = await this.geo.reverseMapbox(latLng, this.value);
break;
case GeoService.GoogleMaps:
this.value = await this.geo.reverseGoogle(latLng, this.value);
break;
case GeoService.AppleMapKit:
this.value = await this.geo.reverseApple(latLng, this.value);
break;
case GeoService.Here:
this.value = await this.geo.reverseHere(latLng, this.value);
break;
default:
throw new Error('Unknown geo service: ' + this.config.geoService);
}
},
onZoom (zoom) {
this.value.zoom = zoom;
},
onPartChange ({ name, value }) {
if (name === 'fullAddress') {
this.value.address = value;
this.fullAddressDirty = value !== '';
} else if (name === 'lat' || name === 'lng') {
this.value[name] = value;
} else {
this.value.parts[name] = value;
if (this.value.address === '' || !this.fullAddressDirty) {
const parts = [];
const keys = Object.keys(this.value.parts);
for (let i = 0, l = keys.length; i < l; i++) {
const k = keys[i];
// Filter out guff google properties
if (['number', 'address', 'city', 'postcode', 'county', 'state', 'country'].indexOf(k) === -1)
continue;
parts.push(this.value.parts[k]);
}
this.value.address = parts.filter(Boolean).join(', ');
}
}
},
onClear () {
this.value = {
address: '',
zoom: 15,
lat: null,
lng: null,
parts: new Parts(),
what3words: null,
};
},
},
};
</script>
<style lang="less" module>
.wrap {
position: relative;
margin: 0 -24px;
min-height: 284px;
overflow: hidden;
@media only screen and (max-width: 767px) {
margin: 0 -12px;
}
:global(.hud) & {
margin: -24px !important;
}
:global(.matrixblock) & {
margin: 0 -14px !important;
}
:global(.superTable-layout-table) &,
:global(.superTable-layout-row) & {
margin: -4px -10px !important;
}
&.no-map {
min-height: 0;
margin: 0;
overflow: visible;
.content {
padding: 0;
:global(.hud) & {
width: 100%;
padding: 24px;
}
}
}
}
.mini {
display: flex;
align-items: center;
justify-content: space-between;
:global(.btn) {
margin-left: 24px;
font-size: 14px;
}
}
.empty {
opacity: 0.5;
}
.content {
position: relative;
z-index: 2;
box-sizing: border-box;
width: 50%;
padding: 24px;
pointer-events: none;
&.noMap {
width: 100%;
}
:global(.matrixblock) &,
:global(.superTable-layout-table) &,
:global(.superTable-layout-row) & {
padding: 14px;
}
@media only screen and (max-width: 767px) {
width: 100%;
padding: 12px;
}
& > * {
pointer-events: all;
}
}
</style>
================================================
FILE: resources/src/common/Geo.js
================================================
import GeoService from '../enums/GeoService';
import Parts from '../models/Parts';
import PartsLegacy from '../models/PartsLegacy';
import waitForGlobal from '../helpers/waitForGlobal';
export default class Geo {
// Properties
// =========================================================================
country = null;
service = null;
token = null;
locale = null;
google = { service: null, session: null };
apple = { Search: null };
// Constructor
// =========================================================================
constructor ({ country, geoService: service, geoToken: token, locale }) {
this.country = country ? country.toLowerCase() : null;
this.service = service;
this.token = token;
this.locale = locale;
if (service === GeoService.GoogleMaps) {
waitForGlobal('google', () => this.initGoogle());
} else if (service === GeoService.AppleMapKit) {
waitForGlobal('mapkit', () => this.initApple(token));
}
}
// Initializers
// =========================================================================
initGoogle () {
this.google = {
service: new window.google.maps.places.AutocompleteService(),
session: new window.google.maps.places.AutocompleteSessionToken(),
geocoder: new window.google.maps.Geocoder(),
places: new window.google.maps.places.PlacesService(
document.createElement('div')
),
};
}
initApple (token) {
window.mapkit.init({
authorizationCallback: done => done(token),
});
this.apple = {
Search: new window.mapkit.Search(),
Geocoder: new window.mapkit.Geocoder(),
Coordinate: window.mapkit.Coordinate,
};
}
// Actions
// =========================================================================
// Actions: Search
// -------------------------------------------------------------------------
/**
* Run the search
*
* @param {string} text
* @returns {Promise<Array>}
*/
async search (text) {
if (!text || text.trim() === '') {
return [];
}
let suggestions = [];
switch (this.service) {
case GeoService.Nominatim:
suggestions = await this.searchNominatim(text);
break;
case GeoService.Mapbox:
suggestions = await this.searchMapbox(text);
break;
case GeoService.GoogleMaps:
suggestions = await this.searchGoogle(text);
break;
case GeoService.AppleMapKit:
suggestions = await this.searchApple(text);
break;
case GeoService.Here:
suggestions = await this.searchHere(text);
break;
default:
throw new Error('Unknown geocoding service: ' + this.service);
}
return suggestions;
}
/**
* Search using Nominatim
*
* @param {string} query
* @returns {Promise<*>}
*/
async searchNominatim (query) {
const params = new URLSearchParams({
q: query,
format: 'jsonv2',
limit: 5,
addressdetails: 1,
countrycodes: this.country,
'accept-language': this.locale,
}).toString();
const data = await fetch(
'https://nominatim.openstreetmap.org/search?' + params
).then(res => res.json());
return data.map(result => ({
address: result.display_name,
lat: result.lat,
lng: result.lon,
parts: new Parts({
...result.address,
type: result.type,
}, GeoService.Nominatim),
}));
}
/**
* Search using Mapbox
*
* @param {string} query
* @returns {Promise<*>}
*/
async searchMapbox (query) {
const rawParams = {
types: 'address,country,postcode,place,locality,district,neighborhood',
limit: 5,
access_token: this.token,
language: this.locale,
};
if (this.country)
rawParams.country = this.country;
const params = new URLSearchParams(rawParams).toString();
const data = await fetch(
'https://api.mapbox.com/geocoding/v5/mapbox.places/' + query + '.json?' + params
).then(res => res.json());
return data.features.map(result => ({
address: result.place_name,
lat: result.center[1],
lng: result.center[0],
parts: new Parts(result, GeoService.Mapbox),
}));
}
/**
* Search using Google Places
*
* @param {string} query
* @returns {Promise<*>}
*/
searchGoogle (query) {
return new Promise(resolve => {
this.google.service.getPlacePredictions({
input: query,
sessionToken: this.google.session,
componentRestrictions: {
country: this.country,
},
}, predictions => {
if (!predictions)
return resolve([]);
return resolve(predictions.map(result => ({
__placeId: result.place_id,
address: result.description,
// See Geo::getGooglePlaceDetails() for `lat`, `lng`, and `parts`
})));
});
});
}
/**
* Search using Apple MapKit
*
* @param {string} query
* @return {Promise<*>}
*/
searchApple (query) {
return new Promise(resolve => {
this.apple.Search.autocomplete(query, (err, data) => {
resolve(data.results.slice(0, 5).map(result => ({
address: result.displayLines.join(', '),
lat: result.coordinate.latitude,
lng: result.coordinate.longitude,
// There's no way to get detailed address information from MapKit :(
parts: new Parts(null, GeoService.AppleMapKit),
})));
});
// TODO: Workout how to support preferred country
});
}
/**
* Search using Here
*
* @param {string} query
* @return {Promise<*>}
*/
async searchHere (query) {
const params = new URLSearchParams({
app_id: this.token.appId,
app_code: this.token.appCode,
query,
country: this.country ? this.country.toUpperCase() : '',
maxresults: 5,
language: this.locale,
}).toString();
const data = await fetch(
'https://autocomplete.geocoder.api.here.com/6.2/suggest.json?' + params
).then(res => res.json());
if (!data.hasOwnProperty('suggestions'))
return [];
return data.suggestions.map(suggestion => ({
__placeId: suggestion.locationId,
address: suggestion.label,
// See Geo::getHerePlaceDetails() for `lat`, `lng`, and `parts`
}));
}
// Actions: Reverse
// -------------------------------------------------------------------------
/**
* Lookup the given lat/lng using Nominatim
*
* @param lat
* @param lng
* @param oldVal
* @return {Promise<{address: *, lng: *, parts: Parts, lat: *}>}
*/
async reverseNominatim ({ lat, lng }, oldVal) {
const params = new URLSearchParams({
lat,
lon: lng,
format: 'jsonv2',
addressdetails: 1,
'accept-language': this.locale,
}).toString();
const result = await fetch(
'https://nominatim.openstreetmap.org/reverse?' + params
).then(res => res.json());
if (!result || (result.hasOwnProperty('error') && result.error))
return { address: '', lat, lng, parts: new Parts() };
return {
...oldVal,
address: result.display_name,
lat,
lng,
parts: new Parts({
...result.address,
type: result.type,
}, GeoService.Nominatim),
};
}
/**
* Lookup the given lat/lng using Mapbox
*
* @param lat
* @param lng
* @param oldVal
* @return {Promise<{address: *, lng: *, parts: Parts, lat: *}>}
*/
async reverseMapbox ({ lat, lng }, oldVal) {
const params = new URLSearchParams({
types: 'address,country,postcode,place,locality,district,neighborhood',
limit: 1,
access_token: this.token,
language: this.locale,
}).toString();
const result = await fetch(
'https://api.mapbox.com/geocoding/v5/mapbox.places/' + lng + ',' + lat + '.json?' + params
).then(res => res.json());
const feature = result.features[0];
if (!feature)
return { address: '', lat, lng, parts: new Parts() };
return {
...oldVal,
address: feature.place_name,
lat,
lng,
parts: new Parts(feature, GeoService.Mapbox),
};
}
/**
* Lookup the given lat/lng using Google Maps
*
* @param latLng
* @param oldVal
* @return {Promise<any>}
*/
reverseGoogle (latLng, oldVal) {
return new Promise(resolve => {
this.google.geocoder.geocode({
location: latLng,
}, results => {
const result = results[0];
resolve({
...oldVal,
address: result.formatted_address,
...latLng,
parts: new PartsLegacy(
result.address_components
),
});
});
});
}
/**
* Lookup the given lat/lng using Apple MapKit
*
* @param lat
* @param lng
* @param oldVal
* @return {Promise<any>}
*/
reverseApple ({ lat, lng }, oldVal) {
return new Promise(resolve => {
this.apple.Geocoder.reverseLookup(
new this.apple.Coordinate(lat, lng),
(err, data) => {
const result = data.results[0];
resolve({
...oldVal,
address: result.formattedAddress,
lat,
lng,
// There's no way to get detailed address information from MapKit :(
parts: new Parts(null, GeoService.AppleMapKit),
});
}
);
});
}
/**
* Lookup the given lat/lng using Here
*
* @param lat
* @param lng
* @param oldVal
* @return {Promise<{address: *, lng: *, parts: Parts, lat: *}>}
*/
async reverseHere ({ lat, lng }, oldVal) {
const params = new URLSearchParams({
app_id: this.token.appId,
app_code: this.token.appCode,
mode: 'retrieveAddresses',
jsonattributes: 1,
limit: 1,
prox: `${lat},${lng},1`,
language: this.locale,
});
const { response } = await fetch(
'https://reverse.geocoder.api.here.com/6.2/reversegeocode.json?' + params
).then(res => res.json());
if (response.view.length === 0)
return { address: '', lat, lng, parts: new Parts() };
const { address } = response.view[0].result[0].location;
return {
...oldVal,
address: address.label,
lat,
lng,
parts: new Parts(address, GeoService.Here),
};
}
// Helpers
// =========================================================================
/**
* Gets the details about the given place
*
* @param {string} placeId
* @param {Object} item
* @returns {Promise<*>}
*/
getGooglePlaceDetails (placeId, item) {
return new Promise(resolve => {
this.google.places.getDetails({
placeId,
fields: [
'geometry',
'address_component',
],
}, place => {
resolve({
...item,
address: item.address,
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
parts: new PartsLegacy(
place.address_components
),
});
});
});
}
/**
* Gets the details about the given location
*
* @param {string} locationId
* @param {Object} item
* @return {Promise<*>}
*/
async getHerePlaceDetails (locationId, item) {
const params = new URLSearchParams({
app_id: this.token.appId,
app_code: this.token.appCode,
locationid: locationId,
jsonattributes: 1,
gen: 9,
language: this.locale,
}).toString();
const data = await fetch(
'https://geocoder.api.here.com/6.2/geocode.json?' + params
).then(res => res.json());
const place = data.response.view[0].result[0].location;
return {
...item,
lat: place.displayPosition.latitude,
lng: place.displayPosition.longitude,
parts: new Parts(
place.address,
GeoService.Here
),
};
}
}
================================================
FILE: resources/src/components/Address.vue
================================================
<template>
<div :class="cls" :style="styl">
<Fragment v-if="showLatLng">
<Input
:label="labels.lat"
:value="value.lat"
@input="onInput('lat', $event)"
/>
<Input
:label="labels.lng"
:value="value.lng"
@input="onInput('lng', $event)"
/>
</Fragment>
<div :class="[$style.full, $style.row]">
<Input
:label="labels.fullAddress"
:value="value.address"
@input="onInput('fullAddress', $event)"
:disabled="hide"
/>
<button
:class="$style.btn"
@click="onClear()"
type="button"
@mouseenter="onDeleteEnter"
@mouseleave="onDeleteLeave"
:title="labels.clear"
>
<svg width="14" height="14" viewBox="0 0 14 14">
<path fill="#29323D" d="M7 14c-3.832 0-7-3.167-7-6.997C0 3.167 3.162 0 6.993 0 10.825 0 14 3.167 14 7.003 14 10.833 10.832 14 7 14zM4.65 9.994a.65.65 0 0 0 .468-.19l1.875-1.88 1.889 1.88c.115.116.27.19.46.19.366 0 .65-.29.65-.65a.674.674 0 0 0-.19-.46l-1.888-1.88 1.889-1.895a.581.581 0 0 0 .196-.447.64.64 0 0 0-.65-.643.59.59 0 0 0-.453.19L6.993 6.097 5.104 4.216a.607.607 0 0 0-.453-.19.64.64 0 0 0-.65.643c0 .17.074.331.196.447L6.08 7.003 4.197 8.898a.608.608 0 0 0-.196.447c0 .358.291.65.65.65z"/>
</svg>
</button>
</div>
<Input
:label="labels.what3words"
:value="'/// ' + (value.what3words || '')"
:class="$style.full"
v-if="showW3wField"
@input="onW3WInput"
/>
<Fragment v-if="!hide">
<Input
:label="labels.number"
:value="value.parts.number"
@input="onInput('number', $event)"
/>
<Input
:label="labels.address"
:value="value.parts.address"
@input="onInput('address', $event)"
:class="$style.right"
/>
<Input
:label="labels.city"
:value="value.parts.city"
@input="onInput('city', $event)"
/>
<Input
:label="labels.postcode"
:value="value.parts.postcode"
@input="onInput('postcode', $event)"
:class="$style.right"
/>
<Input
:label="labels.county"
:value="value.parts.county"
@input="onInput('county', $event)"
/>
<Input
:label="labels.state"
:value="value.parts.state"
@input="onInput('state', $event)"
:class="$style.right"
/>
<Input
:label="labels.country"
:value="value.parts.country"
@input="onInput('country', $event)"
/>
</Fragment>
</div>
</template>
<script lang="js">
import Input from './Input';
import Fragment from './Fragment';
import { t } from '../filters/craft';
export default {
props: {
value: {
type: Object,
default: () => ({
address: '',
lat: 0,
lng: 0,
parts: {},
what3words: '',
}),
},
showLatLng: Boolean,
fullAddressDirty: Boolean,
hide: Boolean,
hasSearch: Boolean,
hasMap: Boolean,
size: String,
openOffset: Number,
showW3wField: Boolean,
},
components: {
Input,
Fragment,
},
data () {
return {
hoverDelete: false,
labels: {
fullAddress: t('Full Address'),
number: t('Name / Number'),
address: t('Street Address'),
city: t('Town / City'),
postcode: t('Postcode'),
county: t('County'),
state: t('State'),
country: t('Country'),
lat: t('Latitude'),
lng: t('Longitude'),
clear: t('Clear address'),
what3words: t('what3words'),
}
};
},
computed: {
cls () {
const cls = [this.$style.grid];
if (this.hasMap && this.openOffset > 0)
cls.push(this.$style.fade);
if (!this.hasSearch)
cls.push(this.$style['no-search']);
if (!this.hasMap)
cls.push(this.$style['no-map']);
if (this.hoverDelete)
cls.push(this.$style.delete);
if (this.hasValue)
cls.push(this.$style['show-clear']);
return cls;
},
styl () {
if (!this.hasMap)
return {};
return {
transform: `translateY(${this.openOffset}px)`,
};
},
hasValue () {
return this.value.address !== null;
},
},
methods: {
onInput (name, e) {
this.$emit('changed', {
name,
value: e.target.value,
});
},
async onW3WInput (e) {
this.$emit(
'w3w-change',
e.target.value.trim().replace(/\/|\s/g, '').toLowerCase()
);
},
onClear () {
this.$emit('clear');
},
onDeleteEnter () {
this.hoverDelete = true;
},
onDeleteLeave () {
this.hoverDelete = false;
},
},
};
</script>
<style lang="less" module>
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.20);
overflow: hidden;
transition: transform 0.3s ease, opacity 0.3s ease;
&:not(:first-child) {
margin-top: 24px;
}
&.fade {
opacity: 0.8;
}
&.no-map {
box-shadow: none;
border: 1px solid #DCE4EA;
}
@media only screen and (max-width: 1199px) {
grid-template-columns: 1fr;
label {
border-right: none;
}
}
@media only screen and (max-width: 767px) {
&:not(.no-map):not(.no-search) {
margin-top: 200px !important;
}
&:not(.no-map).no-search {
margin-top: 260px !important;
}
}
&, * {
box-sizing: border-box;
}
label.right {
border-right: none;
}
.full,
label:last-child {
grid-column: span 2;
border-right: none;
@media only screen and (max-width: 1199px) {
grid-column: span 1;
}
}
}
.row {
display: flex;
&:last-child > * {
border-bottom: none;
}
label {
flex-grow: 1;
border-right: none;
}
}
.btn {
appearance: none;
background: none;
border: none;
border-bottom: 1px solid #DCE4EA;
border-radius: 0 5px 0 0;
cursor: pointer;
pointer-events: none;
svg {
opacity: 0.5;
transform: translateX(200%);
transition: transform 0.3s ease, opacity 0.15s ease;
}
path {
transition: fill 0.15s ease;
}
.show-clear & {
pointer-events: auto;
svg {
transform: translateX(0);
}
}
&:hover {
svg {
opacity: 1;
}
path {
fill: #F22C26;
}
}
}
.delete input {
color: #F22C26;
}
</style>
================================================
FILE: resources/src/components/Fragment.vue
================================================
<script>
export default {
functional: true,
render (h, ctx) {
return ctx.children;
}
};
</script>
================================================
FILE: resources/src/components/Input.vue
================================================
<template>
<Label :label="label" :class="className">
<input
:class="$style.input"
:type="type"
:name="name"
:value="value"
:disabled="disabled"
@input="$emit('input', $event)"
/>
</Label>
</template>
<script lang="js">
import Label from './Label';
export default {
props: {
label: String,
name: String,
type: {
type: String,
default: 'text',
},
value: [String, Number],
disabled: Boolean,
className: Boolean,
},
components: {
Label,
},
};
</script>
<style lang="less" module>
.input {
width: 100%;
padding: 29px 0 10px;
color: #29323D;
font-size: 15px;
letter-spacing: 0;
text-indent: 12px;
appearance: none;
background: none;
border: none;
border-radius: 0;
transition: color 0.15s ease;
&:disabled {
opacity: 0.5;
}
}
</style>
================================================
FILE: resources/src/components/Label.vue
================================================
<template>
<label :class="$style.label">
<span :class="$style.name">
{{ label }}
</span>
<slot></slot>
</label>
</template>
<script lang="js">
export default {
props: {
label: String,
},
};
</script>
<style lang="less" module>
.label {
position: relative;
display: block;
border-right: 1px solid #DCE4EA;
&:not(:last-child) {
border-bottom: 1px solid #DCE4EA;
}
}
.name {
position: absolute;
top: 9px;
left: 12px;
display: block;
color: rgba(41, 50, 61, 0.41);
font-size: 12px;
font-weight: 500;
letter-spacing: 0;
}
</style>
================================================
FILE: resources/src/components/Map.vue
================================================
<template>
<div :class="cls"></div>
</template>
<script lang="js">
// TODO: Only load mutants in if they're needed
import L from 'leaflet';
import 'leaflet.gridlayer.googlemutant';
import 'leaflet.mapkitmutant';
import MapTiles from '../enums/MapTiles';
import 'leaflet/dist/leaflet.css';
import waitForGlobal from '../helpers/waitForGlobal';
import { t } from '../filters/craft';
import createW3WGrid from '../helpers/createW3WGrid';
const icon = (w, h, f = '#E7433B') => `<svg width="${w}" height="${h}" viewBox="0 0 14 20"><path fill="${f}" d="M6.976.478C3.482.478.634 3.313.634 6.79c0 2.381 1.716 4.247 2.945 6.09 1.23 1.844 2 3.706 2.664 6.17a.78.78 0 0 0 .733.56c.308 0 .64-.217.733-.56.724-2.69 1.49-4.537 2.704-6.324 1.213-1.786 2.906-3.56 2.906-5.936 0-3.476-2.849-6.31-6.343-6.31zm.04 3.874c1.21 0 2.18.968 2.18 2.174A2.17 2.17 0 0 1 7.016 8.7a2.17 2.17 0 0 1-2.18-2.174 2.17 2.17 0 0 1 2.18-2.174z"/></svg>`;
const posIcon = (w, h) => `<svg width="${w}" height="${h}" viewBox="0 0 48 48"><path fill="rgba(41, 50, 61, 0.75)" d="M24 16c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm17.88 6c-.92-8.34-7.54-14.96-15.88-15.88v-4.12h-4v4.12c-8.34.92-14.96 7.54-15.88 15.88h-4.12v4h4.12c.92 8.34 7.54 14.96 15.88 15.88v4.12h4v-4.12c8.34-.92 14.96-7.54 15.88-15.88h4.12v-4h-4.12zm-17.88 16c-7.73 0-14-6.27-14-14s6.27-14 14-14 14 6.27 14 14-6.27 14-14 14z"/></svg>`;
export default {
props: {
tiles: String,
token: [String, Object],
latLng: Object,
zoom: Number,
minZoom: Number,
maxZoom: Number,
hideSearch: Boolean,
hideAddress: Boolean,
showCurrentLocation: Boolean,
w3wEnabled: Boolean,
showW3wGrid: Boolean,
},
data () {
return {
map: null,
marker: null,
};
},
mounted () {
this.map = L.map(this.$el, {
minZoom: this.minZoom,
maxZoom: this.maxZoom,
scrollWheelZoom: false,
zoomControl: false,
}).setView(this.latLng, this.zoom);
const self = this;
L.Control.CustomZoom = L.Control.extend({
onAdd: map => {
const wrap = L.DomUtil.create('div')
, zIn = L.DomUtil.create('button')
, zOut = L.DomUtil.create('button')
, center = L.DomUtil.create('button')
, current = L.DomUtil.create('button');
wrap.classList.add(self.$style.control);
zIn.textContent = '+';
zOut.textContent = '-';
center.innerHTML = icon(
14 * 0.75,
20 * 0.75,
'rgba(41, 50, 61, 0.75)'
);
current.innerHTML = posIcon(
20 * 0.75,
20 * 0.75,
);
zIn.setAttribute('title', t('Zoom In'));
zOut.setAttribute('title', t('Zoom Out'));
center.setAttribute('title', t('Center on Marker'));
current.setAttribute('title', t('Current Location'));
wrap.appendChild(zIn);
wrap.appendChild(zOut);
wrap.appendChild(center);
if (this.showCurrentLocation && 'geolocation' in navigator)
wrap.appendChild(current);
zIn.addEventListener('click', e => {
e.preventDefault();
map.setZoomAround(
self.getOffsetPoint(map.getCenter(), true),
map.getZoom() + map.options.zoomDelta,
);
});
zOut.addEventListener('click', e => {
e.preventDefault();
map.setZoomAround(
self.getOffsetPoint(map.getCenter(), true),
map.getZoom() - map.options.zoomDelta,
);
});
center.addEventListener('click', e => {
e.preventDefault();
self.panTo(self.latLng);
});
current.addEventListener('click', e => {
e.preventDefault();
navigator.geolocation.getCurrentPosition(({ coords }) => {
const latLng = { lat: coords.latitude, lng: coords.longitude };
self.panTo(latLng);
self.marker.setLatLng(latLng);
self.$emit('change', latLng);
}, err => {
window.Craft.cp.displayError(`<strong>Maps:</strong> ${err.message}`);
}, {
enableHighAccuracy: true,
});
});
return wrap;
},
onRemove: () => {},
});
L.control.customZoom = function (opts) {
return new L.Control.CustomZoom(opts);
};
L.control.customZoom({ position: 'topright' }).addTo(this.map);
this.map.panBy(
L.point(this.offsetAmount()),
{ animate: false }
);
if (this.tiles.indexOf('google') > -1) {
this._googleMutant();
} else if (this.tiles.indexOf('mapkit') > -1) {
this._mapKitMutant();
} else {
const opts = {
attribution: this.tileLayer.attr,
...(this.tileLayer.opts || {}),
};
if (this.tileLayer.subdomains)
opts.subdomains = this.tileLayer.subdomains;
const tileLayer = L.tileLayer(
this.tileLayer.url,
opts
);
this.map.addLayer(tileLayer);
}
if (this.w3wEnabled && this.showW3wGrid) {
const drawGrid = createW3WGrid(L, this.map);
this.map.whenReady(drawGrid);
this.map.on('move', drawGrid);
}
this.map.on('zoom', this.onZoom);
this.setMarker();
// Re-draw the map if it was hidden
const io = new IntersectionObserver(entries => {
if (entries[0].intersectionRatio <= 0)
return;
this.map.invalidateSize(true);
});
io.observe(this.$el);
},
computed: {
cls () {
return [this.$style.map];
},
tileLayer () {
const scale = L.Browser.retina ? '@2x.png' : '.png'
, hereScale = L.Browser.retina ? '512' : '256'
, mbScale = L.Browser.retina ? '@2x' : ''
, style = this.tiles.split(/\.(.+)/)[1];
switch (this.tiles) {
case MapTiles.Wikimedia:
return {
url: `https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}${scale}`,
attr: '© <a href="http://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer">OpenStreetMap</a>, © <a href="https://maps.wikimedia.org" target="_blank" rel="noreferrer">Wikimedia</a>',
};
case MapTiles.OpenStreetMap:
return {
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attr: '© <a href="http://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer">OpenStreetMap</a>',
};
case MapTiles.CartoVoyager:
case MapTiles.CartoPositron:
case MapTiles.CartoDarkMatter:
return {
url: `https://{s}.basemaps.cartocdn.com/${style}/{z}/{x}/{y}${scale}`,
attr: '© <a href="http://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer">OpenStreetMap</a>, © <a href="https://carto.com/attribution" target="_blank" rel="noreferrer">CARTO</a>',
};
case MapTiles.MapboxOutdoors:
case MapTiles.MapboxStreets:
case MapTiles.MapboxLight:
case MapTiles.MapboxDark: {
let v = '-v11';
if (this.tiles === MapTiles.MapboxLight || this.tiles === MapTiles.MapboxDark)
v = '-v10';
return {
url: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}{scale}?access_token={accessToken}',
attr: '© <a href="http://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer">OpenStreetMap</a>, © <a href="https://www.mapbox.com/" target="_blank" rel="noreferrer">Mapbox</a>',
opts: {
accessToken: this.token,
id: `mapbox/${style}${v}`,
tileSize: 512,
zoomOffset: -1,
scale: mbScale,
},
};
}
case MapTiles.HereNormalDay:
case MapTiles.HereNormalDayGrey:
case MapTiles.HereNormalDayTransit:
case MapTiles.HereReduced:
case MapTiles.HerePedestrian:
return {
url: `https://{s}.base.maps.api.here.com/maptile/2.1/maptile/newest/${style}/{z}/{x}/{y}/${hereScale}/png8?app_id=${this.token.appId}&app_code=${this.token.appCode}`,
attr: '© <a href="http://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer">OpenStreetMap</a>, © <a href="https://here.com/" target="_blank" rel="noreferrer">Here</a>',
subdomains: '1234',
};
case MapTiles.HereTerrain:
case MapTiles.HereSatellite:
case MapTiles.HereHybrid:
return {
url: `https://{s}.aerial.maps.api.here.com/maptile/2.1/maptile/newest/${style}/{z}/{x}/{y}/${hereScale}/png8?app_id=${this.token.appId}&app_code=${this.token.appCode}`,
attr: '© <a href="http://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer">OpenStreetMap</a>, © <a href="https://here.com/" target="_blank" rel="noreferrer">Here</a>',
subdomains: '1234',
};
default:
throw new Error('Unknown map tiles service: ' + this.tiles);
}
},
icon () {
const w = 14 * 2
, h = 20 * 2;
return L.divIcon({
html: icon(w, h),
iconSize: [w, h],
iconAnchor: [w / 2, h],
className: '',
});
},
},
watch: {
latLng: {
deep: true,
/**
* Watches the latLng prop for changes, then updates the map location
* accordingly
*/
handler (next, old) {
if (next.lat === old.lat && next.lng === old.lng)
return;
this.panTo(this.latLng);
this.setMarker();
},
},
},
methods: {
offsetAmount () {
let x = 0;
if (!window.matchMedia('(max-width: 767px)').matches)
x = -(this.$el.getBoundingClientRect().width / 4);
return { x, y: this.hideSearch ? 5 : -15 };
},
getOffsetPoint (latLng, invert = false) {
const point = this.map.latLngToContainerPoint(latLng);
if (invert) {
point.x -= this.offsetAmount().x;
point.y -= this.offsetAmount().y;
} else {
point.x += this.offsetAmount().x;
point.y += this.offsetAmount().y;
}
return point;
},
panTo (latLng) {
this.map.panTo(
this.map.containerPointToLatLng(
this.getOffsetPoint(latLng)
)
);
},
setMarker () {
if (this.marker)
this.map.removeLayer(this.marker);
this.marker = L.marker(this.latLng, {
icon: this.icon,
draggable: true,
autoPan: true,
});
this.map.addLayer(this.marker);
this.marker.on('dragend', () => {
this.$emit('change', this.marker.getLatLng());
});
},
/**
* Listens to map zoom event and triggers component zoom event.
*/
onZoom () {
this.$emit('zoom', this.map.getZoom());
},
/**
* Sets up Leaflet to use Google Maps
*
* @private
*/
_googleMutant () {
waitForGlobal('google', () => {
L.gridLayer.googleMutant({
type: this.tiles.split('.')[1],
}).addTo(this.map);
});
},
/**
* Sets up Leaflet to use Apple MapKit
*
* @private
*/
_mapKitMutant () {
waitForGlobal('mapkit', () => {
L.mapkitMutant({
type: this.tiles.split('.')[1],
authorizationCallback: done => done(this.token),
language: window.Craft.language,
}).addTo(this.map);
});
},
},
};
</script>
<style lang="less" module>
.map {
position: absolute;
z-index: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
box-sizing: border-box;
@media only screen and (max-width: 767px) {
height: 300px;
bottom: auto;
}
}
.control {
margin: 24px !important;
font-size: 0;
border-radius: 5px;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.20);
@media only screen and (max-width: 767px) {
margin: 14px !important;
}
button {
display: block;
width: 100%;
padding: 2px 10px 3px;
color: rgba(41, 50, 61, 0.75);
font-size: 20px;
font-weight: bold;
line-height: normal;
appearance: none;
background-color: #fff;
border: none;
cursor: pointer;
transition: background-color 0.15s ease;
&:not(:last-child) {
border-bottom: 1px solid #DCE4EA;
}
&:first-child {
padding-bottom: 4px;
border-radius: 5px 5px 0 0;
}
&:last-child {
padding-top: 4px;
border-radius: 0 0 5px 5px;
}
&:hover {
background-color: #f0f9ff;
}
}
}
</style>
================================================
FILE: resources/src/components/Search.vue
================================================
<template>
<label :class="cls">
<svg width="17" height="17" viewBox="0 0 17 17">
<path fill="#29323D" d="M6.938 13.893c-3.805 0-6.917-3.112-6.917-6.917C.021 3.17 3.133.059 6.938.059c3.797 0 6.917 3.11 6.917 6.917a6.846 6.846 0 0 1-1.265 3.963l3.832 3.841c.246.246.36.572.36.906 0 .72-.536 1.292-1.274 1.292-.343 0-.677-.124-.923-.37l-3.85-3.858a6.874 6.874 0 0 1-3.797 1.143zm0-1.846c2.778 0 5.072-2.285 5.072-5.071 0-2.778-2.294-5.072-5.072-5.072-2.786 0-5.07 2.294-5.07 5.072 0 2.786 2.284 5.07 5.07 5.07z"/>
</svg>
<vue-autosuggest
:suggestions="suggestions"
:render-suggestion="renderSuggestion"
:get-suggestion-value="getSuggestionValue"
:should-render-suggestions="shouldRenderSuggestions"
:input-props="inputProps"
@selected="onSelected"
@input="onInputChange"
ref="self"
:component-attr-class-autosuggest-results-container="$style.resultsWrap"
:component-attr-class-autosuggest-results="$style.results"
/>
</label>
</template>
<script lang="jsx">
import { VueAutosuggest } from 'vue-autosuggest';
import { t } from '../filters/craft';
import GeoService from '../enums/GeoService';
import Geo from '../common/Geo';
let to;
export default {
props: {
geo: Geo,
service: String,
defaultValue: String,
hasMap: Boolean,
size: String,
},
components: {
VueAutosuggest,
},
data () {
return {
shouldShow: false,
suggestions: [{ data: [] }],
};
},
computed: {
cls () {
const cls = [this.$style.wrap];
if (this.isOpen)
cls.push(this.$style.open);
if (!this.hasMap)
cls.push(this.$style['no-map']);
return cls;
},
inputProps () {
const cls = [this.$style.input];
if (!this.hasMap)
cls.push(this.$style['no-map']);
return {
class: cls,
initialValue: this.initialValue,
placeholder: t('Search for a location'),
};
},
isOpen () {
return this.suggestions[0].data.length > 0 && this.shouldShow;
},
suggestLengthShow () {
return `${this.shouldShow}${this.suggestions[0].data.length}`;
},
},
watch: {
suggestLengthShow () {
this.emitHeightChange();
},
},
methods: {
shouldRenderSuggestions (count, loading) {
this.shouldShow = count > 0 && !loading;
return () => true;
},
/**
* Fired on autocomplete input change
*/
onInputChange (text) {
if (text.trim() === '')
this.shouldShow = false;
clearTimeout(to);
to = setTimeout(async () => {
const data = await this.geo.search(text);
this.suggestions = [{ data }];
this.emitHeightChange();
}, 500);
},
/**
* When an item from the autocomplete is selected
*
* @param selected
*/
async onSelected (selected) {
if (!selected)
return;
let item = selected.item;
const placeId = item.__placeId;
delete item.__placeId;
// eslint-disable-next-line default-case
switch (this.service) {
case GeoService.GoogleMaps:
item = await this.geo.getGooglePlaceDetails(placeId, item);
break;
case GeoService.Here:
item = await this.geo.getHerePlaceDetails(placeId, item);
break;
}
this.$emit('selected', item);
},
/**
* Renders the given item in the autocomplete dropdown
*
* TODO: Highlight search term?
*
* @param item
* @returns {*|string}
*/
renderSuggestion: ({ item }) => item.address,
/**
* Converts the given autocomplete suggestion to a string
* (to auto-fill the input)
*
* @param suggestion
* @returns {string}
*/
getSuggestionValue: suggestion => suggestion.item.address,
/**
* Notifies the parent of a height change in the dropdown
*/
emitHeightChange () {
if (!this.hasMap)
return;
const list = this.$refs.self.$el.lastElementChild;
if (!this.isOpen || !list.firstElementChild || window.matchMedia('(max-width: 767px)').matches) {
this.$emit('open-offset', 0);
return;
}
setTimeout(() => {
this.$emit('open-offset', list.firstElementChild.getBoundingClientRect().height);
});
},
},
};
</script>
<style lang="less" module>
.wrap {
position: relative;
display: block;
@media only screen and (max-width: 767px) {
margin-right: 44px;
}
&, * {
box-sizing: border-box;
}
> svg {
position: absolute;
top: 17px;
left: 16px;
pointer-events: none;
}
&.no-map {
margin-right: 0;
}
}
.input {
width: 100%;
padding: 16px 0 15px 48px;
font-size: 16px;
appearance: none;
background-color: #fff;
border: none;
border-bottom: 1px solid transparent;
border-radius: 5px;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.20);
&.no-map {
box-shadow: none;
border: 1px solid #DCE4EA;
}
.open & {
border-radius: 5px 5px 0 0;
border-bottom-color: #D2DBE1;
}
}
.resultsWrap {
position: absolute;
top: 100%;
left: -20px;
width: calc(100% + 40px);
height: 210px;
padding: 0 20px 20px;
overflow: hidden;
pointer-events: none;
}
.results {
position: relative;
z-index: 2;
width: 100%;
padding: 7px 0;
background-color: #fff;
box-shadow: 0 5px 15px 0 rgba(0, 0, 0, 0.10);
border-radius: 0 0 5px 5px;
transform: translateY(calc(-100% - 15px));
transition: transform 0.3s ease;
pointer-events: all;
.open & {
transform: translateY(0);
}
li {
padding: 7px 14px;
transition: background-color 0.15s ease;
&[class*="highlighted"] {
background-color: #e4edf3;
cursor: pointer;
}
}
}
</style>
================================================
FILE: resources/src/enums/GeoService.js
================================================
const GeoService = {
Nominatim: 'nominatim',
Mapbox: 'mapbox',
GoogleMaps: 'google',
AppleMapKit: 'apple',
Here: 'here',
};
export default GeoService;
================================================
FILE: resources/src/enums/MapTiles.js
================================================
const MapTiles = {
// Open Source
// -------------------------------------------------------------------------
// Wikimedia
// https://foundation.wikimedia.org/wiki/Maps_Terms_of_Use
Wikimedia: 'wikimedia',
// OpenStreetMaps
// https://operations.osmfoundation.org/policies/tiles/
OpenStreetMap: 'openstreetmap',
// Carto
// https://github.com/CartoDB/basemap-styles
CartoVoyager: 'carto.rastertiles/voyager',
CartoPositron: 'carto.light_all',
CartoDarkMatter: 'carto.dark_all',
// Requires API Key (Token)
// -------------------------------------------------------------------------
// Mapbox
MapboxOutdoors: 'mapbox.outdoors',
MapboxStreets: 'mapbox.streets',
MapboxLight: 'mapbox.light',
MapboxDark: 'mapbox.dark',
// Google Maps
GoogleRoadmap: 'google.roadmap',
GoogleTerrain: 'google.terrain',
GoogleHybrid: 'google.hybrid',
// Apple MapKit
MapKitStandard: 'mapkit.standard',
MapKitMutedStandard: 'mapkit.muted',
MapKitSatellite: 'mapkit.satellite',
MapKitHybrid: 'mapkit.hybrid',
// Here
HereNormalDay: 'here.normal.day',
HereNormalDayGrey: 'here.normal.day.grey',
HereNormalDayTransit: 'here.normal.day.transit',
HereReduced: 'here.reduced.day',
HerePedestrian: 'here.pedestrian.day',
HereTerrain: 'here.terrain.day',
HereSatellite: 'here.satellite.day',
HereHybrid: 'here.hybrid.day',
};
export default MapTiles;
================================================
FILE: resources/src/filters/craft.js
================================================
export function t (message, params = null) {
// @ts-ignore
return window.Craft.t('simplemap', message, params);
}
================================================
FILE: resources/src/helpers/createW3WGrid.js
================================================
export default function createW3WGrid (L, map) {
let gridLayer;
return function drawGrid () {
const zoom = map.getZoom();
const loadFeatures = zoom > 17;
if (loadFeatures) { // Zoom level is high enough
const ne = map.getBounds().getNorthEast();
const sw = map.getBounds().getSouthWest();
// Call the what3words Grid API to obtain the grid squares within the current visble bounding box
window.what3words.api
.gridSectionGeoJson({
southwest: {
lat: sw.lat,
lng: sw.lng
},
northeast: {
lat: ne.lat,
lng: ne.lng
}
}).then(data => {
// If the grid layer is already present, remove it as it will need to be replaced by the new grid section
if (gridLayer)
map.removeLayer(gridLayer);
// Create a new GeoJSON layer, based on the GeoJSON returned from the what3words API
gridLayer = L.geoJSON(data, {
style: () => ({
color: '#777',
stroke: true,
weight: 0.5
}),
}).addTo(map);
// eslint-disable-next-line
}).catch(console.error);
} else if (gridLayer) {
// If the grid layer already exists, remove it as the zoom level no longer requires the grid to be displayed
map.removeLayer(gridLayer);
}
};
}
================================================
FILE: resources/src/helpers/debounce.js
================================================
/**
* ## Debounce
*
* A function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds.
*
* If `immediate` is passed, trigger the function on the leading edge,
* instead of the trailing.
*
* ```jsx
*
* // ...
*
* <input onInput={this.handleInput}>
*
* // ...
*
* handleInput = debounce(e => { /* ... *\/ });
*
* ```
*
* @param {function} func - The function to debounce
* @param {number=} wait - How long, in milliseconds, to delay between attempts
* @param {boolean=} immediate - Fire on the leading edge
* @returns {Function}
*/
export default function debounce (func, wait = 500, immediate = false) {
let timeout;
if (wait === 0) {
return function () {
func.apply(this, arguments);
};
}
return function () {
const context = this
, args = arguments;
if (args[0].constructor.name === 'SyntheticEvent')
args[0].persist();
const later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
================================================
FILE: resources/src/helpers/waitForGlobal.js
================================================
export default function waitForGlobal (property, callback) {
if (window.hasOwnProperty(property)) {
callback();
return;
}
const i = setInterval(() => {
if (window.hasOwnProperty(property)) {
callback();
clearTimeout(i);
}
});
}
================================================
FILE: resources/src/main.js
================================================
import App from './App.vue';
import { t } from './filters/craft';
const VueSimpleMapPlugin = {
install (Vue) {
Vue.filter('t', t);
Vue.component('simple-map', App);
}
};
if (typeof window !== 'undefined' && window.Vue)
window.Vue.use(VueSimpleMapPlugin);
================================================
FILE: resources/src/models/Parts.js
================================================
import GeoService from '../enums/GeoService';
export default class Parts {
// Properties
// =========================================================================
number = '';
address = '';
city = '';
postcode = '';
county = '';
state = '';
country = '';
constructor (parts, service) {
switch (service) {
case GeoService.Nominatim:
this._nominatim(parts);
break;
case GeoService.Mapbox:
this._mapbox(parts);
break;
case GeoService.GoogleMaps:
this._google(parts);
break;
case GeoService.Here:
this._here(parts);
break;
default:
return this;
}
}
/**
* Create from existing parts (i.e. from server)
*
* @param parts
* @return {Parts}
*/
static from (parts) {
const p = new Parts();
p.number = parts.number || '';
p.address = parts.address || '';
p.city = parts.city || '';
p.postcode = parts.postcode || '';
p.county = parts.county || '';
p.state = parts.state || '';
p.country = parts.country || '';
return p;
}
// Helpers
// =========================================================================
/**
* Parse Nominatim parts
*
* @param parts
* @private
*/
_nominatim (parts) {
this.number = this._join([
parts.house_number,
parts.address29,
[
'pedestrian',
'footway',
'path',
'road',
'neighbourhood',
'suburb',
'village',
'town',
'city_district',
'city',
].indexOf(parts.type) === -1 ? parts[parts.type] : null,
]);
this.address = this._join([
parts.pedestrian,
parts.footway,
parts.path,
parts.road,
parts.neighbourhood,
parts.suburb,
]);
this.city = this._join([
parts.village,
parts.town,
parts.city_district,
parts.city,
]);
this.postcode = parts.postcode;
this.county = parts.county;
this.state = this._join([
parts.state_district,
parts.state,
]);
this.country = parts.country;
}
/**
* Parse Mapbox parts
*
* @param parts
* @private
*/
_mapbox (parts) {
if (!parts.context) {
this.number = null;
this.address = null;
this.city = null;
this.postcode = null;
this.county = null;
this.state = null;
this.country = null;
return;
}
parts = parts.context.reduce((a, part) => {
const key = part.id.split('.')[0];
a[key] = part.text;
return a;
}, {
number: parts.address,
[parts.place_type[0]]: parts.text,
});
this.number = parts.number;
this.address = parts.address;
this.city = parts.place;
this.postcode = parts.postcode;
this.county = parts.district;
this.state = parts.region;
this.country = parts.country;
}
/**
* Parse Google Maps parts
*
* @param parts
* @private
*/
_google (parts) {
if (Array.isArray(parts)) {
parts = parts.reduce((a, part) => {
const key = part.types[0];
a[key] = part.long_name;
return a;
}, {});
}
this.number = this._join([
parts.subpremise,
parts.premise,
parts.street_number,
]);
this.address = this._join([
parts.route,
parts.neighborhood,
parts.sublocality_level_5,
parts.sublocality_level_4,
parts.sublocality_level_3,
parts.sublocality_level_2,
parts.sublocality_level_1,
parts.sublocality,
]);
this.city = this._join([
parts.postal_town,
parts.locality,
]);
this.postcode = parts.postal_code || parts.postal_code_prefix;
this.county = parts.administrative_area_level_2;
this.state = parts.administrative_area_level_1;
this.country = parts.country;
}
/**
* Parse Here parts
*
* @param parts
* @private
*/
_here (parts) {
parts = {
...parts,
...parts.additionalData.reduce((a, b) => {
a[b.key] = b.value;
return a;
}, {}),
};
delete parts.additionalData;
this.number = parts.houseNumber || '';
this.address = this._join([
parts.street,
parts.district,
]);
this.city = parts.city || '';
this.postcode = parts.postalCode || '';
this.county = parts.CountyName || parts.county || '';
this.state = parts.StateName || parts.state || '';
this.country = parts.CountryName || parts.country || '';
}
// Helpers
// =========================================================================
/**
* Filters and joins the given array
*
* @param {array} parts
* @return {string}
* @private
*/
_join (parts) {
return parts.filter(Boolean).join(', ');
}
}
================================================
FILE: resources/src/models/PartsLegacy.js
================================================
/* eslint-disable camelcase */
import Parts from './Parts';
import GeoService from '../enums/GeoService';
export default class PartsLegacy extends Parts {
// Properties
// =========================================================================
administrative_area_level_1 = '';
administrative_area_level_2 = '';
administrative_area_level_3 = '';
administrative_area_level_4 = '';
administrative_area_level_5 = '';
airport = '';
bus_station = '';
colloquial_area = '';
establishment = '';
floor = '';
intersection = '';
locality = '';
natural_feature = '';
neighborhood = '';
park = '';
parking = '';
point_of_interest = '';
political = '';
post_box = '';
postal_code = '';
postal_code_prefix = '';
postal_town = '';
premise = '';
room = '';
route = '';
street_address = '';
street_number = '';
sublocality = '';
sublocality_level_1 = '';
sublocality_level_2 = '';
sublocality_level_3 = '';
sublocality_level_4 = '';
sublocality_level_5 = '';
subpremise = '';
train_station = '';
transit_station = '';
administrative_area_level_1_short = '';
administrative_area_level_2_short = '';
administrative_area_level_3_short = '';
administrative_area_level_4_short = '';
administrative_area_level_5_short = '';
airport_short = '';
bus_station_short = '';
colloquial_area_short = '';
establishment_short = '';
floor_short = '';
intersection_short = '';
locality_short = '';
natural_feature_short = '';
neighborhood_short = '';
park_short = '';
parking_short = '';
point_of_interest_short = '';
political_short = '';
post_box_short = '';
postal_code_short = '';
postal_code_prefix_short = '';
postal_town_short = '';
premise_short = '';
room_short = '';
route_short = '';
street_address_short = '';
street_number_short = '';
sublocality_short = '';
sublocality_level_1_short = '';
sublocality_level_2_short = '';
sublocality_level_3_short = '';
sublocality_level_4_short = '';
sublocality_level_5_short = '';
subpremise_short = '';
train_station_short = '';
transit_station_short = '';
constructor (parts = {}) {
super(parts, GeoService.GoogleMaps);
if (Array.isArray(parts)) {
parts = parts.reduce((a, part) => {
const key = part.types[0];
a[key] = part.long_name;
return a;
}, {});
}
Object.keys(parts).forEach(key => {
this[key] = parts[key];
});
}
}
================================================
FILE: resources/vue.config.js
================================================
module.exports = {
filenameHashing: false,
outputDir: '../src/web/assets/map',
devServer: {
https: true,
headers: { "Access-Control-Allow-Origin": "*" },
disableHostCheck: true,
port: 8080,
},
configureWebpack: config => {
config.output.library = 'EtherMaps';
// config.plugins.push(
// new require('webpack-bundle-analyzer').BundleAnalyzerPlugin()
// );
},
};
================================================
FILE: src/SimpleMap.php
================================================
<?php
/**
* SimpleMap for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap;
use Craft;
use craft\base\Model;
use craft\base\Plugin;
use craft\events\RegisterComponentTypesEvent;
use craft\events\RegisterGqlTypesEvent;
use craft\events\RegisterUrlRulesEvent;
use craft\helpers\UrlHelper;
use craft\services\Fields;
use craft\services\Gql;
use craft\web\Application;
use craft\web\twig\variables\CraftVariable;
use craft\web\UrlManager;
use craft\wpimport\Command as WpImportCommand;
use ether\simplemap\acfadapters\GoogleMap as GoogleMapAcfAdapter;
use ether\simplemap\fields\MapField as MapField;
use ether\simplemap\integrations\feedme\FeedMeMaps;
use ether\simplemap\integrations\graphql\MapPartsType;
use ether\simplemap\integrations\graphql\MapType;
use ether\simplemap\models\Settings;
use ether\simplemap\services\EmbedService;
use ether\simplemap\services\GeoLocationService;
use ether\simplemap\services\MapService;
use ether\simplemap\services\StaticService;
use ether\simplemap\web\Variable;
use Exception;
use yii\base\Event;
use yii\base\InvalidConfigException;
/**
* Class SimpleMap
*
* @author Ether Creative
* @package ether\simplemap
* @property MapService $map
* @property StaticService $static
* @property EmbedService $embed
* @property GeoLocationService $geolocation
*/
class SimpleMap extends Plugin
{
const EDITION_LITE = 'lite';
const EDITION_PRO = 'pro';
// Properties
// =========================================================================
public bool $hasCpSettings = true;
// Static
// =========================================================================
public static function editions (): array
{
return [
self::EDITION_LITE,
self::EDITION_PRO,
];
}
// Craft
// =========================================================================
public function init ()
{
parent::init();
Craft::setAlias(
'simplemap',
__DIR__
);
Craft::setAlias(
'simplemapimages',
__DIR__ . '/web/assets/imgs'
);
$this->setComponents([
'map' => MapService::class,
'static' => StaticService::class,
'embed' => EmbedService::class,
'geolocation' => GeoLocationService::class,
]);
Event::on(
UrlManager::class,
UrlManager::EVENT_REGISTER_CP_URL_RULES,
[$this, 'onRegisterCPUrlRules']
);
Event::on(
Fields::class,
Fields::EVENT_REGISTER_FIELD_TYPES,
[$this, 'onRegisterFieldTypes']
);
Event::on(
CraftVariable::class,
CraftVariable::EVENT_INIT,
[$this, 'onRegisterVariable']
);
if (class_exists(Gql::class))
{
Event::on(
Gql::class,
Gql::EVENT_REGISTER_GQL_TYPES,
[$this, 'onRegisterGqlTypes']
);
}
if (class_exists(\craft\feedme\Plugin::class))
{
Event::on(
\craft\feedme\services\Fields::class,
\craft\feedme\services\Fields::EVENT_REGISTER_FEED_ME_FIELDS,
[$this, 'onRegisterFeedMeFields']
);
}
$request = Craft::$app->getRequest();
if (
!$request->getIsConsoleRequest()
&& $request->getMethod() === 'GET'
&& $request->getIsSiteRequest()
&& !$request->getIsPreview()
&& !$request->getIsActionRequest()
) {
Event::on(
Application::class,
Application::EVENT_INIT,
[$this, 'onApplicationInit']
);
}
if (class_exists(WpImportCommand::class)) {
Event::on(
WpImportCommand::class,
WpImportCommand::EVENT_REGISTER_ACF_ADAPTERS,
static function (RegisterComponentTypesEvent $event) {
$event->types[] = GoogleMapAcfAdapter::class;
}
);
}
}
protected function beforeUninstall (): void
{
if ($this->getSettings()->geoLocationService === GeoLocationService::MaxMindLite)
GeoLocationService::purgeDb();
}
// Settings
// =========================================================================
protected function createSettingsModel (): Model
{
return new Settings();
}
/**
* @return Model
*/
public function getSettings (): Model
{
return parent::getSettings();
}
protected function settingsHtml (): ?string
{
// Redirect to our settings page
Craft::$app->controller->redirect(
UrlHelper::cpUrl('maps/settings')
);
return null;
}
public function afterSaveSettings (): void
{
parent::afterSaveSettings();
$service = $this->getSettings()->geoLocationService;
if ($service !== GeoLocationService::MaxMindLite)
GeoLocationService::purgeDb();
else if (!GeoLocationService::dbExists())
GeoLocationService::dbQueueDownload();
}
// Events
// =========================================================================
public function onRegisterCPUrlRules (RegisterUrlRulesEvent $event)
{
$event->rules['maps/settings'] = 'simplemap/settings';
}
public function onRegisterFieldTypes (RegisterComponentTypesEvent $event)
{
$event->types[] = MapField::class;
}
/**
* @param Event $event
*
* @throws InvalidConfigException
*/
public function onRegisterVariable (Event $event)
{
/** @var CraftVariable $variable */
$variable = $event->sender;
$variable->set('simpleMap', Variable::class);
$variable->set('maps', Variable::class);
}
public function onRegisterFeedMeFields (\craft\feedme\events\RegisterFeedMeFieldsEvent $event)
{
$event->fields[] = FeedMeMaps::class;
}
public function onRegisterGqlTypes (RegisterGqlTypesEvent $event)
{
$event->types[] = MapType::class;
$event->types[] = MapPartsType::class;
}
/**
* @throws Exception
*/
public function onApplicationInit ()
{
if ($this->getSettings()->geoLocationAutoRedirect)
$this->geolocation->redirect();
}
// Helpers
// =========================================================================
public static function t ($message, $params = []): string
{
return Craft::t('simplemap', $message, $params);
}
public static function v ($version, $operator = '='): bool
{
return SimpleMap::getInstance()->is($version, $operator);
}
}
================================================
FILE: src/acfadapters/GoogleMap.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2024 Ether Creative
*/
namespace ether\simplemap\acfadapters;
use craft\base\FieldInterface;
use craft\wpimport\BaseAcfAdapter;
use ether\simplemap\fields\MapField;
/**
* Class GoogleMap
*
* @author Ether Creative
* @author Brandon Kelly
* @package ether\simplemap\acfadapters
*/
class GoogleMap extends BaseAcfAdapter
{
public static function type(): string
{
return 'google_map';
}
public function create(array $data): FieldInterface
{
$field = new MapField();
if ($data['center_lat']) {
$field->lat = $data['center_lat'];
}
if ($data['center_lng']) {
$field->lng = $data['center_lng'];
}
if ($data['zoom']) {
$field->zoom = $data['zoom'];
}
return $field;
}
public function normalizeValue(mixed $value, array $data): mixed
{
return [
'address' => $value['address'],
'lat' => $value['lat'],
'lng' => $value['lng'],
'zoom' => $value['zoom'],
'parts' => [
'number' => $value['street_number'],
'address' => $value['street_name'],
'city' => $value['city'],
'postcode' => $value['post_code'],
'state' => $value['state'],
'country' => $value['country'],
'administrative_area_level_1' => $value['state'],
'locality' => $value['city'],
'postal_code' => $value['post_code'],
'route' => $value['street_name'],
'street_number' => $value['street_number'],
],
];
}
}
================================================
FILE: src/config.php
================================================
<?php
use ether\simplemap\enums\GeoService;
use ether\simplemap\enums\MapTiles;
return [
'mapTiles' => MapTiles::Wikimedia,
'mapToken' => '',
'geoService' => GeoService::Nominatim,
'geoToken' => '',
'w3wToken' => '',
];
================================================
FILE: src/controllers/SettingsController.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\controllers;
use craft\web\Controller;
use ether\simplemap\enums\GeoService;
use ether\simplemap\enums\MapTiles;
use ether\simplemap\services\GeoLocationService;
use ether\simplemap\SimpleMap;
use yii\web\Response as YiiResponse;
/**
* Class SettingsController
*
* @author Ether Creative
* @package ether\simplemap\controllers
*/
class SettingsController extends Controller
{
public function actionIndex (): YiiResponse
{
return $this->renderTemplate(
'simplemap/settings',
[
'isLite' => SimpleMap::v(SimpleMap::EDITION_LITE),
'settings' => SimpleMap::getInstance()->getSettings(),
'mapTileOptions' => MapTiles::getSelectOptions(),
'geoServiceOptions' => GeoService::getSelectOptions(),
'geoLocationOptions' => GeoLocationService::getSelectOptions(),
]
);
}
}
================================================
FILE: src/controllers/StaticController.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\controllers;
use Craft;
use craft\web\Controller;
use ether\simplemap\utilities\StaticMap;
use Exception;
use Yii;
use yii\web\BadRequestHttpException;
/**
* Class StaticController
*
* @author Ether Creative
* @package ether\simplemap\controllers
*/
class StaticController extends Controller
{
protected int|bool|array $allowAnonymous = true;
/**
* @throws BadRequestHttpException
* @throws Exception
*/
public function actionIndex ()
{
$request = Craft::$app->getRequest();
if (!$request->validateCsrfToken($request->getRequiredQueryParam('csrf')))
throw new BadRequestHttpException(Yii::t('yii', 'Unable to verify your data submission.'));
return (new StaticMap(
$request->getRequiredQueryParam('lat'),
$request->getRequiredQueryParam('lng'),
$request->getRequiredQueryParam('width'),
$request->getRequiredQueryParam('height'),
$request->getRequiredQueryParam('zoom'),
$request->getRequiredQueryParam('scale'),
$request->getQueryParam('markers')
))->render();
}
}
================================================
FILE: src/enums/GeoService.php
================================================
<?php
/**
* SimpleMap for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\enums;
use ether\simplemap\SimpleMap;
/**
* Class GeoService
*
* @author Ether Creative
* @package ether\simplemap\enums
*/
abstract class GeoService
{
// Consts
// =========================================================================
// Open Source
// -------------------------------------------------------------------------
// https://operations.osmfoundation.org/policies/nominatim/
const Nominatim = 'nominatim';
// Requires API Key (Token)
// -------------------------------------------------------------------------
const Mapbox = 'mapbox';
const GoogleMaps = 'google';
const AppleMapKit = 'apple';
const Here = 'here';
// Helpers
// =========================================================================
public static function getSelectOptions (): array
{
$isLite = SimpleMap::v(SimpleMap::EDITION_LITE);
return [
[ 'optgroup' => SimpleMap::t('Open Source') ],
self::Nominatim => SimpleMap::t('Nominatim'),
[ 'optgroup' => SimpleMap::t('Requires API Key (Token)') ],
self::GoogleMaps => SimpleMap::t('Google Maps'),
self::Mapbox => MapTiles::pro('Mapbox', $isLite),
// MapKit lacks both separate address parts and country restriction
// on the front-end, and any sort of server-side API, so it's
// disabled for now.
// self::AppleMapKit => MapTiles::pro('Apple MapKit', $isLite),
self::Here => MapTiles::pro('Here', $isLite),
];
}
}
================================================
FILE: src/enums/MapTiles.php
================================================
<?php
/**
* SimpleMap for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\enums;
use ether\simplemap\models\Settings;
use ether\simplemap\SimpleMap;
use ether\simplemap\services\GeoService;
use Exception;
/**
* Class MapTiles
*
* @author Ether Creative
* @package ether\simplemap\enums
*/
abstract class MapTiles
{
// Consts
// =========================================================================
// Open Source
// -------------------------------------------------------------------------
// Wikimedia
// https://foundation.wikimedia.org/wiki/Maps_Terms_of_Use
// No longer available, falling back to osm in MapField.php::_renderMap
const Wikimedia = 'wikimedia';
// OpenStreetMaps
// https://operations.osmfoundation.org/policies/tiles/
const OpenStreetMap = 'openstreetmap';
// Carto
// https://github.com/CartoDB/basemap-styles
const CartoVoyager = 'carto.rastertiles/voyager';
const CartoPositron = 'carto.light_all';
const CartoDarkMatter = 'carto.dark_all';
// Requires API Key (Token)
// -------------------------------------------------------------------------
// Mapbox
const MapboxOutdoors = 'mapbox.outdoors';
const MapboxStreets = 'mapbox.streets';
const MapboxLight = 'mapbox.light';
const MapboxDark = 'mapbox.dark';
// Google Maps
const GoogleRoadmap = 'google.roadmap';
const GoogleTerrain = 'google.terrain';
const GoogleHybrid = 'google.hybrid';
// Apple MapKit
const MapKitStandard = 'mapkit.standard';
const MapKitMutedStandard = 'mapkit.muted';
const MapKitSatellite = 'mapkit.satellite';
const MapKitHybrid = 'mapkit.hybrid';
// Here
const HereNormalDay = 'here.normal.day';
const HereNormalDayGrey = 'here.normal.day.grey';
const HereNormalDayTransit = 'here.normal.day.transit';
const HereReduced = 'here.reduced.day';
const HerePedestrian = 'here.pedestrian.day';
const HereTerrain = 'here.terrain.day';
const HereSatellite = 'here.satellite.day';
const HereHybrid = 'here.hybrid.day';
// Methods
// =========================================================================
public static function getSelectOptions (): array
{
$isLite = SimpleMap::v(SimpleMap::EDITION_LITE);
return [
['optgroup' => SimpleMap::t('Open Source')],
// self::Wikimedia => SimpleMap::t('Wikimedia'),
self::OpenStreetMap => SimpleMap::t('OpenStreetMap'),
self::CartoVoyager => SimpleMap::t('Carto: Voyager'),
self::CartoPositron => SimpleMap::t('Carto: Positron'),
self::CartoDarkMatter => SimpleMap::t('Carto: Dark Matter'),
['optgroup' => SimpleMap::t('Requires API Key (Token)')],
self::GoogleRoadmap => SimpleMap::t('Google Maps: Roadmap'),
self::GoogleTerrain => SimpleMap::t('Google Maps: Terrain'),
self::GoogleHybrid => SimpleMap::t('Google Maps: Hybrid'),
self::MapboxOutdoors => self::pro('Mapbox: Outdoors', $isLite),
self::MapboxStreets => self::pro('Mapbox: Streets', $isLite),
self::MapboxLight => self::pro('Mapbox: Light', $isLite),
self::MapboxDark => self::pro('Mapbox: Dark', $isLite),
self::MapKitStandard => self::pro('Apple MapKit: Standard', $isLite),
self::MapKitMutedStandard => self::pro('Apple MapKit: Muted Standard', $isLite),
self::MapKitSatellite => self::pro('Apple MapKit: Satellite', $isLite),
self::MapKitHybrid => self::pro('Apple MapKit: Hybrid', $isLite),
self::HereNormalDay => self::pro('Here: Normal Day', $isLite),
self::HereNormalDayGrey => self::pro('Here: Normal Day Grey', $isLite),
self::HereNormalDayTransit => self::pro('Here: Normal Day Transit', $isLite),
self::HereReduced => self::pro('Here: Reduced', $isLite),
self::HerePedestrian => self::pro('Here: Pedestrian', $isLite),
self::HereTerrain => self::pro('Here: Terrain', $isLite),
self::HereSatellite => self::pro('Here: Satellite', $isLite),
self::HereHybrid => self::pro('Here: Hybrid', $isLite),
];
}
/**
* Get the tiles url for the given type and scale
*
* @param string $type
* @param int $scale
*
* @return array
* @throws Exception
*/
public static function getTiles (string $type, int $scale = 1): array
{
$scale = $scale == 1 ? '.png' : '@2x.png';
$style = str_contains($type, '.') ? explode('.', $type, 2)[1] : '';
switch ($type)
{
case self::Wikimedia:
return [
'url' => 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}' . $scale,
'size' => 512,
];
case self::OpenStreetMap:
return [
'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
'size' => 256,
];
case self::CartoVoyager:
case self::CartoPositron:
case self::CartoDarkMatter:
return [
'url' => 'https://a.basemaps.cartocdn.com/' . $style . '/{z}/{x}/{y}' . $scale,
'size' => 256,
];
}
throw new Exception('Unknown tile type "' . $type . '"');
}
// Helpers
// =========================================================================
public static function pro ($label, $isLite): array
{
return [
'label' => SimpleMap::t($label) . ($isLite ? ' (Pro)' : ''),
'disabled' => $isLite,
];
}
}
================================================
FILE: src/fields/MapField.php
================================================
<?php
/**
* SimpleMap for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\fields;
use Craft;
use craft\base\Element;
use craft\base\ElementInterface;
use craft\base\Event;
use craft\base\Field;
use craft\base\PreviewableFieldInterface;
use craft\elements\db\ElementQuery;
use craft\elements\db\ElementQueryInterface;
use craft\errors\InvalidFieldException;
use craft\events\CancelableEvent;
use craft\helpers\Json;
use ether\simplemap\enums\GeoService as GeoEnum;
use ether\simplemap\enums\MapTiles;
use ether\simplemap\integrations\graphql\MapType;
use ether\simplemap\models\Settings;
use ether\simplemap\services\GeoService;
use ether\simplemap\SimpleMap;
use ether\simplemap\models\Map;
use ether\simplemap\web\assets\MapAsset;
use Exception;
use GraphQL\Type\Definition\Type;
use Throwable;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\Markup;
use yii\base\InvalidConfigException;
use yii\db\ExpressionInterface;
use yii\db\Schema;
/**
* Class Map
*
* @author Ether Creative
* @package ether\simplemap\fields
*/
class MapField extends Field implements PreviewableFieldInterface
{
// Properties
// =========================================================================
/**
* @var float - The maps latitude
*/
public float $lat = 51.272154;
/**
* @var float - The maps longitude
*/
public float $lng = 0.514951;
/**
* @var int - The maps zoom level
*/
public int $zoom = 15;
/**
* @var float - The maps min zoom level (how far OUT it can be zoomed)
*/
public float $minZoom = 3;
/**
* @var float - The maps max zoom level (how far IN it can be zoomed)
*/
public float $maxZoom = 18;
/**
* @var string|null - The preferred country when searching
*/
public ?string $country = null;
/**
* @var bool - If true, the location search will not be displayed
*/
public bool $hideSearch = false;
/**
* @var bool - If true, the map will not be displayed
*/
public bool $hideMap = false;
/**
* @var bool - If true, the address fields will not be displayed
*/
public bool $hideAddress = false;
/**
* @var bool - If true, show the lat/lng fields
*/
public bool $showLatLng = false;
/**
* @var bool - If true, will show a button to centre the map on the users
* current location.
*/
public bool $showCurrentLocation = false;
/**
* @var string - The size of the field
* (can be either "normal", "mini")
*/
public string $size = 'normal';
/**
* @var bool - Will show the what3words overlay when true
*/
public bool $showW3WGrid = false;
/**
* @var bool - Show the what3words field on the map field
*/
public bool $showW3WField = false;
/**
* @deprecated
*/
public $hideLatLng;
/**
* @deprecated
*/
public $height;
/**
* @deprecated
*/
public $countryRestriction;
/**
* @deprecated
*/
public $typeRestriction;
/**
* @deprecated
*/
public $boundaryRestrictionNELat;
/**
* @deprecated
*/
public $boundaryRestrictionNELng;
/**
* @deprecated
*/
public $boundaryRestrictionSWLat;
/**
* @deprecated
*/
public $boundaryRestrictionSWLng;
/**
* @deprecated
*/
public string $boundary = '""';
private static $searchParams = null;
// Methods
// =========================================================================
public function init(): void
{
Event::on(
ElementQuery::class,
ElementQuery::EVENT_AFTER_PREPARE,
[$this, 'afterPrepareElementQuery'],
);
parent::init();
}
// Methods: Static
// -------------------------------------------------------------------------
public static function displayName (): string
{
return SimpleMap::t('Map');
}
public static function hasContentColumn (): bool
{
return true;
}
public static function icon (): string
{
return 'map-pin';
}
public function getContentColumnType (): string
{
return Schema::TYPE_TEXT;
}
public static function supportedTranslationMethods (): array
{
return [
self::TRANSLATION_METHOD_NONE,
self::TRANSLATION_METHOD_SITE,
self::TRANSLATION_METHOD_SITE_GROUP,
self::TRANSLATION_METHOD_LANGUAGE,
self::TRANSLATION_METHOD_CUSTOM,
];
}
public static function queryCondition(array $instances, mixed $value, array &$params): array|string|ExpressionInterface|false|null
{
if (empty($instances) || empty($value))
return null;
self::$searchParams = [
'field' => $instances[0],
'value' => $value,
];
return null;
}
// Methods: Instance
// -------------------------------------------------------------------------
public function setCountryRestriction ($value)
{
$this->country = $value;
}
public function rules (): array
{
$rules = parent::rules();
$rules[] = [
['zoom', 'minZoom', 'maxZoom'],
'required',
];
$rules[] = [
['minZoom', 'maxZoom'],
'double',
'min' => 0,
'max' => 18,
];
$rules[] = [
['lat'],
'double',
'min' => -90,
'max' => 90,
];
$rules[] = [
['lng'],
'double',
'min' => -180,
'max' => 180,
];
return $rules;
}
public function normalizeValue (mixed $value, ElementInterface|Element $element = null): Map
{
if (is_string($value))
$value = Json::decodeIfJson($value);
if ($value instanceof Map)
$map = $value;
elseif (is_array($value))
$map = new Map($value);
else
$map = new Map([
'lat' => null,
'lng' => null,
'zoom' => $this->zoom,
]);
SimpleMap::getInstance()->map->populateMissingData($map, $this);
$map->fieldId = $this->id;
if ($element)
{
$map->ownerId = $element->id;
$map->ownerSiteId = $element->siteId;
$handle = $this->handle;
$element->setFieldValue($handle, $map);
}
return $map;
}
public function getSettingsHtml (): string
{
$value = new Map();
$value->lat = $this->lat;
$value->lng = $this->lng;
$value->zoom = $this->zoom;
$originalHandle = $this->handle;
$originalCountry = $this->country;
$originalHideSearch = $this->hideSearch;
$originalHideMap = $this->hideMap;
$originalHideAddress = $this->hideAddress;
$originalShowCurrentLocation = $this->showCurrentLocation;
$originalSize = $this->size;
$originalShowW3WGrid = $this->showW3WGrid;
$originalShowW3WField = $this->showW3WField;
$this->handle = '__settings__';
$this->country = null;
$this->hideSearch = false;
$this->hideMap = false;
$this->hideAddress = true;
$this->showCurrentLocation = true;
$this->size = 'normal';
$this->showW3WGrid = false;
$this->showW3WField = false;
$mapField = new Markup(
$this->_renderMap($value, true),
'utf-8'
);
$this->handle = $originalHandle;
$this->country = $originalCountry;
$this->hideSearch = $originalHideSearch;
$this->hideMap = $originalHideMap;
$this->hideAddress = $originalHideAddress;
$this->showCurrentLocation = $originalShowCurrentLocation;
$this->size = $originalSize;
$this->showW3WGrid = $originalShowW3WGrid;
$this->showW3WField = $originalShowW3WField;
$view = Craft::$app->getView();
$countries = array_merge([
'*' => SimpleMap::t('All Countries'),
], GeoService::$countries);
return $view->renderTemplate('simplemap/field-settings', [
'map' => $mapField,
'field' => $this,
'countries' => $countries,
'settings' => SimpleMap::getInstance()->getSettings(),
]);
}
public function getInputHtml ($value = null, ElementInterface $element = null): string
{
if ($element !== null && $element->hasEagerLoadedElements($this->handle))
$value = $element->getEagerLoadedElements($this->handle);
return new Markup(
$this->_renderMap($value ?: new Map()),
'utf-8'
);
}
public function getTableAttributeHtml (mixed $value, ElementInterface $element): string
{
return $this->normalizeValue($value, $element)->address;
}
public function isValueEmpty ($value, ElementInterface $element): bool
{
return $this->normalizeValue($value)->isValueEmpty();
}
// GraphQl
// -------------------------------------------------------------------------
/**
* @inheritdoc
*/
public function getContentGqlType (): Type|array
{
return MapType::getType();
}
public function getContentGqlQueryArgumentType (): Type|array
{
return MapType::getQueryType();
}
public function getContentGqlMutationArgumentType (): Type|array
{
return MapType::getInputType();
}
// Methods: Events
// -------------------------------------------------------------------------
public function beforeSave (bool $isNew): bool
{
$this->lat = (float) $this->lat;
$this->lng = (float) $this->lng;
$this->zoom = (int) $this->zoom;
if ($this->country === '*')
$this->country = null;
return parent::beforeSave($isNew);
}
public function beforeElementSave (ElementInterface $element, bool $isNew): bool
{
if (!SimpleMap::getInstance()->map->validateField($this, $element))
return false;
return parent::beforeElementSave($element, $isNew);
}
public function afterElementSave (ElementInterface $element, bool $isNew): void
{
SimpleMap::getInstance()->map->saveField($this, $element);
parent::afterElementSave($element, $isNew);
}
public function afterPrepareElementQuery (CancelableEvent $event): void
{
if (!self::$searchParams) return;
/** @var ElementQueryInterface $query */
$query = $event->sender;
SimpleMap::getInstance()->map->modifyElementsQuery(
$query,
self::$searchParams['value'],
self::$searchParams['field'],
);
// Clear search params to prevent it being applied to unrelated queries.
self::$searchParams = null;
}
// Helpers
// =========================================================================
/**
* Renders the map input
*
* @param Map $value
* @param bool $isSettings
*
* @return string
* @throws InvalidConfigException
*/
private function _renderMap (Map $value, bool $isSettings = false): string
{
$view = Craft::$app->getView();
$containerId = 'map-' . $this->id . 'container';
$vueContainerId = $view->namespaceInputId($containerId);
$view->registerAssetBundle(MapAsset::class);
$view->registerJs('new Vue({ el: \'#' . $vueContainerId . '\' });');
$view->registerTranslations('simplemap', [
'Search for a location',
'Clear address',
'Full Address',
'Name / Number',
'Street Address',
'Town / City',
'Postcode',
'County',
'State',
'Country',
'Latitude',
'Longitude',
'Zoom In',
'Zoom Out',
'Center on Marker',
'Current Location',
'No address selected',
'what3words',
]);
/** @var Settings $settings */
$settings = SimpleMap::getInstance()->getSettings();
$country = $this->country;
// Convert ISO2 to ISO3 for Here autocomplete
if ($country && $settings->geoService === GeoEnum::Here)
$country = GeoService::$countriesIso3[$country];
$tiles = $settings->mapTiles;
if ($tiles === MapTiles::Wikimedia)
$tiles = MapTiles::OpenStreetMap;
$opts = [
'config' => [
'isSettings' => $isSettings,
'name' => $view->namespaceInputName($this->handle),
'country' => $country,
'hideSearch' => (bool) $this->hideSearch,
'hideMap' => (bool) $this->hideMap,
'hideAddress' => (bool) $this->hideAddress,
'showLatLng' => (bool) $this->showLatLng,
'showCurrentLocation' => (bool) $this->showCurrentLocation,
'minZoom' => $isSettings ? 0 : (float) $this->minZoom,
'maxZoom' => $isSettings ? 18 : (float) $this->maxZoom,
'size' => $this->size,
'mapTiles' => $tiles,
'mapToken' => GeoService::getToken(
$settings->getMapToken(),
$tiles
),
'geoService' => $settings->geoService,
'geoToken' => GeoService::getToken(
$settings->getGeoToken(),
$settings->geoService
),
'w3wEnabled' => $settings->isW3WEnabled(),
'showW3WGrid' => (bool) $this->showW3WGrid,
'showW3WField' => (bool) $this->showW3WField,
'locale' => Craft::$app->locale->getLanguageID(),
],
'value' => [
'address' => $value->address,
'lat' => self::_parseFloat($value->lat),
'lng' => self::_parseFloat($value->lng),
'zoom' => $value->zoom,
'parts' => $value->parts,
'what3words' => $value->what3words,
],
'defaultValue' => [
'address' => null,
'lat' => self::_parseFloat($this->lat),
'lng' => self::_parseFloat($this->lng),
'zoom' => $this->zoom,
'parts' => null,
'what3words' => null,
],
];
// Map Services
// ---------------------------------------------------------------------
if (str_contains($tiles, 'google'))
{
if ($settings->getMapToken() !== $settings->getGeoToken())
{
$view->registerJsFile(
'https://maps.googleapis.com/maps/api/js?key=' .
$settings->getMapToken()
);
}
}
elseif (str_contains($tiles, 'mapkit'))
{
$view->registerJsFile(
'https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js'
);
}
// Geo Services
// ---------------------------------------------------------------------
if ($settings->geoService === GeoEnum::GoogleMaps)
{
$view->registerJsFile(
'https://maps.googleapis.com/maps/api/js?libraries=places&key=' .
$settings->getGeoToken()
);
}
elseif ($settings->geoService === GeoEnum::AppleMapKit)
{
$view->registerJsFile(
'https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js'
);
}
// what3words
// ---------------------------------------------------------------------
if ($settings->w3wEnabled && !empty($settings->getW3WToken()))
{
$view->registerJsFile(
'https://assets.what3words.com/sdk/v3/what3words.js?key=' .
$settings->getW3WToken()
);
}
// ---------------------------------------------------------------------
$options = preg_replace(
'/\'/',
''',
json_encode($opts)
);
if ($this->size === 'normal')
$children = '<div style="height:360px"></div>';
else
$children = $value->address;
return '<div id="' . $containerId . '"><simple-map options=\'' . $options . '\'>' . $children . '</simple-map></div>';
}
// Helpers
// =========================================================================
/**
* Will cast the given value to a float if not null
*
* @param null $value
*
* @return float|null
*/
private static function _parseFloat ($value = null): ?float
{
if ($value === null)
return null;
return (float) $value;
}
}
================================================
FILE: src/integrations/feedme/FeedMeMaps.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\integrations\feedme;
use Cake\Utility\Hash;
use craft\feedme\base\Field;
use craft\feedme\base\FieldInterface;
use craft\feedme\helpers\DataHelper;
use ether\simplemap\models\Map;
use ether\simplemap\fields\MapField;
use ether\simplemap\models\Parts;
/**
* Class FeedMeMaps
*
* @author Ether Creative
* @package ether\simplemap\integrations\feedme
*/
class FeedMeMaps extends Field implements FieldInterface
{
// Properties
// =========================================================================
public static $name = 'Maps';
public static $class = MapField::class;
// Methods
// =========================================================================
public function getMappingTemplate (): string
{
return 'simplemap/_feedme-mapping';
}
public function parseField (): ?Map
{
$preppedData = [];
$fields = Hash::get($this->fieldInfo, 'fields');
if (!$fields)
return null;
foreach ($fields as $subFieldHandle => $subFieldInfo)
{
if ($subFieldHandle === 'parts') {
foreach ($subFieldInfo as $handle => $info)
$preppedData[$subFieldHandle][$handle] = DataHelper::fetchValue(
$this->feedData,
$info
);
continue;
}
$preppedData[$subFieldHandle] = DataHelper::fetchValue(
$this->feedData,
$subFieldInfo
);
}
if (isset($preppedData['parts']))
$preppedData['parts'] = new Parts($preppedData['parts']);
if (!$preppedData)
return null;
return new Map($preppedData);
}
}
================================================
FILE: src/integrations/graphql/MapPartsType.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\integrations\graphql;
use craft\gql\GqlEntityRegistry;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
/**
* Class MapPartsType
*
* @author Ether Creative
* @package ether\simplemap\integrations\graphql
*/
class MapPartsType
{
public static function getName (): string
{
return 'Ether_MapParts';
}
public static function getFieldDefinitions (): array
{
return [
'number' => [
'name' => 'number',
'type' => Type::string(),
'description' => 'The address name / number.',
],
'address' => [
'name' => 'address',
'type' => Type::string(),
'description' => 'The street address.',
],
'city' => [
'name' => 'city',
'type' => Type::string(),
'description' => 'The city.',
],
'postcode' => [
'name' => 'postcode',
'type' => Type::string(),
'description' => 'The postal code.',
],
'county' => [
'name' => 'county',
'type' => Type::string(),
'description' => 'The county.',
],
'state' => [
'name' => 'state',
'type' => Type::string(),
'description' => 'The state.',
],
'country' => [
'name' => 'country',
'type' => Type::string(),
'description' => 'The country.',
],
];
}
public static function getType (): Type
{
if ($type = GqlEntityRegistry::getEntity(static::class))
return $type;
return GqlEntityRegistry::createEntity(
static::class,
new ObjectType(
[
'name' => static::getName(),
'fields' => static::class . '::getFieldDefinitions',
]
)
);
}
public static function getInputType (): InputType
{
$name = static::class . 'Input';
if ($type = GqlEntityRegistry::getEntity($name))
return $type;
return GqlEntityRegistry::createEntity(
$name,
new InputObjectType([
'name' => static::getName() . 'Input',
'fields' => static::class . '::getFieldDefinitions',
])
);
}
}
================================================
FILE: src/integrations/graphql/MapType.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\integrations\graphql;
use craft\gql\GqlEntityRegistry;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
/**
* Class MapType
*
* @author Ether Creative
* @package ether\simplemap\integrations\graphql
*/
class MapType
{
public static function getName (): string
{
return 'Ether_Map';
}
public static function getFieldDefinitions (): array
{
return [
'lat' => [
'name' => 'lat',
'type' => Type::float(),
'description' => 'The maps latitude.',
],
'lng' => [
'name' => 'lng',
'type' => Type::float(),
'description' => 'The maps longitude.',
],
'zoom' => [
'name' => 'zoom',
'type' => Type::int(),
'description' => 'The maps zoom level.',
],
'distance' => [
'name' => 'distance',
'type' => Type::float(),
'description' => 'The distance to this location.',
],
'address' => [
'name' => 'address',
'type' => Type::string(),
'description' => 'The full address.',
],
'parts' => [
'name' => 'parts',
'type' => MapPartsType::getType(),
'description' => 'The maps address parts.',
],
];
}
public static function getInputDefinitions (): array
{
$fields = static::getFieldDefinitions();
unset($fields['distance']);
$fields['parts']['type'] = MapPartsType::getInputType();
return $fields;
}
public static function getQueryInputDefinitions (): array
{
return [
'coordinate' => [
'name' => 'coordinate',
'type' => static::getCoordsType(),
],
'location' => [
'name' => 'location',
'type' => Type::string(),
],
'country' => [
'name' => 'country',
'type' => Type::string(),
],
'radius' => [
'name' => 'radius',
'type' => Type::float(),
],
'unit' => [
'name' => 'unit',
'type' => static::getUnitType(),
],
];
}
public static function getType (): Type
{
if ($type = GqlEntityRegistry::getEntity(static::getName()))
return $type;
return GqlEntityRegistry::createEntity(
static::getName(),
new ObjectType([
'name' => static::getName(),
'fields' => static::class . '::getFieldDefinitions',
])
);
}
public static function getInputType (): InputType
{
$name = static::getName() . 'Input';
if ($type = GqlEntityRegistry::getEntity($name))
return $type;
return GqlEntityRegistry::createEntity(
$name,
new InputObjectType([
'name' => static::getName() . 'Input',
'fields' => static::class . '::getInputDefinitions',
])
);
}
public static function getQueryType (): InputType
{
$name = static::getName() . 'Query';
if ($type = GqlEntityRegistry::getEntity($name))
return $type;
return GqlEntityRegistry::createEntity(
$name,
new InputObjectType([
'name' => static::getName() . 'Query',
'fields' => static::class . '::getQueryInputDefinitions',
])
);
}
public static function getCoordsType (): InputType
{
$name = static::getName() . 'Coords';
if ($type = GqlEntityRegistry::getEntity($name))
return $type;
return GqlEntityRegistry::createEntity(
$name,
new InputObjectType([
'name' => static::getName() . 'Coords',
'fields' => [
'lat' => [
'name' => 'lat',
'type' => Type::nonNull(Type::float()),
],
'lng' => [
'name' => 'lng',
'type' => Type::nonNull(Type::float()),
],
],
])
);
}
public static function getUnitType (): EnumType
{
$name = static::getName() . 'Unit';
if ($type = GqlEntityRegistry::getEntity($name))
return $type;
return GqlEntityRegistry::createEntity(
$name,
new EnumType([
'name' => static::getName() . 'Unit',
'values' => [
'Miles' => 'mi',
'Kilometres' => 'km',
],
])
);
}
}
================================================
FILE: src/jobs/MaxMindDBDownloadJob.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\jobs;
use Craft;
use craft\helpers\FileHelper;
use craft\queue\BaseJob;
use craft\queue\QueueInterface;
use ether\simplemap\services\GeoLocationService;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use yii\queue\Queue;
/**
* Class MaxMindDBDownloadJob
*
* @author Ether Creative
* @package ether\simplemap\jobs
*/
class MaxMindDBDownloadJob extends BaseJob
{
protected function defaultDescription (): string
{
return 'Downloading MaxMind DB';
}
/**
* @param Queue|QueueInterface $queue The queue the job belongs to
*
* @throws Exception|GuzzleException
*/
public function execute ($queue): void
{
try {
$temp = tempnam(sys_get_temp_dir(), 'mmdb');
$target = Craft::getAlias(
GeoLocationService::DB_STORAGE . DIRECTORY_SEPARATOR . 'default.mmdb'
);
$client = Craft::createGuzzleClient();
$client->get(
'https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz',
[
'save_to' => $temp,
'progress' => function ($total, $current) use ($queue) {
if ($total > 0)
$queue->setProgress(($current / $total) * 100);
},
]
);
$extracted = '';
$gz = gzopen($temp, 'r');
while ($data = gzread($gz, 10000000))
$extracted .= $data;
gzclose($gz);
if (!file_exists($target))
FileHelper::createDirectory(pathinfo($target)['dirname']);
$saved = file_put_contents($target, $extracted);
@unlink($temp);
if (!$saved)
Craft::error('Unable to save MaxMind DB!', 'maps');
Craft::$app->getCache()->delete('maps_db_updating');
} catch (Exception $e) {
Craft::$app->getCache()->delete('maps_db_updating');
throw $e;
}
}
}
================================================
FILE: src/migrations/Install.php
================================================
<?php
/**
* SimpleMap for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\migrations;
use craft\db\Migration;
use craft\db\Table;
use ether\simplemap\records\Map;
/**
* Class Install
*
* @author Ether Creative
* @package ether\simplemap\migrations
*/
class Install extends Migration
{
public function safeUp ()
{
// Create
$this->createTable(
Map::TableName,
[
'id' => $this->primaryKey(),
'ownerId' => $this->integer()->notNull(),
'ownerSiteId' => $this->integer(),
'fieldId' => $this->integer()->notNull(),
'lat' => $this->decimal(11, 9),
'lng' => $this->decimal(12, 9),
'dateCreated' => $this->dateTime()->notNull(),
'dateUpdated' => $this->dateTime()->notNull(),
'uid' => $this->uid()->notNull(),
]
);
// Indexes
$this->createIndex(
null,
Map::TableName,
['ownerId', 'ownerSiteId', 'fieldId'],
true
);
$this->createIndex(
null,
Map::TableName,
['lat']
);
$this->createIndex(
null,
Map::TableName,
['lng']
);
// Relations
$this->addForeignKey(
null,
Map::TableName,
['ownerId'],
Table::ELEMENTS,
['id'],
'CASCADE'
);
$this->addForeignKey(
null,
Map::TableName,
['ownerSiteId'],
Table::SITES,
['id'],
'CASCADE',
'CASCADE'
);
$this->addForeignKey(
null,
Map::TableName,
['fieldId'],
Table::FIELDS,
['id'],
'CASCADE'
);
// Upgrade from Craft 2
if ($this->db->tableExists('{{%simplemap_maps}}'))
(new m190226_143809_craft3_upgrade())->safeUp();
}
public function safeUpPre34 ()
{
// Create
$this->createTable(
Map::TableName,
[
'id' => $this->primaryKey(),
'ownerId' => $this->integer()->notNull(),
'ownerSiteId' => $this->integer(),
'fieldId' => $this->integer()->notNull(),
'lat' => $this->decimal(11, 9),
'lng' => $this->decimal(12, 9),
'zoom' => $this->integer(2),
'address' => $this->string(255),
'parts' => $this->text(),
'dateCreated' => $this->dateTime()->notNull(),
'dateUpdated' => $this->dateTime()->notNull(),
'uid' => $this->uid()->notNull(),
]
);
// Indexes
$this->createIndex(
null,
Map::TableName,
['ownerId', 'ownerSiteId', 'fieldId'],
true
);
$this->createIndex(
null,
Map::TableName,
['lat']
);
$this->createIndex(
null,
Map::TableName,
['lng']
);
}
public function safeDown ()
{
$this->dropTableIfExists(Map::TableName);
}
}
================================================
FILE: src/migrations/m190226_143809_craft3_upgrade.php
================================================
<?php
namespace ether\simplemap\migrations;
use Craft;
use craft\db\Migration;
use craft\db\Query;
use craft\db\Table;
use craft\helpers\Json;
use craft\services\Plugins;
use craft\validators\HandleValidator;
use ether\simplemap\enums\GeoService;
use ether\simplemap\enums\MapTiles;
use ether\simplemap\models\Settings;
use ether\simplemap\models\Map;
use ether\simplemap\records\Map as MapRecord;
use ether\simplemap\fields\MapField;
use ether\simplemap\SimpleMap;
/**
* m190226_143809_craft3_upgrade migration.
*/
class m190226_143809_craft3_upgrade extends Migration
{
// Properties
// =========================================================================
static array $sitesByOldLocale = [];
// Methods
// =========================================================================
/**
* @inheritdoc
*
* @throws \Throwable
* @throws \yii\base\Exception
* @throws \yii\db\Exception
*/
public function safeUp()
{
// 1. Run the install migration
if (!$this->db->tableExists(MapRecord::OldTableName) && !$this->db->tableExists(MapRecord::TableName))
(new Install())->safeUp();
// 2. Upgrade the data
if ($this->db->tableExists('{{%simplemap_maps}}'))
$this->_upgrade2();
else
$this->_upgrade3();
}
/**
* @inheritdoc
*/
public function safeDown()
{
echo "m190226_143809_craft3_upgrade cannot be reverted.\n";
return false;
}
/**
* Upgrade from Craft 2
*
* @throws \Throwable
* @throws \yii\base\Exception
* @throws \yii\db\Exception
*/
private function _upgrade2 ()
{
$mapService = SimpleMap::getInstance()->map;
$fieldsService = Craft::$app->getFields();
// Delete the old plugin row
$this->delete(Table::PLUGINS, ['handle' => 'simple-map']);
// Update the old data
echo ' > Start map data upgrade' . PHP_EOL;
$rows = (new Query())
->select('*')
->from('{{%simplemap_maps}}')
->all();
foreach ($rows as $row)
{
echo ' > Upgrade map value ' . $row['address'] . PHP_EOL;
$site = $this->getSiteByLocale($row['ownerLocale']);
$map = new Map();
$map->ownerId = $row['ownerId'];
$map->ownerSiteId = $site->id;
$map->fieldId = $row['fieldId'];
$map->lat = $row['lat'];
$map->lng = $row['lng'];
$mapService->saveRecord($map, true);
}
$this->dropTable('{{%simplemap_maps}}');
// Update old field types
echo ' > Upgrade map field type upgrade' . PHP_EOL;
$fieldContexts = [];
$fieldContextsData = (new \craft\db\Query())
->select(['context'])
->from(['{{%fields}}'])
->all();
foreach ($fieldContextsData as $fieldData) {
$fieldContexts[] = $fieldData['context'];
}
$fieldContexts = array_unique($fieldContexts);
$fields = $fieldsService->getAllFields($fieldContexts);
foreach ($fields as $field)
{
if ($field instanceof \craft\fields\MissingField && $field->expectedType === 'SimpleMap_Map') {
echo ' > Upgrade map field ' . $field->handle . PHP_EOL;
$oldSettings = Json::decodeIfJson($field->settings);
$newField = new MapField([
'id' => $field->id,
'groupId' => $field->groupId,
'name' => $field->name,
'handle' => $field->handle,
'instructions' => $field->instructions,
'searchable' => $field->searchable,
'translationMethod' => $field->translationMethod,
'translationKeyFormat' => $field->translationKeyFormat,
'lat' => $oldSettings['lat'],
'lng' => $oldSettings['lng'],
'zoom' => $oldSettings['zoom'] ?? 15,
'country' => strtoupper($oldSettings['countryRestriction'] ?? '') ?: null,
'hideMap' => $oldSettings['hideMap'],
]);
$fieldsService->saveField($newField);
}
}
// Update the plugin settings
$this->updatePluginSettings();
}
/**
* Upgrade from SimpleMap (3.3.x)
*
* @throws \Throwable
* @throws \yii\base\Exception
*/
private function _upgrade3 ()
{
$mapService = SimpleMap::getInstance()->map;
// 1. Store the old data
echo ' > Start map data upgrade' . PHP_EOL;
$rows = (new Query())
->select([
'ownerId',
'ownerSiteId',
'fieldId',
'lat',
'lng',
'zoom',
'address',
'parts',
])
->from(MapRecord::OldTableName)
->all();
// 2. Re-create the table
$this->dropTable(MapRecord::OldTableName);
if (!$this->db->tableExists(MapRecord::TableName))
(new Install())->safeUpPre34();
// 3. Store the old data as new
$dupeKeys = [];
foreach ($rows as $row)
{
$key = $row['ownerId'] . '_' . $row['ownerSiteId'] . '_' . $row['fieldId'];
if (in_array($key, $dupeKeys))
continue;
$dupeKeys[] = $key;
echo ' > Upgrade map value ' . $row['address'] . PHP_EOL;
$map = new Map($row);
$map->ownerId = $row['ownerId'];
$map->ownerSiteId = $row['ownerSiteId'];
$map->fieldId = $row['fieldId'];
if (!$map->zoom)
$map->zoom = 15;
$mapService->saveRecord($map, true);
}
// 4. Update field settings
echo ' > Upgrade map field type upgrade' . PHP_EOL;
$rows = (new Query())
->select(['id', 'settings', 'handle'])
->from(Table::FIELDS)
->where(['type' => MapField::class])
->all();
foreach ($rows as $row)
{
echo ' > Upgrade map field ' . $row['handle'] . PHP_EOL;
$id = $row['id'];
$oldSettings = Json::decodeIfJson($row['settings']);
$newSettings = [
'lat' => $oldSettings['lat'],
'lng' => $oldSettings['lng'],
'zoom' => $oldSettings['zoom'] ?? 15,
'country' => strtoupper($oldSettings['countryRestriction']),
'hideMap' => $oldSettings['hideMap'],
];
$this->db->createCommand()
->update(
Table::FIELDS,
[ 'settings' => Json::encode($newSettings) ],
compact('id')
)
->execute();
}
$this->updatePluginSettings();
}
// Helpers
// =========================================================================
/**
* Returns a site handle based on a given locale.
*
* @param string $locale
*
* @return string
*/
private function locale2handle (string $locale): string
{
if (
!preg_match('/^' . HandleValidator::$handlePattern . '$/', $locale) ||
in_array(strtolower($locale), HandleValidator::$baseReservedWords, true)
) {
$localeParts = array_filter(preg_split('/[^a-zA-Z0-9]/', $locale));
return $localeParts ? '_' . implode('_', $localeParts) : '';
}
return $locale;
}
/**
* Gets the new site based off the old locale
*
* @param string $locale
*
* @return \craft\models\Site
*/
private function getSiteByLocale ($locale)
{
$sites = \Craft::$app->sites;
if ($locale === null)
return static::$sitesByOldLocale[$locale] = $sites->primarySite;
if (array_key_exists($locale, static::$sitesByOldLocale))
return static::$sitesByOldLocale[$locale];
$handle = $this->locale2handle($locale);
$siteId = (new Query())
->select('id')
->from(Table::SITES)
->where(['like', 'handle', '%' . $handle])
->column();
if (!empty($siteId))
return static::$sitesByOldLocale[$locale] = $sites->getSiteById($siteId[0]);
return static::$sitesByOldLocale[$locale] = $sites->primarySite;
}
/**
* Updates the plugins settings
* @throws \craft\errors\InvalidPluginException
*/
private function updatePluginSettings ()
{
echo ' > Upgrade Maps settings' . PHP_EOL;
/** @var Settings $settings */
$settings = SimpleMap::getInstance()->getSettings()->toArray();
$newSettings = SimpleMap::getInstance()->getSettings()->toArray();
$craft2Settings = \Craft::$app->projectConfig->get(
Plugins::CONFIG_PLUGINS_KEY . '.simple-map.settings'
);
if (is_array($craft2Settings) && !empty($craft2Settings))
{
$settings = [
'apiKey' => @$craft2Settings['browserApiKey'] ?: '',
'unrestrictedApiKey' => @$craft2Settings['serverApiKey'] ?: '',
];
}
if ($settings['unrestrictedApiKey'])
{
$newSettings['geoService'] = GeoService::GoogleMaps;
$newSettings['geoToken'] = $settings['unrestrictedApiKey'];
}
if ($settings['apiKey'])
{
$newSettings['mapTiles'] = MapTiles::GoogleRoadmap;
$newSettings['mapToken'] = $settings['apiKey'];
if (!$settings['unrestrictedApiKey'])
{
$newSettings['geoService'] = GeoService::GoogleMaps;
$newSettings['geoToken'] = $settings['apiKey'];
}
}
\Craft::$app->plugins->savePluginSettings(
SimpleMap::getInstance(),
$newSettings
);
\Craft::$app->plugins->enablePlugin(SimpleMap::getInstance()->handle);
}
}
================================================
FILE: src/migrations/m190325_130533_repair_map_elements.php
================================================
<?php
namespace ether\simplemap\migrations;
use craft\db\Migration;
use craft\db\Query;
use craft\db\Table;
use ether\simplemap\records\Map;
use ether\simplemap\elements\Map as MapElement;
/**
* m190325_130533_repair_map_elements migration.
*/
class m190325_130533_repair_map_elements extends Migration
{
/**
* @inheritdoc
* @return bool
* @throws \yii\base\Exception
* @throws \yii\base\NotSupportedException
*/
public function safeUp()
{
if (!$this->db->columnExists(Map::TableName, 'elementId'))
return true;
echo ' > Start map data fix' . PHP_EOL;
$rows = (new Query())
->select('*')
->from(Map::TableName)
->orderBy('dateUpdated DESC')
->all();
$validMapElementIds = (new Query())
->select('id')
->from(Table::ELEMENTS)
->where(['=', 'type', MapElement::class])
->column();
$this->dropTable(Map::TableName);
(new Install())->safeUp();
$updatedElementIds = [];
foreach ($rows as $row)
{
// Skip any rows that don't have a matching element
if (!in_array($row['elementId'], $validMapElementIds))
continue;
// Skip and duplicate elements
if (in_array($row['elementId'], $updatedElementIds))
continue;
echo ' > Fix map value ' . $row['address'] . PHP_EOL;
$record = new Map();
$record->id = $row['elementId'];
$record->ownerId = $row['ownerId'];
$record->ownerSiteId = $row['ownerSiteId'];
$record->fieldId = $row['fieldId'];
$record->lat = $row['lat'];
$record->lng = $row['lng'];
$record->zoom = $row['zoom'];
$record->address = $row['address'];
$record->parts = $row['parts'];
$record->save(false);
$updatedElementIds[] = $record->id;
}
return true;
}
/**
* @inheritdoc
*/
public function safeDown()
{
echo "m190325_130533_repair_map_elements cannot be reverted.\n";
return false;
}
}
================================================
FILE: src/migrations/m190712_104805_new_data_format.php
================================================
<?php
namespace ether\simplemap\migrations;
use Craft;
use craft\base\FieldInterface;
use craft\db\Migration;
use craft\db\Query;
use craft\fields\Matrix;
use craft\helpers\Json;
use ether\simplemap\fields\MapField;
use ether\simplemap\models\Map;
use ether\simplemap\records\Map as MapRecord;
use ether\simplemap\SimpleMap;
use verbb\supertable\fields\SuperTableField;
/**
* m190712_104805_new_data_format migration.
*/
class m190712_104805_new_data_format extends Migration
{
/**
* @inheritdoc
* @throws \yii\db\Exception
* @throws \Exception
*/
public function safeUp ()
{
$db = $this->getDb();
$mapService = SimpleMap::getInstance()->map;
$matrixService = Craft::$app->getMatrix();
$superTableService = null;
$hasSuperTable = class_exists(SuperTableField::class);
// 1. Add content columns
// ---------------------------------------------------------------------
echo '1. Creating Maps content columns' . PHP_EOL;
$matrixFields = [];
$superTableFields = [];
$fields = array_reduce(
Craft::$app->getFields()->getAllFields(),
function ($carry, FieldInterface $field) use ($hasSuperTable, &$matrixFields, &$superTableFields) {
if ($field instanceof MapField)
$carry[$field->id] = $field;
elseif ($field instanceof Matrix)
$matrixFields[] = $field;
elseif ($hasSuperTable && $field instanceof SuperTableField)
$superTableFields[] = $field;
return $carry;
},
[]
);
$matrixMapFields = [];
$superTableMapFields = [];
$matrixMapFields = array_merge(
$matrixMapFields,
$this->_reduceMatrixFields(
$matrixFields,
$hasSuperTable,
$matrixMapFields,
$superTableMapFields
)
);
$superTableMapFields = array_merge(
$superTableMapFields,
$this->_reduceSuperTableFields(
$superTableFields,
$matrixMapFields,
$superTableMapFields
)
);
$fieldIdToMatrixBlockHandle = [];
if (!empty($matrixMapFields))
{
foreach ($matrixFields as $field)
{
$blockTypes = $matrixService->getBlockTypesByFieldId($field->id);
foreach ($blockTypes as $blockType)
foreach ($blockType->getFields() as $field)
$fieldIdToMatrixBlockHandle[$field->id] = $blockType->handle;
}
}
$columnType = (new MapField())->getContentColumnType();
$contentTable = Craft::$app->getContent()->contentTable;
$fieldColumnPrefix = Craft::$app->getContent()->fieldColumnPrefix;
/** @var MapField $field */
foreach ($fields as $field)
{
echo '- Create content column for ' . $field->name . ' in content table' . PHP_EOL;
$exists = $this->db->columnExists(
$contentTable,
$fieldColumnPrefix . $field->handle
);
if ($exists)
{
$this->alterColumn(
$contentTable,
$fieldColumnPrefix . $field->handle,
$columnType
);
continue;
}
$this->addColumn(
$contentTable,
$fieldColumnPrefix . $field->handle,
$columnType
);
}
foreach ($matrixMapFields as $table => $mmFields)
{
foreach ($mmFields as $field)
{
if (!$blockTypeHandle = @$fieldIdToMatrixBlockHandle[$field->id])
continue;
echo '- Create content column for ' . $field->name . ' in matrix ' . $blockTypeHandle . PHP_EOL;
$handle =
$fieldColumnPrefix . $blockTypeHandle . '_' . $field->handle;
$exists = $this->db->columnExists(
$table,
$handle
);
if ($exists)
{
$this->alterColumn(
$table,
$handle,
$columnType
);
continue;
}
$this->addColumn(
$table,
$handle,
$columnType
);
}
}
foreach ($superTableMapFields as $table => $stFields)
{
foreach ($stFields as $field)
{
echo '- Create content column for ' . $field->name . ' in super table' . PHP_EOL;
$exists = $this->db->columnExists(
$table,
$fieldColumnPrefix . $field->handle
);
if ($exists)
{
$this->alterColumn(
$table,
$fieldColumnPrefix . $field->handle,
$columnType
);
continue;
}
$this->addColumn(
$table,
$fieldColumnPrefix . $field->handle,
$columnType
);
}
}
// 2. Create new maps table
// ---------------------------------------------------------------------
echo '2. Creating new Maps table' . PHP_EOL;
if ($this->db->tableExists(MapRecord::TableName))
{
$rawTableName = $this->getDb()->getSchema()->getRawTableName(
MapRecord::TableName
);
if ($this->getDb()->getDriverName() === 'pgsql')
{
$indexNames = $this->getDb()->createCommand(
'SELECT indexname FROM pg_indexes WHERE [[tablename]]=:tablename',
['tablename' => $rawTableName]
)->queryColumn();
foreach ($indexNames as $name)
$this->getDb()->createCommand(
'ALTER INDEX "' . $name . '" RENAME TO "' . $name . '_old"'
)->execute();
}
else
{
$indexNames = $this->getDb()->createCommand(
'SHOW INDEX FROM ' . $rawTableName
)->queryAll();
$indexNames = array_unique(array_reduce(
$indexNames,
function ($carry, $row) {
if ($row['Key_name'] === 'PRIMARY')
return $carry;
$carry[] = $row['Key_name'];
return $carry;
},
[]
));
// Look, I know this is hacky but whatever who still uses MySQL
// anyway? You know Postgres exists, right?
$this->getDb()->createCommand('SET foreign_key_checks = 0;')->execute();
foreach ($indexNames as $name)
$this->dropIndex($name, MapRecord::TableName);
$this->getDb()->createCommand('SET foreign_key_checks = 1;')->execute();
}
$this->renameTable(MapRecord::TableName, MapRecord::OldTableName);
}
(new Install())->safeUp();
// 3. Move content to table
// ---------------------------------------------------------------------
echo '3. Moving existing maps content' . PHP_EOL;
$contentRows = (new Query())
->select('id, elementId, siteId')
->from($contentTable);
foreach ($contentRows->each() as $row)
{
$mapContent = $this->_getMapContent(
$row['elementId'],
$row['siteId']
);
if (empty($mapContent))
continue;
foreach ($mapContent as $mapData)
{
$map = new Map($mapData);
echo '- Moving ' . $map->address . ' (' . $mapData['id'] . ') to ' . $contentTable . PHP_EOL;
$map->ownerId = $row['elementId'];
$map->ownerSiteId = $row['siteId'];
$map->fieldId = $mapData['fieldId'];
$field = @$fields[$mapData['fieldId']];
// Skip if the field no longer exists
if (!$field)
{
echo '- Skipping ' . $map->address . ' (' . $mapData['id'] . ') - Field no longer exists' . PHP_EOL;
continue;
}
$col = $fieldColumnPrefix . $field->handle;
$db->createCommand()
->update(
$contentTable,
[$col => Json::encode($map)],
['id' => $row['id']]
)
->execute();
$mapService->saveRecord($map, true);
}
}
foreach ($matrixMapFields as $contentTable => $fields)
{
$contentRows = (new Query())
->select('id, elementId, siteId')
->from($contentTable);
foreach ($contentRows->each() as $row)
{
$mapContent = $this->_getMapContent(
$row['elementId'],
$row['siteId']
);
if (empty($mapContent))
continue;
foreach ($mapContent as $mapData)
{
if (!$blockHandle = @$fieldIdToMatrixBlockHandle[$mapData['fieldId']])
continue;
$map = new Map($mapData);
$map->ownerId = $row['elementId'];
$map->ownerSiteId = $row['siteId'];
$map->fieldId = $mapData['fieldId'];
$field = $fields[$mapData['fieldId']];
$col = $fieldColumnPrefix . $blockHandle . '_' . $field->handle;
echo '- Moving ' . $map->address . ' (' . $mapData['id'] . ') to ' . $contentTable . ', ' . $col . PHP_EOL;
$db->createCommand()
->update(
$contentTable,
[$col => Json::encode($map)],
['id' => $row['id']]
)
->execute();
$mapService->saveRecord($map, true);
}
}
}
foreach ($superTableMapFields as $contentTable => $fields)
{
$contentRows = (new Query())
->select('id, elementId, siteId')
->from($contentTable);
foreach ($contentRows->each() as $row)
{
$mapContent = $this->_getMapContent(
$row['elementId'],
$row['siteId']
);
if (empty($mapContent))
continue;
foreach ($mapContent as $mapData)
{
$map = new Map($mapData);
$map->ownerId = $row['elementId'];
$map->ownerSiteId = $row['siteId'];
$map->fieldId = $mapData['fieldId'];
$field = $fields[$mapData['fieldId']];
$col = $fieldColumnPrefix . $field->handle;
echo '- Moving ' . $map->address . ' (' . $mapData['id'] . ') to ' . $contentTable . ', ' . $col . PHP_EOL;
$db->createCommand()
->update(
$contentTable,
[$col => Json::encode($map)],
['id' => $row['id']]
)
->execute();
$mapService->saveRecord($map, true);
}
}
}
// 4. Drop old data table
// ---------------------------------------------------------------------
$this->dropTableIfExists(MapRecord::OldTableName);
}
/**
* @inheritdoc
*/
public function safeDown()
{
echo "m190712_104805_new_data_format cannot be reverted.\n";
return false;
}
// Helpers
// =========================================================================
private function _reduceMatrixFields ($matrixFields, $hasSuperTable, &$matrixMapFields, &$superTableMapFields)
{
return array_reduce(
$matrixFields,
function ($carry, Matrix $matrix) use (
$hasSuperTable, &$matrixMapFields, &$superTableMapFields
) {
$fields = [];
foreach ($matrix->getBlockTypeFields() as $field)
{
if ($field instanceof MapField)
$fields[$field->id] = $field;
elseif ($hasSuperTable && $field instanceof SuperTableField)
$superTableMapFields = array_merge(
$superTableMapFields,
$this->_reduceSuperTableFields(
[$field],
$matrixMapFields,
$superTableMapFields
)
);
}
if (!empty($fields))
$carry[$matrix->contentTable] = $fields;
return $carry;
},
[]
);
}
private function _reduceSuperTableFields ($superTableFields, &$matrixMapFields, &$superTableMapFields)
{
return array_reduce(
$superTableFields,
function ($carry, SuperTableField $superTable) use (
&$matrixMapFields, &$superTableMapFields
) {
$fields = [];
foreach ($superTable->getBlockTypeFields() as $field)
{
if ($field instanceof MapField)
$fields[$field->id] = $field;
elseif ($field instanceof Matrix)
$matrixMapFields = array_merge(
$matrixMapFields,
$this->_reduceMatrixFields(
[$field],
true,
$matrixMapFields,
$superTableMapFields
)
);
}
if (!empty($fields))
$carry[$superTable->contentTable] = $fields;
return $carry;
},
[]
);
}
private function _getMapContent ($elementId, $siteId)
{
return (new Query())
->select(
'id, ownerId, ownerSiteId, fieldId, lat, lng, zoom, address, parts'
)
->from(MapRecord::OldTableName)
->where([
'ownerId' => $elementId,
'ownerSiteId' => $siteId,
])
->groupBy('id, fieldId')
->orderBy('dateUpdated')
->all();
}
}
================================================
FILE: src/migrations/m190723_105637_fix_map_field_column_type.php
================================================
<?php
namespace ether\simplemap\migrations;
use Craft;
use craft\db\Migration;
use ether\simplemap\fields\MapField;
use yii\base\ErrorException;
use yii\base\Exception;
use yii\base\NotSupportedException;
use yii\web\ServerErrorHttpException;
/**
* m190723_105637_fix_map_field_column_type migration.
*/
class m190723_105637_fix_map_field_column_type extends Migration
{
/**
* @inheritdoc
* @throws NotSupportedException
* @throws ErrorException
* @throws Exception
* @throws ServerErrorHttpException
*/
public function safeUp ()
{
$pc = Craft::$app->getProjectConfig();
$fields = $pc->get('fields', true) ?? [];
$matrixBlockTypes = $pc->get('matrixBlockTypes', true) ?? [];
$superTableBlockTypes = $pc->get('superTableBlockTypes', true) ?? [];
$updates = [];
foreach ($fields as $f => $field)
if (!empty($field['type']) && $field['type'] === MapField::class && $field['contentColumnType'] !== 'text')
$updates["fields.$f.contentColumnType"] = 'text';
foreach ($matrixBlockTypes as $b => $blockType)
if (array_key_exists('fields', $blockType) && is_array($blockType['fields']))
foreach ($blockType['fields'] as $f => $field)
if ($field['type'] === MapField::class && $field['contentColumnType'] !== 'text')
$updates["matrixBlockTypes.$b.fields.$f.contentColumnType"] = 'text';
foreach ($superTableBlockTypes as $b => $blockType)
if (array_key_exists('fields', $blockType) && is_array($blockType['fields']))
foreach ($blockType['fields'] as $f => $field)
if ($field['type'] === MapField::class && $field['contentColumnType'] !== 'text')
$updates["superTableBlockTypes.$b.fields.$f.contentColumnType"] = 'text';
foreach ($updates as $path => $value)
$pc->set($path, $value);
}
/**
* @inheritdoc
*/
public function safeDown ()
{
echo "m190723_105637_fix_map_field_column_type cannot be reverted.\n";
return false;
}
}
================================================
FILE: src/models/BaseLocation.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\models;
use craft\helpers\Json;
use craft\helpers\Typecast;
use Twig\Markup;
use yii\base\Model;
/**
* Class BaseLocation
*
* @author Ether Creative
* @package ether\simplemap\models
*/
abstract class BaseLocation extends Model
{
// Properties
// =========================================================================
/** @var float|null */
public ?float $lat = null;
/** @var float|null */
public ?float $lng = null;
/** @var string|null */
public ?string $address = null;
/** @var PartsLegacy|Parts|array|null */
public PartsLegacy|Parts|array|null $parts = null;
/** @var string|null */
public ?string $what3words = null;
// Constructor
// =========================================================================
public function __construct ($config = [])
{
Typecast::properties(static::class, $config);
parent::__construct($config);
if ($this->address === null)
$this->address = '';
if ($this->parts === null)
{
$this->parts = new Parts();
}
else if (!($this->parts instanceof Parts))
{
if ($this->parts && !is_array($this->parts))
$this->parts = Json::decodeIfJson($this->parts, true);
if (Parts::isLegacy($this->parts))
$this->parts = new PartsLegacy($this->parts);
else
$this->parts = new Parts($this->parts);
}
}
// Methods
// =========================================================================
/**
* Output the address in an easily formatted way
*
* @param array $exclude - An array of parts to exclude from the output
* @param string $glue - The glue to join the parts together
*
* @return Markup
*/
public function address (array $exclude = [], string $glue = '<br/>'): Markup
{
$addr = [];
if (!is_array($exclude))
$exclude = [$exclude];
foreach ([['number', 'address'], 'city', 'county', 'state', 'postcode', 'country'] as $part)
{
if (is_array($part))
{
$line = [];
foreach ($part as $p)
{
if (in_array($p, $exclude))
continue;
$line[] = $this->parts->$p;
}
$addr[] = implode(' ', array_filter($line));
continue;
}
if (in_array($part, $exclude))
continue;
$addr[] = $this->parts->$part;
}
$addr = array_filter($addr);
return new Markup(implode($glue, $addr), 'utf8');
}
public function __toString(): string
{
return (string) $this->address([], ', ');
}
}
================================================
FILE: src/models/EmbedOptions.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\models;
use craft\helpers\StringHelper;
/**
* Class EmbedOptions
*
* @author Ether Creative
* @package ether\simplemap\models
*/
class EmbedOptions extends StaticOptions
{
// Properties
// =========================================================================
/** @var string|null The ID of the map (unique ID will be generated if null) */
public ?string $id = null;
/** @var array Options to be passed to the JS map */
public array $options = [];
// Constructor
// =========================================================================
public function __construct ($config = [])
{
parent::__construct($config);
if (!$this->id)
$this->id = StringHelper::appendUniqueIdentifier('map');
}
}
================================================
FILE: src/models/Map.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\models;
use craft\base\Model;
use craft\helpers\Json;
use ether\simplemap\SimpleMap;
use Exception;
use Twig\Markup;
use yii\base\InvalidConfigException;
/**
* Class Map
*
* @author Ether Creative
* @package ether\simplemap\models
*/
class Map extends BaseLocation
{
// Properties
// =========================================================================
/** @var int|null */
public ?int $id = null;
/** @var int|null */
public ?int $ownerId = null;
/** @var int|null */
public ?int $ownerSiteId = null;
/** @var int|null */
public ?int $fieldId = null;
/** @var int */
public ?int $zoom = 15;
/** @var int|float|null */
public int|null|float $distance = null;
// Constructor
// =========================================================================
public function __construct ($config = [])
{
foreach (['id', 'ownerId', 'ownerSiteId', 'fieldId'] as $key)
if (array_key_exists($key, $config))
unset($config[$key]);
parent::__construct($config);
$this->distance = SimpleMap::getInstance()->map->getDistance($this);
}
// Getters
// =========================================================================
public function __get ($name)
{
$isPart = property_exists($this->parts, $name) || $name === 'streetAddress';
if (in_array($name, PartsLegacy::$legacyKeys) && !$isPart)
return null;
else if ($isPart)
return $this->parts->$name;
return parent::__get($name);
}
public function canGetProperty ($name, $checkVars = true, $checkBehaviors = true): bool
{
try
{
if (
property_exists($this->parts, $name) ||
$name === 'streetAddress' ||
in_array($name, PartsLegacy::$legacyKeys)
) return true;
} catch (Exception $e) {
return false;
}
return parent::canGetProperty($name, $checkVars, $checkBehaviors);
}
// Methods
// =========================================================================
public function rules (): array
{
$rules = parent::rules();
$rules[] = [
['zoom'],
'required',
];
$rules[] = [
['lat'],
'double',
'min' => -90,
'max' => 90,
];
$rules[] = [
['lng'],
'double',
'min' => -180,
'max' => 180,
];
return $rules;
}
public function isValueEmpty (): bool
{
return empty($this->lat) && empty($this->lng);
}
// Render Map
// =========================================================================
// Render Map: Image
// -------------------------------------------------------------------------
/**
* Output the map field as a static image
*
* @param array $options
*
* @return string|void
* @throws Exception
*/
public function img (array $options = [])
{
return SimpleMap::getInstance()->static->generate(
$this->_getMapOptions($options)
);
}
/**
* Output the map ready for srcset
*
* @param array $options
*
* @return string
* @throws Exception
*/
public function imgSrcSet (array $options = []): string
{
$options = $this->_getMapOptions($options);
$x1 = $this->img(array_merge($options, ['scale' => 1]));
$x2 = $this->img(array_merge($options, ['scale' => 2]));
return $x1 . ' 1x, ' . $x2 . ' 2x';
}
/**
* Output an interactive map
*
* @param array $options
*
* @return string|void
* @throws InvalidConfigException
*/
public function embed (array $options = [])
{
$options = $this->_getMapOptions($options);
return SimpleMap::getInstance()->embed->embed($options);
}
// Helpers
// =========================================================================
/**
* Merge options w/ map properties
*
* @param array $options
*
* @return array
*/
private function _getMapOptions (array $options): array
{
return array_merge($options, [
'center' => [
$this->lat,
$this->lng,
],
'zoom' => $this->zoom,
]);
}
}
================================================
FILE: src/models/Marker.php
================================================
<?php
/**
* Maps for Craft CMS
*
* @link https://ethercreative.co.uk
* @copyright Copyright (c) 2019 Ether Creative
*/
namespace ether\simplemap\models;
use craft\helpers\Json;
use ether\simplemap\services\GeoService;
use Exception;
use Yii;
use yii\base\InvalidConfigException;
/**
* Class Marker
*
* @author Ether Creative
* @package ether\simplemap\models
*/
class Marker
{
// Properties
// =========================================================================
/** @var string|array Can be an address string, or a [lat, lng] or ['lat' => lat, 'lng' => lng] array */
public string|array $location;
/** @var string The colour of the marker in Hex format */
public string $color = '#ff0000';
/** @var string|null A single character label, or null for no label */
public mixed $label = null;
// Constructor
// =========================================================================
public function __construct ($config = [])
{
if (!empty($config))
Yii::configure($this, $config);
if (empty($this->location))
throw new InvalidCo
gitextract_o1ic_4j1/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── craft-3-issue.md
│ │ └── craft-4-issue.md
│ ├── ISSUE_TEMPLATE.md
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── docs/
│ ├── .docs.json
│ ├── geolocation/
│ │ ├── get.md
│ │ └── redirect.md
│ ├── getting-started/
│ │ ├── config.md
│ │ ├── installation.md
│ │ └── usage.md
│ ├── how-to/
│ │ ├── graphql.md
│ │ └── search.md
│ ├── index.md
│ └── rendering/
│ ├── embed.md
│ └── static.md
├── resources/
│ ├── .editorconfig
│ ├── .gitignore
│ ├── README.md
│ ├── babel.config.js
│ ├── package.json
│ ├── src/
│ │ ├── App.vue
│ │ ├── common/
│ │ │ └── Geo.js
│ │ ├── components/
│ │ │ ├── Address.vue
│ │ │ ├── Fragment.vue
│ │ │ ├── Input.vue
│ │ │ ├── Label.vue
│ │ │ ├── Map.vue
│ │ │ └── Search.vue
│ │ ├── enums/
│ │ │ ├── GeoService.js
│ │ │ └── MapTiles.js
│ │ ├── filters/
│ │ │ └── craft.js
│ │ ├── helpers/
│ │ │ ├── createW3WGrid.js
│ │ │ ├── debounce.js
│ │ │ └── waitForGlobal.js
│ │ ├── main.js
│ │ └── models/
│ │ ├── Parts.js
│ │ └── PartsLegacy.js
│ └── vue.config.js
└── src/
├── SimpleMap.php
├── acfadapters/
│ └── GoogleMap.php
├── config.php
├── controllers/
│ ├── SettingsController.php
│ └── StaticController.php
├── enums/
│ ├── GeoService.php
│ └── MapTiles.php
├── fields/
│ └── MapField.php
├── 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_LICENSE.txt
├── 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
└── map/
├── css/
│ ├── app.css
│ └── chunk-vendors.css
├── index.html
└── js/
├── app.js
└── chunk-vendors.js
SYMBOL INDEX (497 symbols across 46 files)
FILE: resources/src/common/Geo.js
class Geo (line 6) | class Geo {
method constructor (line 22) | constructor ({ country, geoService: service, geoToken: token, locale }) {
method initGoogle (line 38) | initGoogle () {
method initApple (line 49) | initApple (token) {
method search (line 73) | async search (text) {
method searchNominatim (line 109) | async searchNominatim (query) {
method searchMapbox (line 140) | async searchMapbox (query) {
method searchGoogle (line 171) | searchGoogle (query) {
method searchApple (line 198) | searchApple (query) {
method searchHere (line 219) | async searchHere (query) {
method reverseNominatim (line 254) | async reverseNominatim ({ lat, lng }, oldVal) {
method reverseMapbox (line 291) | async reverseMapbox ({ lat, lng }, oldVal) {
method reverseGoogle (line 324) | reverseGoogle (latLng, oldVal) {
method reverseApple (line 351) | reverseApple ({ lat, lng }, oldVal) {
method reverseHere (line 379) | async reverseHere ({ lat, lng }, oldVal) {
method getGooglePlaceDetails (line 418) | getGooglePlaceDetails (placeId, item) {
method getHerePlaceDetails (line 447) | async getHerePlaceDetails (locationId, item) {
FILE: resources/src/filters/craft.js
function t (line 1) | function t (message, params = null) {
FILE: resources/src/helpers/createW3WGrid.js
function createW3WGrid (line 1) | function createW3WGrid (L, map) {
FILE: resources/src/helpers/debounce.js
function debounce (line 28) | function debounce (func, wait = 500, immediate = false) {
FILE: resources/src/helpers/waitForGlobal.js
function waitForGlobal (line 1) | function waitForGlobal (property, callback) {
FILE: resources/src/main.js
method install (line 5) | install (Vue) {
FILE: resources/src/models/Parts.js
class Parts (line 3) | class Parts {
method constructor (line 16) | constructor (parts, service) {
method from (line 41) | static from (parts) {
method _nominatim (line 64) | _nominatim (parts) {
method _mapbox (line 115) | _mapbox (parts) {
method _google (line 152) | _google (parts) {
method _here (line 196) | _here (parts) {
method _join (line 229) | _join (parts) {
FILE: resources/src/models/PartsLegacy.js
class PartsLegacy (line 6) | class PartsLegacy extends Parts {
method constructor (line 85) | constructor (parts = {}) {
FILE: src/SimpleMap.php
class SimpleMap (line 49) | class SimpleMap extends Plugin
method editions (line 63) | public static function editions (): array
method init (line 74) | public function init ()
method beforeUninstall (line 157) | protected function beforeUninstall (): void
method createSettingsModel (line 166) | protected function createSettingsModel (): Model
method getSettings (line 174) | public function getSettings (): Model
method settingsHtml (line 179) | protected function settingsHtml (): ?string
method afterSaveSettings (line 189) | public function afterSaveSettings (): void
method onRegisterCPUrlRules (line 204) | public function onRegisterCPUrlRules (RegisterUrlRulesEvent $event)
method onRegisterFieldTypes (line 209) | public function onRegisterFieldTypes (RegisterComponentTypesEvent $event)
method onRegisterVariable (line 219) | public function onRegisterVariable (Event $event)
method onRegisterFeedMeFields (line 227) | public function onRegisterFeedMeFields (\craft\feedme\events\RegisterF...
method onRegisterGqlTypes (line 232) | public function onRegisterGqlTypes (RegisterGqlTypesEvent $event)
method onApplicationInit (line 241) | public function onApplicationInit ()
method t (line 250) | public static function t ($message, $params = []): string
method v (line 255) | public static function v ($version, $operator = '='): bool
FILE: src/acfadapters/GoogleMap.php
class GoogleMap (line 22) | class GoogleMap extends BaseAcfAdapter
method type (line 24) | public static function type(): string
method create (line 29) | public function create(array $data): FieldInterface
method normalizeValue (line 44) | public function normalizeValue(mixed $value, array $data): mixed
FILE: src/controllers/SettingsController.php
class SettingsController (line 24) | class SettingsController extends Controller
method actionIndex (line 27) | public function actionIndex (): YiiResponse
FILE: src/controllers/StaticController.php
class StaticController (line 24) | class StaticController extends Controller
method actionIndex (line 33) | public function actionIndex ()
FILE: src/enums/GeoService.php
class GeoService (line 19) | abstract class GeoService
method getSelectOptions (line 45) | public static function getSelectOptions (): array
FILE: src/enums/MapTiles.php
class MapTiles (line 22) | abstract class MapTiles
method getSelectOptions (line 79) | public static function getSelectOptions (): array
method getTiles (line 130) | public static function getTiles (string $type, int $scale = 1): array
method pro (line 162) | public static function pro ($label, $isLite): array
FILE: src/fields/MapField.php
class MapField (line 47) | class MapField extends Field implements PreviewableFieldInterface
method init (line 167) | public function init(): void
method displayName (line 181) | public static function displayName (): string
method hasContentColumn (line 186) | public static function hasContentColumn (): bool
method icon (line 191) | public static function icon (): string
method getContentColumnType (line 196) | public function getContentColumnType (): string
method supportedTranslationMethods (line 201) | public static function supportedTranslationMethods (): array
method queryCondition (line 212) | public static function queryCondition(array $instances, mixed $value, ...
method setCountryRestriction (line 228) | public function setCountryRestriction ($value)
method rules (line 233) | public function rules (): array
method normalizeValue (line 266) | public function normalizeValue (mixed $value, ElementInterface|Element...
method getSettingsHtml (line 298) | public function getSettingsHtml (): string
method getInputHtml (line 355) | public function getInputHtml ($value = null, ElementInterface $element...
method getTableAttributeHtml (line 366) | public function getTableAttributeHtml (mixed $value, ElementInterface ...
method isValueEmpty (line 371) | public function isValueEmpty ($value, ElementInterface $element): bool
method getContentGqlType (line 382) | public function getContentGqlType (): Type|array
method getContentGqlQueryArgumentType (line 387) | public function getContentGqlQueryArgumentType (): Type|array
method getContentGqlMutationArgumentType (line 392) | public function getContentGqlMutationArgumentType (): Type|array
method beforeSave (line 400) | public function beforeSave (bool $isNew): bool
method beforeElementSave (line 412) | public function beforeElementSave (ElementInterface $element, bool $is...
method afterElementSave (line 420) | public function afterElementSave (ElementInterface $element, bool $isN...
method afterPrepareElementQuery (line 427) | public function afterPrepareElementQuery (CancelableEvent $event): void
method _renderMap (line 456) | private function _renderMap (Map $value, bool $isSettings = false): st...
method _parseFloat (line 624) | private static function _parseFloat ($value = null): ?float
FILE: src/integrations/feedme/FeedMeMaps.php
class FeedMeMaps (line 25) | class FeedMeMaps extends Field implements FieldInterface
method getMappingTemplate (line 38) | public function getMappingTemplate (): string
method parseField (line 43) | public function parseField (): ?Map
FILE: src/integrations/graphql/MapPartsType.php
class MapPartsType (line 23) | class MapPartsType
method getName (line 26) | public static function getName (): string
method getFieldDefinitions (line 31) | public static function getFieldDefinitions (): array
method getType (line 72) | public static function getType (): Type
method getInputType (line 88) | public static function getInputType (): InputType
FILE: src/integrations/graphql/MapType.php
class MapType (line 24) | class MapType
method getName (line 27) | public static function getName (): string
method getFieldDefinitions (line 32) | public static function getFieldDefinitions (): array
method getInputDefinitions (line 68) | public static function getInputDefinitions (): array
method getQueryInputDefinitions (line 78) | public static function getQueryInputDefinitions (): array
method getType (line 104) | public static function getType (): Type
method getInputType (line 118) | public static function getInputType (): InputType
method getQueryType (line 134) | public static function getQueryType (): InputType
method getCoordsType (line 150) | public static function getCoordsType (): InputType
method getUnitType (line 175) | public static function getUnitType (): EnumType
FILE: src/jobs/MaxMindDBDownloadJob.php
class MaxMindDBDownloadJob (line 26) | class MaxMindDBDownloadJob extends BaseJob
method defaultDescription (line 29) | protected function defaultDescription (): string
method execute (line 39) | public function execute ($queue): void
FILE: src/migrations/Install.php
class Install (line 21) | class Install extends Migration
method safeUp (line 24) | public function safeUp ()
method safeUpPre34 (line 101) | public function safeUpPre34 ()
method safeDown (line 148) | public function safeDown ()
FILE: src/migrations/m190226_143809_craft3_upgrade.php
class m190226_143809_craft3_upgrade (line 23) | class m190226_143809_craft3_upgrade extends Migration
method safeUp (line 40) | public function safeUp()
method safeDown (line 56) | public function safeDown()
method _upgrade2 (line 69) | private function _upgrade2 ()
method _upgrade3 (line 155) | private function _upgrade3 ()
method locale2handle (line 252) | private function locale2handle (string $locale): string
method getSiteByLocale (line 273) | private function getSiteByLocale ($locale)
method updatePluginSettings (line 301) | private function updatePluginSettings ()
FILE: src/migrations/m190325_130533_repair_map_elements.php
class m190325_130533_repair_map_elements (line 14) | class m190325_130533_repair_map_elements extends Migration
method safeUp (line 22) | public function safeUp()
method safeDown (line 81) | public function safeDown()
FILE: src/migrations/m190712_104805_new_data_format.php
class m190712_104805_new_data_format (line 20) | class m190712_104805_new_data_format extends Migration
method safeUp (line 28) | public function safeUp ()
method safeDown (line 397) | public function safeDown()
method _reduceMatrixFields (line 406) | private function _reduceMatrixFields ($matrixFields, $hasSuperTable, &...
method _reduceSuperTableFields (line 440) | private function _reduceSuperTableFields ($superTableFields, &$matrixM...
method _getMapContent (line 475) | private function _getMapContent ($elementId, $siteId)
FILE: src/migrations/m190723_105637_fix_map_field_column_type.php
class m190723_105637_fix_map_field_column_type (line 16) | class m190723_105637_fix_map_field_column_type extends Migration
method safeUp (line 26) | public function safeUp ()
method safeDown (line 58) | public function safeDown ()
FILE: src/models/BaseLocation.php
class BaseLocation (line 22) | abstract class BaseLocation extends Model
method __construct (line 46) | public function __construct ($config = [])
method address (line 82) | public function address (array $exclude = [], string $glue = '<br/>'):...
method __toString (line 118) | public function __toString(): string
FILE: src/models/EmbedOptions.php
class EmbedOptions (line 19) | class EmbedOptions extends StaticOptions
method __construct (line 34) | public function __construct ($config = [])
FILE: src/models/Map.php
class Map (line 24) | class Map extends BaseLocation
method __construct (line 51) | public function __construct ($config = [])
method __get (line 65) | public function __get ($name)
method canGetProperty (line 77) | public function canGetProperty ($name, $checkVars = true, $checkBehavi...
method rules (line 96) | public function rules (): array
method isValueEmpty (line 120) | public function isValueEmpty (): bool
method img (line 139) | public function img (array $options = [])
method imgSrcSet (line 154) | public function imgSrcSet (array $options = []): string
method embed (line 172) | public function embed (array $options = [])
method _getMapOptions (line 188) | private function _getMapOptions (array $options): array
FILE: src/models/Marker.php
class Marker (line 23) | class Marker
method __construct (line 41) | public function __construct ($config = [])
method __toString (line 66) | public function __toString ()
method getLocation (line 75) | public function getLocation ($toLatLng = false): array|string
method getCenter (line 89) | public function getCenter (): array|string|null
method _expandHex (line 103) | private static function _expandHex ($hex): string
FILE: src/models/Parts.php
class Parts (line 21) | class Parts extends BaseObject
method __construct (line 72) | public function __construct ($parts = null, string $service = null)
method getStreetAddress (line 101) | public function getStreetAddress (): string
method _nominatim (line 114) | private function _nominatim (array $parts)
method _mapbox (line 191) | private function _mapbox (array $parts)
method _google (line 221) | private function _google ($parts)
method _here (line 274) | private function _here ($parts)
method isLegacy (line 306) | public static function isLegacy (array $parts = null): bool
method _join (line 329) | private function _join (array $parts): string
method _fromArray (line 339) | private function _fromArray (array $parts)
method _isAssoc (line 357) | protected function _isAssoc (array $arr): bool
FILE: src/models/PartsLegacy.php
class PartsLegacy (line 21) | class PartsLegacy extends Parts
method __construct (line 153) | public function __construct ($parts = null)
method __set (line 174) | public function __set ($name, $value)
FILE: src/models/Point.php
class Point (line 22) | class Point implements PointInterface
method __construct (line 41) | public function __construct (int $x, int $y)
method getX (line 52) | public function getX (): int
method getY (line 62) | public function getY (): int
method in (line 72) | public function in (BoxInterface $box): bool
method move (line 82) | public function move ($amount): Point|PointInterface
method __toString (line 92) | public function __toString ()
FILE: src/models/Settings.php
class Settings (line 27) | class Settings extends Model
method __construct (line 105) | public function __construct ($config = [])
method isW3WEnabled (line 118) | public function isW3WEnabled (): bool
method getMapToken (line 126) | public function getMapToken (): bool|array|string|null
method getGeoToken (line 131) | public function getGeoToken (): bool|array|string|null
method getW3WToken (line 136) | public function getW3WToken (): bool|array|string|null
method getGeoLocationToken (line 141) | public function getGeoLocationToken (): bool|array|string|null
method _parseEnv (line 149) | private function _parseEnv ($value): array|bool|string|null
FILE: src/models/StaticOptions.php
class StaticOptions (line 22) | class StaticOptions
method __construct (line 62) | public function __construct (array $config = [])
method getCenter (line 107) | public function getCenter (): array|string|null
method getSize (line 118) | public function getSize (): string
FILE: src/models/UserLocation.php
class UserLocation (line 23) | class UserLocation extends BaseLocation
method distance (line 52) | public function distance (array $to, string $unit = 'km'): float|int
FILE: src/records/Map.php
class Map (line 30) | class Map extends ActiveRecord
method tableName (line 45) | public static function tableName (): string
method getOwner (line 50) | public function getOwner (): ActiveQueryInterface
method getOwnerSite (line 55) | public function getOwnerSite (): ActiveQueryInterface
method getField (line 60) | public function getField (): ActiveQueryInterface
FILE: src/services/EmbedService.php
class EmbedService (line 30) | class EmbedService extends Component
method embed (line 48) | public function embed (array $options = [])
method _embedGoogle (line 104) | private function _embedGoogle (EmbedOptions $options, Settings $settin...
method _embedApple (line 186) | private function _embedApple (EmbedOptions $options, Settings $setting...
method _embedMapbox (line 288) | private function _embedMapbox (EmbedOptions $options, Settings $settin...
method _embedHere (line 373) | private function _embedHere (EmbedOptions $options, Settings $settings...
method _embedDefault (line 489) | private function _embedDefault (EmbedOptions $options, Settings $setti...
method _js (line 595) | private function _js (string $url, array $options = [], string $pre = ...
method _compareUrls (line 627) | private function _compareUrls ($a, $b): bool
method _trim (line 635) | private function _trim ($str): string
method _iconSvg (line 643) | private function _iconSvg ()
method _getCss (line 663) | private function _getCss (EmbedOptions $options): ?string
FILE: src/services/GeoLocationService.php
class GeoLocationService (line 29) | class GeoLocationService extends Component
method lookup (line 53) | public function lookup (string $ip = null): ?UserLocation
method redirect (line 95) | public function redirect ()
method getSelectOptions (line 161) | public static function getSelectOptions (): array
method dbExists (line 181) | public static function dbExists (string $filename = 'default.mmdb'): bool
method dbShouldUpdate (line 196) | public static function dbShouldUpdate (string $filename = 'default.mmd...
method dbQueueDownload (line 209) | public static function dbQueueDownload ()
method purgeDb (line 218) | public static function purgeDb ($filename = 'default.mmdb')
method _getUserLocationFromCache (line 238) | private function _getUserLocationFromCache (string $ip, Settings $sett...
method _cacheUserLocation (line 254) | private function _cacheUserLocation (UserLocation $userLocation, Setti...
method _lookup_IpStack (line 269) | private function _lookup_IpStack ($token, $ip): ?UserLocation
method _lookup_MaxMind (line 303) | private function _lookup_MaxMind ($token, $ip): ?UserLocation
method _lookup_MaxMindLite (line 333) | private function _lookup_MaxMindLite ($ip): ?UserLocation
method _client (line 368) | private static function _client ()
method _isValidIp (line 385) | private static function _isValidIp (string $ip): mixed
method _populateMaxMind (line 400) | private static function _populateMaxMind ($ip, $record): UserLocation
method _validateProps (line 426) | private static function _validateProps (UserLocation $location, $props...
FILE: src/services/GeoService.php
class GeoService (line 31) | class GeoService extends Component
method getToken (line 553) | public static function getToken (array|string $token, string $service)...
method latLngFromAddress (line 581) | public static function latLngFromAddress (string $address, string $cou...
method addressFromLatLng (line 619) | public static function addressFromLatLng (float $lat, float $lng): ?array
method normalizeDistance (line 656) | public static function normalizeDistance (string $unit): string
method normalizeLocation (line 674) | public static function normalizeLocation (mixed $location, string $cou...
method _latLngFromAddress_Here (line 696) | private static function _latLngFromAddress_Here ($token, $address, $co...
method _latLngFromAddress_Google (line 726) | private static function _latLngFromAddress_Google ($token, $address, $...
method _latLngFromAddress_Mapbox (line 752) | private static function _latLngFromAddress_Mapbox ($token, $address, $...
method _latLngFromAddress_Nominatim (line 786) | private static function _latLngFromAddress_Nominatim ($address, $count...
method _addressFromLatLng_Here (line 814) | private static function _addressFromLatLng_Here ($token, $lat, $lng): ...
method _addressFromLatLng_Google (line 837) | private static function _addressFromLatLng_Google ($token, $lat, $lng)...
method _addressFromLatLng_Mapbox (line 858) | private static function _addressFromLatLng_Mapbox ($token, $lat, $lng)...
method _addressFromLatLng_Nominatim (line 880) | private static function _addressFromLatLng_Nominatim ($lat, $lng): ?array
method _client (line 907) | private static function _client ()
method _validateCountryCode (line 917) | private static function _validateCountryCode (string $code): bool
FILE: src/services/MapService.php
class MapService (line 32) | class MapService extends Component
method validateField (line 51) | public function validateField (MapField $field, ElementInterface $owne...
method saveField (line 70) | public function saveField (MapField $field, ElementInterface $owner)
method saveRecord (line 97) | public function saveRecord (Map $map, $isNew)
method getDistance (line 134) | public function getDistance (Map $map): float|int|null
method modifyElementsQuery (line 166) | public function modifyElementsQuery (ElementQueryInterface $query, mix...
method populateMissingData (line 247) | public function populateMissingData (Map $map, MapField $field)
method _searchLocation (line 309) | private function _searchLocation (ElementQuery $query, mixed $value, s...
method _replaceOrderBy (line 400) | private function _replaceOrderBy (ElementQuery $query, string $search ...
FILE: src/services/StaticService.php
class StaticService (line 29) | class StaticService extends Component
method generate (line 38) | public function generate (array $options = []): string
method _generateGoogle (line 87) | private function _generateGoogle (StaticOptions $options, Settings $se...
method _generateApple (line 142) | private function _generateApple (StaticOptions $options, Settings $set...
method _generateMapbox (line 200) | private function _generateMapbox (StaticOptions $options, Settings $se...
method _generateHere (line 259) | private function _generateHere (StaticOptions $options, Settings $sett...
method _generateDefault (line 324) | private function _generateDefault (StaticOptions $options): string
method _getTld (line 346) | private function _getTld (): array
method _encode (line 353) | private function _encode ($data): string
FILE: src/services/What3WordsService.php
class What3WordsService (line 20) | class What3WordsService
method convertLatLngToW3W (line 23) | public static function convertLatLngToW3W ($lat, $lng)
method _geocoder (line 28) | private static function _geocoder ()
FILE: src/utilities/StaticMap.php
class StaticMap (line 41) | class StaticMap
method __construct (line 83) | public function __construct (
method render (line 124) | public function render ()
method _initCoords (line 147) | private function _initCoords (): void
method _createBaseMap (line 153) | private function _createBaseMap (): void
method _placeMarkers (line 214) | private function _placeMarkers (): void
method _send (line 243) | private function _send ($file)
method getLabelColour (line 260) | public static function getLabelColour ($color): string
method _join (line 269) | private static function _join (): string
method _mkdirRecursive (line 280) | private static function _mkdirRecursive ($pathname, $mode): bool
method _getImageDriver (line 289) | private function _getImageDriver ()
method _getImagine (line 307) | private function _getImagine ()
method _getFont (line 323) | private function _getFont (ColorInterface $colour, $size = 10): \Imagi...
method _renderMarker (line 343) | private function _renderMarker ($colour, $label = null): ImageInterface
method _latToTile (line 383) | private function _latToTile ($lat): float|int
method _lngToTile (line 388) | private function _lngToTile ($lng): float|int
method _fetchTile (line 396) | private function _fetchTile ($url): StreamInterface|string
method _getMapId (line 412) | private function _getMapId (): string
method _tileCache (line 431) | private static function _tileCache (): bool|string
method _mapCache (line 436) | private static function _mapCache (): bool|string
method _tileUrlToFilename (line 441) | private function _tileUrlToFilename ($url): string
method _mapCacheIdToFilename (line 449) | private function _mapCacheIdToFilename (): string
method _checkTileCache (line 461) | private function _checkTileCache ($url): bool|string|null
method _checkMapCache (line 468) | private function _checkMapCache (): bool
method _writeTileToCache (line 473) | private function _writeTileToCache ($url, $data): void
FILE: src/web/Variable.php
class Variable (line 26) | class Variable
method getMapToken (line 34) | public function getMapToken (): string
method getApiKey (line 52) | public function getApiKey (): string
method getUserLocation (line 70) | public function getUserLocation (string $ip = null): ?UserLocation
method getLatLngFromAddress (line 84) | public function getLatLngFromAddress (string $address, string $country...
method getImg (line 109) | public function getImg ($options): string
method getImgSrcSet (line 122) | public function getImgSrcSet ($options): string
method getEmbed (line 138) | public function getEmbed ($options)
FILE: src/web/assets/MapAsset.php
class MapAsset (line 21) | class MapAsset extends AssetBundle
method init (line 24) | public function init ()
FILE: src/web/assets/map/js/app.js
function t (line 1) | function t(t){for(var n,s,i=t[0],c=t[1],l=t[2],p=0,h=[];p<i.length;p++)s...
function a (line 1) | function a(){for(var e,t=0;t<o.length;t++){for(var a=o[t],n=!0,i=1;i<a.l...
function s (line 1) | function s(t){if(n[t])return n[t].exports;var a=n[t]={i:t,l:!1,exports:{...
function p (line 1) | function p(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){va...
function h (line 1) | function h(e){for(var t=1;t<arguments.length;t++){var a=null!=arguments[...
function e (line 1) | function e(t,a){switch(Object(i["a"])(this,e),Object(o["a"])(this,"numbe...
function t (line 1) | function t(){var e,a=arguments.length>0&&void 0!==arguments[0]?arguments...
function y (line 1) | function y(e,t){if(window.hasOwnProperty(e))t();else var a=setInterval(f...
function w (line 1) | function w(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){va...
function O (line 1) | function O(e){for(var t=1;t<arguments.length;t++){var a=null!=arguments[...
function e (line 1) | function e(t){var a=this,n=t.country,r=t.geoService,s=t.geoToken,c=t.loc...
function t (line 1) | function t(t){return e.apply(this,arguments)}
function t (line 1) | function t(t){return e.apply(this,arguments)}
function t (line 1) | function t(t){return e.apply(this,arguments)}
function t (line 1) | function t(t){return e.apply(this,arguments)}
function t (line 1) | function t(t,a){return e.apply(this,arguments)}
function t (line 1) | function t(t,a){return e.apply(this,arguments)}
function t (line 1) | function t(t,a){return e.apply(this,arguments)}
function t (line 1) | function t(t,a){return e.apply(this,arguments)}
function R (line 1) | function R(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[...
function t (line 1) | function t(t){return e.apply(this,arguments)}
function I (line 1) | function I(e){this["$style"]=E["default"].locals||E["default"]}
function q (line 1) | function q(e){this["$style"]=J["default"].locals||J["default"]}
function ae (line 1) | function ae(e){this["$style"]=te["default"].locals||te["default"]}
function t (line 1) | function t(t){return e.apply(this,arguments)}
function ce (line 1) | function ce(e){this["$style"]=ie["default"].locals||ie["default"]}
function me (line 1) | function me(e,t){var a;return function(){var n=t.getZoom(),r=n>17;if(r){...
function ve (line 1) | function ve(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){v...
function ye (line 1) | function ye(e){for(var t=1;t<arguments.length;t++){var a=null!=arguments...
function xe (line 1) | function xe(e){this["$style"]=ke["default"].locals||ke["default"]}
function Le (line 1) | function Le(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){v...
function Ce (line 1) | function Ce(e){for(var t=1;t<arguments.length;t++){var a=null!=arguments...
function t (line 1) | function t(t){return e.apply(this,arguments)}
function t (line 1) | function t(t){return e.apply(this,arguments)}
function t (line 1) | function t(t){return e.apply(this,arguments)}
function t (line 1) | function t(t){return e.apply(this,arguments)}
function t (line 1) | function t(t){return e.apply(this,arguments)}
function Re (line 1) | function Re(e){this["$style"]=$e["default"].locals||$e["default"]}
FILE: src/web/assets/map/js/chunk-vendors.js
function i (line 1) | function i(t){if(void 0===t)throw new ReferenceError("this hasn't been i...
function o (line 1) | function o(t,e){return!!t.className.match(new RegExp("(\\s|^)"+e+"(\\s|$...
function r (line 1) | function r(t,e){o(t,e)||(t.className+=" "+e)}
function s (line 1) | function s(t,e){t.classList&&t.classList.remove(e)}
function i (line 1) | function i(t,e,n,i,o,r,s,a){var u,h="function"===typeof t?t.options:t;if...
function a (line 1) | function a(t){return a="function"===typeof s.a&&"symbol"===typeof o.a?fu...
function u (line 1) | function u(t){return u="function"===typeof s.a&&"symbol"===a(o.a)?functi...
function c (line 1) | function c(t,e){return!e||"object"!==u(e)&&"function"!==typeof e?Object(...
function r (line 1) | function r(t,e,n,i,r,s,a){try{var u=t[s](a),h=u.value}catch(c){return vo...
function s (line 1) | function s(t){return function(){var e=this,n=arguments;return new o.a(fu...
function a (line 1) | function a(t,e){return a=s.a||function(t,e){return t.__proto__=e,t},a(t,e)}
function u (line 1) | function u(t,e){if("function"!==typeof e&&null!==e)throw new TypeError("...
function o (line 1) | function o(t){var e,n;this.promise=new t(function(t,i){if(void 0!==e||vo...
function a (line 1) | function a(t){return a=s.a?o.a:function(t){return t.__proto__||o()(t)},a...
function u (line 1) | function u(t,e,n,i){var o=e&&e.prototype instanceof _?e:_,r=Object.creat...
function h (line 1) | function h(t,e,n){try{return{type:"normal",arg:t.call(e,n)}}catch(i){ret...
function _ (line 1) | function _(){}
function m (line 1) | function m(){}
function v (line 1) | function v(){}
function w (line 1) | function w(t){["next","throw","return"].forEach(function(e){t[e]=functio...
function L (line 1) | function L(t){function e(n,o,r,s){var a=h(t[n],t,o);if("throw"!==a.type)...
function P (line 1) | function P(t,e,n){var i=c;return function(o,r){if(i===f)throw new Error(...
function T (line 1) | function T(t,n){var i=t.iterator[n.method];if(i===e){if(n.delegate=null,...
function S (line 1) | function S(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.f...
function M (line 1) | function M(t){var e=t.completion||{};e.type="normal",delete e.arg,t.comp...
function C (line 1) | function C(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(S,this),this.r...
function E (line 1) | function E(t){if(t){var n=t[r];if(n)return n.call(t);if("function"===typ...
function O (line 1) | function O(){return{value:e,done:!0}}
function o (line 1) | function o(i,o){return a.type="throw",a.arg=t,n.next=i,o&&(n.method="nex...
function m (line 1) | function m(t,e,i,r,s,a){var u=i+t.length,h=r.length,c=d;return void 0!==...
function o (line 1) | function o(t){var e,n;this.promise=new t(function(t,i){if(void 0!==e||vo...
function r (line 1) | function r(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.en...
function s (line 1) | function s(t,e,n){return e&&r(t.prototype,e),n&&r(t,n),t}
function r (line 1) | function r(t,e,n){return e in t?o()(t,e,{value:n,enumerable:!0,configura...
function i (line 1) | function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a ...
function i (line 6) | function i(t){var e,n,i,o;for(n=1,i=arguments.length;n<i;n++)for(e in o=...
function t (line 6) | function t(){}
function r (line 6) | function r(t,e){var n=Array.prototype.slice;if(t.bind)return t.bind.appl...
function a (line 6) | function a(t){return t._leaflet_id=t._leaflet_id||++s,t._leaflet_id}
function u (line 6) | function u(t,e,n){var i,o,r,s;return s=function(){i=!1,o&&(r.apply(n,o),...
function h (line 6) | function h(t,e,n){var i=e[1],o=e[0],r=i-o;return t===i&&n?t:((t-o)%r+r)%...
function c (line 6) | function c(){return!1}
function l (line 6) | function l(t,e){return e=void 0===e?6:e,+(Math.round(t+"e+"+e)+"e-"+e)}
function f (line 6) | function f(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}
function d (line 6) | function d(t){return f(t).split(/\s+/)}
function p (line 6) | function p(t,e){for(var n in t.hasOwnProperty("options")||(t.options=t.o...
function _ (line 6) | function _(t,e,n){var i=[];for(var o in t)i.push(encodeURIComponent(n?o....
function v (line 6) | function v(t,e){return t.replace(m,function(t,n){var i=e[n];if(void 0===...
function y (line 6) | function y(t,e){for(var n=0;n<t.length;n++)if(t[n]===e)return n;return-1}
function x (line 6) | function x(t){return window["webkit"+t]||window["moz"+t]||window["ms"+t]}
function P (line 6) | function P(t){var e=+new Date,n=Math.max(0,16-(e-w));return w=e+n,window...
function M (line 6) | function M(t,e,n){if(!n||T!==P)return T.call(window,r(t,e));t.call(e)}
function C (line 6) | function C(t){t&&S.call(window,t)}
function O (line 6) | function O(){}
function z (line 6) | function z(t){if("undefined"!==typeof L&&L&&L.Mixin){t=g(t)?t:[t];for(va...
function A (line 6) | function A(t,e,n){this.x=n?Math.round(t):t,this.y=n?Math.round(e):e}
function B (line 6) | function B(t,e,n){return t instanceof A?t:g(t)?new A(t[0],t[1]):void 0==...
function j (line 6) | function j(t,e){if(t)for(var n=e?[t,e]:t,i=0,o=n.length;i<o;i++)this.ext...
function R (line 6) | function R(t,e){return!t||t instanceof j?t:new j(t,e)}
function N (line 6) | function N(t,e){if(t)for(var n=e?[t,e]:t,i=0,o=n.length;i<o;i++)this.ext...
function D (line 6) | function D(t,e){return t instanceof N?t:new N(t,e)}
function F (line 6) | function F(t,e,n){if(isNaN(t)||isNaN(e))throw new Error("Invalid LatLng ...
function W (line 6) | function W(t,e,n){return t instanceof F?t:g(t)&&"object"!==typeof t[0]?3...
function q (line 6) | function q(t,e,n,i){if(g(t))return this._a=t[0],this._b=t[1],this._c=t[2...
function K (line 6) | function K(t,e,n,i){return new q(t,e,n,i)}
function X (line 6) | function X(t){return document.createElementNS("http://www.w3.org/2000/sv...
function J (line 6) | function J(t,e){var n,i,o,r,s,a,u="";for(n=0,o=t.length;n<o;n++){for(s=t...
function zt (line 6) | function zt(t){return navigator.userAgent.toLowerCase().indexOf(t)>=0}
function Ft (line 6) | function Ft(t,e,n,i){return"touchstart"===e?Ht(t,n,i):"touchmove"===e?Kt...
function Wt (line 6) | function Wt(t,e,n){var i=t["_leaflet_"+e+n];return"touchstart"===e?t.rem...
function Ht (line 6) | function Ht(t,e,n){var i=r(function(t){if("mouse"!==t.pointerType&&t.MSP...
function Ut (line 6) | function Ut(t){Rt[t.pointerId]=t,Dt++}
function Vt (line 6) | function Vt(t){Rt[t.pointerId]&&(Rt[t.pointerId]=t)}
function Gt (line 6) | function Gt(t){delete Rt[t.pointerId],Dt--}
function qt (line 6) | function qt(t,e){for(var n in t.touches=[],Rt)t.touches.push(Rt[n]);t.ch...
function Kt (line 6) | function Kt(t,e,n){var i=function(t){(t.pointerType!==t.MSPOINTER_TYPE_M...
function $t (line 6) | function $t(t,e,n){var i=function(t){qt(t,e)};t["_leaflet_touchend"+n]=i...
function Qt (line 6) | function Qt(t,e,n){var i,o,r=!1,s=250;function a(t){var e;if(Lt){if(!nt|...
function te (line 6) | function te(t,e){var n=t[Jt+Yt+e],i=t[Jt+Xt+e],o=t[Jt+"dblclick"+e];retu...
function he (line 6) | function he(t){return"string"===typeof t?document.getElementById(t):t}
function ce (line 6) | function ce(t,e){var n=t.style[e]||t.currentStyle&&t.currentStyle[e];if(...
function le (line 6) | function le(t,e,n){var i=document.createElement(t);return i.className=e|...
function fe (line 6) | function fe(t){var e=t.parentNode;e&&e.removeChild(t)}
function de (line 6) | function de(t){while(t.firstChild)t.removeChild(t.firstChild)}
function pe (line 6) | function pe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}
function _e (line 6) | function _e(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,...
function me (line 6) | function me(t,e){if(void 0!==t.classList)return t.classList.contains(e);...
function ve (line 6) | function ve(t,e){if(void 0!==t.classList)for(var n=d(e),i=0,o=n.length;i...
function ge (line 6) | function ge(t,e){void 0!==t.classList?t.classList.remove(e):ye(t,f((" "+...
function ye (line 6) | function ye(t,e){void 0===t.className.baseVal?t.className=e:t.className....
function be (line 6) | function be(t){return t.correspondingElement&&(t=t.correspondingElement)...
function xe (line 6) | function xe(t,e){"opacity"in t.style?t.style.opacity=e:"filter"in t.styl...
function we (line 6) | function we(t,e){var n=!1,i="DXImageTransform.Microsoft.Alpha";try{n=t.f...
function Le (line 6) | function Le(t){for(var e=document.documentElement.style,n=0;n<t.length;n...
function Pe (line 6) | function Pe(t,e,n){var i=e||new A(0,0);t.style[se]=(_t?"translate("+i.x+...
function Te (line 6) | function Te(t,e){t._leaflet_pos=e,gt?Pe(t,e):(t.style.left=e.x+"px",t.st...
function Se (line 6) | function Se(t){return t._leaflet_pos||new A(0,0)}
function Ce (line 6) | function Ce(){Ze(window,"dragstart",He)}
function Ee (line 6) | function Ee(){je(window,"dragstart",He)}
function Oe (line 6) | function Oe(t){while(-1===t.tabIndex)t=t.parentNode;t.style&&(ze(),oe=t,...
function ze (line 6) | function ze(){oe&&(oe.style.outline=re,oe=void 0,re=void 0,je(window,"ke...
function Ie (line 6) | function Ie(t){do{t=t.parentNode}while((!t.offsetWidth||!t.offsetHeight)...
function ke (line 6) | function ke(t){var e=t.getBoundingClientRect();return{x:e.width/t.offset...
function Ze (line 6) | function Ze(t,e,n,i){if("object"===typeof e)for(var o in e)Re(t,o,e[o],n...
function je (line 6) | function je(t,e,n,i){if("object"===typeof e)for(var o in e)Ne(t,o,e[o],n...
function Re (line 6) | function Re(t,e,n,i){var o=e+a(n)+(i?"_"+a(i):"");if(t[Be]&&t[Be][o])ret...
function Ne (line 6) | function Ne(t,e,n,i){var o=e+a(n)+(i?"_"+a(i):""),r=t[Be]&&t[Be][o];if(!...
function De (line 6) | function De(t){return t.stopPropagation?t.stopPropagation():t.originalEv...
function Fe (line 6) | function Fe(t){return Re(t,"mousewheel",De),this}
function We (line 6) | function We(t){return Ze(t,"mousedown touchstart dblclick",De),Re(t,"cli...
function He (line 6) | function He(t){return t.preventDefault?t.preventDefault():t.returnValue=...
function Ue (line 6) | function Ue(t){return He(t),De(t),this}
function Ve (line 6) | function Ve(t,e){if(!e)return new A(t.clientX,t.clientY);var n=ke(e),i=n...
function qe (line 6) | function qe(t){return nt?t.wheelDeltaY/2:t.deltaY&&0===t.deltaMode?-t.de...
function Ye (line 6) | function Ye(t){$e[t.type]=!0}
function Xe (line 6) | function Xe(t){var e=$e[t.type];return $e[t.type]=!1,e}
function Je (line 6) | function Je(t,e){var n=e.relatedTarget;if(!n)return!0;try{while(n&&n!==t...
function Qe (line 6) | function Qe(t,e){var n=t.timeStamp||t.originalEvent&&t.originalEvent.tim...
function f (line 6) | function f(t){var e=t?-1:1,n=t?u:a,i=u*u-a*a+e*l*l*h*h,o=2*n*l*h,r=i/o,s...
function d (line 6) | function d(t){return(Math.exp(t)-Math.exp(-t))/2}
function p (line 6) | function p(t){return(Math.exp(t)+Math.exp(-t))/2}
function _ (line 6) | function _(t){return d(t)/p(t)}
function v (line 6) | function v(t){return a*(p(m)/p(m+c*t))}
function g (line 6) | function g(t){return a*(p(m)*_(m+c*t)-d(m))/l}
function y (line 6) | function y(t){return 1-Math.pow(1-t,1.5)}
function L (line 6) | function L(){var n=(Date.now()-b)/w,r=y(n)*x;n<=1?(this._flyToFrame=M(L,...
function on (line 6) | function on(t,e){return new nn(t,e)}
function i (line 6) | function i(i,o){var r=e+i+" "+e+o;t[i+o]=le("div",r,n)}
function wn (line 6) | function wn(t,e){if(!e||!t.length)return t.slice();var n=e*e;return t=Mn...
function Ln (line 6) | function Ln(t,e,n){return Math.sqrt(In(t,e,n,!0))}
function Pn (line 6) | function Pn(t,e,n){return In(t,e,n)}
function Tn (line 6) | function Tn(t,e){var n=t.length,i=typeof Uint8Array!==void 0+""?Uint8Arr...
function Sn (line 6) | function Sn(t,e,n,i,o){var r,s,a,u=0;for(s=i+1;s<=o-1;s++)a=In(t[s],t[i]...
function Mn (line 6) | function Mn(t,e){for(var n=[t[0]],i=1,o=0,r=t.length;i<r;i++)zn(t[i],t[o...
function Cn (line 6) | function Cn(t,e,n,i,o){var r,s,a,u=i?mn:On(t,n),h=On(e,n);mn=h;while(1){...
function En (line 6) | function En(t,e,n,i,o){var r,s,a=e.x-t.x,u=e.y-t.y,h=i.min,c=i.max;retur...
function On (line 6) | function On(t,e){var n=0;return t.x<e.min.x?n|=1:t.x>e.max.x&&(n|=2),t.y...
function zn (line 6) | function zn(t,e){var n=e.x-t.x,i=e.y-t.y;return n*n+i*i}
function In (line 6) | function In(t,e,n,i){var o,r=e.x,s=e.y,a=n.x-r,u=n.y-s,h=a*a+u*u;return ...
function kn (line 6) | function kn(t){return!g(t[0])||"object"!==typeof t[0][0]&&"undefined"!==...
function An (line 6) | function An(t){return console.warn("Deprecated use of _flat, please use ...
function Bn (line 6) | function Bn(t,e,n){var i,o,r,s,a,u,h,c,l,f=[1,4,2,8];for(o=0,h=t.length;...
function Yn (line 6) | function Yn(t){return new $n(t)}
function ti (line 6) | function ti(t,e){return new Qn(t,e)}
function ii (line 6) | function ii(t,e){return new ni(t,e)}
function ri (line 6) | function ri(t,e,n){return new oi(t,e,n)}
function ai (line 6) | function ai(t,e){return new si(t,e)}
function hi (line 6) | function hi(t,e){return new ui(t,e)}
function li (line 6) | function li(t,e){var n,i,o,r,s="Feature"===t.type?t.geometry:t,a=s?s.coo...
function fi (line 6) | function fi(t){return new F(t[1],t[0],t[2])}
function di (line 6) | function di(t,e,n){for(var i,o=[],r=0,s=t.length;r<s;r++)i=e?di(t[r],e-1...
function pi (line 6) | function pi(t,e){return e="number"===typeof e?e:6,void 0!==t.alt?[l(t.ln...
function _i (line 6) | function _i(t,e,n,i){for(var o=[],r=0,s=t.length;r<s;r++)o.push(e?_i(t[r...
function mi (line 6) | function mi(t,e){return t.feature?i({},t.feature,{geometry:e}):vi(e)}
function vi (line 6) | function vi(t){return"Feature"===t.type||"FeatureCollection"===t.type?t:...
function yi (line 6) | function yi(t,e){return new ci(t,e)}
function Pi (line 6) | function Pi(t,e,n){return new Li(t,e,n)}
function Si (line 6) | function Si(t,e,n){return new Ti(t,e,n)}
function ki (line 6) | function ki(t){return new Ii(t)}
function Zi (line 6) | function Zi(t){return new Ai(t)}
function ji (line 6) | function ji(t,e){return new Bi(t,e)}
function Ni (line 6) | function Ni(t,e){return new Ri(t,e)}
function Wi (line 6) | function Wi(t){return Ct?new Fi(t):null}
function qi (line 6) | function qi(t){return Et||Ot?new Gi(t):null}
function $i (line 6) | function $i(t,e){return new Ki(t,e)}
Condensed preview — 95 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (699K chars).
[
{
"path": ".editorconfig",
"chars": 240,
"preview": "root = true\n\n[*]\nindent_style = tab\nindent_size = 4\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_tr"
},
{
"path": ".gitattributes",
"chars": 240,
"preview": "README.md export-ignore\nCHANGELOG.md export-ignore\n/resources export-ignore\n.gitignore export-ignore\n"
},
{
"path": ".github/CODE_OF_CONDUCT.md",
"chars": 35,
"preview": "# Code of Conduct\n\nDon't be a twat."
},
{
"path": ".github/CONTRIBUTING.md",
"chars": 937,
"preview": "# Contributing\n\n### New Features\n\nYou can request new features by submitting an issue with `[FR]` prefix in the title.\n\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 14,
"preview": "github: [tam]\n"
},
{
"path": ".github/ISSUE_TEMPLATE/craft-3-issue.md",
"chars": 126,
"preview": "---\nname: Craft 3 Issue\nabout: An issue with the Craft 3 version of the plugin\ntitle: ''\nlabels: Craft 3\nassignees: ''\n\n"
},
{
"path": ".github/ISSUE_TEMPLATE/craft-4-issue.md",
"chars": 126,
"preview": "---\nname: Craft 4 Issue\nabout: An issue with the Craft 4 version of the plugin\ntitle: ''\nlabels: Craft 4\nassignees: ''\n\n"
},
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 166,
"preview": "### Description\n\n\n\n### Steps to reproduce\n\n1.\n2.\n\n\n### Additional info\n\n- Craft version:\n- Maps version:\n- PHP version:\n"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 59,
"preview": "Fixes # .\n\nChanges proposed in this pull request:\n\n- \n- \n- "
},
{
"path": ".gitignore",
"chars": 411,
"preview": "### OSX\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appea"
},
{
"path": "CHANGELOG.md",
"chars": 16360,
"preview": "## 5.0.4 - 2024-11-11\n### Added\n- Add support for WordPress ACF -> Craft import (via @brandonkelly)\n\n## 5.0.3 - 2024-09-"
},
{
"path": "LICENSE",
"chars": 2225,
"preview": "Copyright © Ether Creative\n\nPermission is hereby granted to any person obtaining a copy of this software\n(the “Software”"
},
{
"path": "README.md",
"chars": 5227,
"preview": "\n\n# Maps\n\nA beautifully simple, yet deceptively powerful, Map field that works o"
},
{
"path": "composer.json",
"chars": 1188,
"preview": "{\n \"name\": \"ether/simplemap\",\n \"description\": \"A beautifully simple Map field type for Craft CMS\",\n \"type\": \"craft-pl"
},
{
"path": "docs/.docs.json",
"chars": 585,
"preview": "{\n \"nav\": {\n \"index\": \"Introduction\",\n \"getting-started\": {\n \"_label\": \"Getting Started\",\n \"installatio"
},
{
"path": "docs/geolocation/get.md",
"chars": 1711,
"preview": "---\ntitle: Get User Location\n---\n\n# Get User Location\n\nGetting the current users location based of their IP address is e"
},
{
"path": "docs/geolocation/redirect.md",
"chars": 2395,
"preview": "---\ntitle: User Location Redirecting\n---\n\n# User Location Redirecting\n\nWith Maps it is now possible to redirect the user"
},
{
"path": "docs/getting-started/config.md",
"chars": 5546,
"preview": "---\ntitle: Configuration\n---\n\n# Configuration\n\nThere are two ways to configure **Maps**, via the Craft CP or using a con"
},
{
"path": "docs/getting-started/installation.md",
"chars": 2228,
"preview": "---\ntitle: Installation\n---\n\n# Installation\n\n## Plugin Store\n\nYou can install **Maps** from the [Craft Plugin Store](htt"
},
{
"path": "docs/getting-started/usage.md",
"chars": 4082,
"preview": "---\ntitle: Usage\n---\n\n# Usage\n\n## Creating a Map field\n\nYou can create a Map field in the same way you would create any "
},
{
"path": "docs/how-to/graphql.md",
"chars": 930,
"preview": "---\ntitle: Querying in GraphQL\n---\n\n# Querying in GraphQL\n\nThe query input can support all the parameters that you can u"
},
{
"path": "docs/how-to/search.md",
"chars": 1785,
"preview": "---\ntitle: Search by Location\n---\n\n# Search by Location\n\nBeing able to search your maps by a location is one of the most"
},
{
"path": "docs/index.md",
"chars": 775,
"preview": "---\ntitle: Maps\n---\n\n\n\n# Maps\n\nA beautifully simple, yet deceptively powerful map field that work"
},
{
"path": "docs/rendering/embed.md",
"chars": 2904,
"preview": "---\ntitle: Embed Dynamic Maps\n---\n\n# Embed Dynamic Maps\n\nWith **Maps** you can quickly and easily output an interactive "
},
{
"path": "docs/rendering/static.md",
"chars": 3358,
"preview": "---\ntitle: Static Map Images\n---\n\n# Static Map Images\n\n**Maps** makes it really easy to render static map images in twig"
},
{
"path": "resources/.editorconfig",
"chars": 125,
"preview": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = tab\nindent_size = 4\nend_of_line = lf\ninsert_final_newline = true\nmax_line_length "
},
{
"path": "resources/.gitignore",
"chars": 214,
"preview": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn"
},
{
"path": "resources/README.md",
"chars": 367,
"preview": "# simplemap\n\n## Project setup\n```\nyarn install\n```\n\n### Compiles and hot-reloads for development\n```\nyarn run serve\n```\n"
},
{
"path": "resources/babel.config.js",
"chars": 56,
"preview": "module.exports = {\n presets: [\n '@vue/app',\n ],\n};\n"
},
{
"path": "resources/package.json",
"chars": 2138,
"preview": "{\n \"name\": \"simplemap\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"PORT=8080 vue-cli-service s"
},
{
"path": "resources/src/App.vue",
"chars": 8682,
"preview": "<template>\n\t<div>\n\t\t<div :class=\"wrapCls\" ref=\"field\">\n\t\t\t<Map\n\t\t\t\tv-if=\"!config.hideMap\"\n\t\t\t\t:tiles=\"config.mapTiles\"\n\t"
},
{
"path": "resources/src/common/Geo.js",
"chars": 10970,
"preview": "import GeoService from '../enums/GeoService';\nimport Parts from '../models/Parts';\nimport PartsLegacy from '../models/Pa"
},
{
"path": "resources/src/components/Address.vue",
"chars": 6050,
"preview": "<template>\n\t<div :class=\"cls\" :style=\"styl\">\n\t\t<Fragment v-if=\"showLatLng\">\n\t\t\t<Input\n\t\t\t\t:label=\"labels.lat\"\n\t\t\t\t:value"
},
{
"path": "resources/src/components/Fragment.vue",
"chars": 109,
"preview": "<script>\n\texport default {\n\t\tfunctional: true,\n\t\trender (h, ctx) {\n\t\t\treturn ctx.children;\n\t\t}\n\t};\n</script>\n"
},
{
"path": "resources/src/components/Input.vue",
"chars": 835,
"preview": "<template>\n\t<Label :label=\"label\" :class=\"className\">\n\t\t<input\n\t\t\t:class=\"$style.input\"\n\t\t\t:type=\"type\"\n\t\t\t:name=\"name\"\n"
},
{
"path": "resources/src/components/Label.vue",
"chars": 586,
"preview": "<template>\n\t<label :class=\"$style.label\">\n\t\t<span :class=\"$style.name\">\n\t\t\t{{ label }}\n\t\t</span>\n\t\t<slot></slot>\n\t</labe"
},
{
"path": "resources/src/components/Map.vue",
"chars": 11774,
"preview": "<template>\n\t<div :class=\"cls\"></div>\n</template>\n\n<script lang=\"js\">\n\t// TODO: Only load mutants in if they're needed\n\ti"
},
{
"path": "resources/src/components/Search.vue",
"chars": 5575,
"preview": "<template>\n\t<label :class=\"cls\">\n\t\t<svg width=\"17\" height=\"17\" viewBox=\"0 0 17 17\">\n\t\t\t<path fill=\"#29323D\" d=\"M6.938 13"
},
{
"path": "resources/src/enums/GeoService.js",
"chars": 157,
"preview": "const GeoService = {\n\tNominatim: 'nominatim',\n\tMapbox: 'mapbox',\n\tGoogleMaps: 'google',\n\tAppleMapKit: 'apple',\n\tHere: 'h"
},
{
"path": "resources/src/enums/MapTiles.js",
"chars": 1368,
"preview": "const MapTiles = {\n\t// Open Source\n\t// -------------------------------------------------------------------------\n\n\t// Wi"
},
{
"path": "resources/src/filters/craft.js",
"chars": 122,
"preview": "export function t (message, params = null) {\n // @ts-ignore\n return window.Craft.t('simplemap', message, params);\n"
},
{
"path": "resources/src/helpers/createW3WGrid.js",
"chars": 1238,
"preview": "export default function createW3WGrid (L, map) {\n\tlet gridLayer;\n\n\treturn function drawGrid () {\n\t\tconst zoom = map.getZ"
},
{
"path": "resources/src/helpers/debounce.js",
"chars": 1218,
"preview": "/**\n * ## Debounce\n *\n * A function, that, as long as it continues to be invoked, will not\n * be triggered. The function"
},
{
"path": "resources/src/helpers/waitForGlobal.js",
"chars": 247,
"preview": "export default function waitForGlobal (property, callback) {\n\tif (window.hasOwnProperty(property)) {\n\t\tcallback();\n\t\tret"
},
{
"path": "resources/src/main.js",
"chars": 264,
"preview": "import App from './App.vue';\nimport { t } from './filters/craft';\n\nconst VueSimpleMapPlugin = {\n\tinstall (Vue) {\n\t\tVue.f"
},
{
"path": "resources/src/models/Parts.js",
"chars": 4432,
"preview": "import GeoService from '../enums/GeoService';\n\nexport default class Parts {\n\n\t// Properties\n\t// ========================"
},
{
"path": "resources/src/models/PartsLegacy.js",
"chars": 2364,
"preview": "/* eslint-disable camelcase */\n\nimport Parts from './Parts';\nimport GeoService from '../enums/GeoService';\n\nexport defau"
},
{
"path": "resources/vue.config.js",
"chars": 384,
"preview": "module.exports = {\n\tfilenameHashing: false,\n\toutputDir: '../src/web/assets/map',\n\tdevServer: {\n\t\thttps: true,\n\t\theaders:"
},
{
"path": "src/SimpleMap.php",
"chars": 5953,
"preview": "<?php\n/**\n * SimpleMap for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether"
},
{
"path": "src/acfadapters/GoogleMap.php",
"chars": 1477,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2024 Ether Crea"
},
{
"path": "src/config.php",
"chars": 229,
"preview": "<?php\n\nuse ether\\simplemap\\enums\\GeoService;\nuse ether\\simplemap\\enums\\MapTiles;\n\nreturn [\n\t'mapTiles' => MapTiles::Wiki"
},
{
"path": "src/controllers/SettingsController.php",
"chars": 989,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/controllers/StaticController.php",
"chars": 1178,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/enums/GeoService.php",
"chars": 1587,
"preview": "<?php\n/**\n * SimpleMap for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether"
},
{
"path": "src/enums/MapTiles.php",
"chars": 5300,
"preview": "<?php\n/**\n * SimpleMap for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether"
},
{
"path": "src/fields/MapField.php",
"chars": 14828,
"preview": "<?php\n/**\n * SimpleMap for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether"
},
{
"path": "src/integrations/feedme/FeedMeMaps.php",
"chars": 1636,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/integrations/graphql/MapPartsType.php",
"chars": 2290,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/integrations/graphql/MapType.php",
"chars": 4120,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/jobs/MaxMindDBDownloadJob.php",
"chars": 1834,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/migrations/Install.php",
"chars": 2625,
"preview": "<?php\n/**\n * SimpleMap for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether"
},
{
"path": "src/migrations/m190226_143809_craft3_upgrade.php",
"chars": 8945,
"preview": "<?php\n\nnamespace ether\\simplemap\\migrations;\n\nuse Craft;\nuse craft\\db\\Migration;\nuse craft\\db\\Query;\nuse craft\\db\\Table;"
},
{
"path": "src/migrations/m190325_130533_repair_map_elements.php",
"chars": 2151,
"preview": "<?php\n\nnamespace ether\\simplemap\\migrations;\n\nuse craft\\db\\Migration;\nuse craft\\db\\Query;\nuse craft\\db\\Table;\nuse ether\\"
},
{
"path": "src/migrations/m190712_104805_new_data_format.php",
"chars": 12219,
"preview": "<?php\n\nnamespace ether\\simplemap\\migrations;\n\nuse Craft;\nuse craft\\base\\FieldInterface;\nuse craft\\db\\Migration;\nuse craf"
},
{
"path": "src/migrations/m190723_105637_fix_map_field_column_type.php",
"chars": 1952,
"preview": "<?php\n\nnamespace ether\\simplemap\\migrations;\n\nuse Craft;\nuse craft\\db\\Migration;\nuse ether\\simplemap\\fields\\MapField;\nus"
},
{
"path": "src/models/BaseLocation.php",
"chars": 2535,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/models/EmbedOptions.php",
"chars": 884,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/models/Map.php",
"chars": 3979,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/models/Marker.php",
"chars": 2687,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/models/Parts.php",
"chars": 7487,
"preview": "<?php\n/**\n * SimpleMap for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether"
},
{
"path": "src/models/PartsLegacy.php",
"chars": 4397,
"preview": "<?php\n/**\n * Maps for Craft CMS 3\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Cr"
},
{
"path": "src/models/Point.php",
"chars": 1518,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/models/Settings.php",
"chars": 4190,
"preview": "<?php\n/**\n * SimpleMap for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether"
},
{
"path": "src/models/StaticOptions.php",
"chars": 3044,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/models/UserLocation.php",
"chars": 1735,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/records/Map.php",
"chars": 1363,
"preview": "<?php\n/**\n * SimpleMap for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether"
},
{
"path": "src/resources/OpenSans_LICENSE.txt",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "src/services/EmbedService.php",
"chars": 17360,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/services/GeoLocationService.php",
"chars": 9962,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/services/GeoService.php",
"chars": 21966,
"preview": "<?php\n/**\n * Maps for Craft CMS 3\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Cr"
},
{
"path": "src/services/MapService.php",
"chars": 9552,
"preview": "<?php\n/**\n * Maps for Craft CMS 3\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Cr"
},
{
"path": "src/services/StaticService.php",
"chars": 8701,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/services/What3WordsService.php",
"chars": 695,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2020 Ether Crea"
},
{
"path": "src/templates/_feedme-mapping.twig",
"chars": 2165,
"preview": "{# ------------------------ #}\n{# Available Variables #}\n{# ------------------------ #}\n{# Attributes: #}\n{# type, name,"
},
{
"path": "src/templates/field-settings.twig",
"chars": 2913,
"preview": "{% import '_includes/forms' as forms %}\n\n{{ forms.field({\n\tlabel: 'Initial Location'|t('simplemap'),\n\tinstructions: 'The"
},
{
"path": "src/templates/settings.twig",
"chars": 23613,
"preview": "{% extends '_layouts/cp' %}\n{% set title = 'Settings'|t('app') %}\n{% set showHeader = false %}\n{% set fullPageForm = tru"
},
{
"path": "src/translations/en/simplemap.php",
"chars": 8127,
"preview": "<?php\n/**\n * SimpleMap for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether"
},
{
"path": "src/utilities/StaticMap.php",
"chars": 11544,
"preview": "<?php\n/**\n * Maps for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Crea"
},
{
"path": "src/web/Variable.php",
"chars": 3032,
"preview": "<?php\n/**\n * Maps for Craft CMS 3\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether Cr"
},
{
"path": "src/web/assets/MapAsset.php",
"chars": 832,
"preview": "<?php\n/**\n * SimpleMap for Craft CMS\n *\n * @link https://ethercreative.co.uk\n * @copyright Copyright (c) 2019 Ether"
},
{
"path": "src/web/assets/map/css/app.css",
"chars": 7394,
"preview": ".Search_wrap_rNtoB{position:relative;display:block}@media only screen and (max-width:767px){.Search_wrap_rNtoB{margin-ri"
},
{
"path": "src/web/assets/map/css/chunk-vendors.css",
"chars": 15630,
"preview": ".leaflet-image-layer,.leaflet-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane,.leaflet-pane>canvas,.leaf"
},
{
"path": "src/web/assets/map/index.html",
"chars": 590,
"preview": "<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content=\"IE=edge\"><meta name=viewport co"
},
{
"path": "src/web/assets/map/js/app.js",
"chars": 47641,
"preview": "var EtherMaps=function(e){function t(t){for(var n,s,i=t[0],c=t[1],l=t[2],p=0,h=[];p<i.length;p++)s=i[p],Object.prototype"
},
{
"path": "src/web/assets/map/js/chunk-vendors.js",
"chars": 230318,
"preview": "(window[\"webpackJsonpEtherMaps\"]=window[\"webpackJsonpEtherMaps\"]||[]).push([[\"chunk-vendors\"],{\"013f\":function(t,e,n){\"u"
}
]
About this extraction
This page contains the full source code of the ethercreative/simplemap GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 95 files (634.2 KB), approximately 202.3k tokens, and a symbol index with 497 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.