diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dcf8516 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,35 @@ +# For more information about the properties used in +# this file, please see the EditorConfig documentation: +# http://editorconfig.org/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +# Docs say 80 ideally, 100 ok, no more than 120 +# http://doc.silverstripe.org/en/getting_started/coding_conventions/ +max_line_length = 100 + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 +indent_style = space + +#PSR 2 +[**.php] +indent_style = space +indent_size = 4 + +[{.travis.yml,package.json}] +# The indent size used in the `package.json` file cannot be changed +# https://github.com/npm/npm/pull/3180#issuecomment-16336516 +indent_size = 2 +indent_style = space diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..93c0fde --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,26 @@ +inherit: true + +#Copied from https://www.adayinthelifeof.nl/2013/11/20/external-code-coverage-with-travis-scrutinizer/ +tools: + external_code_coverage: + timeout: 600 + php_code_sniffer: + config: + standard: PSR2 + php_cs_fixer: + extensions: + # Default: + - php + fixers: [] + enabled: false + filter: + paths: [tests/*,code/*] + excluded_paths: [] +coding_style: + php: + indentation: + general: + use_tabs: false + +filter: + paths: [tests/*,code/*] diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5ffc367 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,71 @@ +language: php + +sudo: false + +addons: + apt: + packages: + - tidy + +before_install: + - pip install --user codecov + +env: + global: + - DB=MYSQL CORE_RELEASE=3.1 + - MODULE_PATH=mappable + +matrix: + allow_failures: + - php: hhvm-nightly + include: + - php: 5.6 + env: DB=MYSQL + - php: 5.6 + env: DB=PGSQL + - php: 5.6 + env: DB=SQLITE + - php: 5.5 + env: DB=MYSQL + - php: 5.4 + env: DB=MYSQL + - php: 5.3 + env: DB=MYSQL + - php: hhvm + env: DB=MYSQL + - php: 5.6 + env: DB=MYSQL CORE_RELEASE=3.2 + - php: 5.6 + env: DB=PGSQL CORE_RELEASE=3.2 + - php: 5.6 + env: DB=SQLITE CORE_RELEASE=3.2 + - php: 5.5 + env: DB=MYSQL CORE_RELEASE=3.2 + - php: 5.4 + env: DB=MYSQL CORE_RELEASE=3.2 + - php: 5.3 + env: DB=MYSQL CORE_RELEASE=3.2 + - php: hhvm + env: DB=MYSQL CORE_RELEASE=3.2 + + +before_script: + - phpenv rehash + - composer self-update || true + - git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support + - php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss + - cd ~/builds/ss + - composer require satooshi/php-coveralls + +script: + - vendor/bin/phpunit --coverage-clover=coverage.clover -c $MODULE_PATH/phpunit.xml $MODULE_PATH/tests/ + +after_success: + - cp coverage.clover ~/coverage.xml + - mkdir -p build/logs + - travis_retry php vendor/bin/coveralls -v --coverage_clover coverage.clover + - mv coverage.clover ~/build/$TRAVIS_REPO_SLUG/ + - cd ~/build/$TRAVIS_REPO_SLUG + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover coverage.clover + - codecov diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dd68108 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,48 @@ +# Change Log + +## [3.2.1](https://github.com/gordonbanderson/Mappable/tree/3.2.1) (2016-01-05) +[Full Changelog](https://github.com/gordonbanderson/Mappable/compare/3.2.0...3.2.1) + +## [3.2.0](https://github.com/gordonbanderson/Mappable/tree/3.2.0) (2016-01-05) +[Full Changelog](https://github.com/gordonbanderson/Mappable/compare/3.1.0...3.2.0) + +**Closed issues:** + +- Icon Width and Height Not Used [\#42](https://github.com/gordonbanderson/Mappable/issues/42) +- Can not disable clusterer [\#29](https://github.com/gordonbanderson/Mappable/issues/29) +- Multiple markers on one map [\#28](https://github.com/gordonbanderson/Mappable/issues/28) +- Move Points of Interest to a Separate Module [\#27](https://github.com/gordonbanderson/Mappable/issues/27) +- Autozoom Overly Enabled for Map Extension when Layers Extensions Also Enabled [\#26](https://github.com/gordonbanderson/Mappable/issues/26) +- Change to Unobtrusive JavaScript [\#25](https://github.com/gordonbanderson/Mappable/issues/25) +- StaticMap doesn't respect getMappableMapPin [\#22](https://github.com/gordonbanderson/Mappable/issues/22) +- Ensure That Map Extension Returns a String, not an Image File [\#19](https://github.com/gordonbanderson/Mappable/issues/19) +- Ensure Clusterer Settings Used [\#16](https://github.com/gordonbanderson/Mappable/issues/16) +- refactor maputil.js [\#15](https://github.com/gordonbanderson/Mappable/issues/15) +- Implement Guide Markers for POIs [\#12](https://github.com/gordonbanderson/Mappable/issues/12) +- Combine and Minify JavaScript Files [\#11](https://github.com/gordonbanderson/Mappable/issues/11) +- SilverStripe Code Guidline Compliance [\#10](https://github.com/gordonbanderson/Mappable/issues/10) +- Add Escape Button Option to Close Full Screen [\#9](https://github.com/gordonbanderson/Mappable/issues/9) +- Clusterer: merge info windows for cluster [\#5](https://github.com/gordonbanderson/Mappable/issues/5) +- Apparent debug text showing in map editing interface [\#4](https://github.com/gordonbanderson/Mappable/issues/4) + +**Merged pull requests:** + +- Convert to PSR2 [\#45](https://github.com/gordonbanderson/Mappable/pull/45) ([gordonbanderson](https://github.com/gordonbanderson)) +- Scrutinizer Auto-Fixes [\#43](https://github.com/gordonbanderson/Mappable/pull/43) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Scrutinizer Auto-Fixes [\#39](https://github.com/gordonbanderson/Mappable/pull/39) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Scrutinizer Auto-Fixes [\#38](https://github.com/gordonbanderson/Mappable/pull/38) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Scrutinizer Auto-Fixes [\#36](https://github.com/gordonbanderson/Mappable/pull/36) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Scrutinizer Auto-Fixes [\#34](https://github.com/gordonbanderson/Mappable/pull/34) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Scrutinizer Auto-Fixes [\#33](https://github.com/gordonbanderson/Mappable/pull/33) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Scrutinizer Auto-Fixes [\#32](https://github.com/gordonbanderson/Mappable/pull/32) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- Scrutinizer Auto-Fixes [\#31](https://github.com/gordonbanderson/Mappable/pull/31) ([scrutinizer-auto-fixer](https://github.com/scrutinizer-auto-fixer)) +- StaticMap update, fixes \#22 [\#23](https://github.com/gordonbanderson/Mappable/pull/23) ([wernerkrauss](https://github.com/wernerkrauss)) +- some refinement to filter dataobjects [\#21](https://github.com/gordonbanderson/Mappable/pull/21) ([wernerkrauss](https://github.com/wernerkrauss)) +- do not use window.onload - can be overwritten later [\#20](https://github.com/gordonbanderson/Mappable/pull/20) ([wernerkrauss](https://github.com/wernerkrauss)) +- Make template suffix for info window configurable [\#3](https://github.com/gordonbanderson/Mappable/pull/3) ([wernerkrauss](https://github.com/wernerkrauss)) +- get\_map now accepts more generic SS\_List instead of DataList [\#2](https://github.com/gordonbanderson/Mappable/pull/2) ([wernerkrauss](https://github.com/wernerkrauss)) +- Fixes for 3.1 [\#1](https://github.com/gordonbanderson/Mappable/pull/1) ([wernerkrauss](https://github.com/wernerkrauss)) + + + +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..447169c --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,31 @@ +module.exports = function (grunt) { + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + cssmin: { + css: { + src: 'css/mapField.css', + dest: 'css/mapField.min.css' + } + }, + uglify: { + js: { + files : { + 'javascript/google/mappablegoogle.min.js' : [ + 'javascript/google/FullScreenControl.js', + 'javascript/google/markerclusterer.js', + 'javascript/google/maputil.js' + ], + + 'javascript/mapField.min.js' : [ + 'javascript/mapField.js' + ] + + } + }, + }, + }); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-cssmin'); + grunt.registerTask('default', ['cssmin:css', 'uglify:js']); +}; diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..743b1b8 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,12 @@ +Copyright (c) 2012-2016, Gordon B Anderson +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bce7a7d --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Mappable +[![Build Status](https://travis-ci.org/gordonbanderson/Mappable.svg?branch=3.1)](https://travis-ci.org/gordonbanderson/Mappable) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/gordonbanderson/Mappable/badges/quality-score.png?b=3.1)](https://scrutinizer-ci.com/g/gordonbanderson/Mappable/?branch=3.1) +[![Code Coverage](https://scrutinizer-ci.com/g/gordonbanderson/Mappable/badges/coverage.png?b=3.1)](https://scrutinizer-ci.com/g/gordonbanderson/Mappable/?branch=3.1) +[![Build Status](https://scrutinizer-ci.com/g/gordonbanderson/Mappable/badges/build.png?b=3.1)](https://scrutinizer-ci.com/g/gordonbanderson/Mappable/build-status/3.1) +[![codecov.io](https://codecov.io/github/gordonbanderson/Mappable/coverage.svg?branch=3.1)](https://codecov.io/github/gordonbanderson/Mappable?branch=3.1) + +[![Latest Stable Version](https://poser.pugx.org/weboftalent/mappable/version)](https://packagist.org/packages/weboftalent/mappable) +[![Latest Unstable Version](https://poser.pugx.org/weboftalent/mappable/v/unstable)](//packagist.org/packages/weboftalent/mappable) +[![Total Downloads](https://poser.pugx.org/weboftalent/mappable/downloads)](https://packagist.org/packages/weboftalent/mappable) +[![License](https://poser.pugx.org/weboftalent/mappable/license)](https://packagist.org/packages/weboftalent/mappable) +[![Monthly Downloads](https://poser.pugx.org/weboftalent/mappable/d/monthly)](https://packagist.org/packages/weboftalent/mappable) +[![Daily Downloads](https://poser.pugx.org/weboftalent/mappable/d/daily)](https://packagist.org/packages/weboftalent/mappable) + +[![Dependency Status](https://www.versioneye.com/php/weboftalent:mappable/badge.svg)](https://www.versioneye.com/php/weboftalent:mappable) +[![Reference Status](https://www.versioneye.com/php/weboftalent:mappable/reference_badge.svg?style=flat)](https://www.versioneye.com/php/weboftalent:mappable/references) + +![codecov.io](https://codecov.io/github/gordonbanderson/Mappable/branch.svg?branch=3.1) + +## Maintainers + +* Gordon Anderson (Nickname: nontgor) + + +##Introduction + +This module provides mapping functionality for SilverStripe DataObjects in a +manner that is simple as possible for both the programmer and content editor. +Short codes are also provided to render Google Street View and Google maps. + +##Documentation +* [Installation](./docs/en/Installation.md) +* [Adding a Map to a DataObject](./docs/en/AddingMapToADataObject.md) +* [Multiple Maps on the Same Page](./docs/en/MultipleMapsSamePage.md) +* [Mapping a DataList](./docs/en/MappingDataList.md) +* [Map Layers](./docs/en/MapLayers.md) +* [Adding Lines to Maps](./docs/en/AddingLinesToMaps.md) +* [Google Map Short Codes](./docs/en/GoogleMapShortCodes.md) +* [Google Streeview Short Codes](./docs/en/GoogleStreetViewShortCodes.md) + +For more documentation about the module see the provided documentation located +inside the docs folder. + +##Requirements +* SilverStripe 3.1 or 3.2 + +##TODO +* Add other mapping services such as Leaflet diff --git a/_config.php b/_config.php index 5f9a8bd..035cbae 100755 --- a/_config.php +++ b/_config.php @@ -1,5 +1,12 @@ register('GoogleStreetView', array('GoogleStreetViewShortCodeHandler','parse_googlestreetview')); +ShortcodeParser::get('default')->register('GoogleMap', array('GoogleMapShortCodeHandler','parse_googlemap')); + +// Cache for a day +SS_Cache::set_cache_lifetime('mappablegeocoder', 24*60*60); diff --git a/_config/_config.yml b/_config/_config.yml new file mode 100644 index 0000000..9038b26 --- /dev/null +++ b/_config/_config.yml @@ -0,0 +1,3 @@ +# allow geographical format files to be uploaded +File: + $allowed_extensions: ['gpx', 'kml'] diff --git a/_config/extensions.yml b/_config/extensions.yml new file mode 100644 index 0000000..94b4bb7 --- /dev/null +++ b/_config/extensions.yml @@ -0,0 +1,15 @@ +--- +Name: mappable +After: 'framework/*','cms/*' +--- +DataObject: + extensions: + - MappableData + +DataList: + extensions: + - MappableDataObjectSet + +ArrayList: + extensions: + - MappableDataObjectSet diff --git a/_config/maps.yml b/_config/maps.yml new file mode 100644 index 0000000..be0c6e1 --- /dev/null +++ b/_config/maps.yml @@ -0,0 +1,15 @@ +--- +Name: mappable +After: 'framework/*','cms/*' +--- +Mappable: + allow_full_screen: true + use_compressed_assets: false + mapping_service: 'Google' + #service_key: 'YOUR SERVICE KEY' + + #Language to load the Map Service in + language: 'en' + +MapExtension: + map_info_window_suffix: '_MapInfoWindow' diff --git a/code/GoogleMapAPI.php b/code/GoogleMapAPI.php deleted file mode 100644 index b7c52ec..0000000 --- a/code/GoogleMapAPI.php +++ /dev/null @@ -1,756 +0,0 @@ - -* @copyright (c) 2009 CERDAN Yohann, All rights reserved -* @ version 18:13 26/05/2009 -*/ - -class GoogleMapAPI extends ViewableData -{ - - /** GoogleMap key **/ - protected $googleMapKey = ''; - - /** GoogleMap ID for the HTML DIV **/ - protected $googleMapId = 'googlemapapi'; - - /** GoogleMap Direction ID for the HTML DIV **/ - protected $googleMapDirectionId = 'route'; - - /** Width of the gmap **/ - protected $width = 800; - - /** Height of the gmap **/ - protected $height = 600; - - /** Icon width of the gmarker **/ - protected $iconWidth = 24; - - /** Icon height of the gmarker **/ - protected $iconHeight = 24; - - /** - * @var int Infowindow width of the gmarker - **/ - protected $infoWindowWidth = 250; - - /** Default zoom of the gmap **/ - protected $zoom = 9; - - /** Enable the zoom of the Infowindow **/ - protected $enableWindowZoom = false; - - /** Default zoom of the Infowindow **/ - protected $infoWindowZoom = 13; - - /** Lang of the gmap **/ - protected $lang = 'en'; - - /**Center of the gmap **/ - protected $center = 'Paris, France'; - - protected $latLongCenter = null; - - /** - * Type of the gmap, can be: - * 'G_NORMAL_MAP' (roadmap), - * 'G_SATELLITE_MAP' (sattelite) - * 'G_HYBRID_MAP' (hybrid) - * 'G_PHYSICAL_MAP' (terrain) - */ - - protected $mapType = 'G_NORMAL_MAP'; - - /** Content of the HTML generated **/ - protected $content = ''; - - /** Add the direction button to the infowindow **/ - protected $displayDirectionFields = false; - - /** Hide the marker by default **/ - protected $defaultHideMarker = false; - - /** Extra content (marker, etc...) **/ - protected $contentMarker = ''; - - /** Use clusterer to display a lot of markers on the gmap **/ - protected $useClusterer = false; - protected $gridSize = 100; - protected $maxZoom = 9; - protected $clustererLibrarypath = 'markerclusterer_packed.js'; - - /** Enable automatic center/zoom **/ - protected $enableAutomaticCenterZoom = false; - - /** maximum longitude of all markers **/ - protected $maxLng = -1000000; - - /** minimum longitude of all markers **/ - protected $minLng = 1000000; - - /** max latitude of all markers **/ - protected $maxLat = -1000000; - - /** min latitude of all markers **/ - protected $minLat = 1000000; - - /** map center latitude (horizontal), calculated automatically as markers are added to the map **/ - protected $centerLat = null; - - /** map center longitude (vertical), calculated automatically as markers are added to the map **/ - protected $centerLng = null; - - /** factor by which to fudge the boundaries so that when we zoom encompass, the markers aren't too close to the edge **/ - protected $coordCoef = 0.01; - - protected $lines = array (); - - protected $contentLines = ''; - - protected static $jsIncluded = false; - - - /** - * Class constructor - * - * @param string $googleMapKey the googleMapKey - * - * @return void - */ - - public function __construct($googleMapKey='') - { - $this->googleMapKey = $googleMapKey; - } - - /** - * Set the key of the gmap - * - * @param string $googleMapKey the googleMapKey - * - * @return void - */ - - public function setKey($googleMapKey) - { - $this->googleMapKey = $googleMapKey; - } - - /** - * Set the useClusterer parameter (optimization to display a lot of marker) - * - * @param boolean $useClusterer use cluster or not - * @param string $clusterIcon the cluster icon - * @param int $maxVisibleMarkers max visible markers - * @param int $gridSize grid size - * @param int $minMarkersPerClusterer minMarkersPerClusterer - * @param int $maxLinesPerInfoBox maxLinesPerInfoBox - * - * @return void - */ - - public function setClusterer($useClusterer,$gridSize=100,$maxZoom=9,$clustererLibraryPath='mappable/javascript/clusterer.js') - { - $this->useClusterer = $useClusterer; - $this->gridSize = $gridSize; - $this->maxZoom = $maxZoom; - $this->clustererLibraryPath = $clustererLibraryPath; - } - - /** - * Set the ID of the default gmap DIV - * - * @param string $googleMapId the google div ID - * - * @return void - */ - - public function setDivId($googleMapId) - { - $this->googleMapId = $googleMapId; - } - - /** - * Set the ID of the default gmap direction DIV - * - * @param string $googleMapDirectionId GoogleMap Direction ID for the HTML DIV - * - * @return void - */ - - public function setDirectionDivId($googleMapDirectionId) - { - $this->googleMapDirectionId = $googleMapDirectionId; - } - - /** - * Set the size of the gmap - * - * @param int $width GoogleMap width - * @param int $height GoogleMap height - * - * @return void - */ - - public function setSize($width,$height) - { - $this->width = $width; - $this->height = $height; - } - - /** - * Set the with of the gmap infowindow (on marker clik) - * - * @param int $infoWindowWidth GoogleMap info window width - * - * @return void - */ - - public function setInfoWindowWidth ($infoWindowWidth) - { - $this->infoWindowWidth = $infoWindowWidth; - } - - /** - * Set the size of the icon markers - * - * @param int $iconWidth GoogleMap marker icon width - * @param int $iconHeight GoogleMap marker icon height - * - * @return void - */ - - public function setIconSize($iconWidth,$iconHeight) - { - $this->iconWidth = $iconWidth; - $this->iconHeight = $iconHeight; - } - - /** - * Set the lang of the gmap - * - * @param string $lang GoogleMap lang : fr,en,.. - * - * @return void - */ - - public function setLang($lang) - { - $this->lang = $lang; - } - - /** - * Set the zoom of the gmap - * - * @param int $zoom GoogleMap zoom. - * - * @return void - */ - - public function setZoom($zoom) - { - $this->zoom = $zoom; - } - - /** - * Set the zoom of the infowindow - * - * @param int $zoom GoogleMap zoom. - * - * @return void - */ - - public function setInfoWindowZoom($infoWindowZoom) - { - $this->infoWindowZoom = $infoWindowZoom; - } - - /** - * Enable the zoom on the marker when you click on it - * - * @param int $zoom GoogleMap zoom. - * - * @return void - */ - - public function setEnableWindowZoom($enableWindowZoom) - { - $this->enableWindowZoom = $enableWindowZoom; - } - - /** - * Enable theautomatic center/zoom at the gmap load - * - * @param int $zoom GoogleMap zoom. - * - * @return void - */ - - public function setEnableAutomaticCenterZoom($enableAutomaticCenterZoom) - { - $this->enableAutomaticCenterZoom = $enableAutomaticCenterZoom; - } - - /** - * Set the center of the gmap (an address) - * - * @param string $center GoogleMap center (an address) - * - * @return void - */ - - public function setCenter($center) - { - $this->center = $center; - } - - /** - * Set the type of the gmap - * - * @param string $mapType ( can be 'G_NORMAL_MAP', 'G_SATELLITE_MAP', 'G_HYBRID_MAP', 'G_PHYSICAL_MAP') - * - * @return void - */ - - public function setMapType($mapType) - { - $this->mapType = $mapType; - } - - - - public function setLatLongCenter($center) - { - $this->latLongCenter = $center; - } - - /** - * Set the center of the gmap - * - * @param boolean $displayDirectionFields display directions or not in the info window - * - * @return void - */ - - public function setDisplayDirectionFields($displayDirectionFields) - { - $this->displayDirectionFields = $displayDirectionFields; - } - - /** - * Set the defaultHideMarker - * - * @param boolean $defaultHideMarker hide all the markers on the map by default - * - * @return void - */ - - public function setDefaultHideMarker($defaultHideMarker) - { - $this->defaultHideMarker = $defaultHideMarker; - } - - /** - * Get the google map content - * - * @return string the google map html code - */ - - public function getGoogleMap() - { - return $this->content; - } - - /** - * Get URL content using cURL. - * - * @param string $url the url - * - * @return string the html code - * - * @todo add proxy settings - */ - - public function getContent($url) - { - $curl = curl_init(); - curl_setopt($curl, CURLOPT_TIMEOUT, 10); - curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE); - curl_setopt($curl, CURLOPT_URL, $url); - $data = curl_exec($curl); - curl_close ($curl); - return $data; - } - - /** - * Geocoding an address (address -> lat,lng) - * - * @param string $address an address - * - * @return array array with precision, lat & lng - */ - - public function geocoding($address) - { - $encodeAddress = urlencode($address); - $url = "http://maps.google.com/maps/geo?q=".$encodeAddress."&output=csv&key=".$this->googleMapKey; - - if(function_exists('curl_init')) { - $data = $this->getContent($url); - } else { - $data = file_get_contents($url); - } - - $csvSplit = preg_split("/,/",$data); - $status = $csvSplit[0]; - - if (strcmp($status, "200") == 0) { - $return = $csvSplit; // successful geocode, $precision = $csvSplit[1],$lat = $csvSplit[2],$lng = $csvSplit[3]; - } else { - $return = null; // failure to geocode - } - - return $return; - } - - /** - * Add marker by his coord - * - * @param string $lat lat - * @param string $lng lngs - * @param string $html html code display in the info window - * @param string $category marker category - * @param string $icon an icon url - * - * @return void - */ - - public function addMarkerByCoords($lat,$lng,$html='',$category='',$icon='') - { - // Save the lat/lon to enable the automatic center/zoom - $this->maxLng = (float) max((float)$lng, $this->maxLng); - $this->minLng = (float) min((float)$lng, $this->minLng); - $this->maxLat = (float) max((float)$lat, $this->maxLat); - $this->minLat = (float) min((float)$lat, $this->minLat); - $this->centerLng = (float) ($this->minLng + $this->maxLng) / 2; - $this->centerLat = (float) ($this->minLat + $this->maxLat) / 2; - - $this->contentMarker .= "\t\t\t".'createMarker('.$lat.','.$lng.',"'.$html.'","'.$category.'","'.$icon.'");'."\n"; - } - - /** - * Add marker by his address - * - * @param string $address an ddress - * @param string $content html code display in the info window - * @param string $category marker category - * @param string $icon an icon url - * - * @return void - */ - - public function addMarkerByAddress($address,$content='',$category='',$icon='') - { - $point = $this->geocoding($address); - if ($point!==null) { - $this->addMarkerByCoords($point[2], $point[3], $content, $category, $icon); - } else { - // throw new Exception('Adress not found : '.$address); - } - } - - /** - * Add marker by an array of coord - * - * @param string $coordtab an array of lat,lng,content - * @param string $category marker category - * @param string $icon an icon url - * - * @return void - */ - - public function addArrayMarkerByCoords($coordtab,$category='',$icon='') - { - foreach ($coordtab as $coord) { - $this->addMarkerByCoords($coord[0], $coord[1], $coord[2], $category, $icon); - } - } - - - /** - * Adds a {@link ViewableData} object that implements {@link Mappable} - * to the map. - * - * @param ViewableData $obj - */ - public function addMarkerAsObject(ViewableData $obj) { - if($obj instanceof Mappable) { - if(($obj->getLatitude() > 0) || ($obj->getLongitude() > 0)) { - $cat = $obj->hasMethod('getMapCategory') ? $obj->getMapCategory() : "default"; - $this->addMarkerByCoords($obj->getLatitude(), $obj->getLongitude(), $obj->getMapContent(), $cat, $obj->getMapPin()); - } - } - } - - - /** - * Draws a line between two {@link ViewableData} objects - * - * @param ViewableData $one The first point - * @param ViewableData $two The second point - * @param string $color The hexidecimal color of the line - */ - public function connectPoints(ViewableData $one, ViewableData $two, $color = "#FF3300") { - $this->addLine( - array($one->getLatitude(), $one->getLongitude()), - array($two->getLatitude(), $two->getLongitude()), - $color - ); - } - - - public function forTemplate() { - $this->generate(); - return $this->getGoogleMap(); - } - - - - /** - * Add marker by an array of address - * - * @param string $coordtab an array of address - * @param string $category marker category - * @param string $icon an icon url - * - * @return void - */ - - public function addArrayMarkerByAddress($coordtab,$category='',$icon='') - { - foreach ($coordtab as $coord) { - $this->addMarkerByAddress($coord[0], $coord[1], $category, $icon); - } - } - - /** - * Set a direction between 2 addresss and set a text panel - * - * @param string $from an address - * @param string $to an address - * @param string $idpanel id of the div panel - * - * @return void - */ - - public function addDirection($from,$to,$idpanel='') - { - $this->contentMarker .= 'addDirection("'.$from.'","'.$to.'","'.$idpanel.'");'; - } - - /** - * Parse a KML file and add markers to a category - * - * @param string $url url of the kml file compatible with gmap and gearth - * @param string $category marker category - * @param string $icon an icon url - * - * @return void - */ - - public function addKML ($url,$category='',$icon='') - { - $xml = new SimpleXMLElement($url, null, true); - foreach ($xml->Document->Folder->Placemark as $item) { - $coordinates = explode(',', (string) $item->Point->coordinates); - $name = (string) $item->name; - $this->addMarkerByCoords($coordinates[1], $coordinates[0], $name, $category, $icon); - } - } - - - public function addLine($from = array(), $to = array(), $color = "#FF3300") { - $this->contentLines .= "var points = [new GLatLng({$from[0]},{$from[1]}), new GLatLng({$to[0]},{$to[1]})];\n"; - $this->contentLines .= "map.addOverlay(new GPolyline(points,'{$color}',4,0.6));\n"; - } - /** - * Initialize the javascript code - * - * @return void - */ - - public function includeGMapsJS() { - if(self::$jsIncluded) return; - // Google map JS - $this->content .= ''."\n"; - - // Clusterer JS - if ($this->useClusterer==true) { - // Source: http://gmaps-utility-library.googlecode.com/svn/trunk/markerclusterer/1.0/src/ - $this->content .= ''."\n"; - } - - self::$jsIncluded = true; - - } - - public function init() - { - $this->includeGMapsJS(); - // JS variable init - $this->content .= "\t".''."\n"; - - // Google map DIV - $this->content .= "\t".'
'."\n"; - } - - /** - * Generate the gmap - * - * @return void - */ - - public function generate() - { - - $this->init(); - - $this->content .= "\t".''."\n"; - - // Center of the GMap - $geocodeCentre = ($this->latLongCenter) ? $this->latLongCenter : $this->geocoding($this->center); - - if ($geocodeCentre[0]=="200") { // success - $latlngCentre = $geocodeCentre[2].",".$geocodeCentre[3]; - } else { // Paris - $latlngCentre = "48.8792,2.34778"; - } - - $this->content .= "\t".''."\n"; - - } -} \ No newline at end of file diff --git a/code/GoogleMapUtil.php b/code/GoogleMapUtil.php deleted file mode 100644 index 51a3578..0000000 --- a/code/GoogleMapUtil.php +++ /dev/null @@ -1,197 +0,0 @@ -setDivId(self::$div_id."_".self::$instances); - $gmap->setEnableAutomaticCenterZoom(self::$automatic_center); - $gmap->setDisplayDirectionFields(self::$direction_fields); - $gmap->setSize(self::$map_width, self::$map_height); - $gmap->setDefaultHideMarker(self::$hide_marker); - $gmap->setMapType(self::$map_type); - $gmap->setInfoWindowWidth(self::$info_window_width); - $gmap->setCenter(self::$center); - $gmap->setIconSize(self::$iconWidth, self::$iconHeight); - return $gmap; - } - - - /** - * Sanitize a string of HTML content for safe inclusion in the JavaScript - * for a Google Map - * - * @return string - */ - public static function sanitize($content) { - return addslashes(str_replace(array("\n","\r"),array("",""),$content)); - } - - - /** - * Creates a new {@link GoogleMapsAPI} object loaded with the default settings - * and places all of the items in a {@link DataObjectSet} on the map - * - * @param DataObjectSet $set - * @return GoogleMapsAPI - */ - public static function get_map(DataObjectSet $set) { - $gmap = self::instance(); - if($set) { - foreach($set as $obj) { - $gmap->addMarkerAsObject($obj); - } - } - return $gmap; - } - -} \ No newline at end of file diff --git a/code/LatLongField.php b/code/LatLongField.php index 8d4e1b4..c7fb05f 100755 --- a/code/LatLongField.php +++ b/code/LatLongField.php @@ -1,66 +1,132 @@ addressFields = $addressFields; - $this->buttonText = $buttonText ? $buttonText : _t('LatLongField.LOOKUP','Look up'); - $this->latField = $children[0]->Name(); - $this->longField = $children[1]->Name(); - $name = ""; - foreach($children as $field) { - $name .= $field->Name(); - } - - - $this->name = $name; - } - - - public function hasData() {return true;} - - public function FieldHolder() { - Requirements::javascript(THIRDPARTY_DIR.'/jquery/jquery.js'); - Requirements::javascript(THIRDPARTY_DIR.'/jquery-metadata/jquery.metadata.js'); - Requirements::javascript('mappable/javascript/lat_long_field.js'); - Requirements::css('mappable/css/lat_long_field.css'); - $this->FieldSet()->push(new LiteralField('geocode_'.$this->id(), sprintf(''. - $this->buttonText. - '', implode(',',$this->addressFields), $this->latField, $this->longField))); - $map = GoogleMapUtil::instance(); - $map->setDivId('geocode_map_'.$this->id()); - $map->setEnableAutomaticCenterZoom(false); - - $mapHtml = $map->forTemplate(); - - $this->FieldSet()->push(new LiteralField ('geocode_map_field'.$this->id(),$mapHtml)); - return parent::FieldHolder(); - } - - public function geocode(SS_HTTPRequest $r) { - if($address = $r->requestVar('address')) { - if($json = @file_get_contents("http://maps.googleapis.com/maps/api/geocode/json?sensor=false&address=".urlencode($address))) { - $response = Convert::json2array($json); - $location = $response['results'][0]->geometry->location; - return new SS_HTTPResponse($location->lat.",".$location->lng); - } - } - } - -} \ No newline at end of file +class LatLongField extends FieldGroup +{ + protected $latField; + + protected $longField; + + protected $zoomField; + + protected $buttonText; + + private $guidePoints = null; + + private static $ctr = 0; + + /** + * @param string[] $buttonText + */ + public function __construct($children = array(), $buttonText = null) + { + ++self::$ctr; + + if ((sizeof($children) < 2) || (sizeof($children) > 3) || + (!$children[0] instanceof FormField) || + (!$children[1] instanceof FormField) + ) { + user_error('LatLongField argument 1 must be an array containing at least two FormField '. + 'objects for Lat/Long values, respectively.', E_USER_ERROR); + } + + parent::__construct($children); + + $this->buttonText = $buttonText ? $buttonText : _t('LatLongField.LOOKUP', 'Search'); + $this->latField = $children[0]->getName(); + $this->longField = $children[1]->getName(); + + if (sizeof($children) == 3) { + $this->zoomField = $children[2]->getName(); + } + $name = ''; + foreach ($children as $field) { + $name .= $field->getName(); + } + + // hide the lat long and zoom fields from the interface + foreach ($this->FieldList() as $fieldToHide) { + $fieldToHide->addExtraClass('hide'); + } + + $this->name = $name; + } + + public function FieldHolder($properties = array()) + { + Requirements::javascript(THIRDPARTY_DIR.'/jquery/jquery.js'); + Requirements::javascript(THIRDPARTY_DIR.'/jquery-livequery/jquery.livequery.js'); + Requirements::javascript(THIRDPARTY_DIR.'/jquery-metadata/jquery.metadata.js'); + Requirements::javascript(MAPPABLE_MODULE_PATH.'/javascript/mapField.js'); + + $attributes = array( + 'class' => 'editableMap', + 'id' => 'GoogleMap', + 'data-LatFieldName' => $this->latField, + 'data-LonFieldName' => $this->longField, + 'data-ZoomFieldName' => $this->zoomField, + 'data-UseMapBounds' => false, + ); + + Requirements::css('mappable/css/mapField.css'); + + // check for and if required add guide points + if (!empty($this->guidePoints)) { + $latlongps = array(); + + foreach ($this->guidePoints as $guidepoint) { + array_push($latlongps, $guidepoint); + } + + $guidePointsJSON = json_encode($latlongps); + // convert the mappable guidepoints to lat lon + + $attributes['data-GuidePoints'] = $guidePointsJSON; + + // we only wish to change the bounds to those of all the points iff + // the item currently has no location + $attributes['data-useMapBounds'] = true; + } + $content = '
'.$this->create_tag( + 'div', + $attributes + ).'
'; + + $this->FieldList()->push(new LiteralField('locationEditor', $content)); + + $content2 = << + + +
+
+ +HTML; + + $this->FieldList()->push(new LiteralField('mapSearch', $content2)); + + return parent::FieldHolder(); + } + + /* + Set guidance points for the map being edited. For example in a photographic set show the map + position of some other images so that subsequent photo edits do not start with a map centred + at the origin + + @var newGuidePoints array of points expressed as associative arrays containing keys latitude + and longitude mapping to geographical locations + */ + public function setGuidePoints($newGuidePoints) + { + $this->guidePoints = $newGuidePoints; + } + + /** + * Accessor to guidepoints. For testing purposes. + * + * @return array guidepoints + */ + public function getGuidePoints() + { + return $this->guidePoints; + } +} diff --git a/code/MapAPI.php b/code/MapAPI.php new file mode 100644 index 0000000..d4543eb --- /dev/null +++ b/code/MapAPI.php @@ -0,0 +1,740 @@ + +* @copyright (c) 2009 CERDAN Yohann, All rights reserved +* @ version 18:13 26/05/2009 +*/ + +class MapAPI extends ViewableData +{ + /** GoogleMap key **/ + protected $googleMapKey = ''; + + /** GoogleMap ID for the HTML DIV **/ + protected $googleMapId = 'googlemapapi'; + + /* Additional CSS classes to use when rendering the map */ + protected $set_additional_css_classes = ''; + + /** Width of the gmap **/ + protected $width = 800; + + /** Height of the gmap **/ + protected $height = 600; + + /* array of lines to be drawn on the map */ + protected $lines = array(); + + /* kml file to be rendered */ + protected $kmlFiles = array(); + + /** Default zoom of the gmap **/ + protected $zoom = 9; + + /** Enable the zoom of the Infowindow **/ + protected $enableWindowZoom = false; + + /** Default zoom of the Infowindow **/ + protected $infoWindowZoom = 13; + + /** Lang of the gmap **/ + protected $lang = 'en'; + + /**Center of the gmap **/ + protected $center = 'Paris, France'; + + /* + Additional CSS classes to render as a class attribute for the div of the + map. Use this if you want more fine grained control over your map using + CSS. If blank it will be ignored + */ + protected $additional_css_classes = ''; + + /* Decided whether or not to show the inline map css style on div creation */ + protected $show_inline_map_div_style = true; + + protected $latLongCenter = null; + + protected $jsonMapStyles = '[]'; + + /** + * Type of the gmap, can be: + * 'road' (roadmap), + * 'satellite' (sattelite/aerial photographs) + * 'hybrid' (hybrid of road and satellite) + * 'terrain' (terrain) + * The JavaScript for the mapping service will convert this into a suitable mapping type. + */ + protected $mapType = 'road'; + + /** Content of the HTML generated **/ + protected $content = ''; + + protected $mapService = 'google'; + + /** Hide the marker by default **/ + protected $defaultHideMarker = false; + + /** Extra content (marker, etc...) **/ + protected $contentMarker = ''; + + // a list of markers, markers being associative arrays + protected $markers = array(); + + /** Use clusterer to display a lot of markers on the gmap **/ + protected $useClusterer = false; + protected $gridSize = 50; + protected $maxZoom = 17; + protected $clustererLibraryPath = '/mappable/javascript/google/markerclusterer.js'; + + /** Enable automatic center/zoom **/ + protected $enableAutomaticCenterZoom = false; + + /** maximum longitude of all markers **/ + protected $maxLng = -1000000; + + /** minimum longitude of all markers **/ + protected $minLng = 1000000; + + /** max latitude of all markers **/ + protected $maxLat = -1000000; + + /** min latitude of all markers **/ + protected $minLat = 1000000; + + /** map center latitude (horizontal), calculated automatically as markers + are added to the map **/ + protected $centerLat = null; + + /** map center longitude (vertical), calculated automatically as markers + are added to the map **/ + protected $centerLng = null; + + /** factor by which to fudge the boundaries so that when we zoom encompass, + the markers aren't too close to the edge **/ + protected $coordCoef = 0.01; + + /* set this to true to render button to maximize / minimize a map */ + protected $allowFullScreen = null; + + /** + * Class constructor. + * + * @param string $googleMapKey the googleMapKey + */ + public function __construct($googleMapKey = '') + { + $this->googleMapKey = $googleMapKey; + } + + public function setShowInlineMapDivStyle($new_show_inline_map_div_style) + { + $this->show_inline_map_div_style = $new_show_inline_map_div_style; + + return $this; + } + + public function setAdditionalCSSClasses($new_additional_css_classes) + { + $this->additional_css_classes = $new_additional_css_classes; + + return $this; + } + + public function setMapStyle($newStyles) + { + $this->jsonMapStyles = $newStyles; + + return $this; + } + + /** + * Set the useClusterer parameter (optimization to display a lot of marker). + * + * @param bool $useClusterer use cluster or not + * @param int $gridSize grid size + * @param int $maxZoom max zoom to cluster at + * + * * @return MapAPI This same object, in order to enable chaining of methods + */ + public function setClusterer( + $useClusterer, + $gridSize = 50, + $maxZoom = 17, + $clustererLibraryPath = '/mappable/javascript/google/markerclusterer.js' + ) { + $this->useClusterer = $useClusterer; + $this->gridSize = $gridSize; + $this->maxZoom = $maxZoom; + $this->clustererLibraryPath = $clustererLibraryPath; + + return $this; + } + + /** + * Set the ID of the default gmap DIV. + * + * @param string $googleMapId the google div ID + * + * @return MapAPI This same object, in order to enable chaining of methods + */ + public function setDivId($googleMapId) + { + $this->googleMapId = $googleMapId; + + return $this; + } + + /** + * Set the size of the gmap. If these values are not provided + * then CSS is used instead. + * + * @param int $width GoogleMap width + * @param int $height GoogleMap height + * + * @return MapAPI This same object, in order to enable chaining of methods + */ + public function setSize($width, $height) + { + $this->width = $width; + $this->height = $height; + + return $this; + } + + /** + * Set the lang of the gmap. + * + * @param string $lang GoogleMap lang : fr,en,.. + * + * @return MapAPI This same object, in order to enable chaining of methods + */ + public function setLang($lang) + { + $this->lang = $lang; + + return $this; + } + + /** + * Set the zoom of the gmap. + * + * @param int $zoom GoogleMap zoom. + * + * @return MapAPI This same object, in order to enable chaining of methods + */ + public function setZoom($zoom) + { + $this->zoom = $zoom; + + return $this; + } + + /** + * Set the zoom of the infowindow. + * + * @param int $infoWindowZoom GoogleMap information window zoom. + * + * @return MapAPI This same object, in order to enable chaining of methods + */ + public function setInfoWindowZoom($infoWindowZoom) + { + $this->infoWindowZoom = $infoWindowZoom; + + return $this; + } + + /** + * Enable the zoom on the marker when you click on it. + * + * @param bool $enableWindowZoom info window enabled zoom. + * + * @return MapAPI This same object, in order to enable chaining of methods + */ + public function setEnableWindowZoom($enableWindowZoom) + { + $this->enableWindowZoom = $enableWindowZoom; + + return $this; + } + + /** + * Enable theautomatic center/zoom at the gmap load. + * + * @param bool $enableAutomaticCenterZoom enable automatic centre zoom + * + * @return MapAPI This same object, in order to enable chaining of methods + */ + public function setEnableAutomaticCenterZoom($enableAutomaticCenterZoom) + { + $this->enableAutomaticCenterZoom = $enableAutomaticCenterZoom; + + return $this; + } + + /** + * Set the center of the gmap (an address). + * + * @param string $center GoogleMap center (an address) + * + * @return MapAPI This same object, in order to enable chaining of methods + */ + public function setCenter($center) + { + $this->center = $center; + + return $this; + } + + /** + * Set the type of the gmap. Also takes into account legacy settings. + * + * FIXME - allow other valid settings in config for map type + * + * @param string $mapType Can be one of road,satellite,hybrid or terrain. Defaults to road + * + * @return MapAPI This same object, in order to enable chaining of methods + */ + public function setMapType($mapType) + { + $this->mapType = $mapType; + + // deal with legacy values for backwards compatbility + switch ($mapType) { + case 'google.maps.MapTypeId.SATELLITE': + $this->mapType = 'satellite'; + break; + case 'google.maps.MapTypeId.G_HYBRID_MAP': + $this->mapType = 'hybrid'; + break; + case 'google.maps.MapTypeId.G_PHYSICAL_MAP': + $this->mapType = 'terrain'; + break; + case 'google.maps.MapTypeId.ROADMAP': + $this->mapType = 'road'; + break; + } + + return $this; + } + + /* + Set whether or not to allow the full screen tools + @return MapAPI This same object, in order to enable chaining of methods + */ + public function setAllowFullScreen($allowed) + { + $this->allowFullScreen = $allowed; + + return $this; + } + + /** + * Set the center of the gmap. + * + * @return MapAPI This same object, in order to enable chaining of methods + **/ + public function setLatLongCenter($center) + { + // error check, we want an associative array with lat,lng keys numeric + + if (!is_array($center)) { + throw new InvalidArgumentException('Center must be an associative array containing lat,lng'); + } + + $keys = array_keys($center); + sort($keys); + if (implode(',', $keys) != 'lat,lng') { + throw new InvalidArgumentException('Keys provided must be lat, lng'); + } + + $this->latLongCenter = $center; + + return $this; + } + + /** + * Set the defaultHideMarker. + * + * @param bool $defaultHideMarker hide all the markers on the map by default + * + * @return MapAPI + */ + public function setDefaultHideMarker($defaultHideMarker) + { + $this->defaultHideMarker = $defaultHideMarker; + + return $this; + } + + /** + * Get the google map content. + * + * @return string the google map html code + */ + public function getGoogleMap() + { + return $this->content; + } + + /** + * Get URL content using cURL. + * + * @param string $url the url + * + * @return string the html code + * + * @todo add proxy settings + */ + public function getContent($url) + { + $curl = curl_init(); + curl_setopt($curl, CURLOPT_TIMEOUT, 10); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_URL, $url); + $data = curl_exec($curl); + curl_close($curl); + + return $data; + } + + /** + * Geocoding an address (address -> lat,lng). + * + * @param string $address an address + * + * @return string array with precision, lat & lng + */ + public function geocoding($address) + { + $geocoder = new MappableGoogleGeocoder(); + $locations = $geocoder->getLocations($address); + $result = null; + if (!empty($locations)) { + $place = $locations[0]; + $location = $place['geometry']['location']; + $result = array( + 'lat' => $location['lat'], + 'lon' => $location['lng'], + 'geocoded' => true, + ); + } else { + $result = array(); // no results + } + + return $result; + } + + /** + * Add marker by his coord. + * + * @param string $lat lat + * @param string $lng lngs + * @param string $html html code display in the info window + * @param string $category marker category + * @param string $icon an icon url + * + * @return MapAPI + */ + public function addMarkerByCoords($lat, $lng, $html = '', $category = '', $icon = '') + { + $m = array( + 'latitude' => $lat, + 'longitude' => $lng, + 'html' => $html, + 'category' => $category, + 'icon' => $icon, + ); + array_push($this->markers, $m); + + return $this; + } + + /** + * Add marker by his address. + * + * @param string $address an ddress + * @param string $content html code display in the info window + * @param string $category marker category + * @param string $icon an icon url + * + * @return MapAPI + */ + public function addMarkerByAddress($address, $content = '', $category = '', $icon = '') + { + $point = $this->geocoding($address); + if ($point !== null) { + $this->addMarkerByCoords($point['lat'], $point['lon'], $content, $category, $icon); + } + + return $this; + } + + /** + * Add marker by an array of coord. + * + * @param array $coordtab an array of lat,lng,content + * @param string $category marker category + * @param string $icon an icon url + * + * @return MapAPI + */ + public function addArrayMarkerByCoords($coordtab, $category = '', $icon = '') + { + foreach ($coordtab as $coord) { + $this->addMarkerByCoords($coord[0], $coord[1], $coord[2], $category, $icon); + } + + return $this; + } + + /** + * Adds a {@link ViewableData} object that implements {@link Mappable} + * to the map. + * + * @param $infowindowtemplateparams Optional array of extra parameters to pass to the map info window + * @param ViewableData $obj + */ + public function addMarkerAsObject(ViewableData $obj, $infowindowtemplateparams = null) + { + $extensionsImplementMappable = false; + $extensions = Object::get_extensions(get_class($obj)); + if (is_array($extensions)) { + foreach ($extensions as $extension) { + $class = new ReflectionClass($extension); + if ($class->implementsInterface('Mappable')) { + $extensionsImplementMappable = true; + } + } + } + + if ($extensionsImplementMappable || + ($obj instanceof Mappable) || + (Object::has_extension($obj->ClassName, 'MapExtension')) + ) { + $cat = $obj->hasMethod('getMappableMapCategory') ? $obj->getMappableMapCategory() : 'default'; + if ($infowindowtemplateparams !== null) { + foreach ($infowindowtemplateparams as $key => $value) { + $obj->{$key} = $value; + } + } + $this->addMarkerByCoords( + $obj->getMappableLatitude(), + $obj->getMappableLongitude(), + $obj->getMappableMapContent(), + $cat, + $obj->getMappableMapPin() + ); + } + + return $this; + } + + /** + * Draws a line between two {@link ViewableData} objects. + * + * @param ViewableData $one The first point + * @param ViewableData $two The second point + * @param string $color The hexidecimal color of the line + */ + public function connectPoints(ViewableData $one, ViewableData $two, $color = '#FF3300') + { + $this->addLine( + array($one->getMappableLatitude(), $one->getMappableLongitude()), + array($two->getMappableLatitude(), $two->getMappableLongitude()), + $color + ); + } + + public function forTemplate() + { + $this->generate(); + MapUtil::set_map_already_rendered(true); + + return $this->getGoogleMap(); + } + + /** + * Add a KML file which will be rendered on this map. Normally used for likes + * of GPS traces from activities. + * + * @param string $url url of the kml file compatible with gmap and gearth + * + * @return MapAPI + */ + public function addKML($url) + { + array_push($this->kmlFiles, $url); + + return $this; + } + + /* + Add a line to the map + + */ + public function addLine($from = array(), $to = array(), $color = '#FF3300') + { + $line = array( + 'lat1' => $from[0], + 'lon1' => $from[1], + 'lat2' => $to[0], + 'lon2' => $to[1], + 'color' => $color, + ); + + array_push($this->lines, $line); + + return $this; + } + + /* + For php 5.3 + */ + public static function jsonRemoveUnicodeSequences($struct) + { + return preg_replace( + '/\\\\u([a-f0-9]{4})/e', + "iconv('UCS-4LE','UTF-8',pack('V', hexdec('U$1')))", + json_encode($struct) + ); + } + + /** + * Generate the gmap. + */ + public function generate() + { + // from http://stackoverflow.com/questions/3586401/cant-decode-json-string-in-php + $jsonMarkers = null; + $linesJson = null; + $kmlJson = null; + + // prior to PHP version 5.4, one needs to use regex + if (PHP_VERSION_ID < 50400) { + $jsonMarkers = stripslashes(self::jsonRemoveUnicodeSequences($this->markers)); + $linesJson = stripslashes(self::jsonRemoveUnicodeSequences($this->lines)); + $kmlJson = stripslashes(self::jsonRemoveUnicodeSequences($this->kmlFiles)); + } else { + $jsonMarkers = stripslashes(json_encode($this->markers, JSON_UNESCAPED_UNICODE)); + $linesJson = stripslashes(json_encode($this->lines, JSON_UNESCAPED_UNICODE)); + $kmlJson = stripslashes(json_encode($this->kmlFiles, JSON_UNESCAPED_UNICODE)); + } + + // Center of the GMap - text centre takes precedence + $geocodeCentre = ($this->latLongCenter) ? + $this->latLongCenter : $this->geocoding($this->center); + + $latlngCentre = null; + // coordinates for centre depending on which method used + if (isset($geocodeCentre['geocoded'])) { + $latlngCentre = array( + 'lat' => $geocodeCentre['lat'], + 'lng' => $geocodeCentre['lon'], + ); + } elseif (is_array($this->latLongCenter)) { + $latlngCentre = $this->latLongCenter; + } + + $this->LatLngCentreJSON = stripslashes(json_encode($latlngCentre)); + + $lenLng = $this->maxLng - $this->minLng; + $lenLat = $this->maxLat - $this->minLat; + $this->minLng -= $lenLng * $this->coordCoef; + $this->maxLng += $lenLng * $this->coordCoef; + $this->minLat -= $lenLat * $this->coordCoef; + $this->maxLat += $lenLat * $this->coordCoef; + + // add the css class mappable as a handle onto the map styling + $this->additional_css_classes .= ' mappable'; + + if (!$this->enableAutomaticCenterZoom) { + $this->enableAutomaticCenterZoom = 'false'; + } + + if (!$this->useClusterer) { + $this->useClusterer = 'false'; + } + + if (!$this->defaultHideMarker) { + $this->defaultHideMarker = 'false'; + } + + // initialise full screen as the config value if not already set + if ($this->allowFullScreen === null) { + $this->allowFullScreen = Config::inst()->get('Mappable', 'allow_full_screen'); + } + + if (!$this->allowFullScreen) { + $this->allowFullScreen = 'false'; + } + + if (!$this->enableWindowZoom) { + $this->enableWindowZoom = 'false'; + } + + $vars = new ArrayData( + array( + + 'JsonMapStyles' => $this->jsonMapStyles, + 'AdditionalCssClasses' => $this->additional_css_classes, + 'Width' => $this->width, + 'Height' => $this->height, + 'ShowInlineMapDivStyle' => $this->show_inline_map_div_style, + 'InfoWindowZoom' => $this->infoWindowZoom, + 'EnableWindowZoom' => $this->enableWindowZoom, + 'MapMarkers' => $jsonMarkers, + 'DefaultHideMarker' => $this->defaultHideMarker, + 'LatLngCentre' => $this->LatLngCentreJSON, + 'EnableAutomaticCenterZoom' => $this->enableAutomaticCenterZoom, + 'Zoom' => $this->zoom, + 'MaxZoom' => $this->maxZoom, + 'GridSize' => $this->gridSize, + 'MapType' => $this->mapType, + 'GoogleMapID' => $this->googleMapId, + 'Lang' => $this->lang, + 'UseClusterer' => $this->useClusterer, + 'ClustererLibraryPath' => $this->clustererLibraryPath, + 'ClustererMaxZoom' => $this->maxZoom, + 'ClustererGridSize' => $this->gridSize, + 'Lines' => $linesJson, + 'KmlFiles' => $kmlJson, + 'AllowFullScreen' => $this->allowFullScreen, + 'UseCompressedAssets' => Config::inst()->get('Mappable', 'use_compressed_assets'), + ) + ); + + if (!MapUtil::get_map_already_rendered()) { + $vars->setField('GoogleMapKey', $this->googleMapKey); + $vars->setField('GoogleMapLang', $this->lang); + } + + // HTML component of the map + $this->content = $this->processTemplateHTML('Map', $vars); + } + + /** + * @param string $templateName + * @param ArrayData $templateVariables + */ + public function processTemplateHTML($templateName, $templateVariables = null) + { + if (!$templateVariables) { + $templateVariables = new ArrayList(); + } + $mappingService = Config::inst()->get('Mappable', 'mapping_service'); + $result = $templateVariables->renderWith($templateName.$mappingService.'HTML'); + + return $result; + } +} diff --git a/code/MapExtension.php b/code/MapExtension.php new file mode 100644 index 0000000..41e4b81 --- /dev/null +++ b/code/MapExtension.php @@ -0,0 +1,191 @@ + 'Decimal(18,15)', + 'Lon' => 'Decimal(18,15)', + 'ZoomLevel' => 'Int', + 'MapPinEdited' => 'Boolean', + ); + + public static $has_one = array( + 'MapPinIcon' => 'Image', + ); + + public static $defaults = array( + 'Lat' => 0, + 'Lon' => 0, + 'Zoom' => 4, + 'MapPinEdited' => false, + ); + + /* + Map editing field + */ + private $mapField = null; + + /* + Add a Location tab containing the map + */ + public function updateCMSFields(FieldList $fields) + { + // These fields need removed, as they may have already been created by the form scaffolding + $fields->removeByName('Lat'); + $fields->removeByName('Lon'); + $fields->removeByName('ZoomLevel'); + $fields->removeByName('MapPinIcon'); + $fields->removeByName('MapPinEdited'); + + $fields->addFieldToTab( + 'Root.Location', + $this->getMapField() + ); + + $fields->addFieldToTab('Root.Location', $uf = new UploadField( + 'MapPinIcon', + _t('Mappable.MAP_PIN', 'Map Pin Icon. Leave this blank for default pin to show') + )); + $uf->setFolderName('mapicons'); + } + + public function getMappableLatitude() + { + return $this->owner->Lat; + } + + public function getMappableLongitude() + { + return $this->owner->Lon; + } + + /** + * Renders the map info window for the DataObject. + * + * Be sure to define a template for that, named by the decorated class suffixed with _MapInfoWindow + * e.g. MyPage_MapInfoWindow + * + * You can change the suffix globally by editing the MapExtension.map_info_window_suffix config val + * + * @return string + */ + public function getMappableMapContent() + { + $defaultTemplate = 'MapInfoWindow'; + $classTemplate = + SSViewer::get_templates_by_class( + $this->owner->ClassName, + Config::inst()->get('MapExtension', 'map_info_window_suffix') + ); + + $template = count($classTemplate) ? $classTemplate : $defaultTemplate; + + return MapUtil::sanitize($this->owner->renderWith($template)); + } + + /* + If the marker pin is not at position 0,0 mark the pin as edited. This provides the option of + filtering out (0,0) point which is often irrelevant for plots + */ + public function onBeforeWrite() + { + $latzero = ($this->owner->Lat == 0); + $lonzero = ($this->owner->Lon == 0); + $latlonzero = $latzero && $lonzero; + + // if both latitude and longitude still default, do not set the map location as edited + if (!$latlonzero) { + $this->owner->MapPinEdited = true; + } + } + + /* + If a user has uploaded a map pin icon display that, otherwise + */ + public function getMappableMapPin() + { + $result = false; + if ($this->owner->MapPinIconID != 0) { + $mapPin = $this->owner->MapPinIcon(); + $result = $mapPin->getAbsoluteURL(); + } else { + // check for a cached map pin already having been provided for the layer + if ($this->owner->CachedMapPinURL) { + $result = $this->owner->CachedMapPinURL; + } + } + + return $result; + } + + /* + Check for non zero coordinates, on the assumption that (0,0) will never be the desired coordinates + */ + public function HasGeo() + { + $isOrigin = ($this->owner->Lat == 0) && ($this->owner->Lon == 0); + $result = !$isOrigin; + if ($this->owner->hasExtension('MapLayerExtension')) { + if ($this->owner->MapLayers()->count() > 0) { + $result = true; + } + } + $this->owner->extend('updateHasGeo', $result); + return $result; + } + + /* + Render a map at the provided lat,lon, zoom from the editing functions, + */ + public function BasicMap() + { + $map = $this->owner->getRenderableMap()-> + setZoom($this->owner->ZoomLevel)-> + setAdditionalCSSClasses('fullWidthMap')-> + setShowInlineMapDivStyle(true); + + $autozoom = false; + + + + $this->owner->extend('updateBasicMap', $map, $autozoom); + + $map->setEnableAutomaticCenterZoom($autozoom); + $map->setShowInlineMapDivStyle(true); + + return $map; + } + + /** + * Access the map editing field for the purpose of adding guide points. + * + * @return [LatLongField] instance of location editing field + */ + public function getMapField() + { + if (!isset($this->mapField)) { + $this->mapField = new LatLongField( + array( + new TextField('Lat', 'Latitude'), + new TextField('Lon', 'Longitude'), + new TextField('ZoomLevel', 'Zoom'), + ) + ); + } + + return $this->mapField; + } + + /** + * Template helper, used to decide whether or not to use compressed assets. + */ + public function UseCompressedAssets() + { + return Config::inst()->get('Mappable', 'use_compressed_assets'); + } +} diff --git a/code/MapField.php b/code/MapField.php new file mode 100755 index 0000000..b796bc2 --- /dev/null +++ b/code/MapField.php @@ -0,0 +1,59 @@ + to
HTML tag. Default: 2 + */ + protected $headingLevel = 2; + private $divId; + + /** + * @param string $name + * @param string $title + */ + public function __construct($name, $title = null, $headingLevel = 2, $allowHTML = false, $form = null) + { + $this->divId = $name; + // legacy handling for old parameters: $title, $heading, ... + // instead of new handling: $name, $title, $heading, ... + $args = func_get_args(); + if (!isset($args[1]) || is_numeric($args[1])) { + $title = (isset($args[0])) ? $args[0] : null; + // Use "HeaderField(title)" as the default field name for a HeaderField; + // if it's just set to title then we risk causing accidental duplicate-field creation. + + // this means i18nized fields won't be easily accessible through fieldByName() + $name = 'MapField'.$title; + $headingLevel = (isset($args[1])) ? $args[1] : null; + $allowHTML = (isset($args[2])) ? $args[2] : null; + $form = (isset($args[3])) ? $args[3] : null; + } + + if ($headingLevel) { + $this->headingLevel = $headingLevel; + } + $this->allowHTML = $allowHTML; + parent::__construct($name, $title, null, $allowHTML, $form); + } + + public function Field($properties = array()) + { + Requirements::javascript('framework/thirdparty/jquery/jquery.js'); + Requirements::javascript('framework/thirdparty/jquery-livequery/jquery.livequery.js'); + $attributes = array( + 'class' => 'middleColumn', + 'id' => $this->divId, + 'style' => 'width:100%;height:300px;margin:5px 0px 5px 5px;position:relative;', + ); + + Requirements::css('mappable/css/mapField.css'); + + return '
'.$this->createTag( + 'div', + $attributes + ).'
'; + } +} diff --git a/code/MapLayer.php b/code/MapLayer.php new file mode 100644 index 0000000..5efa59e --- /dev/null +++ b/code/MapLayer.php @@ -0,0 +1,12 @@ + 'Varchar(255)', + ); + + public static $has_one = array( + 'KmlFile' => 'File', + ); +} diff --git a/code/MapLayerExtension.php b/code/MapLayerExtension.php new file mode 100644 index 0000000..f408fa8 --- /dev/null +++ b/code/MapLayerExtension.php @@ -0,0 +1,59 @@ + 'MapLayer', + ); + + public static $belongs_many_many_extraFields = array( + 'MapLayers' => array( + 'SortOrder' => 'Int', + ), + ); + + public function updateCMSFields(FieldList $fields) + { + $gridConfig2 = GridFieldConfig_RelationEditor::create(); + $gridConfig2->getComponentByType( + 'GridFieldAddExistingAutocompleter' + )-> + setSearchFields( + array('Title') + ); + $gridConfig2->getComponentByType('GridFieldPaginator')->setItemsPerPage(100); + $gridField2 = new GridField( + 'Map Layers', + 'Map Layers:', + $this->owner->MapLayers(), + $gridConfig2 + ); + $fields->addFieldToTab('Root.MapLayers', $gridField2); + } + + /** + * Only set has geo to true if layers exist + * @param boolean &$hasGeo will be set to true if any layers + */ + public function updateHasGeo(&$hasGeo) + { + if ($this->owner->MapLayers()->count() > 0) { + $hasGeo = true; + } + } + + /** + * Add layers if the exist to a map from the MapExtension + * @param MapAPI &$map object representing the map + * @param boolean &$autozoom true to auto zoom, false not to + */ + public function updateBasicMap(&$map, &$autozoom) + { + // add any KML map layers + foreach ($this->owner->MapLayers() as $layer) { + $map->addKML($layer->KmlFile()->getAbsoluteURL()); + // we have a layer, so turn on autozoom + $autozoom = true; + } + } +} diff --git a/code/MapMarkerSetsExtension.php b/code/MapMarkerSetsExtension.php new file mode 100644 index 0000000..5ef6fab --- /dev/null +++ b/code/MapMarkerSetsExtension.php @@ -0,0 +1,33 @@ + 'MapMarkerSet', + ); + + public static $belongs_many_many_extraFields = array( + 'MapMarkerSets' => array( + 'SortOrder' => 'Int', + ), + ); + + public function updateCMSFields(FieldList $fields) + { + $gridConfig2 = GridFieldConfig_RelationEditor::create(); + $gridConfig2->getComponentByType( + 'GridFieldAddExistingAutocompleter' + )->setSearchFields( + array('Title') + ); + $gridConfig2->getComponentByType('GridFieldPaginator')->setItemsPerPage(100); + + $gridField2 = new GridField( + 'MapMarkerSets', + 'MapMarker Sets', + $this->owner->MapMarkerSets(), + $gridConfig2 + ); + $fields->addFieldToTab('Root.MapMarkerSets', $gridField2); + } +} diff --git a/code/MapUtil.php b/code/MapUtil.php new file mode 100644 index 0000000..6beeb2d --- /dev/null +++ b/code/MapUtil.php @@ -0,0 +1,229 @@ +update('Mappable', 'language', 'en'); + } + + /** + * Set the API key for Google Maps. + * + * @param string $key + */ + public static function set_api_key($key) + { + self::$api_key = $key; + } + + /** + * @param bool $new_map_already_rendered + */ + public static function set_map_already_rendered($new_map_already_rendered) + { + self::$map_already_rendered = $new_map_already_rendered; + } + + public static function get_map_already_rendered() + { + return self::$map_already_rendered; + } + + /** + * Set the default size of the map. + * + * @param int $width + * @param int $height + */ + public static function set_map_size($width, $height) + { + self:: $map_width = $width; + self::$map_height = $height; + } + + /** + * FIXME - NOT USED? + * Set the type of the gmap. + * + * @param string $mapType (can be 'google.maps.MapTypeId.ROADMAP', 'G_SATELLITE_MAP', + * 'G_HYBRID_MAP', 'G_PHYSICAL_MAP') + */ + public static function set_map_type($mapType) + { + self::$map_type = $mapType; + } + + /** + * Set the center of the gmap (an address, using text geocoder query). + * + * @param string $center GoogleMap center (an address) + */ + public static function set_center($center) + { + self::$center = $center; + } + + /** + * Get a new GoogleMapAPI object and load it with the default settings. + * + * @return MapAPI + */ + public static function instance() + { + ++self::$instances; + + if (self::$allow_full_screen == null) { + self::$allow_full_screen = Config::inst()->get('Mappable', 'allow_full_screen'); + } + + $url = Director::absoluteBaseURL(); + + // remove http and https + $url = str_replace('http://', '', $url); + $url = str_replace('https://', '', $url); + $parts = explode('/', $url); + $host = $parts[0]; + + $key = self::$api_key; + + // if an array, get the key by an array keyed by host + if (is_array($key)) { + $key = $key[$host]; + } + + $gmap = new MapAPI($key); + $gmap->setDivId(self::$div_id.'_'.self::$instances); + $gmap->setEnableAutomaticCenterZoom(self::$automatic_center); + $gmap->setSize(self::$map_width, self::$map_height); + $gmap->setDefaultHideMarker(self::$hide_marker); + $gmap->setMapType(self::$map_type); + $gmap->setCenter(self::$center); + $gmap->setAllowFullScreen(self::$allow_full_screen); + $language = Config::inst()->get('Mappable', 'language'); + $gmap->setLang($language); + + return $gmap; + } + + /** + * Sanitize a string of HTML content for safe inclusion in the JavaScript + * for a Google Map. + * + * @return string + */ + public static function sanitize($content) + { + return addslashes(str_replace(array("\n", "\r", "\t"), '', $content)); + } + + /** + * Creates a new {@link GoogleMapsAPI} object loaded with the default settings + * and places all of the items in a {@link SS_List} + * e.g. {@link DataList} or {@link ArrayList} on the map. + * + * @param SS_List list of objects to display on a map + * @param array $infowindowtemplateparams Optional array of extra parameters to pass to the map info window + * + * @return MapAPI + */ + public static function get_map(SS_List $list, $infowindowtemplateparams) + { + $gmap = self::instance(); + if ($list) { + foreach ($list as $mappable) { + if (self::ChooseToAddDataobject($mappable)) { + $gmap->addMarkerAsObject($mappable, $infowindowtemplateparams); + } + } + } + + return $gmap; + } + + /** + * Determines if the current DataObject should be included to the map + * Checks if it has Mappable interface implemented + * If it has MapExtension included, the value of MapPinEdited is also checked. + * + * @param DataObject $do + * + * @return bool + */ + private static function ChooseToAddDataobject(DataObject $do) + { + $isMappable = $do->is_a('Mappable'); + + foreach ($do->getExtensionInstances() as $extension) { + $isMappable = $isMappable || $extension instanceof Mappable; + } + + $filterMapPinEdited = $do->hasExtension('MapExtension') + ? $do->MapPinEdited + : true; + + return $isMappable && $filterMapPinEdited; + } +} diff --git a/code/Mappable.php b/code/Mappable.php index 57a261c..67d7b78 100755 --- a/code/Mappable.php +++ b/code/Mappable.php @@ -5,62 +5,60 @@ * helper class. * * @author Uncle Cheese - * @package mappable */ -interface Mappable { +interface Mappable +{ + /** + * An accessor method for the latitude field. + * + * @example + * + * return $this->Lat; + * + * + * @return string + */ + public function getMappableLatitude(); - /** - * An accessor method for the latitude field. - * @example - * - * return $this->Lat; - * - * - * @return string - */ - public function getLatitude(); - + /** + * An accessor method for the longitude field. + * + * @example + * + * return $this->Long; + * + * + * @return string + */ + public function getMappableLongitude(); - /** - * An accessor method for the longitude field. - * @example - * - * return $this->Long; - * - * - * @return string - */ - public function getLongitude(); - - - /** - * An accessor method for the path to the marker pin for this point on the map. - * If null or false, use the default Google Maps icon. - * @example - * - * return "mysite/images/map_icon.png"; - * - * - * @return string - */ - public function getMapPin(); - - - /** - * An accessor method that returns the content for the map bubble popup. - * It is best to use the {@see ViewableData::renderWith()} method to take advantaging - * of templating syntax when rendering the object's content. - * - * Note: it is critical that the content be sanitized for safe inclusino in the rendered - * JavaScript code for the map. {@see GoogleMapsUtil::sanitize()} - * - * @example - * - * return GoogleMapsUtil::sanitize($this->renderWith('MapBubble')); - * - * - * @return string - */ - public function getMapContent(); + /** + * An accessor method for the path to the marker pin for this point on the map. + * If null or false, use the default Google Maps icon. + * + * @example + * + * return "mysite/images/map_icon.png"; + * + * + * @return string + */ + public function getMappableMapPin(); -} \ No newline at end of file + /** + * An accessor method that returns the content for the map bubble popup. + * It is best to use the {@see ViewableData::renderWith()} method to take advantaging + * of templating syntax when rendering the object's content. + * + * Note: it is critical that the content be sanitized for safe inclusino in the rendered + * JavaScript code for the map. {@see GoogleMapsUtil::sanitize()} + * + * @example + * + * return GoogleMapsUtil::sanitize($this->renderWith('MapBubble')); + * + * + * @return string + */ + public function getMappableMapContent(); +} diff --git a/code/MappableData.php b/code/MappableData.php index 51a06ac..e2ea118 100644 --- a/code/MappableData.php +++ b/code/MappableData.php @@ -6,34 +6,99 @@ * @author Uncle Cheese * @package mappable */ -class MappableData extends Extension { - - public function GoogleMap($width = null, $height = null) { - $gmap = GoogleMapUtil::get_map(new DataObjectSet($this->owner)); - $w = $width ? $width : GoogleMapUtil::$map_width; - $h = $height ? $height : GoogleMapUtil::$map_height; - $gmap->setSize($w,$h); - $gmap->setEnableAutomaticCenterZoom(false); - $gmap->setLatLongCenter(array( - '200', - '4', - $this->owner->getLatitude(), - $this->owner->getLongitude() - )); - - return $gmap; - } - public function StaticMap($width = null, $height = null) { - $w = $width ? $width : GoogleMapUtil::$map_width; - $h = $height ? $height : GoogleMapUtil::$map_height; - - $lat = $this->owner->getLatitude(); - $lng = $this->owner->getLongitude(); - - $src = htmlentities("http://maps.google.com/maps/api/staticmap?center=$lat,$lng&markers=$lat,$lng&zoom=13&size=${w}x$h&sensor=false"); - - return ''.$this->owner->Title.''; - - } - -} \ No newline at end of file +class MappableData extends Extension +{ + /** + * Optional template values for the map info windows. + */ + private $MarkerTemplateValues = null; + + /** + * URL of static maps api. + * + * @var string + */ + private static $staticmap_api_url = '//maps.googleapis.com/maps/api/staticmap'; + + /** + * Default zoom for static map. + * + * @var int + */ + private static $staticmap_default_zoom = 13; + + /** + * Pass through values to the markers so that when rendering the map info windows, these + * parameters are available to the template. This is of course optional. + * + * @param array $values hash array of template key to template value + */ + public function setMarkerTemplateValues($values) + { + $this->MarkerTemplateValues = $values; + } + + public function getRenderableMap($width = null, $height = null, $zoom = 9) + { + $gmap = MapUtil::get_map(new ArrayList(array($this->owner)), $this->MarkerTemplateValues); + $w = $width ? $width : MapUtil::$map_width; + $h = $height ? $height : MapUtil::$map_height; + $gmap->setSize($w, $h); + $gmap->setZoom($zoom); + $gmap->setEnableAutomaticCenterZoom(false); + if ($this->owner->MapPinEdited) { + $gmap->setLatLongCenter(array( + 'lat' => $this->owner->getMappableLatitude(), + 'lng' => $this->owner->getMappableLongitude(), + )); + } + + return $gmap; + } + + /** + * returns an with a src set to a static map picture. + * + * You can use MappableData.staticmap_api_url config var to set the domain of the static map. + * You can use MappableData.staticmap_default_zoom config var to set the default zoom for the static map. + * + * @uses Mappable::getMappableMapPin() to draw a special marker, be sure this image is publicly available + * + * @param int $width + * @param int $height + * + * @return string + */ + public function StaticMap($width, $height, $zoom = null, $mapType = 'roadmap') + { + $lat = $this->owner->getMappableLatitude(); + $lng = $this->owner->getMappableLongitude(); + $pin = $this->owner->getMappableMapPin(); + + // use provided zoom or set a default + if ($zoom == null) { + $zoom = Config::inst()->get('MappableData', 'staticmap_default_zoom'); + } + + //https://maps.googleapis.com/maps/api/staticmap?center=Berkeley,CA&zoom=14&size=400x400&key=YOUR_API_KEY + //maps.googleapis.com/maps/api/staticmap'; + + $apiurl = Config::inst()->get('MappableData', 'staticmap_api_url'); + + $urlparts = array( + 'center' => "$lat,$lng", + 'markers' => "$lat,$lng", + 'zoom' => $zoom, + 'size' => "{$width}x{$height}", + 'sensor' => 'false', //@todo: make sensor param configurable + 'maptype' => $mapType, + ); + if ($pin) { + $urlparts['markers'] = "icon:$pin|$lat,$lng"; + } + + $src = htmlentities($apiurl.'?'.http_build_query($urlparts)); + + return ''.$this->owner->Title.''; + } +} diff --git a/code/MappableDataObjectSet.php b/code/MappableDataObjectSet.php index 6baa209..9a28439 100755 --- a/code/MappableDataObjectSet.php +++ b/code/MappableDataObjectSet.php @@ -6,15 +6,31 @@ * @author Uncle Cheese * @package mappable */ -class MappableDataObjectSet extends Extension { +class MappableDataObjectSet extends Extension +{ + /** + * Optional template values for the map info windows. + */ + private $MarkerTemplateValues = null; - public function GoogleMap($width = null, $height = null) { - $gmap = GoogleMapUtil::get_map($this->owner); - $w = $width ? $width : GoogleMapUtil::$map_width; - $h = $height ? $height : GoogleMapUtil::$map_height; - $gmap->setSize($w,$h); - return $gmap; - } - - -} \ No newline at end of file + /** + * Pass through values to the markers so that when rendering the map info windows, these + * parameters are available to the template. This is of course optional. + * + * @param array $values hash array of template key to template value + */ + public function setMarkerTemplateValues($values) + { + $this->MarkerTemplateValues = $values; + } + + public function getRenderableMap($width = null, $height = null) + { + $gmap = MapUtil::get_map($this->owner, $this->MarkerTemplateValues); + $w = $width ? $width : MapUtil::$map_width; + $h = $height ? $height : MapUtil::$map_height; + $gmap->setSize($w, $h); + + return $gmap; + } +} diff --git a/code/shortcodes/GoogleMapShortCodeHandler.php b/code/shortcodes/GoogleMapShortCodeHandler.php new file mode 100644 index 0000000..f2536a1 --- /dev/null +++ b/code/shortcodes/GoogleMapShortCodeHandler.php @@ -0,0 +1,76 @@ + 5, + 'MapType' => 'road', + ); + + // ensure JavaScript for the map service is only downloaded once + $arguments['DownloadJS'] = !MapUtil::get_map_already_rendered(); + MapUtil::set_map_already_rendered(true); + + // convert parameters to CamelCase as per standard template conventions + $arguments['Latitude'] = $arguments['latitude']; + $arguments['Longitude'] = $arguments['longitude']; + + // optional parameter caption + if (isset($arguments['caption'])) { + $arguments['Caption'] = $arguments['caption']; + } + + if (isset($arguments['maptype'])) { + $arguments['MapType'] = $arguments['maptype']; + } + + // optional parameter zoom + if (isset($arguments['zoom'])) { + $arguments['Zoom'] = $arguments['zoom']; + } + + // the id of the dom element to be used to render the street view + $arguments['DomID'] = 'google_sc_map_'.self::$gsv_ctr; + + // fullscreen + $arguments['AllowFullScreen'] = Config::inst()->get('Mappable', 'allow_full_screen'); + + // incrememt the counter to ensure a unique id for each map canvas + ++self::$gsv_ctr; + + // merge defaults and arguments + $customised = array_merge($defaults, $arguments); + + // include JavaScript to be appended at the end of the page, namely params for map rendering + //Requirements::javascriptTemplate("mappable/javascript/google/map.google.template.js", $customised); + + //get map view template and render the HTML + $template = new SSViewer('GoogleMapShortCode'); + + //return the template customised with the parmameters + return $template->process(new ArrayData($customised)); + } + + /** + * This is only used for testing, otherwise the sequence of tests change the number returned. + */ + public static function resetCounter() + { + self::$gsv_ctr = 1; + } +} diff --git a/code/shortcodes/GoogleStreetViewShortCodeHandler.php b/code/shortcodes/GoogleStreetViewShortCodeHandler.php new file mode 100644 index 0000000..0bff756 --- /dev/null +++ b/code/shortcodes/GoogleStreetViewShortCodeHandler.php @@ -0,0 +1,79 @@ + 1, + 'Pitch' => 0, + ); + + // ensure JavaScript for the map service is only downloaded once + $arguments['DownloadJS'] = !MapUtil::get_map_already_rendered(); + MapUtil::set_map_already_rendered(true); + + // convert parameters to CamelCase as per standard template conventions + $arguments['Latitude'] = $arguments['latitude']; + $arguments['Longitude'] = $arguments['longitude']; + $arguments['Heading'] = $arguments['heading']; + + // optional parameter caption + if (isset($arguments['caption'])) { + $arguments['Caption'] = $arguments['caption']; + } + + // optional parameter pitch + if (isset($arguments['pitch'])) { + $arguments['Pitch'] = $arguments['pitch']; + } + + // optional parameter zoom + if (isset($arguments['zoom'])) { + $arguments['Zoom'] = $arguments['zoom']; + } + + // the id of the dom element to be used to render the street view + $arguments['DomID'] = 'google_streetview_'.self::$gsv_ctr; + + // incrememt the counter to ensure a unique id for each map canvas + ++self::$gsv_ctr; + + // merge defaults and arguments + $customised = array_merge($defaults, $arguments); + + // Include google maps JS at the end of the page + //Requirements::javascriptTemplate("mappable/javascript/google/streetview.google.template.js", $customised); + + //get streetview template template + $template = new SSViewer('GoogleStreetView'); + + //return the template customised with the parmameters + return $template->process(new ArrayData($customised)); + } + + /** + * This is only used for testing, otherwise the sequence of tests change the number returned. + */ + public static function resetCounter() + { + self::$gsv_ctr = 1; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..76dc847 --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "weboftalent/mappable", + "description": "Fork of Uncle Cheese's original Mappable module. Inline JavaScript is minimal, and templated such that other mapping systems can be added later. A map editing interface is also included, addable by a one line configuration call.", + "type": "silverstripe-module", + "keywords": ["silverstripe", "google maps", "maps", "mappable"], + "authors": [ + { + "name": "Gordon Anderson", + "email": "gordon.b.anderson@gmail.com", + "homepage": "https://github.com/gordonbanderson/Mappable", + "role": "Developer" + } + ], + "require": { + "silverstripe/cms": "~3.1" + }, + "suggest": { + "weboftalent/mappable-poi": "1.*" + }, + "support": { + "issues": "https://github.com/gordonbanderson/Mappable/issues" + }, + "license": "BSD-3-Clause" +} diff --git a/css/lat_long_field.css b/css/lat_long_field.css index e69de29..13147b2 100755 --- a/css/lat_long_field.css +++ b/css/lat_long_field.css @@ -0,0 +1,7 @@ +#LatLonZoomLevel .fieldholder-small { + margin: 0px; +} + +#LatLonZoomLevel .fieldholder-small-label { + margin: 0px; +} diff --git a/css/mapField.css b/css/mapField.css new file mode 100644 index 0000000..8045f6c --- /dev/null +++ b/css/mapField.css @@ -0,0 +1,25 @@ +.geocodedSearchResults li { + cursor: pointer; + font-weight: bold; + font-size: 120%; + padding-top: 5px; + padding-bottom: 5px; +} + +.editableMap { + height: 300px; + /* width: 90%;*/ + float: none; +} + +.editableMapWrapper { + display: inline-block; + width: 900px; + float: none; + margin-left: auto; + margin-right: auto; +} + +.fieldholder-small { + display: none; +} diff --git a/css/mapField.min.css b/css/mapField.min.css new file mode 100644 index 0000000..1fe477d --- /dev/null +++ b/css/mapField.min.css @@ -0,0 +1 @@ +.geocodedSearchResults li{cursor:pointer;font-weight:700;font-size:120%;padding-top:5px;padding-bottom:5px}.editableMap{height:300px;float:none}.editableMapWrapper{display:inline-block;width:900px;float:none;margin-left:auto;margin-right:auto}.fieldholder-small{display:none} \ No newline at end of file diff --git a/css/mapsimple.css b/css/mapsimple.css new file mode 100644 index 0000000..e95477a --- /dev/null +++ b/css/mapsimple.css @@ -0,0 +1,15 @@ +.streetview, .map { + width: 100%; + height: 500px; + background: none; +} + +// see http://stackoverflow.com/questions/11340468/street-view-not-working-in-firefox +.googlestreetview img, .map img { + border: none !important; + max-width: none !important; +} + +.streetviewcontainer p.caption, .googlemapcontainer p.caption { + text-align: center; +} diff --git a/docs/Credits.md b/docs/Credits.md new file mode 100644 index 0000000..1bd359e --- /dev/null +++ b/docs/Credits.md @@ -0,0 +1,6 @@ +# Credits + +## Map Menu Icons +Sourced from http://www.flaticon.com/free-icon/map-location_32364 under the Creative Commons License. + +
Icon made by Freepik from www.flaticon.com is licensed under CC BY 3.0
diff --git a/docs/en/AddingLinesToMaps.md b/docs/en/AddingLinesToMaps.md new file mode 100644 index 0000000..d3b120e --- /dev/null +++ b/docs/en/AddingLinesToMaps.md @@ -0,0 +1,48 @@ +#Adding Lines to Maps +A line can be added to a map with the following API call: + +```php +$map->addLine( $point1, $point2, $colorHexCode ); +``` + +Each point is an array whose 0th element is the latitude and 1st element is the longitude. +The third parameter is optional and represents the color of the line in standard CSS hex code +colors (RGB). + +An example method to draw a multicolored triangle on a map is as follows: + +```php +/* +Render a triangle around the provided lat,lon, zoom from the editing functions, +*/ +public function MapWithLines() { + $map = $this->owner->getRenderableMap(); + $map->setZoom( $this->ZoomLevel ); + $map->setAdditionalCSSClasses( 'fullWidthMap' ); + $map->setShowInlineMapDivStyle( true ); + + $scale = 0.3; + + // draw a triangle + $point1 = array( + $this->Lat - 0.5*$scale, $this->Lon + ); + $point2 = array( + $this->Lat + 0.5*$scale, $this->Lon-0.7*$scale + ); + + $point3 = array( + $this->Lat + 0.5*$scale, $this->Lon+0.7*$scale + ); + + $map->addLine( $point1, $point2 ); + $map->addLine( $point2, $point3, '#000077' ); + $map->addLine( $point3, $point1, '#007700' ); + + return $map; +} +``` + +Instead of calling $BasicMap call $MapWithLines instead from the template. + +See http://demo.weboftalent.asia/mappable/map-with-lines/ for a working demo. diff --git a/docs/en/AddingMapToADataObject.md b/docs/en/AddingMapToADataObject.md new file mode 100644 index 0000000..cffc083 --- /dev/null +++ b/docs/en/AddingMapToADataObject.md @@ -0,0 +1,44 @@ +#Adding a Map to a DataObject +##Add Extension +Using the standard method of adding extensions in SilverStripe 3.1, add an extension called +'MapExtension' to relevant DataObjects. +```yml +--- +name: weboftalent-example-map-extensions +--- +PageWithMap: + extensions: + ['MapExtension'] +``` +##Editing +Utilising the extensions adds Latitude, Longitude and Zoom fields to the DataObject in question, +in the example above 'PageWithMap'. In addition, the admin interface for PageWithMap now has a +location tab. Location can be changed in 3 ways: +* Use the geocoder and search for a place name +![Map Editing - Searching for a Place Name] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/mapedit-search.png?raw=true +"Map Editing - Searching for a Place Name") +* Drag the map pin +* Right click ![Map Editing - Right Clicking on a Map] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/mapedit-rightclick.png?raw=true +"Map Editing - Right Clicking on a Map") + +The zoom level set by the content editor is also saved. + +##Templates +To render a map in the template, simply called $BasicMap + +```php +

$Title

+$Content +$BasicMap +``` + +For an example of this, +see http://demo.weboftalent.asia/mappable/quick-map-adding-a-map-to-a-dataobject/ + +##Custom Popup +When clicking on the pin a popup with the name of the pin will occur. + +If you want to change the information displayed there you have to setup a template named by the +decorated DataObject suffixed with `_MapInfoWindow`, e.g. `MyPage_MapInfoWindow`. diff --git a/docs/en/GoogleMapShortCodes.md b/docs/en/GoogleMapShortCodes.md new file mode 100644 index 0000000..8a5546b --- /dev/null +++ b/docs/en/GoogleMapShortCodes.md @@ -0,0 +1,48 @@ +# Google Map Short Codes +A content editor can embed a Google Map into their content using a short code as follows: +``` +[GoogleMap latitude='13.2' longitude='100.4519' caption="Test Google Map" zoom="14" maptype="road"] +``` +The parameters latitude and longitude are required. Zoom defaults to 5 and the map type to road. + +Valid map types are +* road +* aerial +* hybrid +* terrain + +# Map Type - Road +``` +[GoogleMap latitude='13.7402946' longitude='100.5525439' caption="Roads in Central Bangkok" +zoom="14" maptype="road"] +``` +![Google Maps - Map Type, Road] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/maptyperoad.png?raw=true +"Google Maps - Map Type, Road") + +# Map Type - Aerial +``` +[GoogleMap latitude='13.815483' longitude='100.5447213' caption="Bang Sue Train Depot, Thailand" +zoom="20" maptype="aerial"] +``` +![Google Maps - Map Type, Aerial] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/maptype-aerial.png?raw=true +"Google Maps - Map Type, Aerial") + +# Map Type - Hybrid +``` +[GoogleMap latitude='13.8309545' longitude='100.5577219' caption="Junction in Bangkok, Thailand" +zoom="18" maptype="hybrid"] +``` +![Google Maps - Map Type, Hybrid] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/maptype-hybrid.png?raw=true +"Google Maps - Map Type, Hybrid") + +* Map Type - Terrain +``` +[GoogleMap latitude='18.8032393' longitude='98.9166518' caption="Mountains west of Chiang Mai" +zoom="14" maptype="terrain"] +``` +![Google Maps - Map Type, Terrain] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/maptype-terrain.png?raw=true +"Google Maps - Map Type, Terrain") diff --git a/docs/en/GoogleStreetViewShortCodes.md b/docs/en/GoogleStreetViewShortCodes.md new file mode 100644 index 0000000..c14e569 --- /dev/null +++ b/docs/en/GoogleStreetViewShortCodes.md @@ -0,0 +1,32 @@ +# Google Street View +One can embed Google Street View in a page as an editor using a short code of the following format: +``` +[GoogleStreetView latitude="13.811841" longitude="100.527309" heading="162.43" pitch="-10" +caption="Canal south from Pracha Rat 1 Soi 28"] +``` +The parameters latitude, longitude, and heading are required. If not provided the short code will +return an empty string. + +For rendering purposes including CSS similar to below in your theme: +```css +.streetview { + width: 100%; + height: 500px; + background: #EEE; +} + +/* Ensure map controls are correct aspect ration, and thatFirefox rendering work, +see http://stackoverflow.com/questions/11340468/street-view-not-working-in-firefox */ +.streetview img { + border: none !important; + max-width: none !important; +} + +.streetviewContainer p.caption { + text-align: center; +} +``` +##Example Rendering +![Example Google Street View Rendering] +(https://raw.githubusercontent.com/gordonbanderson/Mappable/screenshots/screenshots/google-streeview-shortcode-example.png?raw=true +"Example Google Street View Rendering") diff --git a/docs/en/Installation.md b/docs/en/Installation.md new file mode 100644 index 0000000..5f936ea --- /dev/null +++ b/docs/en/Installation.md @@ -0,0 +1,14 @@ +#Installation +There are two options. Run these commands from your project root. + +## Composer +``` +composer require weboftalent/mappable +``` + +## Cloning from git +``` +git clone git@github.com:gordonbanderson/Mappable.git mappable +cd mappable +git checkout 3.1 +``` diff --git a/docs/en/MapLayers.md b/docs/en/MapLayers.md new file mode 100644 index 0000000..8cf0ceb --- /dev/null +++ b/docs/en/MapLayers.md @@ -0,0 +1,45 @@ +#Map Layers +KML layers can be added through the CMS by adding an extension to the class in question. + +```php + +``` + +Add this to extensions.yml + +```yml +PageWithMapAndLayers: + extensions: + ['MapExtension', 'MapLayerExtension'] +``` + +Execute a /dev/build to update your database with the map layers relationship. + +When you add a new page of type PageWithMapAndLayers, there is now an extra tab called 'Map Layers'. +Each layer consists of a human readable name and a file attachment, which in this case have to be +KML files. + +Templating is the same as before, the $BasicMap method takes account of layers when rendering a map. + +##Gotchas +Note you will not be able to see map layers in your dev environment, as the KML file URL needs to be +publicly visible in order that Google's servers can render them. + +##Example Rendering +The following screenshot is of an exported KML file from http://www.plotaroute.com/route/43228, a +cycle route along canals avoiding main roads. + +![Safe Cycle Route in Bangkok] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/maplayers.png?raw=true +"Safe Cycle Route in Bangkok") diff --git a/docs/en/MappingDataList.md b/docs/en/MappingDataList.md new file mode 100644 index 0000000..1321721 --- /dev/null +++ b/docs/en/MappingDataList.md @@ -0,0 +1,45 @@ +#Mapping a DataList + +The principle difference from a simple map is that the renderable map is obtained from the DataList +itself. The objects in the DataList must implement the mappable interface, or use the extension +called MapExtension. This requirement means that all the objects will have the necessary +information to be rendered as a marker on a map. + +The following example is the code that renders the map on this page. Note that the clusterer on +this page is only invoked if the checkbox 'Cluster Example Dataset' is set to true, in this case +it is not. + +```php +public function MapWithDataList() { + $flickrPhotos = DataList::create( 'FlickrPhoto' )->where( 'Lat != 0 AND Lon !=0' ); + if ( $flickrPhotos->count() == 0 ) { + return ''; // don't render a map + } + + $map = $flickrPhotos->getRenderableMap(); + $map->setZoom( $this->ZoomLevel ); + $map->setAdditionalCSSClasses( 'fullWidthMap' ); + $map->setShowInlineMapDivStyle( true ); + if ( $this->ClusterExampleDataset ) { + $map->setClusterer( true ); + } + + return $map; +} + ``` + +The map is positioned so that it shows all of the points automatically. Also note that the host +page of the map does not require to implement mappable or even have a location attached to it, as +the map is rendered entirely from the DataList, in this case $flickrPhotos. + +For clustered and unclustered examples, see: +http://demo.weboftalent.asia/mappable/map-from-a-datalist-unclustered/ +![Non Clustered DataList] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/datalist-unclustered.png?raw=true +"Non Clustered DataList") + +and http://demo.weboftalent.asia/mappable/map-with-datalist-clustered-markers/ + +![Clustered DataList] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/datalist-clustered.png?raw=true +"Clustered DataList") diff --git a/docs/en/MapsWithLines.md b/docs/en/MapsWithLines.md new file mode 100644 index 0000000..552d9b6 --- /dev/null +++ b/docs/en/MapsWithLines.md @@ -0,0 +1,52 @@ +#Adding Lines to Maps +A line can be added to a map with the following API call: + +``` + $map->addLine( $point1, $point2, $colorHexCode ); +``` + +Each point is an array whose 0th element is the latitude and 1st element is the longitude. The +third parameter is optional and represents the color of the line in standard CSS hex code colors +(RGB). + +An example method to draw a multicolored triangle on a map is as follows: + +```php +/* +Render a triangle around the provided lat,lon, zoom from the editing functions, +*/ +public function MapWithLines() { + $map = $this->owner->getRenderableMap(); + $map->setZoom( $this->ZoomLevel ); + $map->setAdditionalCSSClasses( 'fullWidthMap' ); + $map->setShowInlineMapDivStyle( true ); + + $scale = 0.3; + + // draw a triangle + $point1 = array( + $this->Lat - 0.5*$scale, $this->Lon + ); + $point2 = array( + $this->Lat + 0.5*$scale, $this->Lon-0.7*$scale + ); + + $point3 = array( + $this->Lat + 0.5*$scale, $this->Lon+0.7*$scale + ); + + $map->addLine( $point1, $point2 ); + $map->addLine( $point2, $point3, '#000077' ); + $map->addLine( $point3, $point1, '#007700' ); + + return $map; +} +``` + +Instead of calling $BasicMap call $MapWithLines instead from the template. + +See http://demo.weboftalent.asia/mappable/map-with-lines/ for a working demo. + +![Map With Lines] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/mapswithlines.png?raw=true +Map With Lines") diff --git a/docs/en/MayStyles.md b/docs/en/MayStyles.md new file mode 100644 index 0000000..6b22662 --- /dev/null +++ b/docs/en/MayStyles.md @@ -0,0 +1,38 @@ +#Map Styles +The color of a rendered Google map can be changed by setting style parameters in JSON format. For +background on this see https://developers.google.com/maps/documentation/javascript/styling + +``` + $map->setMapStyle( $jsonStyle ); +``` + +A good source of styles to get started with is https://snazzymaps.com/explore, a worked example +appears below. The only difference with normal map rendering is the addition of the setStyle call. + +```php +public function Map() { + $stations = OpenWeatherMapStation::get(); + $vars = array( + 'Link' => $this->Link() + ); + $stations->setMarkerTemplateValues($vars); + $map = $stations->getRenderableMap()-> + setZoom($this->owner->ZoomLevel)-> + setAdditionalCSSClasses('fullWidthMap'); + $map->setEnableAutomaticCenterZoom(true); + $map->setZoom(10); + $map->setShowInlineMapDivStyle(true); + $map->setClusterer(true); + $map->CurrentURL = $this->Link(); + + $style = '[{"featureType":"landscape","stylers":[{"saturation":-100},{"lightness":65},{"visibility":"on"}]},{"featureType":"poi","stylers":[{"saturation":-100},{"lightness":51},{"visibility":"simplified"}]},{"featureType":"road.highway","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"road.arterial","stylers":[{"saturation":-100},{"lightness":30},{"visibility":"on"}]},{"featureType":"road.local","stylers":[{"saturation":-100},{"lightness":40},{"visibility":"on"}]},{"featureType":"transit","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"administrative.province","stylers":[{"visibility":"off"}]},{"featureType":"water","elementType":"labels","stylers":[{"visibility":"on"},{"lightness":-25},{"saturation":-100}]},{"featureType":"water","elementType":"geometry","stylers":[{"hue":"#ffff00"},{"lightness":-25},{"saturation":-97}]}]'; + + $map->setMapStyle($style); + + return $map; + } +``` + +![Styled Map] +(https://github.com/gordonbanderson/Mappable/blob/screenshots/screenshots/styledmap.png?raw=true +Styled Map") diff --git a/docs/en/MultipleMapsSamePage.md b/docs/en/MultipleMapsSamePage.md new file mode 100644 index 0000000..c9311b8 --- /dev/null +++ b/docs/en/MultipleMapsSamePage.md @@ -0,0 +1,85 @@ +#Multiple Maps on the Same Page +Multiple maps can be added to a page. One option is to associate maps with a DataObject whose +relationship to the parent page is 'has many', as in this example of contact page addresses. +##Example +Often a company will have more than one office location that they wish to display, this is a an +example of that use case. It would probably need expanding in order to show the likes of email +address and telephone number, left as an exercise for the reader. + +Firstly, create a parent container page called ContactPage, this has many locations of type +ContactPageAddress. +```php + 'ContactPageAddress' + ); + + function getCMSFields() { + $fields = parent::getCMSFields(); + + $gridConfig = GridFieldConfig_RelationEditor::create(); + $gridConfig->getComponentByType( 'GridFieldAddExistingAutocompleter' )->setSearchFields( array( 'PostalAddress' ) ); + $gridConfig->getComponentByType( 'GridFieldPaginator' )->setItemsPerPage( 100 ); + $gridField = new GridField( "Locations", "List of Addresses:", $this->Locations(), $gridConfig ); + $fields->addFieldToTab( "Root.Addresses", $gridField ); + + return $fields; + } +} + +class ContactPage_Controller extends Page_Controller { + + +} + +``` + +The latter contains the actual map for each location, configured as above using extensions.yml + +```php + 'Text' + ); + + static $has_one = array( 'ContactPage' => 'ContactPage' ); + + + public static $summary_fields = array( + 'PostalAddress' => 'PostalAddress' + ); + + + function getCMSFields() { + $fields = new FieldList(); + $fields->push( new TabSet( "Root", $mainTab = new Tab( "Main" ) ) ); + $mainTab->setTitle( _t( 'SiteTree.TABMAIN', "Main" ) ); + $fields->addFieldToTab( "Root.Main", new TextField( 'PostalAddress' ) ); + + $this->extend( 'updateCMSFields', $fields ); + + return $fields; + } +} +``` + +The template simply loops through the contact page addresses, rendering a map. + +``` +

$Title

+$BriefDescription + +

Addresses

+ +<% loop Locations %> +

$PostalAddress

+$BasicMap +<% end_loop %> + +$Content +``` + +See http://demo.weboftalent.asia/mappable/multiple-maps-on-the-same-page/ for a working demo. diff --git a/geocoder/MappableGeocoder.php b/geocoder/MappableGeocoder.php new file mode 100644 index 0000000..2032250 --- /dev/null +++ b/geocoder/MappableGeocoder.php @@ -0,0 +1,12 @@ +load($cacheKey))) { + if ($json = @file_get_contents( + "http://maps.googleapis.com/maps/api/geocode/json?sensor=false&address=". + urlencode($searchString) + )) { + $response = Convert::json2array($json); + + if ($response['status'] != 'OK') { + if ($response['status'] == 'ZERO_RESULTS') { + $locations = array(); + } else { + throw new Exception('Google status returned error'); + } + } else { + $locations = $response['results']; + } + + // save result in cache + $cache->save($json, $cacheKey); + } + } else { + $cached = true; + } + + if ($cached) { + $response = Convert::json2array($json); + $locations = $response['results']; + } + return $locations; + } +} diff --git a/icons/menuicon.png b/icons/menuicon.png new file mode 100644 index 0000000..2c4d4ae Binary files /dev/null and b/icons/menuicon.png differ diff --git a/javascript/clusterer.js b/javascript/clusterer.js deleted file mode 100644 index 986d19a..0000000 --- a/javascript/clusterer.js +++ /dev/null @@ -1 +0,0 @@ -eval(function(p,a,c,k,e,r){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('7 37(n,v,w){4 o=[];4 m=n;4 t=z;4 q=3;4 r=20;4 x=[36,30,2R,2E,2z];4 s=[];4 u=[];4 p=z;4 i=0;A(i=1;i<=5;++i){s.O({\'18\':"1V://35-31-2Z.2W.2Q/2K/2C/2B/2y/m"+i+".2u",\'S\':x[i-1],\'Z\':x[i-1]})}6(F w==="X"&&w!==z){6(F w.1f==="13"&&w.1f>0){r=w.1f}6(F w.1y==="13"){t=w.1y}6(F w.14==="X"&&w.14!==z&&w.14.9!==0){s=w.14}}7 1t(){6(u.9===0){8}4 a=[];A(i=0;i=0;--i){q.Q(a[i].C,G,a[i].I,b,G)}1t()}3.Q=7(g,j,b,h,a){6(a!==G){6(!1p(g)){u.O(g);8}}4 f=b;4 d=h;4 e=m.M(g.1o());6(F f!=="2A"){f=T}6(F d!=="X"||d===z){d=o}4 k=d.9;4 c=z;A(4 i=k-1;i>=0;i--){c=d[i];4 l=c.1L();6(l===z){1I}l=m.M(l);6(e.x>=l.x-r&&e.x<=l.x+r&&e.y>=l.y-r&&e.y<=l.y+r){c.Q({\'I\':f,\'C\':g});6(!j){c.L()}8}}c=R 1J(3,n);c.Q({\'I\':f,\'C\':g});6(!j){c.L()}d.O(c);6(d!==o){o.O(c)}};3.1C=7(a){A(4 i=0;ia.x)){e=T}6(e&&(b.y+fg.y)){e=T}8 e};3.1L=7(){8 o};3.Q=7(a){6(o===z){o=a.C.1o()}n.O(a)};3.1C=7(a){A(4 i=0;i=a||3.Y()===1){A(i=0;i0&&3.D[0]<3.H){g+=\'S:\'+(3.H-3.D[0])+\'B;1H-1g:\'+3.D[0]+\'B;\'}N{g+=\'S:\'+3.H+\'B;1G-S:\'+3.H+\'B;\'}6(F 3.D[1]==="13"&&3.D[1]>0&&3.D[1]<3.P){g+=\'Z:\'+(3.P-3.D[1])+\'B;1H-1i:\'+3.D[1]+\'B;\'}N{g+=\'Z:\'+3.P+\'B;1F-1E:1D;\'}}N{g+=\'S:\'+3.H+\'B;1G-S:\'+3.H+\'B;\';g+=\'Z:\'+3.P+\'B;1F-1E:1D;\'}4 k=3.19?3.19:\'2x\';j.U.2w=g+\'2v:2t;1g:\'+f.y+"B;1i:"+f.x+"B;2D:"+k+";2s:2F;1h-2r:2q;"+\'1h-2p:2o,2n-2m;1h-2N:2l\';j.2k=3.1m;i.2j(2i).2h(j);4 e=3.1l;17.2g(j,"2f",7(){4 a=i.M(h);4 d=R 1Q(a.x-e,a.y+e);d=i.1B(d);4 b=R 1Q(a.x+e,a.y-e);b=i.1B(b);4 c=i.2e(R 2d(d,b),i.2c());i.2b(h,c)});3.K=j};E.J.1K=7(){3.K.2a.33(3.K)};E.J.28=7(){8 R E(3.15,3.1T,3.1m,3.1R,3.1l)};E.J.1q=7(a){6(!a){8}4 b=3.1P.M(3.15);b.x-=V(3.P/2,10);b.y-=V(3.H/2,10);3.K.U.1g=b.y+"B";3.K.U.1i=b.x+"B"};E.J.1k=7(){3.K.U.1d="1z"};E.J.1a=7(){3.K.U.1d=""};E.J.11=7(){8 3.K.U.1d==="1z"};',62,194,'|||this|var||if|function|return|length||||||||||||||||||||||||||null|for|px|marker|anchor_|ClusterMarker_|typeof|true|height_|isAdded|prototype|div_|redraw_|fromLatLngToDivPixel|else|push|width_|addMarker|new|height|false|style|parseInt|getZoom|object|getTotalMarkers|width||isHidden|clearMarkers|number|styles|latlng_|url_|GEvent|url|textColor_|show|getBounds|removeOverlay|display|getGridSize_|gridSize|top|font|left|getClustersInViewport_|hide|padding_|text_|isInBounds|getLatLng|isMarkerInViewport_|redraw|addOverlay|getStyles_|addLeftMarkers_|addMarkers|splice|getMarkers|getCurrentZoom|maxZoom|none|resetViewport|fromDivPixelToLatLng|removeMarker|center|align|text|line|padding|continue|Cluster|remove|getCenter|getMap_|getMaxZoom_|document|map_|GPoint|styles_|reAddMarkers_|index_|opt_textColor|http|removeListener|while|undefined|getMaximumResolution|60|getCurrentMapType|pow|Math|getNorthEast|getSouthWest|moveend|addListener|copy|getTotalClusters|parentNode|setCenter|getSize|GLatLngBounds|getBoundsZoomLevel|click|addDomListener|appendChild|G_MAP_MAP_PANE|getPane|innerHTML|bold|serif|sans|Arial|family|11px|size|position|pointer|png|cursor|cssText|black|images|90|boolean|markerclusterer|trunk|color|78|absolute|background|src|scale|sizingMethod|svn|AlphaImageLoader|Microsoft|weight|DXImageTransform|progid|com|66|filter|all|div|createElement|googlecode|initialize|GOverlay|library|56|utility|opt_anchor|removeChild|containsLatLng|gmaps|53|MarkerClusterer'.split('|'),0,{})) \ No newline at end of file diff --git a/javascript/geoPosition.js b/javascript/geoPosition.js new file mode 100644 index 0000000..b8d74f4 --- /dev/null +++ b/javascript/geoPosition.js @@ -0,0 +1,263 @@ +// +// javascript-mobile-desktop-geolocation +// https://github.com/estebanav/javascript-mobile-desktop-geolocation +// +// Copyright J. Esteban Acosta Villafañe +// Licensed under the MIT licenses. +// +// Based on Stan Wiechers > geo-location-javascript v0.4.8 > http://code.google.com/p/geo-location-javascript/ +// +// Revision: $Rev: 01 $: +// Author: $Author: estebanav $: +// Date: $Date: 2012-09-07 23:03:53 -0300 (Fri, 07 Sep 2012) $: + +var bb = { + success: 0, + error: 0, + blackberryTimeoutId : -1 + }; + +function handleBlackBerryLocationTimeout() +{ + if(bb.blackberryTimeoutId!=-1) { + bb.error({ message: "Timeout error", + code: 3 + }); + } +} +function handleBlackBerryLocation() +{ + clearTimeout(bb.blackberryTimeoutId); + bb.blackberryTimeoutId=-1; + if (bb.success && bb.error) { + if(blackberry.location.latitude==0 && blackberry.location.longitude==0) { + //http://dev.w3.org/geo/api/spec-source.html#position_unavailable_error + //POSITION_UNAVAILABLE (numeric value 2) + bb.error({message:"Position unavailable", code:2}); + } + else + { + var timestamp=null; + //only available with 4.6 and later + //http://na.blackberry.com/eng/deliverables/8861/blackberry_location_568404_11.jsp + if (blackberry.location.timestamp) + { + timestamp = new Date( blackberry.location.timestamp ); + } + bb.success( { timestamp: timestamp , + coords: { + latitude: blackberry.location.latitude, + longitude: blackberry.location.longitude + } + }); + } + //since blackberry.location.removeLocationUpdate(); + //is not working as described http://na.blackberry.com/eng/deliverables/8861/blackberry_location_removeLocationUpdate_568409_11.jsp + //the callback are set to null to indicate that the job is done + + bb.success = null; + bb.error = null; + } +} + +var geoPosition=function() { + + var pub = {}; + var provider=null; + var u="undefined"; + var ipGeolocationSrv = 'http://freegeoip.net/json/?callback=JSONPCallback'; + + pub.getCurrentPosition = function(success,error,opts) + { + provider.getCurrentPosition(success, error,opts); + } + + pub.jsonp = { + callbackCounter: 0, + + fetch: function(url, callback) { + var fn = 'JSONPCallback_' + this.callbackCounter++; + window[fn] = this.evalJSONP(callback); + url = url.replace('=JSONPCallback', '=' + fn); + + var scriptTag = document.createElement('SCRIPT'); + scriptTag.src = url; + document.getElementsByTagName('HEAD')[0].appendChild(scriptTag); + }, + + evalJSONP: function(callback) { + return function(data) { + callback(data); + } + } + }; + + pub.confirmation = function() + { + return confirm('This Webpage wants to track your physical location.\nDo you allow it?'); + }; + + pub.init = function() + { + try + { + var hasGeolocation = typeof(navigator.geolocation)!=u; + if( !hasGeolocation ){ + if( !pub.confirmation() ){ + return false; + } + } + + if ( ( typeof(geoPositionSimulator)!=u ) && (geoPositionSimulator.length > 0 ) ){ + provider=geoPositionSimulator; + } else if (typeof(bondi)!=u && typeof(bondi.geolocation)!=u ) { + provider=bondi.geolocation; + } else if ( hasGeolocation ) { + provider=navigator.geolocation; + pub.getCurrentPosition = function(success, error, opts) { + function _success(p) { + //for mozilla geode,it returns the coordinates slightly differently + var params; + if(typeof(p.latitude)!=u) { + params = { + timestamp: p.timestamp, + coords: { + latitude: p.latitude, + longitude: p.longitude + } + }; + } else { + params = p; + } + success( params ); + } + provider.getCurrentPosition(_success,error,opts); + } + } else if(typeof(window.blackberry)!=u && blackberry.location.GPSSupported) { + // set to autonomous mode + if(typeof(blackberry.location.setAidMode)==u) { + return false; + } + blackberry.location.setAidMode(2); + //override default method implementation + pub.getCurrentPosition = function(success,error,opts) + { + //passing over callbacks as parameter didn't work consistently + //in the onLocationUpdate method, thats why they have to be set outside + bb.success = success; + bb.error = error; + //function needs to be a string according to + //http://www.tonybunce.com/2008/05/08/Blackberry-Browser-Amp-GPS.aspx + if(opts['timeout']) { + bb.blackberryTimeoutId = setTimeout("handleBlackBerryLocationTimeout()",opts['timeout']); + } else { + //default timeout when none is given to prevent a hanging script + bb.blackberryTimeoutId = setTimeout("handleBlackBerryLocationTimeout()",60000); + } + blackberry.location.onLocationUpdate("handleBlackBerryLocation()"); + blackberry.location.refreshLocation(); + } + provider = blackberry.location; + + } else if ( typeof(Mojo) !=u && typeof(Mojo.Service.Request)!="Mojo.Service.Request") { + provider = true; + pub.getCurrentPosition = function(success, error, opts) { + parameters = {}; + if( opts ) { + //http://developer.palm.com/index.php?option=com_content&view=article&id=1673#GPS-getCurrentPosition + if (opts.enableHighAccuracy && opts.enableHighAccuracy == true ){ + parameters.accuracy = 1; + } + if ( opts.maximumAge ) { + parameters.maximumAge = opts.maximumAge; + } + if (opts.responseTime) { + if( opts.responseTime < 5 ) { + parameters.responseTime = 1; + } else if ( opts.responseTime < 20 ) { + parameters.responseTime = 2; + } else { + parameters.timeout = 3; + } + } + } + + r = new Mojo.Service.Request( 'palm://com.palm.location' , { + method:"getCurrentPosition", + parameters:parameters, + onSuccess: function( p ){ + success( { timestamp: p.timestamp, + coords: { + latitude: p.latitude, + longitude: p.longitude, + heading: p.heading + } + }); + }, + onFailure: function( e ){ + if (e.errorCode==1) { + error({ code: 3, + message: "Timeout" + }); + } else if (e.errorCode==2){ + error({ code: 2, + message: "Position unavailable" + }); + } else { + error({ code: 0, + message: "Unknown Error: webOS-code" + errorCode + }); + } + } + }); + } + + } + else if (typeof(device)!=u && typeof(device.getServiceObject)!=u) { + provider=device.getServiceObject("Service.Location", "ILocation"); + + //override default method implementation + pub.getCurrentPosition = function(success, error, opts){ + function callback(transId, eventCode, result) { + if (eventCode == 4) { + error({message:"Position unavailable", code:2}); + } else { + //no timestamp of location given? + success( { timestamp:null, + coords: { + latitude: result.ReturnValue.Latitude, + longitude: result.ReturnValue.Longitude, + altitude: result.ReturnValue.Altitude, + heading: result.ReturnValue.Heading } + }); + } + } + //location criteria + + var criteria = new Object(); + criteria.LocationInformationClass = "BasicLocationInformation"; + //make the call + provider.ILocation.GetLocation(criteria,callback); + } + } else { + pub.getCurrentPosition = function(success, error, opts) { + pub.jsonp.fetch(ipGeolocationSrv, + function( p ){ success( { timestamp: p.timestamp, + coords: { + latitude: p.latitude, + longitude: p.longitude, + heading: p.heading + } + });}); + } + provider = true; + } + } + catch (e){ + if( typeof(console) != u ) console.log(e); + return false; + } + return provider!=null; + } + return pub; +}(); diff --git a/javascript/google/FullScreenControl.js b/javascript/google/FullScreenControl.js new file mode 100644 index 0000000..8e5a3ac --- /dev/null +++ b/javascript/google/FullScreenControl.js @@ -0,0 +1,137 @@ +/// +function FullScreenControl(map, enterFull, exitFull) { + if (enterFull === void 0) { enterFull = null; } + if (exitFull === void 0) { exitFull = null; } + if (enterFull == null) { + enterFull = "Full screen"; + } + if (exitFull == null) { + exitFull = "Exit full screen"; + } + var controlDiv = document.createElement("div"); + controlDiv.className = "fullScreen"; + controlDiv.index = 1; + controlDiv.style.padding = "5px"; + // Set CSS for the control border. + var controlUI = document.createElement("div"); + controlUI.style.backgroundColor = "white"; + controlUI.style.borderStyle = "solid"; + controlUI.style.borderWidth = "1px"; + controlUI.style.borderColor = "#717b87"; + controlUI.style.cursor = "pointer"; + controlUI.style.textAlign = "center"; + controlUI.style.boxShadow = "rgba(0, 0, 0, 0.298039) 0px 1px 4px -1px"; + controlDiv.appendChild(controlUI); + // Set CSS for the control interior. + var controlText = document.createElement("div"); + controlText.style.fontFamily = "Roboto,Arial,sans-serif"; + controlText.style.fontSize = "11px"; + controlText.style.fontWeight = "400"; + controlText.style.paddingTop = "1px"; + controlText.style.paddingBottom = "1px"; + controlText.style.paddingLeft = "6px"; + controlText.style.paddingRight = "6px"; + controlText.innerHTML = "" + enterFull + ""; + controlUI.appendChild(controlText); + // set print CSS so the control is hidden + var head = document.getElementsByTagName("head")[0]; + var newStyle = document.createElement("style"); + newStyle.setAttribute("type", "text/css"); + newStyle.setAttribute("media", "print"); + var cssText = ".fullScreen { display: none;}"; + var texNode = document.createTextNode(cssText); + try { + newStyle.appendChild(texNode); + } + catch (e) { + // IE8 hack + newStyle.styleSheet.cssText = cssText; + } + head.appendChild(newStyle); + var fullScreen = false; + var interval; + var mapDiv = map.getDiv(); + var divStyle = mapDiv.style; + if (mapDiv.runtimeStyle) { + divStyle = mapDiv.runtimeStyle; + } + var originalPos = divStyle.position; + var originalWidth = divStyle.width; + var originalHeight = divStyle.height; + // IE8 hack + if (originalWidth === "") { + originalWidth = mapDiv.style.width; + } + if (originalHeight === "") { + originalHeight = mapDiv.style.height; + } + var originalTop = divStyle.top; + var originalLeft = divStyle.left; + var originalZIndex = divStyle.zIndex; + var bodyStyle = document.body.style; + if (document.body.runtimeStyle) { + bodyStyle = document.body.runtimeStyle; + } + var originalOverflow = bodyStyle.overflow; + controlDiv.goFullScreen = function () { + var center = map.getCenter(); + mapDiv.style.position = "fixed"; + mapDiv.style.width = "100%"; + mapDiv.style.height = "100%"; + mapDiv.style.top = "0"; + mapDiv.style.left = "0"; + mapDiv.style.zIndex = "100"; + document.body.style.overflow = "hidden"; + controlText.innerHTML = "" + exitFull + ""; + fullScreen = true; + google.maps.event.trigger(map, "resize"); + map.setCenter(center); + // this works around street view causing the map to disappear, which is caused by Google Maps setting the + // CSS position back to relative. There is no event triggered when Street View is shown hence the use of setInterval + interval = setInterval(function () { + if (mapDiv.style.position !== "fixed") { + mapDiv.style.position = "fixed"; + google.maps.event.trigger(map, "resize"); + } + }, 100); + }; + controlDiv.exitFullScreen = function () { + var center = map.getCenter(); + if (originalPos === "") { + mapDiv.style.position = "relative"; + } + else { + mapDiv.style.position = originalPos; + } + mapDiv.style.width = originalWidth; + mapDiv.style.height = originalHeight; + mapDiv.style.top = originalTop; + mapDiv.style.left = originalLeft; + mapDiv.style.zIndex = originalZIndex; + document.body.style.overflow = originalOverflow; + controlText.innerHTML = "" + enterFull + ""; + fullScreen = false; + google.maps.event.trigger(map, "resize"); + map.setCenter(center); + clearInterval(interval); + }; + // Setup the click event listener + google.maps.event.addDomListener(controlUI, "click", function () { + if (!fullScreen) { + controlDiv.goFullScreen(); + } + else { + controlDiv.exitFullScreen(); + } + }); + + document.onkeydown = function(evt) { + evt = evt || window.event; + if (evt.keyCode == 27) { + if (fullScreen) { + controlDiv.exitFullScreen(); + } + } + }; + return controlDiv; +} diff --git a/javascript/google/MapGoogleHTML.ss b/javascript/google/MapGoogleHTML.ss new file mode 100644 index 0000000..566aec4 --- /dev/null +++ b/javascript/google/MapGoogleHTML.ss @@ -0,0 +1,21 @@ +<% include GoogleJavaScript %> +
data-google-map-key="$GoogleMapKey"<% end_if %><% if $GoogleMapLang %> data-google-map-lang="$GoogleMapLang" <% end_if %><% if ShowInlineMapDivStyle %> style="width:{$Width}; height: {$Height};" +<% end_if %><% if AdditionalCssClasses %> class="$AdditionalCssClasses"<% end_if %> +data-map +data-centre='$LatLngCentre' +data-zoom=$Zoom +data-maptype='$MapType' +data-allowfullscreen='$AllowFullScreen' +data-clusterergridsize=$ClustererGridSize, +data-clusterermaxzoom=$ClustererMaxZoom, +data-enableautocentrezoom=$EnableAutomaticCenterZoom +data-enablewindowzoom=$EnableWindowZoom +data-infowindowzoom=$InfoWindowZoom +data-mapmarkers='$MapMarkers' +data-defaulthidemarker=$DefaultHideMarker +data-lines='$Lines' +data-kmlfiles='$KmlFiles' +data-mapstyles='$JsonMapStyles' +data-useclusterer=$UseClusterer +> +
diff --git a/javascript/google/map.google.template.js b/javascript/google/map.google.template.js new file mode 100644 index 0000000..9b31e0c --- /dev/null +++ b/javascript/google/map.google.template.js @@ -0,0 +1,9 @@ +var options = { + latitude: $Latitude, + longitude: $Longitude, + zoom: $Zoom, + maptype: '$MapType', + domid: '$DomID', + allowfullscreen: $AllowFullScreen +} +registerShortcodeMap(options); diff --git a/javascript/google/mappablegoogle.min.js b/javascript/google/mappablegoogle.min.js new file mode 100644 index 0000000..32e67e1 --- /dev/null +++ b/javascript/google/mappablegoogle.min.js @@ -0,0 +1 @@ +function FullScreenControl(a,b,c){void 0===b&&(b=null),void 0===c&&(c=null),null==b&&(b="Full screen"),null==c&&(c="Exit full screen");var d=document.createElement("div");d.className="fullScreen",d.index=1,d.style.padding="5px";var e=document.createElement("div");e.style.backgroundColor="white",e.style.borderStyle="solid",e.style.borderWidth="1px",e.style.borderColor="#717b87",e.style.cursor="pointer",e.style.textAlign="center",e.style.boxShadow="rgba(0, 0, 0, 0.298039) 0px 1px 4px -1px",d.appendChild(e);var f=document.createElement("div");f.style.fontFamily="Roboto,Arial,sans-serif",f.style.fontSize="11px",f.style.fontWeight="400",f.style.paddingTop="1px",f.style.paddingBottom="1px",f.style.paddingLeft="6px",f.style.paddingRight="6px",f.innerHTML=""+b+"",e.appendChild(f);var g=document.getElementsByTagName("head")[0],h=document.createElement("style");h.setAttribute("type","text/css"),h.setAttribute("media","print");var i=".fullScreen { display: none;}",j=document.createTextNode(i);try{h.appendChild(j)}catch(k){h.styleSheet.cssText=i}g.appendChild(h);var l,m=!1,n=a.getDiv(),o=n.style;n.runtimeStyle&&(o=n.runtimeStyle);var p=o.position,q=o.width,r=o.height;""===q&&(q=n.style.width),""===r&&(r=n.style.height);var s=o.top,t=o.left,u=o.zIndex,v=document.body.style;document.body.runtimeStyle&&(v=document.body.runtimeStyle);var w=v.overflow;return d.goFullScreen=function(){var b=a.getCenter();n.style.position="fixed",n.style.width="100%",n.style.height="100%",n.style.top="0",n.style.left="0",n.style.zIndex="100",document.body.style.overflow="hidden",f.innerHTML=""+c+"",m=!0,google.maps.event.trigger(a,"resize"),a.setCenter(b),l=setInterval(function(){"fixed"!==n.style.position&&(n.style.position="fixed",google.maps.event.trigger(a,"resize"))},100)},d.exitFullScreen=function(){var c=a.getCenter();""===p?n.style.position="relative":n.style.position=p,n.style.width=q,n.style.height=r,n.style.top=s,n.style.left=t,n.style.zIndex=u,document.body.style.overflow=w,f.innerHTML=""+b+"",m=!1,google.maps.event.trigger(a,"resize"),a.setCenter(c),clearInterval(l)},google.maps.event.addDomListener(e,"click",function(){m?d.exitFullScreen():d.goFullScreen()}),document.onkeydown=function(a){a=a||window.event,27==a.keyCode&&m&&d.exitFullScreen()},d}function MarkerClusterer(a,b,c){this.extend(MarkerClusterer,google.maps.OverlayView),this.map_=a,this.markers_=[],this.clusters_=[],this.sizes=[53,56,66,78,90],this.styles_=[],this.ready_=!1;var d=c||{};this.gridSize_=d.gridSize||60,this.minClusterSize_=d.minimumClusterSize||2,this.maxZoom_=d.maxZoom||null,this.styles_=d.styles||[],this.imagePath_=d.imagePath||this.MARKER_CLUSTER_IMAGE_PATH_,this.imageExtension_=d.imageExtension||this.MARKER_CLUSTER_IMAGE_EXTENSION_,this.zoomOnClick_=!0,void 0!=d.zoomOnClick&&(this.zoomOnClick_=d.zoomOnClick),this.averageCenter_=!1,void 0!=d.averageCenter&&(this.averageCenter_=d.averageCenter),this.setupStyles_(),this.setMap(a),this.prevZoom_=this.map_.getZoom();var e=this;google.maps.event.addListener(this.map_,"zoom_changed",function(){var a=e.map_.getZoom();e.prevZoom_!=a&&(e.prevZoom_=a,e.resetViewport())}),google.maps.event.addListener(this.map_,"idle",function(){e.redraw()}),b&&b.length&&this.addMarkers(b,!1)}function Cluster(a){this.markerClusterer_=a,this.map_=a.getMap(),this.gridSize_=a.getGridSize(),this.minClusterSize_=a.getMinClusterSize(),this.averageCenter_=a.isAverageCenter(),this.center_=null,this.markers_=[],this.bounds_=null,this.clusterIcon_=new ClusterIcon(this,a.getStyles(),a.getGridSize())}function ClusterIcon(a,b,c){a.getMarkerClusterer().extend(ClusterIcon,google.maps.OverlayView),this.styles_=b,this.padding_=c||0,this.cluster_=a,this.center_=null,this.map_=a.getMap(),this.div_=null,this.sums_=null,this.visible_=!1,this.setMap(this.map_)}function createMarker(a,b,c,d,e,f,g,h,i){mapId=a.getDiv().getAttribute("id");var j=new google.maps.Marker;if(j.setPosition(new google.maps.LatLng(b,c)),j.mycategory=e,f&&""!==f){var k=new google.maps.MarkerImage(f);j.setIcon(k)}return g||j.setMap(a),google.maps.event.addListener(j,"click",function(){h&&a.setCenter(new google.maps.LatLng(b,c),12);var e=infoWindows[mapId];e.setContent(d),e.open(a,this)}),gmarkers[mapId].push(j),i&&j.hide(),j}function getCurrentLat(){return current_lat}function getCurrentLng(){return current_lng}function addAllMarkers(a,b,c,d,e){a.minLat=1e6,a.minLng=1e6,a.maxLat=-1e6,a.maxLng=-1e6;for(var f=[],g=0;ga.maxLat&&(a.maxLat=j),k>a.maxLng&&(a.maxLng=k),kg&&(c=g,d=b)}}if(d&&d.isMarkerInClusterBounds(a))d.addMarker(a);else{var b=new Cluster(this);b.addMarker(a),this.clusters_.push(b)}},MarkerClusterer.prototype.createClusters_=function(){if(this.ready_)for(var a,b=new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(),this.map_.getBounds().getNorthEast()),c=this.getExtendedBounds(b),d=0;a=this.markers_[d];d++)!a.isAdded&&this.isMarkerInBounds_(a,c)&&this.addToClosestCluster_(a)},Cluster.prototype.isMarkerAlreadyAdded=function(a){if(this.markers_.indexOf)return-1!=this.markers_.indexOf(a);for(var b,c=0;b=this.markers_[c];c++)if(b==a)return!0;return!1},Cluster.prototype.addMarker=function(a){if(this.isMarkerAlreadyAdded(a))return!1;if(this.center_){if(this.averageCenter_){var b=this.markers_.length+1,c=(this.center_.lat()*(b-1)+a.getPosition().lat())/b,d=(this.center_.lng()*(b-1)+a.getPosition().lng())/b;this.center_=new google.maps.LatLng(c,d),this.calculateBounds_()}}else this.center_=a.getPosition(),this.calculateBounds_();a.isAdded=!0,this.markers_.push(a);var e=this.markers_.length;if(ef;f++)this.markers_[f].setMap(null);return e>=this.minClusterSize_&&a.setMap(null),this.updateIcon(),!0},Cluster.prototype.getMarkerClusterer=function(){return this.markerClusterer_},Cluster.prototype.getBounds=function(){for(var a,b=new google.maps.LatLngBounds(this.center_,this.center_),c=this.getMarkers(),d=0;a=c[d];d++)b.extend(a.getPosition());return b},Cluster.prototype.remove=function(){this.clusterIcon_.remove(),this.markers_.length=0,delete this.markers_},Cluster.prototype.getSize=function(){return this.markers_.length},Cluster.prototype.getMarkers=function(){return this.markers_},Cluster.prototype.getCenter=function(){return this.center_},Cluster.prototype.calculateBounds_=function(){var a=new google.maps.LatLngBounds(this.center_,this.center_);this.bounds_=this.markerClusterer_.getExtendedBounds(a)},Cluster.prototype.isMarkerInClusterBounds=function(a){return this.bounds_.contains(a.getPosition())},Cluster.prototype.getMap=function(){return this.map_},Cluster.prototype.updateIcon=function(){var a=this.map_.getZoom(),b=this.markerClusterer_.getMaxZoom();if(b&&a>b)for(var c,d=0;c=this.markers_[d];d++)c.setMap(this.map_);else{if(this.markers_.length0&&this.anchor_[0]0&&this.anchor_[1] map.maxLat) { + map.maxLat = latitude; + } + + if (longitude > map.maxLng) { + map.maxLng = longitude; + + } + + if (longitude < map.minLng) { + map.minLng = longitude; + } + + allmarkers.push(marker); + } + + + var centreCoordinates = []; + centreCoordinates.lat = (parseFloat(map.minLat) + parseFloat(map.maxLat)) / 2; + centreCoordinates.lng = (parseFloat(map.minLng) + parseFloat(map.maxLng)) / 2; + map.centreCoordinates = centreCoordinates; + return allmarkers; + } + + + /** + * Add lines to a Google map + * @param {googleMap} map Google map instance + * @param {array} lines Line data loaded from json, lat1,lon1 to lat2,lon2 + */ + function addLines(map, lines) { + for (i = 0; i < lines.length; i++) { + var line = lines[i]; + var point1 = new google.maps.LatLng(line.lat1, line.lon1); + var point2 = new google.maps.LatLng(line.lat2, line.lon2); + var points = [point1, point2]; + var pl = new google.maps.Polyline({ + path: points, + strokeColor: line.color, + strokeWeight: 4, + strokeOpacity: 0.8 + }); + pl.setMap(map); + } + } + + + /** + * Add one or more (max of 25) KML files to a Google map + * @param {GoogleMap} map A Google Map instance + * @param {array} kmlFiles array of URLs for KML files + */ + function addKmlFiles(map, kmlFiles) { + for (var i = 0; i < kmlFiles.length; i++) { + var kmlFile = kmlFiles[i]; + var kmlLayer = new google.maps.KmlLayer(kmlFile, { + suppressInfoWindows: true, + map: map + }); + + } + } + + + /** + * Convert a map type name (road,satellite,hybrid,terrain) to Google map types + * @param String mapTypeName generic name of the map type + * @return google.maps.MapTypeId map type in Google format + */ + function convertMapType(mapTypeName) { + var result = google.maps.MapTypeId.ROADMAP; + switch (mapTypeName) { + case 'aerial': + result = google.maps.MapTypeId.SATELLITE; + break; + case 'hybrid': + result = google.maps.MapTypeId.HYBRID; + break; + case 'terrain': + result = google.maps.MapTypeId.TERRAIN; + break; + } + return result; + } + + + /** + * After the Google Maps API has been loaded init the relevant Google maps + */ + function loadShortCodeStreetView() { + + var svs = $('div[data-streetview]'); + + svs.each(function(index) { + var svnode = $(this); + var svnodeid = svnode.attr('id'); + var lat = parseFloat(svnode.attr('data-latitude')); + var lon = parseFloat(svnode.attr('data-longitude')); + var zoom = parseInt(svnode.attr('data-zoom')); + var heading = parseFloat(svnode.attr('data-heading')); + var pitch = parseFloat(svnode.attr('data-pitch')); + + var location = new google.maps.LatLng(lat, lon); + + var panoramaOptions = { + position: location, + pov: { + heading: heading, + pitch: pitch + }, + zoom: zoom + }; + + + var domNode = document.getElementById(svnodeid); + var pano = new google.maps.StreetViewPanorama( + domNode, + panoramaOptions); + pano.setVisible(true); + }); + + + + } + + + /** + * After the Google Maps API has been loaded init the relevant Google maps + */ + function loadShortCodeMaps() { + + var scms = $('div[data-shortcode-map]'); + + scms.each(function(index) { + var scmnode = $(this); + var scmnodeid = scmnode.attr('id'); + var maptype = convertMapType(scmnode.attr('data-maptype')); + var lat = parseFloat(scmnode.attr('data-latitude')); + var lng = parseFloat(scmnode.attr('data-longitude')); + var zoom = parseInt(scmnode.attr('data-zoom')); + var allowfullscreen = parseInt(scmnode.attr('data-allowfullscreen')); + + // Google Maps API has already been loaded, so init Google Map + var mapOptions = { + center: { + lat: lat, + lng: lng + }, + zoom: zoom, + mapTypeId: maptype + }; + + + + var gmap = new google.maps.Map(document.getElementById(scmnodeid), + mapOptions); + if (allowfullscreen == 1) { + gmap.controls[google.maps.ControlPosition.TOP_RIGHT].push( + FullScreenControl(gmap, "Full Screen", "Original Size") + ); + } + }); + + + } + + + /** + * Callback function after the Google Maps API has been loaded - renders the maps along with + * associated points of interest and layers + */ + primeMap = function loadedGoogleMapsAPI() { + loadShortCodeMaps(); + loadShortCodeStreetView(); + + var maps = $('div[data-map]'); + + maps.each(function(index) { + var mapnode = $(this); + mapdomid = mapnode.attr('id'); + var map = new google.maps.Map(document.getElementById(mapdomid)); + + // initialise geocoder + geocoder = new google.maps.Geocoder(); + + // default of [] renders google maps as per normal + if (mapnode.attr('data-mapstyles')) { + var json = $.parseJSON(mapnode.attr('data-mapstyles')); + map.setOptions({ + styles: json + }); + } + + if (mapnode.data['data-allowfullscreen']) { + map.controls[google.maps.ControlPosition.TOP_RIGHT].push( + FullScreenControl(map, "Full Screen", "Original Size") + ); + } + + var markerjson = $.parseJSON(mapnode.attr('data-mapmarkers')); + var useClusterer = mapnode.attr('data-useclusterer'); + var enableAutomaticCenterZoom = mapnode.attr('data-enableautocentrezoom'); + var infoWindowZoom = mapnode.attr('data-infowindowzoom'); + var defaultHideMarker = mapnode.attr('data-defaulthidemarker'); + var markers = addAllMarkers(map, markerjson, useClusterer, + enableAutomaticCenterZoom, infoWindowZoom, defaultHideMarker); + var allowfullscreen = parseInt(mapnode.attr('data-allowfullscreen')); + + if (enableAutomaticCenterZoom == 1) { + centre = $.parseJSON(mapnode.attr('data-centre')); + map.setCenter(new google.maps.LatLng(centre.lat, centre.lng)); + + var bds = new google.maps.LatLngBounds(new google.maps.LatLng(map.minLat, map.minLng), + new google.maps.LatLng(map.maxLat, map.maxLng)); + map.fitBounds(bds); + } else { + centre = $.parseJSON(mapnode.attr('data-centre')); + map.setCenter(new google.maps.LatLng(centre.lat, centre.lng)); + var zoom = parseInt(mapnode.attr('data-zoom')); + map.setZoom(zoom); + } + + if (allowfullscreen == 1) { + map.controls[google.maps.ControlPosition.TOP_RIGHT].push( + FullScreenControl(map, "Full Screen", "Original Size") + ); + } + + var googlemaptype = convertMapType(mapnode.attr('data-maptype')); + map.setMapTypeId(googlemaptype); + + if (useClusterer == 1) { + // ensure zoom and grid size are integers by prefixing with unary plus + var clustererGridSize = parseInt(mapnode.attr('data-clusterergridsize')); + var clustererMaxZoom = parseInt(mapnode.attr('data-clusterermaxzoom')); + var mcOptions = { + gridSize: clustererGridSize, + maxZoom: clustererMaxZoom + }; + var markerCluster = new MarkerClusterer(map, markers, mcOptions); + } + + var lines = $.parseJSON(mapnode.attr('data-lines')); + addLines(map, lines); + + var kmlFiles = $.parseJSON(mapnode.attr('data-kmlfiles')); + addKmlFiles(map, kmlFiles); + + var infoWindow = new google.maps.InfoWindow({ + content: '' + }); + infoWindows[mapdomid] = infoWindow; + + // trigger an event now that the map has been initialised + // Use this for example to add listeners to the map from another JS file + mapnode.trigger("mapInitialised", [map]); + }); + }; + + + + function nearestPOIs() { + + // normally will only be one + var nears = $('div[data-nearest-poi]'); + + nears.each(function(index) { + var nearnode = $(this); + var layerID = nearnode.attr('data-layer-id'); + + if (geoPosition.init()) { // Geolocation Initialisation + geoPosition.getCurrentPosition(success_callback, error_callback, { + enableHighAccuracy: true + }); + } else { + alert('your location is not available'); + } + //geoPositionSimulator.init(); + }); + } + + + function success_callback(p) { + console.log(p.coords); + // p.latitude : latitude value + // p.longitude : longitude value + // + var url = window.location; + url = url + 'find?lat=' + p.coords.latitude + '&lng=' + p.coords.longitude; + window.location = url; + } + + + function error_callback(p) { + alert('error)'); + } + + window.loadGoogleMapsScript = function() { + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = 'https://maps.googleapis.com/maps/api/js?' + + '&sensor=false&callback=loadedGoogleMapsAPI&hl=en'; + document.body.appendChild(script); + }; + + + window.addEventListener ? + window.addEventListener("load", loadGoogleMapsScript, false) : + window.attachEvent && window.attachEvent("onload", loadGoogleMapsScript); +})(jQuery); + + +function loadedGoogleMapsAPI() { + primeMap(); +} diff --git a/javascript/google/markerclusterer.js b/javascript/google/markerclusterer.js new file mode 100644 index 0000000..2783082 --- /dev/null +++ b/javascript/google/markerclusterer.js @@ -0,0 +1,1290 @@ +// ==ClosureCompiler== +// @compilation_level ADVANCED_OPTIMIZATIONS +// @externs_url http://closure-compiler.googlecode.com/svn/trunk/contrib/externs/maps/google_maps_api_v3_3.js +// ==/ClosureCompiler== + +/** + * @name MarkerClusterer for Google Maps v3 + * @version version 1.0 + * @author Luke Mahe + * @fileoverview + * The library creates and manages per-zoom-level clusters for large amounts of + * markers. + *
+ * This is a v3 implementation of the + * v2 MarkerClusterer. + */ + +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * A Marker Clusterer that clusters markers. + * + * @param {google.maps.Map} map The Google map to attach to. + * @param {Array.=} opt_markers Optional markers to add to + * the cluster. + * @param {Object=} opt_options support the following options: + * 'gridSize': (number) The grid size of a cluster in pixels. + * 'maxZoom': (number) The maximum zoom level that a marker can be part of a + * cluster. + * 'zoomOnClick': (boolean) Whether the default behaviour of clicking on a + * cluster is to zoom into it. + * 'averageCenter': (boolean) Wether the center of each cluster should be + * the average of all markers in the cluster. + * 'minimumClusterSize': (number) The minimum number of markers to be in a + * cluster before the markers are hidden and a count + * is shown. + * 'styles': (object) An object that has style properties: + * 'url': (string) The image url. + * 'height': (number) The image height. + * 'width': (number) The image width. + * 'anchor': (Array) The anchor position of the label text. + * 'textColor': (string) The text color. + * 'textSize': (number) The text size. + * 'backgroundPosition': (string) The position of the backgound x, y. + * @constructor + * @extends google.maps.OverlayView + */ +function MarkerClusterer(map, opt_markers, opt_options) { + // MarkerClusterer implements google.maps.OverlayView interface. We use the + // extend function to extend MarkerClusterer with google.maps.OverlayView + // because it might not always be available when the code is defined so we + // look for it at the last possible moment. If it doesn't exist now then + // there is no point going ahead :) + this.extend(MarkerClusterer, google.maps.OverlayView); + this.map_ = map; + + /** + * @type {Array.} + * @private + */ + this.markers_ = []; + + /** + * @type {Array.} + */ + this.clusters_ = []; + + this.sizes = [53, 56, 66, 78, 90]; + + /** + * @private + */ + this.styles_ = []; + + /** + * @type {boolean} + * @private + */ + this.ready_ = false; + + var options = opt_options || {}; + + /** + * @type {number} + * @private + */ + this.gridSize_ = options['gridSize'] || 60; + + /** + * @private + */ + this.minClusterSize_ = options['minimumClusterSize'] || 2; + + + /** + * @type {?number} + * @private + */ + this.maxZoom_ = options['maxZoom'] || null; + + this.styles_ = options['styles'] || []; + + /** + * @type {string} + * @private + */ + this.imagePath_ = options['imagePath'] || + this.MARKER_CLUSTER_IMAGE_PATH_; + + /** + * @type {string} + * @private + */ + this.imageExtension_ = options['imageExtension'] || + this.MARKER_CLUSTER_IMAGE_EXTENSION_; + + /** + * @type {boolean} + * @private + */ + this.zoomOnClick_ = true; + + if (options['zoomOnClick'] != undefined) { + this.zoomOnClick_ = options['zoomOnClick']; + } + + /** + * @type {boolean} + * @private + */ + this.averageCenter_ = false; + + if (options['averageCenter'] != undefined) { + this.averageCenter_ = options['averageCenter']; + } + + this.setupStyles_(); + + this.setMap(map); + + /** + * @type {number} + * @private + */ + this.prevZoom_ = this.map_.getZoom(); + + // Add the map event listeners + var that = this; + google.maps.event.addListener(this.map_, 'zoom_changed', function() { + var zoom = that.map_.getZoom(); + + if (that.prevZoom_ != zoom) { + that.prevZoom_ = zoom; + that.resetViewport(); + } + }); + + google.maps.event.addListener(this.map_, 'idle', function() { + that.redraw(); + }); + + // Finally, add the markers + if (opt_markers && opt_markers.length) { + this.addMarkers(opt_markers, false); + } +} + + +/** + * The marker cluster image path. + * + * @type {string} + * @private + */ +MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = + 'http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/' + + 'images/m'; + + +/** + * The marker cluster image path. + * + * @type {string} + * @private + */ +MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png'; + + +/** + * Extends a objects prototype by anothers. + * + * @param {Object} obj1 The object to be extended. + * @param {Object} obj2 The object to extend with. + * @return {Object} The new extended object. + * @ignore + */ +MarkerClusterer.prototype.extend = function(obj1, obj2) { + return (function(object) { + for (var property in object.prototype) { + this.prototype[property] = object.prototype[property]; + } + return this; + }).apply(obj1, [obj2]); +}; + + +/** + * Implementaion of the interface method. + * @ignore + */ +MarkerClusterer.prototype.onAdd = function() { + this.setReady_(true); +}; + +/** + * Implementaion of the interface method. + * @ignore + */ +MarkerClusterer.prototype.draw = function() {}; + +/** + * Sets up the styles object. + * + * @private + */ +MarkerClusterer.prototype.setupStyles_ = function() { + if (this.styles_.length) { + return; + } + + for (var i = 0, size; size = this.sizes[i]; i++) { + this.styles_.push({ + url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_, + height: size, + width: size + }); + } +}; + +/** + * Fit the map to the bounds of the markers in the clusterer. + */ +MarkerClusterer.prototype.fitMapToMarkers = function() { + var markers = this.getMarkers(); + var bounds = new google.maps.LatLngBounds(); + for (var i = 0, marker; marker = markers[i]; i++) { + bounds.extend(marker.getPosition()); + } + + this.map_.fitBounds(bounds); +}; + + +/** + * Sets the styles. + * + * @param {Object} styles The style to set. + */ +MarkerClusterer.prototype.setStyles = function(styles) { + this.styles_ = styles; +}; + + +/** + * Gets the styles. + * + * @return {Object} The styles object. + */ +MarkerClusterer.prototype.getStyles = function() { + return this.styles_; +}; + + +/** + * Whether zoom on click is set. + * + * @return {boolean} True if zoomOnClick_ is set. + */ +MarkerClusterer.prototype.isZoomOnClick = function() { + return this.zoomOnClick_; +}; + +/** + * Whether average center is set. + * + * @return {boolean} True if averageCenter_ is set. + */ +MarkerClusterer.prototype.isAverageCenter = function() { + return this.averageCenter_; +}; + + +/** + * Returns the array of markers in the clusterer. + * + * @return {Array.} The markers. + */ +MarkerClusterer.prototype.getMarkers = function() { + return this.markers_; +}; + + +/** + * Returns the number of markers in the clusterer + * + * @return {Number} The number of markers. + */ +MarkerClusterer.prototype.getTotalMarkers = function() { + return this.markers_.length; +}; + + +/** + * Sets the max zoom for the clusterer. + * + * @param {number} maxZoom The max zoom level. + */ +MarkerClusterer.prototype.setMaxZoom = function(maxZoom) { + this.maxZoom_ = maxZoom; +}; + + +/** + * Gets the max zoom for the clusterer. + * + * @return {number} The max zoom level. + */ +MarkerClusterer.prototype.getMaxZoom = function() { + return this.maxZoom_; +}; + + +/** + * The function for calculating the cluster icon image. + * + * @param {Array.} markers The markers in the clusterer. + * @param {number} numStyles The number of styles available. + * @return {Object} A object properties: 'text' (string) and 'index' (number). + * @private + */ +MarkerClusterer.prototype.calculator_ = function(markers, numStyles) { + var index = 0; + var count = markers.length; + var dv = count; + while (dv !== 0) { + dv = parseInt(dv / 10, 10); + index++; + } + + index = Math.min(index, numStyles); + return { + text: count, + index: index + }; +}; + + +/** + * Set the calculator function. + * + * @param {function(Array, number)} calculator The function to set as the + * calculator. The function should return a object properties: + * 'text' (string) and 'index' (number). + * + */ +MarkerClusterer.prototype.setCalculator = function(calculator) { + this.calculator_ = calculator; +}; + + +/** + * Get the calculator function. + * + * @return {function(Array, number)} the calculator function. + */ +MarkerClusterer.prototype.getCalculator = function() { + return this.calculator_; +}; + + +/** + * Add an array of markers to the clusterer. + * + * @param {Array.} markers The markers to add. + * @param {boolean=} opt_nodraw Whether to redraw the clusters. + */ +MarkerClusterer.prototype.addMarkers = function(markers, opt_nodraw) { + for (var i = 0, marker; marker = markers[i]; i++) { + this.pushMarkerTo_(marker); + } + if (!opt_nodraw) { + this.redraw(); + } +}; + + +/** + * Pushes a marker to the clusterer. + * + * @param {google.maps.Marker} marker The marker to add. + * @private + */ +MarkerClusterer.prototype.pushMarkerTo_ = function(marker) { + marker.isAdded = false; + if (marker['draggable']) { + // If the marker is draggable add a listener so we update the clusters on + // the drag end. + var that = this; + google.maps.event.addListener(marker, 'dragend', function() { + marker.isAdded = false; + that.repaint(); + }); + } + this.markers_.push(marker); +}; + + +/** + * Adds a marker to the clusterer and redraws if needed. + * + * @param {google.maps.Marker} marker The marker to add. + * @param {boolean=} opt_nodraw Whether to redraw the clusters. + */ +MarkerClusterer.prototype.addMarker = function(marker, opt_nodraw) { + this.pushMarkerTo_(marker); + if (!opt_nodraw) { + this.redraw(); + } +}; + + +/** + * Removes a marker and returns true if removed, false if not + * + * @param {google.maps.Marker} marker The marker to remove + * @return {boolean} Whether the marker was removed or not + * @private + */ +MarkerClusterer.prototype.removeMarker_ = function(marker) { + var index = -1; + if (this.markers_.indexOf) { + index = this.markers_.indexOf(marker); + } else { + for (var i = 0, m; m = this.markers_[i]; i++) { + if (m == marker) { + index = i; + break; + } + } + } + + if (index == -1) { + // Marker is not in our list of markers. + return false; + } + + marker.setMap(null); + + this.markers_.splice(index, 1); + + return true; +}; + + +/** + * Remove a marker from the cluster. + * + * @param {google.maps.Marker} marker The marker to remove. + * @param {boolean=} opt_nodraw Optional boolean to force no redraw. + * @return {boolean} True if the marker was removed. + */ +MarkerClusterer.prototype.removeMarker = function(marker, opt_nodraw) { + var removed = this.removeMarker_(marker); + + if (!opt_nodraw && removed) { + this.resetViewport(); + this.redraw(); + return true; + } else { + return false; + } +}; + + +/** + * Removes an array of markers from the cluster. + * + * @param {Array.} markers The markers to remove. + * @param {boolean=} opt_nodraw Optional boolean to force no redraw. + */ +MarkerClusterer.prototype.removeMarkers = function(markers, opt_nodraw) { + var removed = false; + + for (var i = 0, marker; marker = markers[i]; i++) { + var r = this.removeMarker_(marker); + removed = removed || r; + } + + if (!opt_nodraw && removed) { + this.resetViewport(); + this.redraw(); + return true; + } +}; + + +/** + * Sets the clusterer's ready state. + * + * @param {boolean} ready The state. + * @private + */ +MarkerClusterer.prototype.setReady_ = function(ready) { + if (!this.ready_) { + this.ready_ = ready; + this.createClusters_(); + } +}; + + +/** + * Returns the number of clusters in the clusterer. + * + * @return {number} The number of clusters. + */ +MarkerClusterer.prototype.getTotalClusters = function() { + return this.clusters_.length; +}; + + +/** + * Returns the google map that the clusterer is associated with. + * + * @return {google.maps.Map} The map. + */ +MarkerClusterer.prototype.getMap = function() { + return this.map_; +}; + + +/** + * Sets the google map that the clusterer is associated with. + * + * @param {google.maps.Map} map The map. + */ +MarkerClusterer.prototype.setMap = function(map) { + this.map_ = map; +}; + + +/** + * Returns the size of the grid. + * + * @return {number} The grid size. + */ +MarkerClusterer.prototype.getGridSize = function() { + return this.gridSize_; +}; + + +/** + * Sets the size of the grid. + * + * @param {number} size The grid size. + */ +MarkerClusterer.prototype.setGridSize = function(size) { + this.gridSize_ = size; +}; + + +/** + * Returns the min cluster size. + * + * @return {number} The grid size. + */ +MarkerClusterer.prototype.getMinClusterSize = function() { + return this.minClusterSize_; +}; + +/** + * Sets the min cluster size. + * + * @param {number} size The grid size. + */ +MarkerClusterer.prototype.setMinClusterSize = function(size) { + this.minClusterSize_ = size; +}; + + +/** + * Extends a bounds object by the grid size. + * + * @param {google.maps.LatLngBounds} bounds The bounds to extend. + * @return {google.maps.LatLngBounds} The extended bounds. + */ +MarkerClusterer.prototype.getExtendedBounds = function(bounds) { + var projection = this.getProjection(); + + // Turn the bounds into latlng. + var tr = new google.maps.LatLng(bounds.getNorthEast().lat(), + bounds.getNorthEast().lng()); + var bl = new google.maps.LatLng(bounds.getSouthWest().lat(), + bounds.getSouthWest().lng()); + + // Convert the points to pixels and the extend out by the grid size. + var trPix = projection.fromLatLngToDivPixel(tr); + trPix.x += this.gridSize_; + trPix.y -= this.gridSize_; + + var blPix = projection.fromLatLngToDivPixel(bl); + blPix.x -= this.gridSize_; + blPix.y += this.gridSize_; + + // Convert the pixel points back to LatLng + var ne = projection.fromDivPixelToLatLng(trPix); + var sw = projection.fromDivPixelToLatLng(blPix); + + // Extend the bounds to contain the new bounds. + bounds.extend(ne); + bounds.extend(sw); + + return bounds; +}; + + +/** + * Determins if a marker is contained in a bounds. + * + * @param {google.maps.Marker} marker The marker to check. + * @param {google.maps.LatLngBounds} bounds The bounds to check against. + * @return {boolean} True if the marker is in the bounds. + * @private + */ +MarkerClusterer.prototype.isMarkerInBounds_ = function(marker, bounds) { + return bounds.contains(marker.getPosition()); +}; + + +/** + * Clears all clusters and markers from the clusterer. + */ +MarkerClusterer.prototype.clearMarkers = function() { + this.resetViewport(true); + + // Set the markers a empty array. + this.markers_ = []; +}; + + +/** + * Clears all existing clusters and recreates them. + * @param {boolean} opt_hide To also hide the marker. + */ +MarkerClusterer.prototype.resetViewport = function(opt_hide) { + // Remove all the clusters + for (var i = 0, cluster; cluster = this.clusters_[i]; i++) { + cluster.remove(); + } + + // Reset the markers to not be added and to be invisible. + for (var i = 0, marker; marker = this.markers_[i]; i++) { + marker.isAdded = false; + if (opt_hide) { + marker.setMap(null); + } + } + + this.clusters_ = []; +}; + +/** + * + */ +MarkerClusterer.prototype.repaint = function() { + var oldClusters = this.clusters_.slice(); + this.clusters_.length = 0; + this.resetViewport(); + this.redraw(); + + // Remove the old clusters. + // Do it in a timeout so the other clusters have been drawn first. + window.setTimeout(function() { + for (var i = 0, cluster; cluster = oldClusters[i]; i++) { + cluster.remove(); + } + }, 0); +}; + + +/** + * Redraws the clusters. + */ +MarkerClusterer.prototype.redraw = function() { + this.createClusters_(); +}; + + +/** + * Calculates the distance between two latlng locations in km. + * @see http://www.movable-type.co.uk/scripts/latlong.html + * + * @param {google.maps.LatLng} p1 The first lat lng point. + * @param {google.maps.LatLng} p2 The second lat lng point. + * @return {number} The distance between the two points in km. + * @private +*/ +MarkerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) { + if (!p1 || !p2) { + return 0; + } + + var R = 6371; // Radius of the Earth in km + var dLat = (p2.lat() - p1.lat()) * Math.PI / 180; + var dLon = (p2.lng() - p1.lng()) * Math.PI / 180; + var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; + return d; +}; + + +/** + * Add a marker to a cluster, or creates a new cluster. + * + * @param {google.maps.Marker} marker The marker to add. + * @private + */ +MarkerClusterer.prototype.addToClosestCluster_ = function(marker) { + var distance = 40000; // Some large number + var clusterToAddTo = null; + var pos = marker.getPosition(); + for (var i = 0, cluster; cluster = this.clusters_[i]; i++) { + var center = cluster.getCenter(); + if (center) { + var d = this.distanceBetweenPoints_(center, marker.getPosition()); + if (d < distance) { + distance = d; + clusterToAddTo = cluster; + } + } + } + + if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) { + clusterToAddTo.addMarker(marker); + } else { + var cluster = new Cluster(this); + cluster.addMarker(marker); + this.clusters_.push(cluster); + } +}; + + +/** + * Creates the clusters. + * + * @private + */ +MarkerClusterer.prototype.createClusters_ = function() { + if (!this.ready_) { + return; + } + + // Get our current map view bounds. + // Create a new bounds object so we don't affect the map. + var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(), + this.map_.getBounds().getNorthEast()); + var bounds = this.getExtendedBounds(mapBounds); + + for (var i = 0, marker; marker = this.markers_[i]; i++) { + if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) { + this.addToClosestCluster_(marker); + } + } +}; + + +/** + * A cluster that contains markers. + * + * @param {MarkerClusterer} markerClusterer The markerclusterer that this + * cluster is associated with. + * @constructor + * @ignore + */ +function Cluster(markerClusterer) { + this.markerClusterer_ = markerClusterer; + this.map_ = markerClusterer.getMap(); + this.gridSize_ = markerClusterer.getGridSize(); + this.minClusterSize_ = markerClusterer.getMinClusterSize(); + this.averageCenter_ = markerClusterer.isAverageCenter(); + this.center_ = null; + this.markers_ = []; + this.bounds_ = null; + this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(), + markerClusterer.getGridSize()); +} + +/** + * Determins if a marker is already added to the cluster. + * + * @param {google.maps.Marker} marker The marker to check. + * @return {boolean} True if the marker is already added. + */ +Cluster.prototype.isMarkerAlreadyAdded = function(marker) { + if (this.markers_.indexOf) { + return this.markers_.indexOf(marker) != -1; + } else { + for (var i = 0, m; m = this.markers_[i]; i++) { + if (m == marker) { + return true; + } + } + } + return false; +}; + + +/** + * Add a marker the cluster. + * + * @param {google.maps.Marker} marker The marker to add. + * @return {boolean} True if the marker was added. + */ +Cluster.prototype.addMarker = function(marker) { + if (this.isMarkerAlreadyAdded(marker)) { + return false; + } + + if (!this.center_) { + this.center_ = marker.getPosition(); + this.calculateBounds_(); + } else { + if (this.averageCenter_) { + var l = this.markers_.length + 1; + var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l; + var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l; + this.center_ = new google.maps.LatLng(lat, lng); + this.calculateBounds_(); + } + } + + marker.isAdded = true; + this.markers_.push(marker); + + var len = this.markers_.length; + if (len < this.minClusterSize_ && marker.getMap() != this.map_) { + // Min cluster size not reached so show the marker. + marker.setMap(this.map_); + } + + if (len == this.minClusterSize_) { + // Hide the markers that were showing. + for (var i = 0; i < len; i++) { + this.markers_[i].setMap(null); + } + } + + if (len >= this.minClusterSize_) { + marker.setMap(null); + } + + this.updateIcon(); + return true; +}; + + +/** + * Returns the marker clusterer that the cluster is associated with. + * + * @return {MarkerClusterer} The associated marker clusterer. + */ +Cluster.prototype.getMarkerClusterer = function() { + return this.markerClusterer_; +}; + + +/** + * Returns the bounds of the cluster. + * + * @return {google.maps.LatLngBounds} the cluster bounds. + */ +Cluster.prototype.getBounds = function() { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + var markers = this.getMarkers(); + for (var i = 0, marker; marker = markers[i]; i++) { + bounds.extend(marker.getPosition()); + } + return bounds; +}; + + +/** + * Removes the cluster + */ +Cluster.prototype.remove = function() { + this.clusterIcon_.remove(); + this.markers_.length = 0; + delete this.markers_; +}; + + +/** + * Returns the center of the cluster. + * + * @return {number} The cluster center. + */ +Cluster.prototype.getSize = function() { + return this.markers_.length; +}; + + +/** + * Returns the center of the cluster. + * + * @return {Array.} The cluster center. + */ +Cluster.prototype.getMarkers = function() { + return this.markers_; +}; + + +/** + * Returns the center of the cluster. + * + * @return {google.maps.LatLng} The cluster center. + */ +Cluster.prototype.getCenter = function() { + return this.center_; +}; + + +/** + * Calculated the extended bounds of the cluster with the grid. + * + * @private + */ +Cluster.prototype.calculateBounds_ = function() { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds); +}; + + +/** + * Determines if a marker lies in the clusters bounds. + * + * @param {google.maps.Marker} marker The marker to check. + * @return {boolean} True if the marker lies in the bounds. + */ +Cluster.prototype.isMarkerInClusterBounds = function(marker) { + return this.bounds_.contains(marker.getPosition()); +}; + + +/** + * Returns the map that the cluster is associated with. + * + * @return {google.maps.Map} The map. + */ +Cluster.prototype.getMap = function() { + return this.map_; +}; + + +/** + * Updates the cluster icon + */ +Cluster.prototype.updateIcon = function() { + var zoom = this.map_.getZoom(); + var mz = this.markerClusterer_.getMaxZoom(); + + if (mz && zoom > mz) { + // The zoom is greater than our max zoom so show all the markers in cluster. + for (var i = 0, marker; marker = this.markers_[i]; i++) { + marker.setMap(this.map_); + } + return; + } + + if (this.markers_.length < this.minClusterSize_) { + // Min cluster size not yet reached. + this.clusterIcon_.hide(); + return; + } + + var numStyles = this.markerClusterer_.getStyles().length; + var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles); + this.clusterIcon_.setCenter(this.center_); + this.clusterIcon_.setSums(sums); + this.clusterIcon_.show(); +}; + + +/** + * A cluster icon + * + * @param {Cluster} cluster The cluster to be associated with. + * @param {Object} styles An object that has style properties: + * 'url': (string) The image url. + * 'height': (number) The image height. + * 'width': (number) The image width. + * 'anchor': (Array) The anchor position of the label text. + * 'textColor': (string) The text color. + * 'textSize': (number) The text size. + * 'backgroundPosition: (string) The background postition x, y. + * @param {number=} opt_padding Optional padding to apply to the cluster icon. + * @constructor + * @extends google.maps.OverlayView + * @ignore + */ +function ClusterIcon(cluster, styles, opt_padding) { + cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView); + + this.styles_ = styles; + this.padding_ = opt_padding || 0; + this.cluster_ = cluster; + this.center_ = null; + this.map_ = cluster.getMap(); + this.div_ = null; + this.sums_ = null; + this.visible_ = false; + + this.setMap(this.map_); +} + + +/** + * Triggers the clusterclick event and zoom's if the option is set. + */ +ClusterIcon.prototype.triggerClusterClick = function() { + var markerClusterer = this.cluster_.getMarkerClusterer(); + + // Trigger the clusterclick event. + google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_); + + if (markerClusterer.isZoomOnClick()) { + // Zoom into the cluster. + this.map_.fitBounds(this.cluster_.getBounds()); + } +}; + + +/** + * Adding the cluster icon to the dom. + * @ignore + */ +ClusterIcon.prototype.onAdd = function() { + this.div_ = document.createElement('DIV'); + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + this.div_.innerHTML = this.sums_.text; + } + + var panes = this.getPanes(); + panes.overlayMouseTarget.appendChild(this.div_); + + var that = this; + google.maps.event.addDomListener(this.div_, 'click', function() { + that.triggerClusterClick(); + }); +}; + + +/** + * Returns the position to place the div dending on the latlng. + * + * @param {google.maps.LatLng} latlng The position in latlng. + * @return {google.maps.Point} The position in pixels. + * @private + */ +ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) { + var pos = this.getProjection().fromLatLngToDivPixel(latlng); + pos.x -= parseInt(this.width_ / 2, 10); + pos.y -= parseInt(this.height_ / 2, 10); + return pos; +}; + + +/** + * Draw the icon. + * @ignore + */ +ClusterIcon.prototype.draw = function() { + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.top = pos.y + 'px'; + this.div_.style.left = pos.x + 'px'; + } +}; + + +/** + * Hide the icon. + */ +ClusterIcon.prototype.hide = function() { + if (this.div_) { + this.div_.style.display = 'none'; + } + this.visible_ = false; +}; + + +/** + * Position and show the icon. + */ +ClusterIcon.prototype.show = function() { + if (this.div_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + this.div_.style.display = ''; + } + this.visible_ = true; +}; + + +/** + * Remove the icon from the map + */ +ClusterIcon.prototype.remove = function() { + this.setMap(null); +}; + + +/** + * Implementation of the onRemove interface. + * @ignore + */ +ClusterIcon.prototype.onRemove = function() { + if (this.div_ && this.div_.parentNode) { + this.hide(); + this.div_.parentNode.removeChild(this.div_); + this.div_ = null; + } +}; + + +/** + * Set the sums of the icon. + * + * @param {Object} sums The sums containing: + * 'text': (string) The text to display in the icon. + * 'index': (number) The style index of the icon. + */ +ClusterIcon.prototype.setSums = function(sums) { + this.sums_ = sums; + this.text_ = sums.text; + this.index_ = sums.index; + if (this.div_) { + this.div_.innerHTML = sums.text; + } + + this.useStyle(); +}; + + +/** + * Sets the icon to the the styles. + */ +ClusterIcon.prototype.useStyle = function() { + var index = Math.max(0, this.sums_.index - 1); + index = Math.min(this.styles_.length - 1, index); + var style = this.styles_[index]; + this.url_ = style['url']; + this.height_ = style['height']; + this.width_ = style['width']; + this.textColor_ = style['textColor']; + this.anchor_ = style['anchor']; + this.textSize_ = style['textSize']; + this.backgroundPosition_ = style['backgroundPosition']; +}; + + +/** + * Sets the center of the icon. + * + * @param {google.maps.LatLng} center The latlng to set as the center. + */ +ClusterIcon.prototype.setCenter = function(center) { + this.center_ = center; +}; + + +/** + * Create the css text based on the position of the icon. + * + * @param {google.maps.Point} pos The position. + * @return {string} The css style text. + */ +ClusterIcon.prototype.createCss = function(pos) { + var style = []; + style.push('background-image:url(' + this.url_ + ');'); + var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0'; + style.push('background-position:' + backgroundPosition + ';'); + + if (typeof this.anchor_ === 'object') { + if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 && + this.anchor_[0] < this.height_) { + style.push('height:' + (this.height_ - this.anchor_[0]) + + 'px; padding-top:' + this.anchor_[0] + 'px;'); + } else { + style.push('height:' + this.height_ + 'px; line-height:' + this.height_ + + 'px;'); + } + if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 && + this.anchor_[1] < this.width_) { + style.push('width:' + (this.width_ - this.anchor_[1]) + + 'px; padding-left:' + this.anchor_[1] + 'px;'); + } else { + style.push('width:' + this.width_ + 'px; text-align:center;'); + } + } else { + style.push('height:' + this.height_ + 'px; line-height:' + + this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;'); + } + + var txtColor = this.textColor_ ? this.textColor_ : 'black'; + var txtSize = this.textSize_ ? this.textSize_ : 11; + + style.push('cursor:pointer; top:' + pos.y + 'px; left:' + + pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' + + txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); + return style.join(''); +}; + + +// Export Symbols for Closure +// If you are not going to compile with closure then you can remove the +// code below. +window['MarkerClusterer'] = MarkerClusterer; +MarkerClusterer.prototype['addMarker'] = MarkerClusterer.prototype.addMarker; +MarkerClusterer.prototype['addMarkers'] = MarkerClusterer.prototype.addMarkers; +MarkerClusterer.prototype['clearMarkers'] = + MarkerClusterer.prototype.clearMarkers; +MarkerClusterer.prototype['fitMapToMarkers'] = + MarkerClusterer.prototype.fitMapToMarkers; +MarkerClusterer.prototype['getCalculator'] = + MarkerClusterer.prototype.getCalculator; +MarkerClusterer.prototype['getGridSize'] = + MarkerClusterer.prototype.getGridSize; +MarkerClusterer.prototype['getExtendedBounds'] = + MarkerClusterer.prototype.getExtendedBounds; +MarkerClusterer.prototype['getMap'] = MarkerClusterer.prototype.getMap; +MarkerClusterer.prototype['getMarkers'] = MarkerClusterer.prototype.getMarkers; +MarkerClusterer.prototype['getMaxZoom'] = MarkerClusterer.prototype.getMaxZoom; +MarkerClusterer.prototype['getStyles'] = MarkerClusterer.prototype.getStyles; +MarkerClusterer.prototype['getTotalClusters'] = + MarkerClusterer.prototype.getTotalClusters; +MarkerClusterer.prototype['getTotalMarkers'] = + MarkerClusterer.prototype.getTotalMarkers; +MarkerClusterer.prototype['redraw'] = MarkerClusterer.prototype.redraw; +MarkerClusterer.prototype['removeMarker'] = + MarkerClusterer.prototype.removeMarker; +MarkerClusterer.prototype['removeMarkers'] = + MarkerClusterer.prototype.removeMarkers; +MarkerClusterer.prototype['resetViewport'] = + MarkerClusterer.prototype.resetViewport; +MarkerClusterer.prototype['repaint'] = + MarkerClusterer.prototype.repaint; +MarkerClusterer.prototype['setCalculator'] = + MarkerClusterer.prototype.setCalculator; +MarkerClusterer.prototype['setGridSize'] = + MarkerClusterer.prototype.setGridSize; +MarkerClusterer.prototype['setMaxZoom'] = + MarkerClusterer.prototype.setMaxZoom; +MarkerClusterer.prototype['onAdd'] = MarkerClusterer.prototype.onAdd; +MarkerClusterer.prototype['draw'] = MarkerClusterer.prototype.draw; + +Cluster.prototype['getCenter'] = Cluster.prototype.getCenter; +Cluster.prototype['getSize'] = Cluster.prototype.getSize; +Cluster.prototype['getMarkers'] = Cluster.prototype.getMarkers; + +ClusterIcon.prototype['onAdd'] = ClusterIcon.prototype.onAdd; +ClusterIcon.prototype['draw'] = ClusterIcon.prototype.draw; +ClusterIcon.prototype['onRemove'] = ClusterIcon.prototype.onRemove; diff --git a/javascript/google/streetview.google.template.js b/javascript/google/streetview.google.template.js new file mode 100644 index 0000000..72209fb --- /dev/null +++ b/javascript/google/streetview.google.template.js @@ -0,0 +1,9 @@ +var options = { + latitude: $Latitude, + longitude: $Longitude, + zoom: $Zoom, + pitch: $Pitch, + heading: $Heading, + domid: '$DomID' +} +registerStreetView(options); diff --git a/javascript/lat_long_field.js b/javascript/lat_long_field.js deleted file mode 100644 index cc3afca..0000000 --- a/javascript/lat_long_field.js +++ /dev/null @@ -1,60 +0,0 @@ -var marker; -(function($) { - $(function() { - $('.geocode_button').live("click", function() { - var $t = $(this); - var address_fields = $t.metadata().aFields; - var parts = address_fields.split(','); - var address_values = []; - for(var i=0; i < parts.length; i++) { - address_values.push($('[name='+parts[i]+']').val()); - } - $.get( - $t.attr('href'), - {address: address_values.join(" " )}, - function(data) { - var loc = data.split(","); - var $lat = $('[name='+$t.metadata().lat+']'); - var $long = $('[name='+$t.metadata().long+']'); - $lat.val(loc[0]); - $long.val(loc[1]); - if($lat.siblings('span.readonly').length) { - $lat.siblings('span.readonly').text(loc[0]); - } - if($long.siblings('span.readonly').length) { - $long.siblings('span.readonly').text(loc[1]); - } - - //update map - var point = new GLatLng($lat.val(), $long.val()); - marker.setPoint(point); - map.setCenter(point,16); - - } - ); - return false; - }); - var $t = $('.geocode_button'); - var $lat = $('[name='+$t.metadata().lat+']'); - var $long = $('[name='+$t.metadata().long+']'); - var center = new GLatLng($lat.val(), $long.val()); - marker = new GMarker(center, {draggable: true}); - map.setCenter(center,16); - map.addOverlay(marker); - GEvent.addListener(marker, "dragend", function(overlay, point) { - var point = marker.getLatLng(); - map.setCenter(point); - var $lat = $('[name='+$t.metadata().lat+']'); - var $long = $('[name='+$t.metadata().long+']'); - $lat.val(point.y); - $long.val(point.x); - if($lat.siblings('span.readonly').length) { - $lat.siblings('span.readonly').text(point.y); - } - if($long.siblings('span.readonly').length) { - $long.siblings('span.readonly').text(point.x); - } - - }); - }); -})(jQuery); diff --git a/javascript/mapField.js b/javascript/mapField.js new file mode 100755 index 0000000..40d8153 --- /dev/null +++ b/javascript/mapField.js @@ -0,0 +1,290 @@ +// FIXME avoid global +var marker; + +var bounds; + +function gmloaded() { + initLivequery(); +} + + +// initialise the map +function initMap() { + var myOptions = { + zoom: 16, + disableDefaultUI: false, + mapTypeId: google.maps.MapTypeId.ROADMAP, + disableDoubleClickZoom: false, + draggable: true, + keyboardShortcuts: false, + scrollwheel: true + }; + + (function($) { + var gm = $('#GoogleMap'); + var latFieldName = gm.attr('data-latfieldname'); + + var latField = $('input[name=' + gm.attr('data-latfieldname') + ']'); + var lonField = $('input[name=' + gm.attr('data-lonfieldname') + ']'); + var zoomField = $('input[name=' + gm.attr('data-zoomfieldname') + ']'); + var guidePointsAttr = gm.attr('data-GuidePoints'); + + // if we have emtpy initial values, set them appropriately, + // otherwise googlemaps codegoes into an infinite tailspin + if (latField.val() === '') { + latField.val(0); + } + + if (lonField.val() === '') { + lonField.val(0); + } + + if (zoomField.val() === '') { + zoomField.val(2); + } + + var guidePoints = []; + if (typeof guidePointsAttr != "undefined") { + guidePoints = JSON.parse(guidePointsAttr); + } + + myOptions.center = new google.maps.LatLng(latField.val(), lonField.val()); + if (zoomField.length) { + myOptions.zoom = parseInt(zoomField.val(), 10); + } + + map = new google.maps.Map(document.getElementById("GoogleMap"), myOptions); + bounds = new google.maps.LatLngBounds(); + + // guide points are grey marked out pins that are used as contextual hints to the current + // desired location. An example of this would be photographs taken on the same bike + // ride or a walk + if (guidePoints.length) { + var sumlat = 0; + var sumlon = 0; + for (var i = guidePoints.length - 1; i >= 0; i--) { + var lat = guidePoints[i].latitude; + var lon = guidePoints[i].longitude; + addGuideMarker(lat, lon); + var latlng = new google.maps.LatLng(lat, lon); + sumlat = sumlat + parseFloat(lat); + sumlon = sumlon + parseFloat(lon); + + // extend bounds + bounds.extend(latlng); + } + + if ((latField.val() === 0) && (lonField.val() === 0)) { + var nPoints = guidePoints.length; + var newMarkerPos = new google.maps.LatLng(sumlat / nPoints, sumlon / nPoints); + } + + map.fitBounds(bounds); + } + + if (latField.val() && lonField.val()) { + marker = null; + setMarker(myOptions.center, true); + } + + // when one right clicks, set the red marker flag to that coordinate + google.maps.event.addListener(map, "rightclick", function(event) { + var lat = event.latLng.lat(); + var lng = event.latLng.lng(); + latField.val(lat); + lonField.val(lng); + setMarker(event.latLng, false); + statusMessage('Location changed to ' + lat + ',' + lng); + }); + + google.maps.event.addListener(map, "zoom_changed", function(e) { + if (zoomField.length) { + zoomField.val(map.getZoom()); + } + }); + + google.maps.event.trigger(map, 'resize'); + map.setZoom(map.getZoom()); + + // When any tab is clicked, resize the map + $('.ui-tabs-anchor').click(function() { + google.maps.event.trigger(map, 'resize'); + var gm = $('#GoogleMap'); + var useMapBounds = gm.attr('data-usemapbounds'); + if (useMapBounds) { + map.fitBounds(bounds); + } else { + map.setCenter(marker.getPosition()); + } + }); + + })(jQuery); + +} + + +// utility functions +function addGuideMarker(lat, lon) { + var latlng = new google.maps.LatLng(lat, lon); + var pinColor = "CCCCCC"; + var pinImage = new google.maps.MarkerImage("//chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|" + pinColor, + new google.maps.Size(21, 34), + new google.maps.Point(0, 0), + new google.maps.Point(10, 34)); + var pinShadow = new google.maps.MarkerImage("//chart.apis.google.com/chart?chst=d_map_pin_shadow", + new google.maps.Size(40, 37), + new google.maps.Point(0, 0), + new google.maps.Point(12, 35)); + var guideMarker = new google.maps.Marker({ + position: latlng, + title: "Marker", + icon: pinImage, + shadow: pinShadow + }); + guideMarker.setMap(map); + +} + + +function setMarker(location, recenter) { + if (marker !== null) { + marker.setPosition(location); + } else { + marker = new google.maps.Marker({ + position: location, + title: "Position", + draggable: true + }); + marker.setMap(map); + google.maps.event.addListener(marker, 'dragend', setCoordByMarker); + } + + if (recenter) { + map.setCenter(location); + } +} + + +function setCoordByMarker(event) { + (function($) { + var gm = $('#GoogleMap'); + var latField = $('input[name=' + gm.attr('data-latfieldname') + ']'); + var lonField = $('input[name=' + gm.attr('data-lonfieldname') + ']'); + var zoomField = $('input[name=' + gm.attr('data-zoomfieldname') + ']'); + var lat = event.latLng.lat(); + var lng = event.latLng.lng(); + latField.val(lat); + lonField.val(lng); + setMarker(event.latLng, true); + statusMessage('Location changed to ' + lat + ',' + lng); + if (zoomField.length) { + zoomField.val(map.getZoom()); + } + map.setCenter(event.latLng); + })(jQuery); +} + + +function searchForAddress(address) { + (function($) { + var geocoder = new google.maps.Geocoder(); + var elevator = new google.maps.ElevationService(); + if (geocoder) { + statusMessage("Searching for:" + address); + geocoder.geocode({ + 'address': address + }, function(results, status) { + if (status == google.maps.GeocoderStatus.OK) { + var l = results.length; + if (l > 0) { + statusMessage("Places found"); + } else if (l === 0) { + errorMessage("No places found"); + } + var html = '
    '; + //mapSearchResults + $.each(results, function(index, value) { + var address = []; + $.each(value.address_components, function(i, v) { + address.push(v.long_name); + }); + html = html + '
  • ' + address + "
  • "; + }); + html = html + "
"; + $('#mapSearchResults').html(html); + } else { + errorMessage("Unable to find any geocoded results"); + } + }); + } + })(jQuery); +} + + +// prime livequery events +function initLivequery() { + (function($) { + + //triggers + $('input[name=action_GetCoords]').livequery('click', function(e) { + // get the data needed to ask coords + var location = $('#Form_EditForm_Location').val(); + setCoordByAddress(location); + return false; + }); + + $('#searchLocationButton').livequery('click', function(e) { + // get the data needed to ask coords + var location = $('#location_search').val(); + searchForAddress(location); + return false; + }); + + //geocodedSearchResults + $('.geocodedSearchResults li').livequery('click', function(e) { + // get the data needed to ask coords + var t = $(this); + var lat = t.attr("lat"); + var lon = t.attr("lon"); + var address = t.html(); + var latlng = new google.maps.LatLng(lat, lon); + statusMessage("Setting map to " + address); + $('.geocodedSearchResults').html(''); + $('#Form_EditForm_Latitude').val(lat); + $('#Form_EditForm_Longitude').val(lon); + + var gm = $('#GoogleMap'); + var latField = $('input[name=' + gm.attr('data-latfieldname') + ']'); + var lonField = $('input[name=' + gm.attr('data-lonfieldname') + ']'); + var zoomField = $('input[name=' + gm.attr('data-zoomfieldname') + ']'); + + latField.val(lat); + lonField.val(lon); + + // zoom in to an appropriate level + map.setZoom(12); + + setMarker(latlng, true); + return false; + }); + $('#GoogleMap').livequery(function() { + initMap(); + }); + })(jQuery); +} + + +(function($) { + function loadGoogleMapsAPI() { + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = "//maps.googleapis.com/maps/api/js?sensor=false&callback=gmloaded"; + document.body.appendChild(script); + } + + + // deal with document ready - note this only gets called once due to the way silverstripe works, until the CMS is refreshed + $(document).ready(function() { + loadGoogleMapsAPI(); + }); +})(jQuery); diff --git a/javascript/mapField.min.js b/javascript/mapField.min.js new file mode 100644 index 0000000..e3beb81 --- /dev/null +++ b/javascript/mapField.min.js @@ -0,0 +1 @@ +function gmloaded(){initLivequery()}function initMap(){var a={zoom:16,disableDefaultUI:!1,mapTypeId:google.maps.MapTypeId.ROADMAP,disableDoubleClickZoom:!1,draggable:!0,keyboardShortcuts:!1,scrollwheel:!0};!function(b){var c=b("#GoogleMap"),d=(c.attr("data-latfieldname"),b("input[name="+c.attr("data-latfieldname")+"]")),e=b("input[name="+c.attr("data-lonfieldname")+"]"),f=b("input[name="+c.attr("data-zoomfieldname")+"]"),g=c.attr("data-GuidePoints");""===d.val()&&d.val(0),""===e.val()&&e.val(0),""===f.val()&&f.val(2);var h=[];if("undefined"!=typeof g&&(h=JSON.parse(g)),a.center=new google.maps.LatLng(d.val(),e.val()),f.length&&(a.zoom=parseInt(f.val(),10)),map=new google.maps.Map(document.getElementById("GoogleMap"),a),bounds=new google.maps.LatLngBounds,h.length){for(var i=0,j=0,k=h.length-1;k>=0;k--){var l=h[k].latitude,m=h[k].longitude;addGuideMarker(l,m);var n=new google.maps.LatLng(l,m);i+=parseFloat(l),j+=parseFloat(m),bounds.extend(n)}if(0===d.val()&&0===e.val()){var o=h.length;new google.maps.LatLng(i/o,j/o)}map.fitBounds(bounds)}d.val()&&e.val()&&(marker=null,setMarker(a.center,!0)),google.maps.event.addListener(map,"rightclick",function(a){var b=a.latLng.lat(),c=a.latLng.lng();d.val(b),e.val(c),setMarker(a.latLng,!1),statusMessage("Location changed to "+b+","+c)}),google.maps.event.addListener(map,"zoom_changed",function(a){f.length&&f.val(map.getZoom())}),google.maps.event.trigger(map,"resize"),map.setZoom(map.getZoom()),b(".ui-tabs-anchor").click(function(){google.maps.event.trigger(map,"resize");var a=b("#GoogleMap"),c=a.attr("data-usemapbounds");c?map.fitBounds(bounds):map.setCenter(marker.getPosition())})}(jQuery)}function addGuideMarker(a,b){var c=new google.maps.LatLng(a,b),d="CCCCCC",e=new google.maps.MarkerImage("//chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|"+d,new google.maps.Size(21,34),new google.maps.Point(0,0),new google.maps.Point(10,34)),f=new google.maps.MarkerImage("//chart.apis.google.com/chart?chst=d_map_pin_shadow",new google.maps.Size(40,37),new google.maps.Point(0,0),new google.maps.Point(12,35)),g=new google.maps.Marker({position:c,title:"Marker",icon:e,shadow:f});g.setMap(map)}function setMarker(a,b){null!==marker?marker.setPosition(a):(marker=new google.maps.Marker({position:a,title:"Position",draggable:!0}),marker.setMap(map),google.maps.event.addListener(marker,"dragend",setCoordByMarker)),b&&map.setCenter(a)}function setCoordByMarker(a){!function(b){var c=b("#GoogleMap"),d=b("input[name="+c.attr("data-latfieldname")+"]"),e=b("input[name="+c.attr("data-lonfieldname")+"]"),f=b("input[name="+c.attr("data-zoomfieldname")+"]"),g=a.latLng.lat(),h=a.latLng.lng();d.val(g),e.val(h),setMarker(a.latLng,!0),statusMessage("Location changed to "+g+","+h),f.length&&f.val(map.getZoom()),map.setCenter(a.latLng)}(jQuery)}function searchForAddress(a){!function(b){{var c=new google.maps.Geocoder;new google.maps.ElevationService}c&&(statusMessage("Searching for:"+a),c.geocode({address:a},function(a,c){if(c==google.maps.GeocoderStatus.OK){var d=a.length;d>0?statusMessage("Places found"):0===d&&errorMessage("No places found");var e='
    ';b.each(a,function(a,c){var d=[];b.each(c.address_components,function(a,b){d.push(b.long_name)}),e=e+'
  • '+d+"
  • "}),e+="
",b("#mapSearchResults").html(e)}else errorMessage("Unable to find any geocoded results")}))}(jQuery)}function initLivequery(){!function(a){a("input[name=action_GetCoords]").livequery("click",function(b){var c=a("#Form_EditForm_Location").val();return setCoordByAddress(c),!1}),a("#searchLocationButton").livequery("click",function(b){var c=a("#location_search").val();return searchForAddress(c),!1}),a(".geocodedSearchResults li").livequery("click",function(b){var c=a(this),d=c.attr("lat"),e=c.attr("lon"),f=c.html(),g=new google.maps.LatLng(d,e);statusMessage("Setting map to "+f),a(".geocodedSearchResults").html(""),a("#Form_EditForm_Latitude").val(d),a("#Form_EditForm_Longitude").val(e);{var h=a("#GoogleMap"),i=a("input[name="+h.attr("data-latfieldname")+"]"),j=a("input[name="+h.attr("data-lonfieldname")+"]");a("input[name="+h.attr("data-zoomfieldname")+"]")}return i.val(d),j.val(e),map.setZoom(12),setMarker(g,!0),!1}),a("#GoogleMap").livequery(function(){initMap()})}(jQuery)}var marker,bounds;!function(a){function b(){var a=document.createElement("script");a.type="text/javascript",a.src="//maps.googleapis.com/maps/api/js?sensor=false&callback=gmloaded",document.body.appendChild(a)}a(document).ready(function(){b()})}(jQuery); \ No newline at end of file diff --git a/lang/en.yml b/lang/en.yml new file mode 100644 index 0000000..19fbaa3 --- /dev/null +++ b/lang/en.yml @@ -0,0 +1,5 @@ +en: + POIMapPage: + PLURALNAME: 'Points of Interest Pages' + SINGULARNAME: 'Points of Interest Page' + DESCRIPTION: 'Page containing layers of points of interest that can be rendered as a map' diff --git a/package.json b/package.json new file mode 100644 index 0000000..a2cc4b8 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "mappable", + "version": "0.1.0", + "devDependencies": { + "grunt": "~0.4.5", + "grunt-contrib-jshint": "~0.11.2", + "grunt-contrib-nodeunit": "~0.4.1", + "grunt-contrib-uglify": "~0.9.1", + "grunt-contrib-concat": "~0.5.1", + "grunt-contrib-cssmin": "~0.12.2", + "grunt-contrib-watch": "~0.6.1", + "grunt-contrib-clean": "~0.6.0" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..a46bdd0 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,23 @@ + + + + ./tests + + + + + + + + + sanitychecks + + + + + + ../mappable + + + + diff --git a/templates/Includes/GoogleJavaScript.ss b/templates/Includes/GoogleJavaScript.ss new file mode 100644 index 0000000..4489c20 --- /dev/null +++ b/templates/Includes/GoogleJavaScript.ss @@ -0,0 +1 @@ +<% require javascript("framework/thirdparty/jquery/jquery.js") %><% if $UseCompressedAssets %><% require javascript("mappable/javascript/google/mappablegoogle.min.js") %><% else %><% require javascript("mappable/javascript/google/FullScreenControl.js") %><% require javascript("mappable/javascript/google/markerclusterer.js") %><% require javascript("mappable/javascript/google/maputil.js") %><% end_if %> diff --git a/templates/Includes/GoogleMapShortCode.ss b/templates/Includes/GoogleMapShortCode.ss new file mode 100644 index 0000000..32385b9 --- /dev/null +++ b/templates/Includes/GoogleMapShortCode.ss @@ -0,0 +1,11 @@ +<% include GoogleJavaScript %> +
+
+<% if $Caption %>

$Caption

<% end_if %> +
diff --git a/templates/Includes/GoogleStreetView.ss b/templates/Includes/GoogleStreetView.ss new file mode 100644 index 0000000..4361048 --- /dev/null +++ b/templates/Includes/GoogleStreetView.ss @@ -0,0 +1,11 @@ +<% include GoogleJavaScript %> +
+
+<% if $Caption %>

$Caption

<% end_if %> +
diff --git a/templates/Layout/MapInfoWindow.ss b/templates/Layout/MapInfoWindow.ss new file mode 100644 index 0000000..5f4079a --- /dev/null +++ b/templates/Layout/MapInfoWindow.ss @@ -0,0 +1 @@ +$Name diff --git a/tests/GoogleMapShortCodeTest.php b/tests/GoogleMapShortCodeTest.php new file mode 100644 index 0000000..bd8371b --- /dev/null +++ b/tests/GoogleMapShortCodeTest.php @@ -0,0 +1,91 @@ +objFromFixture('Page', 'RoadMap'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $expected = << +
+

Roads in Central Bangkok

+ + +TEXT; + $this->assertEquals($expected, $html); + } + + public function testAerialMap() + { + GoogleMapShortCodeHandler::resetCounter(); + $page = $this->objFromFixture('Page', 'AerialMap'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $expected = << +
+

Bang Sue Train Depot, Thailand

+ + +TEXT; + $this->assertEquals($expected, $html); + } + + public function testHybridMap() + { + GoogleMapShortCodeHandler::resetCounter(); + $page = $this->objFromFixture('Page', 'HybridMap'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $expected = << +
+

Junction in Bangkok, Thailand

+ + +TEXT; + $this->assertEquals($expected, $html); + } + + public function testTerrainmap() + { + GoogleMapShortCodeHandler::resetCounter(); + $page = $this->objFromFixture('Page', 'TerrainMap'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $expected = << +
+

Mountains west of Chiang Mai

+ + +TEXT; + $this->assertEquals($expected, $html); + } + + public function testNoLongitude() + { + $page = $this->objFromFixture('Page', 'MapWithNoLongitude'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $this->assertEquals('Some text', $html); + } + + public function testNoLatitude() + { + $page = $this->objFromFixture('Page', 'MapWithNoLatitude'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $this->assertEquals('Some text', $html); + } +} diff --git a/tests/GoogleStreetViewShortCodeTest.php b/tests/GoogleStreetViewShortCodeTest.php new file mode 100644 index 0000000..b73059e --- /dev/null +++ b/tests/GoogleStreetViewShortCodeTest.php @@ -0,0 +1,64 @@ +objFromFixture('Page', 'StreetView'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $expected = << +
+

Canal south from Pracha Rat 1 Soi 28

+ + +TEXT; + $this->assertEquals($expected, $html); + } + + public function testNoLongitude() + { + GoogleStreetViewShortCodeHandler::resetCounter(); + $page = $this->objFromFixture('Page', 'StreetViewNoLongitude'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $this->assertEquals('Some text', $html); + } + + public function testNoLatitude() + { + GoogleStreetViewShortCodeHandler::resetCounter(); + $page = $this->objFromFixture('Page', 'StreetViewNoLatitude'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $this->assertEquals('Some text', $html); + } + + public function testNoHeading() + { + GoogleStreetViewShortCodeHandler::resetCounter(); + $page = $this->objFromFixture('Page', 'StreetViewNoHeading'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $this->assertEquals('Some text', $html); + } + + public function testZoom() + { + GoogleStreetViewShortCodeHandler::resetCounter(); + $page = $this->objFromFixture('Page', 'StreetViewWithZoom'); + $html = ShortcodeParser::get_active()->parse($page->Content); + $expected = <<< TEXT +Some text + +
+
+

Canal south from Pracha Rat 1 Soi 28

+
+ +TEXT; + $this->assertEquals($expected, $html); + } +} diff --git a/tests/LatLongFieldTest.php b/tests/LatLongFieldTest.php new file mode 100644 index 0000000..5cbaf3f --- /dev/null +++ b/tests/LatLongFieldTest.php @@ -0,0 +1,127 @@ +fail('Creation of lat long field should have failed'); + } catch (Exception $e) { + $expected = 'LatLongField argument 1 must be an array containing at' + .' least two FormField objects for Lat/Long values, resp' + .'ectively.'; + $this->assertEquals($expected, $e->getMessage()); + } + } + + public function testConstructTwoFieldsValid() + { + $mapField = new LatLongField( + array( + new TextField('Lat', 'Latitude'), + new TextField('Lon', 'Longitude'), + ) + ); + + $html = $mapField->FieldHolder(); + $this->assertContains( + '', + $html + ); + $this->assertContains( + '', + $html + ); + $this->assertContains( + '', + $html + ); + $this->assertContains( + '', + $html + ); + } + + public function testConstructThreeFieldsValid() + { + $mapField = new LatLongField( + array( + new TextField('Lat', 'Latitude'), + new TextField('Lon', 'Longitude'), + new TextField('ZoomLevel', 'Zoom'), + ) + ); + + $html = $mapField->FieldHolder(); + $this->assertContains( + '', + $html + ); + $this->assertContains( + '', + $html + ); + $this->assertContains( + '', + $html + ); + $this->assertContains( + '', + $html + ); + $this->assertContains( + '', + $html + ); + $this->assertContains( + '', + $html + ); + } + + public function testGeocode() + { + $this->markTestSkipped('TODO'); + } + + public function testSetGuidePoints() + { + $mapField = new LatLongField( + array( + new TextField('Lat', 'Latitude'), + new TextField('Lon', 'Longitude'), + new TextField('ZoomLevel', 'Zoom'), + ) + ); + $guidePoints = array( + array('latitude' => 42, 'longitude' => '113.1'), + array('latitude' => 14.9, 'longitude' => '113.2'), + array('latitude' => 42.3, 'longitude' => '113.4'), + ); + $mapField->setGuidePoints($guidePoints); + + $html = $mapField->FieldHolder(); + $expected = 'data-GuidePoints="[{"latitude":42,"longitude":"113.1&' + .'quot;},{"latitude":14.9,"longitude":"113.2"},{&q' + .'uot;latitude":42.3,"longitude":"113.4"}]"'; + + $this->assertContains($expected, $html); + } +} diff --git a/tests/MapAPITest.php b/tests/MapAPITest.php new file mode 100644 index 0000000..264e602 --- /dev/null +++ b/tests/MapAPITest.php @@ -0,0 +1,590 @@ +requiredExtensions = array( + 'Member' => array('MapExtension'), + ); + parent::setupOnce(); + } + + public function setUp() + { + MapUtil::reset(); + parent::setUp(); + } + + public function testSetClusterer() + { + $map = $this->getMap(); + $map->setClusterer(true); + $html = $map->forTemplate(); + $this->assertContains('data-clusterergridsize=50', $html); + $this->assertContains('data-clusterermaxzoom=17', $html); + $this->assertContains('data-useclusterer=1', $html); + + $map = $this->getMap(); + $map->setClusterer(true, 60, 14); + $html = $map->forTemplate(); + $this->assertContains('data-clusterergridsize=60', $html); + $this->assertContains('data-clusterermaxzoom=14', $html); + $this->assertContains('data-useclusterer=1', $html); + + $map = $this->getMap(); + $map->setClusterer(false); + $html = $map->forTemplate(); + $this->assertContains('data-useclusterer=false', $html); + $this->assertContains('data-clusterergridsize=50', $html); + $this->assertContains('data-clusterermaxzoom=17', $html); + } + + /* + Toggle as to whether or not to include a style= attribute with width/height + */ + public function testSetShowInlineMapDivStyle() + { + $map = $this->getMap(); + $map->setShowInlineMapDivStyle(true); + $html = $map->forTemplate(); + $expected = 'style="width:100%; height: 400px;"'; + $this->assertContains($expected, $html); + + $map->setShowInlineMapDivStyle(false); + $html = $map->forTemplate(); + $this->assertNotContains($expected, $html); + } + + public function testSetAdditionalCSSClasses() + { + $map = $this->getMap(); + $map->setAdditionalCSSClasses('bigMap shadowMap'); + $html = $map->forTemplate(); + $expected = 'class="bigMap shadowMap mappable"'; + $this->assertContains($expected, $html); + $map->setAdditionalCSSClasses('bigMap shadowMap'); + } + + public function testSetMapStyle() + { + $style = << + n/a +#trailsstyleWat bot loop: Segment (1) +1 + +100.502187,13.893589,12.489117 +100.502306,13.893550,11.420303 +100.502403,13.893468,8.498596 +100.502467,13.893355,11.156882 +100.502521,13.893244,12.405776 +100.502569,13.893151,11.533149 +100.502618,13.893058,12.998710 +100.502692,13.892954,10.693356 +100.502768,13.892871,13.830301 +100.502873,13.892789,19.022263 +100.502994,13.892735,12.802382 +100.503103,13.892699,12.251826 +100.503229,13.892690,13.695211 +100.503350,13.892695,11.813846 +100.503473,13.892706,13.870461 +100.503598,13.892708,10.766499 +100.503702,13.892706,13.136336 +100.503827,13.892696,11.557716 +100.503930,13.892685,11.176296 +100.504035,13.892648,8.320370 +100.504122,13.892604,14.492889 +100.504221,13.892550,4.760454 +100.504342,13.892488,5.680111 +100.504445,13.892393,9.436865 +100.504510,13.892323,10.983356 + + + + + diff --git a/tests/mapextensions.yml b/tests/mapextensions.yml new file mode 100644 index 0000000..93d6f1f --- /dev/null +++ b/tests/mapextensions.yml @@ -0,0 +1,4 @@ +Member: + WithLayers: + Title: Member With Map and Layers + Content: Some content diff --git a/tests/shortcodes.yml b/tests/shortcodes.yml new file mode 100644 index 0000000..11bc665 --- /dev/null +++ b/tests/shortcodes.yml @@ -0,0 +1,34 @@ +Page: + RoadMap: + Title: Road Map + Content: Some text[GoogleMap latitude='13.7402946' longitude='100.5525439' caption="Roads in Central Bangkok" zoom="14" maptype="road"] + AerialMap: + Title: Aerial Map + Content: Some text[GoogleMap latitude='13.815483' longitude='100.5447213' caption="Bang Sue Train Depot, Thailand" zoom="20" maptype="aerial"] + HybridMap: + Title: Hybrid Map + Content: Some text[GoogleMap latitude='13.8309545' longitude='100.5577219' caption="Junction in Bangkok, Thailand" zoom="18" maptype="hybrid"] + TerrainMap: + Title: Terrain Map + Content: Some text[GoogleMap latitude='18.8032393' longitude='98.9166518' caption="Mountains west of Chiang Mai" zoom="14" maptype="terrain"] + MapWithNoLatitude: + Title: Map With No Latitude + Content: Some text[GoogleMap longitude='98.9166518' caption="Mountains west of Chiang Mai" zoom="14" maptype="terrain"] + MapWithNoLongitude: + Title: Map With No Longitude + Content: Some text[GoogleMap latitude='18.8032393' caption="Mountains west of Chiang Mai" zoom="14" maptype="terrain"] + StreetView: + Title: Valid StreetView + Content: Some text[GoogleStreetView latitude="13.811841" longitude="100.527309" heading="162.43" pitch="-10" caption="Canal south from Pracha Rat 1 Soi 28"] + StreetViewWithZoom: + Title: Valid StreetView With Zoom + Content: Some text[GoogleStreetView latitude="13.811841" longitude="100.527309" zoom="12" heading="162.43" pitch="-10" caption="Canal south from Pracha Rat 1 Soi 28"] + StreetViewNoHeading: + Title: StreetView with no Heading + Content: Some text[GoogleStreetView longitude="100.527309" latitude="13.811841" pitch="-10" caption="Canal south from Pracha Rat 1 Soi 28"] + StreetViewNoLatitude: + Title: StreetView with no Latitude + Content: Some text[GoogleStreetView longitude="100.527309" heading="162.43" pitch="-10" caption="Canal south from Pracha Rat 1 Soi 28"] + StreetViewNoLongitude: + Title: StreetView with no Longitude + Content: Some text[GoogleStreetView latitude="13.811841" heading="162.43" pitch="-10" caption="Canal south from Pracha Rat 1 Soi 28"] diff --git a/tests/templates/Includes/Member_MapInfoWindow.ss b/tests/templates/Includes/Member_MapInfoWindow.ss new file mode 100644 index 0000000..e4914d1 --- /dev/null +++ b/tests/templates/Includes/Member_MapInfoWindow.ss @@ -0,0 +1 @@ +MEMBER: $Name{$TestKey}