From 8fdc6dd6921bf7b106d82e016cdd414b021a4851 Mon Sep 17 00:00:00 2001 From: cybice Date: Wed, 10 Jun 2015 15:16:41 +0300 Subject: [PATCH] new: google map react --- .babelrc | 3 + .eslintrc | 232 +++++++++++++ .gitignore | 2 + .npmignore | 6 + README.md | 2 + package.json | 47 +++ scripts/build.sh | 3 + src/__tests__/eye_test.js | 37 +++ src/google_map.js | 436 +++++++++++++++++++++++++ src/google_map_map.js | 27 ++ src/google_map_markers.js | 232 +++++++++++++ src/google_map_markers_prerender.js | 29 ++ src/marker_dispatcher.js | 25 ++ src/utils/array_helper.js | 12 + src/utils/detect.js | 31 ++ src/utils/geo.js | 107 ++++++ src/utils/lib_geo/LICENCE.txt | 27 ++ src/utils/lib_geo/lat_lng.js | 29 ++ src/utils/lib_geo/lat_lng_bounds.js | 72 ++++ src/utils/lib_geo/readme.md | 2 + src/utils/lib_geo/transform.js | 121 +++++++ src/utils/lib_geo/wrap.js | 6 + src/utils/loaders/google_map_loader.js | 45 +++ 23 files changed, 1533 insertions(+) create mode 100644 .babelrc create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 README.md create mode 100644 package.json create mode 100755 scripts/build.sh create mode 100644 src/__tests__/eye_test.js create mode 100644 src/google_map.js create mode 100644 src/google_map_map.js create mode 100644 src/google_map_markers.js create mode 100644 src/google_map_markers_prerender.js create mode 100644 src/marker_dispatcher.js create mode 100644 src/utils/array_helper.js create mode 100644 src/utils/detect.js create mode 100644 src/utils/geo.js create mode 100644 src/utils/lib_geo/LICENCE.txt create mode 100644 src/utils/lib_geo/lat_lng.js create mode 100644 src/utils/lib_geo/lat_lng_bounds.js create mode 100644 src/utils/lib_geo/readme.md create mode 100644 src/utils/lib_geo/transform.js create mode 100644 src/utils/lib_geo/wrap.js create mode 100644 src/utils/loaders/google_map_loader.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..ce840ab --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "stage": 0 +} \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..ad6b9f8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,232 @@ +{ + "parser": 'babel-eslint', + "env": { + "browser": true, + "node": true, + "es6": true + }, + "globals": { + "__DEV__": false + }, + "ecmaFeatures": { + "arrowFunctions": true, + "binaryLiterals": true, + "blockBindings": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "forOf": true, + "generators": true, + "modules": true, + "objectLiteralComputedProperties": true, + "objectLiteralDuplicateProperties": true, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "octalLiterals": true, + "regexUFlag": true, + "regexYFlag": true, + "spread": true, + "superInFunctions": true, + "templateStrings": true, + "unicodeCodePointEscapes": true, + "globalReturn": true, + "jsx": true + }, + "plugins": [ + "react" + ], + "rules": { + "react/display-name": 0, + "react/jsx-boolean-value": 1, + "react/jsx-quotes": 1, + "react/jsx-no-undef": 1, + "react/jsx-sort-props": 0, + "react/jsx-sort-prop-types": 0, + "react/jsx-uses-react": 1, + "react/jsx-uses-vars": 1, + "react/no-did-mount-set-state": 1, + "react/no-did-update-set-state": 1, + "react/no-multi-comp": 1, + "react/no-unknown-property": 1, + "react/prop-types": 1, + "react/react-in-jsx-scope": 1, + "react/self-closing-comp": 1, + "react/wrap-multilines": 1, + /** + * Strict mode + */ + // babel inserts "use strict"; for us + // http://eslint.org/docs/rules/strict + "strict": [2, "never"], + /** + * ES6 + */ + "no-var": 2, // http://eslint.org/docs/rules/no-var + + /** + * Variables + */ + "no-shadow": 1, // http://eslint.org/docs/rules/no-shadow + "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names + "no-unused-vars": [ + 1, + { + // http://eslint.org/docs/rules/no-unused-vars + "vars": "local", + "args": "after-used" + } + ], + "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define + + /** + * Possible errors + */ + "comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle + "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign + "no-console": 1, // http://eslint.org/docs/rules/no-console + "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger + "no-alert": 1, // http://eslint.org/docs/rules/no-alert + "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition + "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys + "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case + "no-empty": 2, // http://eslint.org/docs/rules/no-empty + "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign + "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast + "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi + "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign + "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations + "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp + "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace + "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls + "no-reserved-keys": 2, // http://eslint.org/docs/rules/no-reserved-keys + "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays + "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable + "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan + "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var + + /** + * Best practices + */ + "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return + "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly + "default-case": 2, // http://eslint.org/docs/rules/default-case + "dot-notation": [ + 2, + { + // http://eslint.org/docs/rules/dot-notation + "allowKeywords": true + } + ], + "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq + "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in + "no-caller": 2, // http://eslint.org/docs/rules/no-caller + "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return + "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null + "no-eval": 2, // http://eslint.org/docs/rules/no-eval + "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native + "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind + "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough + "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal + "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval + "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks + "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func + "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str + "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign + "no-new": 2, // http://eslint.org/docs/rules/no-new + "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func + "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers + "no-octal": 2, // http://eslint.org/docs/rules/no-octal + "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape + "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign + "no-proto": 2, // http://eslint.org/docs/rules/no-proto + "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare + "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign + "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url + "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare + "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences + "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal + "no-with": 2, // http://eslint.org/docs/rules/no-with + "radix": 2, // http://eslint.org/docs/rules/radix + "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top + "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife + "yoda": 2, // http://eslint.org/docs/rules/yoda + + /** + * Style + */ + "indent": [2, 2], // http://eslint.org/docs/rules/ + "brace-style": [ + 2, // http://eslint.org/docs/rules/brace-style + "1tbs", + { + "allowSingleLine": true + } + ], + "quotes": [ + 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes + ], + "camelcase": [ + 2, + { + // http://eslint.org/docs/rules/camelcase + "properties": "never" + } + ], + "comma-spacing": [ + 2, + { + // http://eslint.org/docs/rules/comma-spacing + "before": false, + "after": true + } + ], + "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style + "eol-last": 2, // http://eslint.org/docs/rules/eol-last + "func-names": 1, // http://eslint.org/docs/rules/func-names + "key-spacing": [ + 2, + { + // http://eslint.org/docs/rules/key-spacing + "beforeColon": false, + "afterColon": true + } + ], + "new-cap": [ + 2, + { + // http://eslint.org/docs/rules/new-cap + "newIsCap": true + } + ], + "no-multiple-empty-lines": [ + 2, + { + // http://eslint.org/docs/rules/no-multiple-empty-lines + "max": 2 + } + ], + "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary + "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object + "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func + "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces + "no-wrap-func": 2, // http://eslint.org/docs/rules/no-wrap-func + "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle + "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var + "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks + "semi": [2, "always"], // http://eslint.org/docs/rules/semi + "semi-spacing": [ + 2, + { + // http://eslint.org/docs/rules/semi-spacing + "before": false, + "after": true + } + ], + "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords + "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks + "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren + "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops + "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case + "spaced-line-comment": 2 // http://eslint.org/docs/rules/spaced-line-comment + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9cd57d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/modules \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..ef2c248 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +src +scripts +__tests__ +examples +.babelrc +.eslintrc \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed9100d --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +#google map react +isomorphic google map react component, allows render react components on the google map diff --git a/package.json b/package.json new file mode 100644 index 0000000..a95cffc --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "google-map-react", + "version": "0.3.0", + "description": "isomorphic google map react component, allows render react components on the google map", + "main": "modules/google_map.js", + "scripts": { + "build": "./scripts/build.sh", + "prepublish": "npm run build", + "eyetest": "babel-node ./src/__tests__/eye_test.js", + "es5eyetest": "node ./modules/__tests__/eye_test.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/istarkov/google-map-react.git" + }, + "keywords": [ + "react", + "reactjs", + "google", + "map", + "maps", + "isomorphic", + "render", + "component", + "javascript", + "react-component" + ], + "author": "istarkov https://github.com/istarkov", + "license": "MIT", + "bugs": { + "url": "https://github.com/istarkov/google-map-react/issues" + }, + "homepage": "https://github.com/istarkov/google-map-react#readme", + "dependencies": { + "eventemitter3": "^1.1.0", + "point-geometry": "0.0.0", + "react-pure-render": "^1.0.1", + "scriptjs": "^2.5.7" + }, + "peerDependencies": { + "react": ">=0.13.0 <0.15.0" + }, + "devDependencies": { + "babel": "^5.5.6", + "react": "^0.13.3" + } +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..7d32a89 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,3 @@ +#!/bin/sh +rm -rf modules +./node_modules/.bin/babel src --out-dir modules diff --git a/src/__tests__/eye_test.js b/src/__tests__/eye_test.js new file mode 100644 index 0000000..cde17e8 --- /dev/null +++ b/src/__tests__/eye_test.js @@ -0,0 +1,37 @@ +// "eye test" (c)somebody - means check output by your eyes :-) +// TODO read how to test react components + +import React, {PropTypes, Component} from 'react'; +import GoogleMap from '../google_map.js'; + +export default class SimpleTest extends Component { + static propTypes = { + center: PropTypes.array, + zoom: PropTypes.number, + greatPlaceCoords: PropTypes.any + }; + + static defaultProps = { + center: [59.938043, 30.337157], + zoom: 9, + greatPlaceCoords: {lat: 59.724465, lng: 30.080121} + }; + + constructor(props) { + super(props); + } + + render() { + return ( + +
----------I-PROMISE-TO---------------
+
-------WRITE-SOME-TESTS-------------
+
+ ); + } +} + +const html = React.renderToString(); +console.log(html); // eslint-disable-line no-console diff --git a/src/google_map.js b/src/google_map.js new file mode 100644 index 0000000..361f5b2 --- /dev/null +++ b/src/google_map.js @@ -0,0 +1,436 @@ +import React, {PropTypes, Component} from 'react'; +import shouldPureComponentUpdate from 'react-pure-render/function'; + +import MarkerDispatcher from './marker_dispatcher.js'; + +import GoogleMapMap from './google_map_map.js'; +import GoogleMapMarkers from './google_map_markers.js'; +import GoogleMapMarkersPrerender from './google_map_markers_prerender.js'; + +import googleMapLoader from './utils/loaders/google_map_loader.js'; +import detectBrowser from './utils/detect.js'; + +import Geo from './utils/geo.js'; +import isArraysEqualEps from './utils/array_helper.js'; + +const kEPS = 0.00001; +const K_GOOGLE_TILE_SIZE = 256; + +const K_MAP_CONTROL_OPTIONS = { + overviewMapControl: false, + streetViewControl: false, + rotateControl: true, + mapTypeControl: false, + // disable poi + styles: [{ featureType: 'poi', elementType: 'labels', stylers: [{ visibility: 'off' }]}], + minZoom: 3 // i need to dynamically calculate possible zoom value +}; + +const style = { + width: '100%', + height: '100%', + margin: 0, + padding: 0, + position: 'relative' +}; + + +function isNumber(n) { + return !Number.isNaN(parseFloat(n)) && Number.isFinite(n); +} + + +export default class GoogleMap extends Component { + + static propTypes = { + apiKey: PropTypes.string, + center: PropTypes.array.isRequired, + zoom: PropTypes.number.isRequired, + onBoundsChange: PropTypes.func, + onChildClick: PropTypes.func, + onChildMouseEnter: PropTypes.func, + onChildMouseLeave: PropTypes.func, + options: PropTypes.any, + distanceToMouse: PropTypes.func, + hoverDistance: PropTypes.number, + debounced: PropTypes.bool, + margin: PropTypes.array, + googleMapLoader: PropTypes.any + }; + + static defaultProps = { + distanceToMouse(pt, mousePos /*, markerProps*/) { + const x = pt.x; + const y = pt.y; // - 20; + return Math.sqrt((x - mousePos.x) * (x - mousePos.x) + (y - mousePos.y) * (y - mousePos.y)); + }, + hoverDistance: 30, + debounced: true, + options: K_MAP_CONTROL_OPTIONS, + googleMapLoader + }; + + shouldComponentUpdate = shouldPureComponentUpdate; + + constructor(props) { + super(props); + this.mounted_ = false; + + this.map_ = null; + this.maps_ = null; + this.prevBounds_ = null; + + this.mouse_ = null; + this.mouseMoveTime_ = 0; + this.boundingRect_ = null; + this.mouseInMap_ = true; + + this.dragTime_ = 0; + this.fireMouseEventOnIdle_ = false; + this.updateCounter_ = 0; + + this.markersDispatcher_ = new MarkerDispatcher(this); + this.geoService_ = new Geo(K_GOOGLE_TILE_SIZE); + if (this._isCenterDefined(this.props.center)) { + this.geoService_.setView(this.props.center, this.props.zoom, 0); + } + + this.state = { + overlayCreated: false + }; + } + + + _initMap = () => { + const center = this.props.center; + this.geoService_.setView(center, this.props.zoom, 0); + + this._onBoundsChanged(); // now we can calculate map bounds center etc... + + this.props.googleMapLoader(this.props.apiKey) + .then(maps => { + if (!this.mounted_) { + return; + } + + const centerLatLng = this.geoService_.getCenter(); + + const propsOptions = { + zoom: this.props.zoom, + center: new maps.LatLng(centerLatLng.lat, centerLatLng.lng) + }; + + const mapOptions = {...this.props.options, ...propsOptions}; + + const map = new maps.Map(React.findDOMNode(this.refs.google_map_dom), mapOptions); + this.map_ = map; + this.maps_ = maps; + + // render in overlay + const this_ = this; + const overlay = Object.assign(new maps.OverlayView(), { + onAdd() { + const K_MAX_WIDTH = (typeof screen !== 'undefined') ? `${screen.width}px` : '2000px'; + const K_MAX_HEIGHT = (typeof screen !== 'undefined') ? `${screen.height}px` : '2000px'; + + const div = document.createElement('div'); + this.div = div; + div.style.backgroundColor = 'transparent'; + div.style.position = 'absolute'; + div.style.left = '0px'; + div.style.top = '0px'; + div.style.width = K_MAX_WIDTH; // prevents some chrome draw defects + div.style.height = K_MAX_HEIGHT; + + const panes = this.getPanes(); + panes.overlayMouseTarget.appendChild(div); + + React.render(( + ), + div, + () => { + // remove prerendered markers + this_.setState({overlayCreated: true}); + } + ); + }, + + draw() { + const div = overlay.div; + const overlayProjection = overlay.getProjection(); + const bounds = map.getBounds(); + const ne = bounds.getNorthEast(); + const sw = bounds.getSouthWest(); + const ptx = overlayProjection.fromLatLngToDivPixel(new maps.LatLng(ne.lat(), sw.lng())); + + // need round for safari still can't find what need for firefox + const ptxRounded = detectBrowser().isSafari ? {x: Math.round(ptx.x), y: Math.round(ptx.y)} : {x: ptx.x, y: ptx.y}; + + this_.updateCounter_++; + this_._onBoundsChanged(map, maps, !this_.props.debounced); + + div.style.left = `${ptxRounded.x}px`; + div.style.top = `${ptxRounded.y}px`; + if (this_.markersDispatcher_) { + this_.markersDispatcher_.emit('kON_CHANGE'); + } + } + }); + + overlay.setMap(map); + + maps.event.addListener(map, 'idle', () => { + if (this.resetSizeOnIdle_) { + this._setViewSize(); + this.resetSizeOnIdle_ = false; + } + + const div = overlay.div; + const overlayProjection = overlay.getProjection(); + const bounds = map.getBounds(); + const ne = bounds.getNorthEast(); + const sw = bounds.getSouthWest(); + const ptx = overlayProjection.fromLatLngToDivPixel(new maps.LatLng(ne.lat(), sw.lng())); + // need round for safari still can't find what need for firefox + const ptxRounded = detectBrowser().isSafari ? {x: Math.round(ptx.x), y: Math.round(ptx.y)} : {x: ptx.x, y: ptx.y}; + + this_.updateCounter_++; + this_._onBoundsChanged(map, maps); + + this_.dragTime_ = 0; + div.style.left = `${ptxRounded.x}px`; + div.style.top = `${ptxRounded.y}px`; + if (this_.markersDispatcher_) { + this_.markersDispatcher_.emit('kON_CHANGE'); + if (this_.fireMouseEventOnIdle_) { + this_.markersDispatcher_.emit('kON_MOUSE_POSITION_CHANGE'); + } + } + }); + + maps.event.addListener(map, 'mouseover', () => { // has advantage over div MouseLeave + this_.mouseInMap_ = true; + }); + + maps.event.addListener(map, 'mouseout', () => { // has advantage over div MouseLeave + this_.mouseInMap_ = false; + this_.mouse_ = null; + this_.markersDispatcher_.emit('kON_MOUSE_POSITION_CHANGE'); + }); + + maps.event.addListener(map, 'drag', () => { + this_.dragTime_ = (new Date()).getTime(); + }); + }) + .catch( e => { + console.error(e); // eslint-disable-line no-console + throw e; + }); + } + + _onChildClick = (...args) => { + if (this.props.onChildClick) { + return this.props.onChildClick(...args); + } + } + + _onChildMouseEnter = (...args) => { + if (this.props.onChildMouseEnter) { + return this.props.onChildMouseEnter(...args); + } + } + + _onChildMouseLeave = (...args) => { + if (this.props.onChildMouseLeave) { + return this.props.onChildMouseLeave(...args); + } + } + + _setViewSize = () => { + const mapDom = React.findDOMNode(this.refs.google_map_dom); + this.geoService_.setViewSize(mapDom.clientWidth, mapDom.clientHeight); + this._onBoundsChanged(); + } + + _onWindowResize = () => { + this.resetSizeOnIdle_ = true; + } + + _onBoundsChanged = (map, maps, callExtBoundsChange) => { + if (map) { + const gmC = map.getCenter(); + this.geoService_.setView([gmC.lat(), gmC.lng()], map.getZoom(), 0); + } + + if (this.props.onBoundsChange && this.geoService_.canProject()) { + const zoom = this.geoService_.getZoom(); + const bounds = this.geoService_.getBounds(); + const centerLatLng = this.geoService_.getCenter(); + + if (!isArraysEqualEps(bounds, this.prevBounds_, kEPS)) { + if (callExtBoundsChange !== false) { + const marginBounds = this.geoService_.getBounds(this.props.margin); + this.props.onBoundsChange([centerLatLng.lat, centerLatLng.lng], zoom, bounds, marginBounds); + this.prevBounds_ = bounds; + } + } + // uncomment for strange bugs + if (process.env.NODE_ENV !== 'production') { // compare with google calculations + if (map) { + const locBounds = map.getBounds(); + const ne = locBounds.getNorthEast(); + const sw = locBounds.getSouthWest(); + + const gmC = map.getCenter(); + // compare with google map + + if (!isArraysEqualEps([centerLatLng.lat, centerLatLng.lng], [gmC.lat(), gmC.lng()], kEPS)) { + console.info('GoogleMap center not eq:', [centerLatLng.lat, centerLatLng.lng], [gmC.lat(), gmC.lng()]); // eslint-disable-line no-console + } + + if (!isArraysEqualEps(bounds, [ne.lat(), sw.lng(), sw.lat(), ne.lng()], kEPS)) { + // this is normal if this message occured on resize + console.info('GoogleMap bounds not eq:', '\n', bounds, '\n', [ne.lat(), sw.lng(), sw.lat(), ne.lng()]); // eslint-disable-line no-console + } + } + } + } + } + + _onMouseMove = (e) => { + if (!this.mouseInMap_) return; + + const currTime = (new Date()).getTime(); + const K_RECALC_CLIENT_RECT_MS = 3000; + + if (currTime - this.mouseMoveTime_ > K_RECALC_CLIENT_RECT_MS) { + this.boundingRect_ = e.currentTarget.getBoundingClientRect(); + } + this.mouseMoveTime_ = currTime; + + const mousePosX = e.clientX - this.boundingRect_.left; + const mousePosY = e.clientY - this.boundingRect_.top; + + if (!this.mouse_) { + this.mouse_ = {x: 0, y: 0, lat: 0, lng: 0}; + } + const K_IDLE_TIMEOUT = 100; + + this.mouse_.x = mousePosX; + this.mouse_.y = mousePosY; + + const latLng = this.geoService_.unproject(this.mouse_, true); + this.mouse_.lat = latLng.lat; + this.mouse_.lng = latLng.lng; + + if (currTime - this.dragTime_ < K_IDLE_TIMEOUT) { + this.fireMouseEventOnIdle_ = true; + } else { + this.markersDispatcher_.emit('kON_MOUSE_POSITION_CHANGE'); + this.fireMouseEventOnIdle_ = false; + } + } + + _onMapClick = () => { + if (this.markersDispatcher_) { + const K_IDLE_TIMEOUT = 100; + const currTime = (new Date()).getTime(); + if (currTime - this.dragTime_ > K_IDLE_TIMEOUT) { + this.markersDispatcher_.emit('kON_CLICK'); + } + } + } + + _isCenterDefined = (center) => { + return center && center.length === 2 && isNumber(center[0]) && isNumber(center[1]); + } + + componentDidMount() { + this.mounted_ = true; + window.addEventListener('resize', this._onWindowResize); + + setTimeout(() => { // to detect size + this._setViewSize(); + if (this._isCenterDefined(this.props.center)) { + this._initMap(); + } else { + this.props.googleMapLoader(this.props.apiKey); // начать подгружать можно уже сейчас + } + }, 0, this); + } + + componentWillUnmount() { + this.mounted_ = false; + + window.removeEventListener('resize', this._onWindowResize); + + if (this.maps_ && this.map_) { + this.maps_.event.clearInstanceListeners(this.map_); + } + + this.map_ = null; + this.maps_ = null; + this.markersDispatcher_.dispose(); + + this.resetSizeOnIdle_ = false; + + delete this.map_; + delete this.markersDispatcher_; + } + + componentWillReceiveProps(nextProps) { + if (!this._isCenterDefined(this.props.center) && this._isCenterDefined(nextProps.center)) { + setTimeout(() => + this._initMap(), 0); + } + + if (this.map_) { + const centerLatLng = this.geoService_.getCenter(); + if (nextProps.center) { + if (Math.abs(nextProps.center[0] - centerLatLng.lat) + Math.abs(nextProps.center[1] - centerLatLng.lng) > kEPS) { + this.map_.panTo({lat: nextProps.center[0], lng: nextProps.center[1]}); + } + } + + // if zoom chaged by user + if (Math.abs(nextProps.zoom - this.props.zoom) > 0) { + this.map_.setZoom(nextProps.zoom); + } + } + } + + componentDidUpdate() { + this.markersDispatcher_.emit('kON_CHANGE'); + } + + render() { + const mapMarkerPrerender = !this.state.overlayCreated ? ( + + ) : null; + + return ( +
+ + + {/*render markers before map load done*/} + {mapMarkerPrerender} +
+ ); + } +} diff --git a/src/google_map_map.js b/src/google_map_map.js new file mode 100644 index 0000000..af76ddd --- /dev/null +++ b/src/google_map_map.js @@ -0,0 +1,27 @@ +import React, {Component} from 'react'; + +const style = { + width: '100%', + height: '100%', + left: 0, + top: 0, + margin: 0, + padding: 0, + position: 'absolute' +}; + +export default class GoogleMapMap extends Component { + constructor(props) { + super(props); + } + + shouldComponentUpdate() { + return false; // disable react on this div + } + + render() { + return ( +
+ ); + } +} diff --git a/src/google_map_markers.js b/src/google_map_markers.js new file mode 100644 index 0000000..68a085a --- /dev/null +++ b/src/google_map_markers.js @@ -0,0 +1,232 @@ +import React, {PropTypes, Component} from 'react'; + +import shouldPureComponentUpdate from 'react-pure-render/function'; + +const mainStyle = { + width: '100%', + height: '100%', + left: 0, + top: 0, + margin: 0, + padding: 0, + position: 'absolute' +}; + +const style = { + width: 0, + height: 0, + left: 0, + top: 0, + backgroundColor: 'transparent', + position: 'absolute' +}; + +export default class GoogleMapMarkers extends Component { + static propTypes = { + geoService: PropTypes.any, + style: PropTypes.any, + distanceToMouse: PropTypes.func, + dispatcher: PropTypes.any, + onChildClick: PropTypes.func, + onChildMouseLeave: PropTypes.func, + onChildMouseEnter: PropTypes.func, + hoverDistance: PropTypes.number, + projectFromLeftTop: PropTypes.bool + }; + + static defaultProps = { + projectFromLeftTop: false + }; + + shouldComponentUpdate = shouldPureComponentUpdate; + + constructor(props) { + super(props); + this.props.dispatcher.on('kON_CHANGE', this._onChangeHandler); + this.props.dispatcher.on('kON_MOUSE_POSITION_CHANGE', this._onMouseChangeHandler); + this.props.dispatcher.on('kON_CLICK', this._onChildClick); + + this.dimesionsCache_ = {}; + this.hoverKey_ = null; + this.hoverChildProps_ = null; + this.allowMouse_ = true; + + this.state = {...this._getState(), hoverKey: null}; + } + + _getState = () => { + return { + children: this.props.dispatcher.getChildren(), + updateCounter: this.props.dispatcher.getUpdateCounter() + }; + } + + _onChangeHandler = () => { + if (!this.dimesionsCache_) { + return; + } + + const state = this._getState(); + this.setState(state); + } + + _onChildClick = () => { + if (this.props.onChildClick) { + if (this.hoverChildProps_) { + const hoverKey = this.hoverKey_; + const childProps = this.hoverChildProps_; + // click works only on hovered item + this.props.onChildClick(hoverKey, childProps); + } + } + } + + _onChildMouseEnter = (hoverKey, childProps) => { + if (!this.dimesionsCache_) { + return; + } + + if (this.props.onChildMouseEnter) { + this.props.onChildMouseEnter(hoverKey, childProps); + } + + this.hoverChildProps_ = childProps; + this.hoverKey_ = hoverKey; + this.setState({hoverKey: hoverKey}); + } + + _onChildMouseLeave = () => { + if (!this.dimesionsCache_) { + return; + } + + const hoverKey = this.hoverKey_; + const childProps = this.hoverChildProps_; + + if (hoverKey !== undefined && hoverKey !== null) { + if (this.props.onChildMouseLeave) { + this.props.onChildMouseLeave(hoverKey, childProps); + } + + this.hoverKey_ = null; + this.hoverChildProps_ = null; + this.setState({hoverKey: null}); + } + } + + + _onMouseAllow = (value) => { + if (!value) { + this._onChildMouseLeave(); + } + + this.allowMouse_ = value; + } + + + _onMouseChangeHandler = () => { + if (this.allowMouse_) { + this._onMouseChangeHandler_raf(); + } + } + + _onMouseChangeHandler_raf = () => { + if (!this.dimesionsCache_) { + return; + } + + const mp = this.props.dispatcher.getMousePosition(); + + if (mp) { + let distances = []; + + React.Children.forEach(this.state.children, (child, childIndex) => { + const childKey = child.key !== undefined && child.key !== null ? child.key : childIndex; + const dist = this.props.distanceToMouse(this.dimesionsCache_[childKey], mp, child.props); + if (dist < this.props.hoverDistance) { + distances.push( + { + key: childKey, + dist: dist, + props: child.props + }); + } + }); + + if (distances.length) { + distances.sort((a, b) => a.dist - b.dist); + const hoverKey = distances[0].key; + const childProps = distances[0].props; + + if (this.hoverKey_ !== hoverKey) { + this._onChildMouseLeave(); + + this._onChildMouseEnter(hoverKey, childProps); + } + } else { + this._onChildMouseLeave(); + } + } else { + this._onChildMouseLeave(); + } + } + + _getDimensions = (key) => { + const childKey = key; + return this.dimesionsCache_[childKey]; + } + + componentWillUnmount() { + this.props.dispatcher.removeListener('kON_CHANGE', this._onChangeHandler); + this.props.dispatcher.removeListener('kON_MOUSE_POSITION_CHANGE', this._onMouseChangeHandler); + this.props.dispatcher.removeListener('kON_CLICK', this._onChildClick); + + this.dimesionsCache_ = null; + } + + render() { + const mainElementStyle = this.props.style || mainStyle; + + this.dimesionsCache_ = {}; + + const markers = React.Children.map(this.state.children, (child, childIndex) => { + const pt = this.props.geoService.project({lat: child.props.lat, lng: child.props.lng}, this.props.projectFromLeftTop); + const stylePtPos = { + left: pt.x, + top: pt.y + }; + + let dx = 0; + let dy = 0; + + if (!this.props.projectFromLeftTop) { // center projection + if (this.props.geoService.hasSize()) { + dx = this.props.geoService.getWidth() / 2; + dy = this.props.geoService.getHeight() / 2; + } + } + + // to prevent rerender on child element i need to pass const params $getDimensions and $dimensionKey instead of dimension object + const childKey = child.key !== undefined && child.key !== null ? child.key : childIndex; + this.dimesionsCache_[childKey] = {x: pt.x + dx, y: pt.y + dy, lat: child.props.lat, lng: child.props.lng}; + + return ( +
+ {React.cloneElement(child, { + $hover: childKey === this.state.hoverKey, + $getDimensions: this._getDimensions, + $dimensionKey: childKey, + $geoService: this.props.geoService, + $onMouseAllow: this._onMouseAllow + })} +
+ ); + }); + + return ( +
+ {markers} +
+ ); + } +} diff --git a/src/google_map_markers_prerender.js b/src/google_map_markers_prerender.js new file mode 100644 index 0000000..6e6a3e6 --- /dev/null +++ b/src/google_map_markers_prerender.js @@ -0,0 +1,29 @@ +import React, {PropTypes, Component} from 'react'; +import GoogleMapMarkers from './google_map_markers.js'; + +const style = { + width: '50%', + height: '50%', + left: '50%', + top: '50%', + // backgroundColor: 'red', + margin: 0, + padding: 0, + position: 'absolute' + // opacity: 0.3 +}; + +export default class GoogleMapMarkersPrerender extends Component { + constructor(props) { + super(props); + } + + + render() { + return ( +
+ +
+ ); + } +} diff --git a/src/marker_dispatcher.js b/src/marker_dispatcher.js new file mode 100644 index 0000000..cbec79a --- /dev/null +++ b/src/marker_dispatcher.js @@ -0,0 +1,25 @@ +import EventEmitter from 'eventemitter3'; + +export default class MarkerDispatcher extends EventEmitter { + constructor(gmapInstance) { + super(); + this.gmapInstance = gmapInstance; + } + + getChildren() { + return this.gmapInstance.props.children; + } + + getMousePosition() { + return this.gmapInstance.mouse_; + } + + getUpdateCounter() { + return this.gmapInstance.updateCounter_; + } + + dispose() { + this.gmapInstance = null; + this.removeAllListeners(); + } +} diff --git a/src/utils/array_helper.js b/src/utils/array_helper.js new file mode 100644 index 0000000..d90ce6d --- /dev/null +++ b/src/utils/array_helper.js @@ -0,0 +1,12 @@ + +export default function isArraysEqualEps(arrayA, arrayB, eps) { + if (arrayA && arrayB) { + for (let i = 0; i !== arrayA.length; ++i) { + if (Math.abs(arrayA[i] - arrayB[i]) > eps) { + return false; + } + } + return true; + } + return false; +} diff --git a/src/utils/detect.js b/src/utils/detect.js new file mode 100644 index 0000000..c9d91ce --- /dev/null +++ b/src/utils/detect.js @@ -0,0 +1,31 @@ +// code here http://stackoverflow.com/questions/5899783/detect-safari-chrome-ie-firefox-opera-with-user-agent +let detectBrowserResult_ = null; + +export default function detectBrowser() { + if (detectBrowserResult_) { + return detectBrowserResult_; + } + + if (typeof navigator !== 'undefined') { + const isExplorer = navigator.userAgent.indexOf('MSIE') > -1; + const isFirefox = navigator.userAgent.indexOf('Firefox') > -1; + const isOpera = navigator.userAgent.toLowerCase().indexOf('op') > -1; + + let isChrome = navigator.userAgent.indexOf('Chrome') > -1; + let isSafari = navigator.userAgent.indexOf('Safari') > -1; + + if ((isChrome) && (isSafari)) { + isSafari = false; + } + + if ((isChrome) && (isOpera)) { + isChrome = false; + } + + detectBrowserResult_ = {isExplorer, isFirefox, isOpera, isChrome, isSafari}; + return detectBrowserResult_; + } + + detectBrowserResult_ = {isChrome: true, isExplorer: false, isFirefox: false, isOpera: false, isSafari: false}; + return detectBrowserResult_; +} diff --git a/src/utils/geo.js b/src/utils/geo.js new file mode 100644 index 0000000..8232f5e --- /dev/null +++ b/src/utils/geo.js @@ -0,0 +1,107 @@ +import LatLng from './lib_geo/lat_lng.js'; +import Point from 'point-geometry'; +import Transform from './lib_geo/transform.js'; + + +export default class Geo { + + constructor(tileSize) { // left_top view пользует гугл + // super(); + this.hasSize_ = false; + this.hasView_ = false; + this.transform_ = new Transform(tileSize || 512); + } + + setView(center, zoom, bearing) { + this.transform_.center = LatLng.convert(center); + this.transform_.zoom = +zoom; + this.transform_.bearing = +bearing; + this.hasView_ = true; + } + + setViewSize(width, height) { + this.transform_.width = width; + this.transform_.height = height; + this.hasSize_ = true; + } + + canProject() { + return this.hasSize_ && this.hasView_; + } + + hasSize() { + return this.hasSize_; + } + + unproject(ptXY, viewFromLeftTop) { + let ptRes; + if (viewFromLeftTop) { + const ptxy = Object.assign({}, ptXY); + ptxy.x -= this.transform_.width / 2; + ptxy.y -= this.transform_.height / 2; + ptRes = this.transform_.pointLocation(Point.convert(ptxy)); + } else { + ptRes = this.transform_.pointLocation(Point.convert(ptXY)); + } + + ptRes.lng -= 360 * Math.round(ptRes.lng / 360); // convert 2 google format + return ptRes; + } + + project(ptLatLng, viewFromLeftTop) { + if (viewFromLeftTop) { + const pt = this.transform_.locationPoint(LatLng.convert(ptLatLng)); + pt.x -= this.transform_.worldSize * Math.round(pt.x / this.transform_.worldSize); + + pt.x += this.transform_.width / 2; + pt.y += this.transform_.height / 2; + + return pt; + } + + return this.transform_.locationPoint(LatLng.convert(ptLatLng)); + } + + getWidth() { + return this.transform_.width; + } + + getHeight() { + return this.transform_.height; + } + + getZoom() { + return this.transform_.zoom; + } + + getCenter() { + let ptRes = this.transform_.pointLocation({x: 0, y: 0}); + + return ptRes; + } + + getBounds(margins, roundFactor) { + const bndT = margins && margins[0] || 0; + const bndR = margins && margins[1] || 0; + const bndB = margins && margins[2] || 0; + const bndL = margins && margins[3] || 0; + + if (this.getWidth() - bndR - bndL > 0 && this.getHeight() - bndT - bndB > 0) { + const topLeftCorner = this.unproject({x: bndL - this.getWidth() / 2, y: bndT - this.getHeight() / 2}); + const bottomRightCorner = this.unproject({x: this.getWidth() / 2 - bndR, y: this.getHeight() / 2 - bndB}); + + let res = [ + topLeftCorner.lat, topLeftCorner.lng, + bottomRightCorner.lat, bottomRightCorner.lng + ]; + + if (roundFactor) { + res = res.map(r => Math.round(r * roundFactor) / roundFactor); + } + return res; + } + + return [0, 0, 0, 0]; + } +} + diff --git a/src/utils/lib_geo/LICENCE.txt b/src/utils/lib_geo/LICENCE.txt new file mode 100644 index 0000000..9bf32b4 --- /dev/null +++ b/src/utils/lib_geo/LICENCE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2014, Mapbox + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * 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. + * Neither the name of Mapbox GL JS 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 OWNER 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/src/utils/lib_geo/lat_lng.js b/src/utils/lib_geo/lat_lng.js new file mode 100644 index 0000000..78ee4a6 --- /dev/null +++ b/src/utils/lib_geo/lat_lng.js @@ -0,0 +1,29 @@ +'use strict'; + +module.exports = LatLng; + +var wrap = require('./wrap.js').wrap; + +function LatLng(lat, lng) { + if (isNaN(lat) || isNaN(lng)) { + throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); + } + this.lat = +lat; + this.lng = +lng; +} + +LatLng.prototype.wrap = function () { + return new LatLng(this.lat, wrap(this.lng, -180, 180)); +}; + +// constructs LatLng from an array if necessary + +LatLng.convert = function (a) { + if (a instanceof LatLng) { + return a; + } + if (Array.isArray(a)) { + return new LatLng(a[0], a[1]); + } + return a; +}; diff --git a/src/utils/lib_geo/lat_lng_bounds.js b/src/utils/lib_geo/lat_lng_bounds.js new file mode 100644 index 0000000..9c6d6b0 --- /dev/null +++ b/src/utils/lib_geo/lat_lng_bounds.js @@ -0,0 +1,72 @@ +'use strict'; + +module.exports = LatLngBounds; + +var LatLng = require('./lat_lng'); + +function LatLngBounds(sw, ne) { + if (!sw) return; + + var latlngs = ne ? [sw, ne] : sw; + + for (var i = 0, len = latlngs.length; i < len; i++) { + this.extend(latlngs[i]); + } +} + +LatLngBounds.prototype = { + + // extend the bounds to contain the given point or bounds + extend: function (obj) { + var sw = this._sw, + ne = this._ne, + sw2, ne2; + + if (obj instanceof LatLng) { + sw2 = obj; + ne2 = obj; + + } else if (obj instanceof LatLngBounds) { + sw2 = obj._sw; + ne2 = obj._ne; + + if (!sw2 || !ne2) return this; + + } else { + return obj ? this.extend(LatLng.convert(obj) || LatLngBounds.convert(obj)) : this; + } + + if (!sw && !ne) { + this._sw = new LatLng(sw2.lat, sw2.lng); + this._ne = new LatLng(ne2.lat, ne2.lng); + + } else { + sw.lat = Math.min(sw2.lat, sw.lat); + sw.lng = Math.min(sw2.lng, sw.lng); + ne.lat = Math.max(ne2.lat, ne.lat); + ne.lng = Math.max(ne2.lng, ne.lng); + } + + return this; + }, + + getCenter: function () { + return new LatLng((this._sw.lat + this._ne.lat) / 2, (this._sw.lng + this._ne.lng) / 2); + }, + + getSouthWest: function () { return this._sw; }, + getNorthEast: function () { return this._ne; }, + getNorthWest: function () { return new LatLng(this.getNorth(), this.getWest()); }, + getSouthEast: function () { return new LatLng(this.getSouth(), this.getEast()); }, + + getWest: function () { return this._sw.lng; }, + getSouth: function () { return this._sw.lat; }, + getEast: function () { return this._ne.lng; }, + getNorth: function () { return this._ne.lat; } +}; + +// constructs LatLngBounds from an array if necessary +LatLngBounds.convert = function (a) { + if (!a || a instanceof LatLngBounds) return a; + return new LatLngBounds(a); +}; diff --git a/src/utils/lib_geo/readme.md b/src/utils/lib_geo/readme.md new file mode 100644 index 0000000..b51d2a9 --- /dev/null +++ b/src/utils/lib_geo/readme.md @@ -0,0 +1,2 @@ +###Код выдран из mapboxgl чтобы преобразования координат не зависели от библиотеки + diff --git a/src/utils/lib_geo/transform.js b/src/utils/lib_geo/transform.js new file mode 100644 index 0000000..c9c585f --- /dev/null +++ b/src/utils/lib_geo/transform.js @@ -0,0 +1,121 @@ + +const LatLng = require('./lat_lng'); +const Point = require('point-geometry'); +const wrap = require('./wrap.js').wrap; + + +// A single transform, generally used for a single tile to be scaled, rotated, and zoomed. + +function Transform(tileSize, minZoom, maxZoom) { + this.tileSize = tileSize || 512; // constant + + this._minZoom = minZoom || 0; + this._maxZoom = maxZoom || 52; + + this.latRange = [-85.05113, 85.05113]; + + this.width = 0; + this.height = 0; + this.zoom = 0; + this.center = new LatLng(0, 0); + this.angle = 0; +} + +Transform.prototype = { + get minZoom() { return this._minZoom; }, + set minZoom(zoom) { + this._minZoom = zoom; + this.zoom = Math.max(this.zoom, zoom); + }, + + get maxZoom() { return this._maxZoom; }, + set maxZoom(zoom) { + this._maxZoom = zoom; + this.zoom = Math.min(this.zoom, zoom); + }, + + get worldSize() { + return this.tileSize * this.scale; + }, + + get centerPoint() { + return new Point(0, 0); // this.size._div(2); + }, + + get size() { + return new Point(this.width, this.height); + }, + + get bearing() { + return -this.angle / Math.PI * 180; + }, + set bearing(bearing) { + this.angle = -wrap(bearing, -180, 180) * Math.PI / 180; + }, + + get zoom() { return this._zoom; }, + set zoom(zoom) { + zoom = Math.min(Math.max(zoom, this.minZoom), this.maxZoom); + this._zoom = zoom; + this.scale = this.zoomScale(zoom); + this.tileZoom = Math.floor(zoom); + this.zoomFraction = zoom - this.tileZoom; + }, + + zoomScale: function(zoom) { return Math.pow(2, zoom); }, + scaleZoom: function(scale) { return Math.log(scale) / Math.LN2; }, + + project: function(latlng, worldSize) { + return new Point( + this.lngX(latlng.lng, worldSize), + this.latY(latlng.lat, worldSize)); + }, + + unproject: function(point, worldSize) { + return new LatLng( + this.yLat(point.y, worldSize), + this.xLng(point.x, worldSize)); + }, + + get x() { + return this.lngX(this.center.lng); + }, + get y() { + return this.latY(this.center.lat); + }, + + get point() { return new Point(this.x, this.y); }, + + // lat/lon <-> absolute pixel coords convertion + lngX: function(lon, worldSize) { + return (180 + lon) * (worldSize || this.worldSize) / 360; + }, + // latitude to absolute y coord + latY: function(lat, worldSize) { + var y = 180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)); + return (180 - y) * (worldSize || this.worldSize) / 360; + }, + + xLng: function(x, worldSize) { + return x * 360 / (worldSize || this.worldSize) - 180; + }, + yLat: function(y, worldSize) { + var y2 = 180 - y * 360 / (worldSize || this.worldSize); + return 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90; + }, + + + locationPoint: function(latlng) { + var p = this.project(latlng); + return this.centerPoint._sub(this.point._sub(p)._rotate(this.angle)); + }, + + pointLocation: function(p) { + var p2 = this.centerPoint._sub(p)._rotate(-this.angle); + return this.unproject(this.point.sub(p2)); + }, + + +}; + +module.exports = Transform; diff --git a/src/utils/lib_geo/wrap.js b/src/utils/lib_geo/wrap.js new file mode 100644 index 0000000..9789b00 --- /dev/null +++ b/src/utils/lib_geo/wrap.js @@ -0,0 +1,6 @@ +'use strict'; + +exports.wrap = function (n, min, max) { + var d = max - min; + return n === max ? n : ((n - min) % d + d) % d + min; +}; diff --git a/src/utils/loaders/google_map_loader.js b/src/utils/loaders/google_map_loader.js new file mode 100644 index 0000000..cc7db98 --- /dev/null +++ b/src/utils/loaders/google_map_loader.js @@ -0,0 +1,45 @@ +let $script_ = null; + +let _loadPromise; + +// TODO add libraries language and other map options +module.exports = function googleMapLoader(apiKey) { + if (!$script_) { + $script_ = require('scriptjs'); + } + + if (_loadPromise) { + return _loadPromise; + } + + _loadPromise = new Promise((resolve, reject) => { + if (typeof window === 'undefined') { + reject(new Error('google map cannot be loaded outside browser env')); + return; + } + + if (window.google && window.google.maps) { + resolve(window.google.maps); + return; + } + + if (typeof window._$_google_map_initialize_$_ !== 'undefined') { + reject(new Error('google map initialization error')); + } + + window._$_google_map_initialize_$_ = () => { + delete window._$_google_map_initialize_$_; + resolve(window.google.maps); + }; + + const apiKeyString = apiKey ? `&key=${apiKey}` : ''; + + $script_(`https://maps.googleapis.com/maps/api/js?callback=_$_google_map_initialize_$_${apiKeyString}`, () => { + if (typeof window.google === 'undefined') { + reject(new Error('google map initialization error (not loaded)')); + } + }); + }); + + return _loadPromise; +};