├── .editorconfig ├── .gitignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config.xml ├── config ├── gmap-mock.js ├── helpers.js ├── karma-shim.js ├── karma.conf.js ├── webpack.config.js └── webpack.test.js ├── coverage └── .gitignore ├── ionic.config.json ├── ionic.project ├── karma.conf.js ├── package.json ├── resources ├── android │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ └── splash │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png ├── icon.png ├── ios │ ├── icon │ │ ├── icon-40.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-50.png │ │ ├── icon-50@2x.png │ │ ├── icon-60.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72.png │ │ ├── icon-72@2x.png │ │ ├── icon-76.png │ │ ├── icon-76@2x.png │ │ ├── icon-83.5@2x.png │ │ ├── icon-small.png │ │ ├── icon-small@2x.png │ │ ├── icon-small@3x.png │ │ ├── icon.png │ │ └── icon@2x.png │ └── splash │ │ ├── Default-568h@2x~iphone.png │ │ ├── Default-667h.png │ │ ├── Default-736h.png │ │ ├── Default-Portrait@2x~ipad.png │ │ ├── Default-Portrait~ipad.png │ │ ├── Default@2x~iphone.png │ │ └── Default~iphone.png └── splash.png ├── src ├── app │ ├── app.component.ts │ ├── app.mock.spec.ts │ ├── app.module.ts │ ├── app.template.html │ └── main.ts ├── assets │ ├── icon │ │ └── favicon.ico │ └── img │ │ └── taxi.png ├── components │ └── map │ │ ├── map.spec.ts │ │ └── map.ts ├── declarations.d.ts ├── index.html ├── ionic-mock.ts ├── manifest.json ├── pages │ ├── about │ │ ├── about.html │ │ ├── about.scss │ │ └── about.ts │ ├── base-page.ts │ ├── confirmation │ │ ├── confirmation.scss │ │ ├── confirmation.spec.ts │ │ ├── confirmation.tpl.html │ │ └── confirmation.ts │ ├── home │ │ ├── home.scss │ │ ├── home.spec.ts │ │ ├── home.tpl.html │ │ └── home.ts │ ├── ride-list │ │ ├── ride-list.scss │ │ ├── ride-list.spec.ts │ │ ├── ride-list.tpl.html │ │ └── ride-list.ts │ └── search │ │ ├── search.spec.ts │ │ ├── search.tpl.html │ │ └── search.ts ├── providers │ ├── map │ │ ├── geocoder-mock.service.ts │ │ ├── geocoder.service.ts │ │ ├── map-mock.service.ts │ │ ├── map.constants.ts │ │ └── map.service.ts │ └── ride │ │ ├── ride-mock.service.ts │ │ ├── ride.model.ts │ │ └── ride.service.ts ├── service-worker.js └── theme │ └── variables.scss ├── tsconfig.json ├── tslint.json ├── typedoc.json ├── www └── .gitignore └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | 10 | # We recommend you to keep these unchanged 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | # Webstorm/Vscode/Sublime 5 | .idea/ 6 | *.sublime-project 7 | *.sublime-workspace 8 | .vscode/ 9 | 10 | # Logs and trash 11 | npm-debug.log* 12 | *~ 13 | *.sw[mnpcod] 14 | *.log 15 | *.tmp 16 | log.txt 17 | $RECYCLE.BIN/ 18 | tsconfig.tmp.json 19 | .DS_Store 20 | Thumbs.db 21 | UserInterfaceState.xcuserstate 22 | 23 | # Build folders 24 | .sass-cache/ 25 | .tmp/ 26 | *.tmp.* 27 | 28 | # Coverege folders 29 | config/coverage 30 | coverage/* 31 | dist/ 32 | tmp/ 33 | temp/ 34 | 35 | # Doc 36 | doc/ 37 | 38 | # Dependencies 39 | node_modules/ 40 | platforms/ 41 | plugins/ 42 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 7.5.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6.3" 5 | 6 | notifications: 7 | email: true 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | script: 14 | - npm run lint 15 | - npm test 16 | 17 | before_install: 18 | - export CHROME_BIN=chromium-browser 19 | - export DISPLAY=:99.0 20 | - sh -e /etc/init.d/xvfb start 21 | - sleep 3 22 | - npm i -g npm@^3 23 | - npm install -g cordova ionic 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [0.1.0](https://github.com/ddellamico/ionic2-taxi-app/compare/v0.0.1...v0.1.0) (2017-03-14) 7 | 8 | 9 | ### Features 10 | 11 | * **dependency management:** add yarn to manage dependency management ([fb41b14](https://github.com/ddellamico/ionic2-taxi-app/commit/fb41b14)) 12 | * **ionic:** update to ionic 2 final ([491257e](https://github.com/ddellamico/ionic2-taxi-app/commit/491257e)) 13 | 14 | 15 | 16 | 17 | ## 0.0.1 (2016-09-07) 18 | 19 | 20 | ### Features 21 | 22 | * **app:** base app structure ([678b882](https://github.com/ddellamico/ionic2-taxi-app/commit/678b882)) 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 Drifty Co. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ionic 2 Taxi App 2 | 3 | [![Build Status](https://travis-ci.org/ddellamico/ionic2-taxi-app.svg?branch=master)](https://travis-ci.org/ddellamico/ionic2-taxi-app) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?style=flat-square)](http://commitizen.github.io/cz-cli/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 4 | [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT) [![Dependency Status](https://david-dm.org/ddellamico/ionic2-taxi-app.svg)](https://david-dm.org/ddellamico/ionic2-taxi-app) [![devDependency Status](https://david-dm.org/ddellamico/ionic2-taxi-app/dev-status.svg)](https://david-dm.org/ddellamico/ionic2-taxi-app#info=devDependencies) 5 | 6 | The main purpose for this project, it's provide a simple starting point for building 'on-demand/rideshare taxi' ionic application ( something like Uber, Lyft or Sidecar .. ) or more generally, applications using extensively Google Maps JavaScript API. 7 | 8 | When you start the app, you see a map centering around your location, with a marker permanently fixed to the center of map. 9 | When the user stop moving the map, an InfoWindow shows the new position. To achieve this, I use google maps geocoder service along with rxjs, that let you easily handle 'OVER_QUERY_LIMIT' response ( API Usage Limits ). 10 | 11 | ** Build configuration and test setup is heavily inspired from this great Ionic 2 boilerplate, take a look [here](https://github.com/marcoturi/ionic2-boilerplate). ** 12 | 13 | If you are looking for a more complex and complete Ionic 2 app sample, [check out here](https://github.com/ddellamico/ionic-conference-app). 14 | 15 | **Note: This project is under development.** 16 | 17 | ## App Preview 18 | 19 |

20 | Home 21 | Search 22 | AutoComplete 23 | Confirmation 24 | Rides 25 | Menu 26 |

27 | 28 | ## Features 29 | * Ionic 2 Final: 30 | * [TypeScript](http://www.typescriptlang.org/) 31 | * [RxJS](https://github.com/Reactive-Extensions/RxJS) 32 | * [Webpack](http://webpack.github.io/) 33 | * [Yarn](https://github.com/yarnpkg/yarn) for fast, reliable, and secure dependency management. 34 | * [BetterScripts](https://github.com/benoror/better-npm-run) for better NPM scripts. 35 | * [tslint](https://github.com/palantir/tslint) 36 | * [Codelyzer](https://github.com/mgechev/codelyzer) 37 | * [Typedoc](https://github.com/TypeStrong/typedoc) 38 | * [NVM](https://github.com/creationix/nvm) to manage multiple active node.js versions 39 | 40 | ## Install 41 | **Make sure you have Node version >= 6.X and NPM >= 3** (node.js version used 7.5.0 and NPM v. 4.1.2) 42 | 43 | ```bash 44 | # Clone the repo 45 | $ git clone https://github.com/ddellamico/ionic2-taxi-app 46 | ----- 47 | # change directory to our repo 48 | cd ionic2-taxi-app 49 | ----- 50 | # install the repo with yarn 51 | yarn 52 | ----- 53 | # restore plugins and platforms 54 | cordova prepare 55 | ----- 56 | # start the server (webpack-dev-server) 57 | npm run dev 58 | ``` 59 | 60 | go to [http://0.0.0.0:8100](http://0.0.0.0:8100) or [http://localhost:8100](http://localhost:8100) in your browser 61 | 62 | ## Commands 63 | ```bash 64 | $ npm run dev --> Run ionic serve ( development ) 65 | $ npm run build --> build files inside www folder ( production ) 66 | $ npm run test --> run test with Karma 67 | $ npm run ios:dev --> start ios simulator (ionic run ios) 68 | $ npm run ios:release --> build files for ios platform and generate xcodeproj (ionic build ios) 69 | $ npm run android:dev --> start android simulator (ionic run android) 70 | $ npm run android:release --> build files for android platform and generate apk (ionic build android) 71 | ``` 72 | 73 | ## Commit: 74 | 75 | Follows [AngularJS's commit message convention](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#-git-commit-guidelines) 76 | ```sh 77 | # Lint and execute tests before committing code. 78 | npm run commit 79 | # OR 80 | # use git commit directly with correct message convention. 81 | git commit -m "chore(ghooks): Add pre-commit and commit-msg ghook" 82 | ``` 83 | 84 | ## Tests 85 | 86 | ```sh 87 | $ npm test 88 | ``` 89 | 90 | ## Changelog 91 | 92 | You can check the changelog [here](https://github.com/ddellamico/ionic2-taxi-app/blob/master/CHANGELOG.md) 93 | 94 | ## Todo 95 | 96 | * Add more test with karma 97 | * Add protractor (E2E testing) 98 | * Add HMR 99 | 100 | ## License 101 | 102 | MIT 103 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ionic Taxi 4 | 5 | Ionic Taxi App. 6 | 7 | 8 | Damien Dell'Amico 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /config/gmap-mock.js: -------------------------------------------------------------------------------- 1 | // CONVERTED FROM : https://raw.githubusercontent.com/sgruhier/google_maps_mock/master/google_maps_mock.coffee 2 | 3 | var GoogleMapsMock, 4 | extend = function (child, parent) { 5 | for (var key in parent) { 6 | if (hasProp.call(parent, key)) child[key] = parent[key]; 7 | } 8 | function ctor() { 9 | this.constructor = child; 10 | } 11 | 12 | ctor.prototype = parent.prototype; 13 | child.prototype = new ctor(); 14 | child.__super__ = parent.prototype; 15 | return child; 16 | }, 17 | hasProp = {}.hasOwnProperty; 18 | 19 | this.google = { 20 | maps: { 21 | event: { 22 | addDomListener: function () { 23 | }, 24 | addDomListenerOnce: function () { 25 | }, 26 | addListener: function () { 27 | }, 28 | addListenerOnce: function () { 29 | }, 30 | bind: function () { 31 | }, 32 | clearInstanceListeners: function () { 33 | }, 34 | clearListeners: function () { 35 | }, 36 | forward: function () { 37 | }, 38 | removeListener: function () { 39 | }, 40 | trigger: function () { 41 | }, 42 | vf: function () { 43 | } 44 | } 45 | } 46 | }; 47 | 48 | GoogleMapsMock = (function () { 49 | function GoogleMapsMock() { 50 | } 51 | 52 | GoogleMapsMock.prototype.setMap = function () { 53 | }; 54 | 55 | GoogleMapsMock.prototype.getMap = function () { 56 | }; 57 | 58 | return GoogleMapsMock; 59 | 60 | })(); 61 | 62 | google.maps.LatLng = (function (superClass) { 63 | extend(LatLng, superClass); 64 | 65 | function LatLng(lat, lng) { 66 | this.latitude = parseFloat(lat); 67 | this.longitude = parseFloat(lng); 68 | } 69 | 70 | LatLng.prototype.lat = function () { 71 | return this.latitude; 72 | }; 73 | 74 | LatLng.prototype.lng = function () { 75 | return this.longitude; 76 | }; 77 | 78 | return LatLng; 79 | 80 | })(GoogleMapsMock); 81 | 82 | google.maps.LatLngBounds = (function (superClass) { 83 | extend(LatLngBounds, superClass); 84 | 85 | function LatLngBounds(ne, sw) { 86 | this.ne = ne; 87 | this.sw = sw; 88 | } 89 | 90 | LatLngBounds.prototype.getSouthWest = function () { 91 | return this.sw; 92 | }; 93 | 94 | LatLngBounds.prototype.getNorthEast = function () { 95 | return this.ne; 96 | }; 97 | 98 | return LatLngBounds; 99 | 100 | })(GoogleMapsMock); 101 | 102 | google.maps.OverlayView = (function (superClass) { 103 | extend(OverlayView, superClass); 104 | 105 | function OverlayView() { 106 | return OverlayView.__super__.constructor.apply(this, arguments); 107 | } 108 | 109 | return OverlayView; 110 | 111 | })(GoogleMapsMock); 112 | 113 | google.maps.Marker = (function (superClass) { 114 | extend(Marker, superClass); 115 | 116 | function Marker() { 117 | return Marker.__super__.constructor.apply(this, arguments); 118 | } 119 | 120 | Marker.prototype.getAnimation = function () { 121 | }; 122 | 123 | Marker.prototype.getClickable = function () { 124 | }; 125 | 126 | Marker.prototype.getCursor = function () { 127 | }; 128 | 129 | Marker.prototype.getDraggable = function () { 130 | }; 131 | 132 | Marker.prototype.getFlat = function () { 133 | }; 134 | 135 | Marker.prototype.getIcon = function () { 136 | }; 137 | 138 | Marker.prototype.getPosition = function () { 139 | }; 140 | 141 | Marker.prototype.getShadow = function () { 142 | }; 143 | 144 | Marker.prototype.getShape = function () { 145 | }; 146 | 147 | Marker.prototype.getTitle = function () { 148 | }; 149 | 150 | Marker.prototype.getVisible = function () { 151 | }; 152 | 153 | Marker.prototype.getZIndex = function () { 154 | }; 155 | 156 | Marker.prototype.setAnimation = function () { 157 | }; 158 | 159 | Marker.prototype.setClickable = function () { 160 | }; 161 | 162 | Marker.prototype.setCursor = function () { 163 | }; 164 | 165 | Marker.prototype.setDraggable = function () { 166 | }; 167 | 168 | Marker.prototype.setFlat = function () { 169 | }; 170 | 171 | Marker.prototype.setIcon = function () { 172 | }; 173 | 174 | Marker.prototype.setPosition = function () { 175 | }; 176 | 177 | Marker.prototype.setShadow = function () { 178 | }; 179 | 180 | Marker.prototype.setShape = function () { 181 | }; 182 | 183 | Marker.prototype.setTitle = function () { 184 | }; 185 | 186 | Marker.prototype.setVisible = function () { 187 | }; 188 | 189 | Marker.prototype.setZIndex = function () { 190 | }; 191 | 192 | Marker.prototype.setMap = function () { 193 | }; 194 | 195 | Marker.prototype.getMap = function () { 196 | }; 197 | 198 | return Marker; 199 | 200 | })(GoogleMapsMock); 201 | 202 | google.maps.MarkerImage = (function (superClass) { 203 | extend(MarkerImage, superClass); 204 | 205 | function MarkerImage() { 206 | return MarkerImage.__super__.constructor.apply(this, arguments); 207 | } 208 | 209 | return MarkerImage; 210 | 211 | })(GoogleMapsMock); 212 | 213 | google.maps.Map = (function (superClass) { 214 | extend(Map, superClass); 215 | 216 | function Map() { 217 | return Map.__super__.constructor.apply(this, arguments); 218 | } 219 | 220 | Map.prototype.setCenter = function () { 221 | }; 222 | 223 | Map.prototype.getCenter = function () { 224 | return new google.maps.LatLng(4, 12); 225 | }; 226 | 227 | Map.prototype.setZoom = function () { 228 | }; 229 | 230 | Map.prototype.getDiv = function () { 231 | }; 232 | 233 | return Map; 234 | 235 | })(GoogleMapsMock); 236 | 237 | google.maps.Point = (function (superClass) { 238 | extend(Point, superClass); 239 | 240 | function Point() { 241 | return Point.__super__.constructor.apply(this, arguments); 242 | } 243 | 244 | return Point; 245 | 246 | })(GoogleMapsMock); 247 | 248 | google.maps.Size = (function (superClass) { 249 | extend(Size, superClass); 250 | 251 | function Size() { 252 | return Size.__super__.constructor.apply(this, arguments); 253 | } 254 | 255 | return Size; 256 | 257 | })(GoogleMapsMock); 258 | 259 | google.maps.InfoWindow = (function (superClass) { 260 | extend(InfoWindow, superClass); 261 | 262 | function InfoWindow() { 263 | return InfoWindow.__super__.constructor.apply(this, arguments); 264 | } 265 | 266 | return InfoWindow; 267 | 268 | })(GoogleMapsMock); 269 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Helper functions 4 | const ROOT = path.resolve(__dirname, '..'); 5 | 6 | function root(args) { 7 | args = Array.prototype.slice.call(arguments, 0); 8 | return path.join.apply(path, [ROOT].concat(args)); 9 | } 10 | 11 | exports.root = root; 12 | -------------------------------------------------------------------------------- /config/karma-shim.js: -------------------------------------------------------------------------------- 1 | /* 2 | * When testing with webpack and ES6, we have to do some extra 3 | * things to get testing to work right. Because we are gonna write tests 4 | * in ES6 too, we have to compile those as well. That's handled in 5 | * karma.conf.js with the karma-webpack plugin. This is the entry 6 | * file for webpack test. Just like webpack will create a bundle.js 7 | * file for our client, when we run test, it will compile and bundle them 8 | * all here! Crazy huh. So we need to do some setup 9 | */ 10 | Error.stackTraceLimit = Infinity; 11 | 12 | require('core-js/client/shim'); 13 | require('reflect-metadata'); 14 | 15 | // Typescript emit helpers polyfill 16 | require('ts-helpers'); 17 | 18 | require('zone.js/dist/zone'); 19 | require('zone.js/dist/long-stack-trace-zone'); 20 | require('zone.js/dist/proxy'); // since zone.js 0.6.15 21 | require('zone.js/dist/sync-test'); 22 | require('zone.js/dist/jasmine-patch'); // put here since zone.js 0.6.14 23 | require('zone.js/dist/async-test'); 24 | require('zone.js/dist/fake-async-test'); 25 | 26 | // RxJS 27 | require('rxjs/Rx'); 28 | 29 | var testing = require('@angular/core/testing'); 30 | var browser = require('@angular/platform-browser-dynamic/testing'); 31 | 32 | testing.TestBed.initTestEnvironment( 33 | browser.BrowserDynamicTestingModule, 34 | browser.platformBrowserDynamicTesting() 35 | ); 36 | 37 | /* 38 | * Ok, this is kinda crazy. We can use the context method on 39 | * require that webpack created in order to tell webpack 40 | * what files we actually want to require or import. 41 | * Below, context will be a function/object with file names as keys. 42 | * Using that regex we are saying look in ../src then find 43 | * any file that ends with spec.ts and get its path. By passing in true 44 | * we say do this recursively 45 | */ 46 | var testContext = require.context('../src', true, /\.spec\.ts/); 47 | 48 | /* 49 | * get all the files, for each file, call the context function 50 | * that will require the file and load it up here. Context will 51 | * loop and require those spec files here 52 | */ 53 | function requireAll(requireContext) { 54 | return requireContext.keys().map(requireContext); 55 | } 56 | 57 | // requires and returns all modules that match 58 | var modules = requireAll(testContext); 59 | -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = (config) => { 2 | 3 | var testWebpackConfig = require('./webpack.test.js')({}); 4 | 5 | const configuration = { 6 | 7 | // base path that will be used to resolve all patterns (e.g. files, exclude) 8 | basePath: '', 9 | frameworks: ['jasmine'], 10 | files: [ 11 | { pattern: './config/karma-shim.js', watched: false }, 12 | { pattern: './config/gmap-mock.js', watched: false }, 13 | { pattern: './src/assets/**/*', watched: false, included: false, served: true, nocache: false }], 14 | preprocessors: { './config/karma-shim.js': ['coverage', 'webpack', 'sourcemap'] }, 15 | webpack: testWebpackConfig, 16 | webpackMiddleware: { 17 | // webpack-dev-middleware configuration 18 | // i.e. 19 | noInfo: true, 20 | // and use stats to turn off verbose output 21 | stats: { 22 | // options i.e. 23 | chunks: false 24 | } 25 | }, 26 | reporters: ['mocha'], 27 | port: 9876, 28 | colors: true, 29 | client: { 30 | captureConsole: false 31 | }, 32 | /* 33 | * By default all assets are served at http://localhost:[PORT]/base/ 34 | */ 35 | proxies: { 36 | "/assets/": "/base/src/assets/" 37 | }, 38 | /* 39 | * level of logging 40 | * possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 41 | */ 42 | logLevel: config.LOG_WARN, 43 | // enable / disable watching file and executing tests whenever any file changes 44 | autoWatch: false, 45 | /* 46 | * start these browsers 47 | * available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 48 | */ 49 | browsers: [ 50 | 'PhantomJS' 51 | ], 52 | singleRun: true 53 | }; 54 | 55 | if (process.env.NO_COVERAGE !== 'true') { 56 | configuration.reporters.push('coverage', 'remap-coverage'); 57 | configuration.coverageReporter = { 58 | type: 'in-memory' 59 | }; 60 | 61 | configuration.remapCoverageReporter = { 62 | 'text-summary': null, 63 | html: './coverage/istanbul' 64 | }; 65 | } 66 | 67 | 68 | config.set(configuration); 69 | }; 70 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | // reference : https://github.com/marcoturi/ionic2-boilerplate/blob/master/config/webpack.config.js 2 | 3 | /** 4 | * If you are not using webpack to build your ionic app, this configuration will not affect your code, 5 | * se rollup.config.js instead. 6 | */ 7 | 8 | const webpackConfig = require('../node_modules/@ionic/app-scripts/config/webpack.config'); 9 | const webpack = require('webpack'); 10 | 11 | const nodeEnv = JSON.stringify(process.env.NODE_ENV) || JSON.stringify('development'); 12 | const platform = JSON.stringify(process.env.PLATFORM) || JSON.stringify('android'); 13 | 14 | /** 15 | * Plugin: DefinePlugin 16 | * Description: Define free variables. 17 | * Useful for having development builds with debug logging or adding global constants. 18 | * 19 | * Environment helpers 20 | * 21 | * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin 22 | */ 23 | 24 | webpackConfig.plugins.push( 25 | new webpack.DefinePlugin({ 26 | 'process.env.NODE_ENV': nodeEnv, 27 | 'process.env.PLATFORM': platform 28 | }) 29 | ); 30 | -------------------------------------------------------------------------------- /config/webpack.test.js: -------------------------------------------------------------------------------- 1 | // reference : https://github.com/marcoturi/ionic2-boilerplate/blob/master/config/webpack.test.js 2 | 3 | /** 4 | * This config file was inspired by https://github.com/AngularClass/angular2-webpack-starter/ 5 | * all credits go to them! 6 | */ 7 | 8 | const helpers = require('./helpers'); 9 | const path = require('path'); 10 | 11 | const ProvidePlugin = require('webpack/lib/ProvidePlugin'); 12 | const DefinePlugin = require('webpack/lib/DefinePlugin'); 13 | const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 14 | const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin'); 15 | 16 | const ENV = process.env.ENV = process.env.NODE_ENV = 'test'; 17 | const coverageEnabled = process.env.NO_COVERAGE !== 'true'; 18 | 19 | 20 | /** 21 | * Webpack configuration 22 | * 23 | * See: http://webpack.github.io/docs/configuration.html#cli 24 | */ 25 | module.exports = (options) => { 26 | 27 | /** 28 | * Instruments JS files with Istanbul for subsequent code coverage reporting. 29 | * Instrument only testing sources. 30 | * 31 | * See: https://github.com/deepsweet/istanbul-instrumenter-loader 32 | * 33 | * @hack: Disabling coverage if NO_COVERAGE env var is set to 'true'. 34 | * This is useful for karma debug. 35 | * 36 | * See: https://github.com/AngularClass/angular2-webpack-starter/issues/361?_pjax=%23js-repo-pjax-container 37 | * See: https://github.com/gotwarlost/istanbul/issues/212 38 | * 39 | */ 40 | let postLoaders = {}; 41 | if (coverageEnabled) { 42 | postLoaders = { 43 | enforce: 'post', 44 | test: /\.(js|ts)$/, loader: 'istanbul-instrumenter-loader', 45 | include: helpers.root('src'), 46 | exclude: [ 47 | /\.(e2e|spec)\.ts$/, 48 | /node_modules/ 49 | ] 50 | }; 51 | 52 | } 53 | 54 | return { 55 | 56 | /** 57 | * Source map for Karma from the help of karma-sourcemap-loader & karma-webpack 58 | * 59 | * Do not change, leave as is or it wont work. 60 | * See: https://github.com/webpack/karma-webpack#source-maps 61 | */ 62 | devtool: 'inline-source-map', 63 | 64 | /** 65 | * Options affecting the resolving of modules. 66 | * 67 | * See: http://webpack.github.io/docs/configuration.html#resolve 68 | */ 69 | resolve: { 70 | 71 | /** 72 | * An array of extensions that should be used to resolve modules. 73 | * 74 | * See: http://webpack.github.io/docs/configuration.html#resolve-extensions 75 | */ 76 | extensions: ['.ts', '.js'], 77 | 78 | /** 79 | * Make sure root is src 80 | */ 81 | modules: [ path.resolve(__dirname, 'src'), 'node_modules' ] 82 | 83 | }, 84 | 85 | /** 86 | * Options affecting the normal modules. 87 | * 88 | * See: http://webpack.github.io/docs/configuration.html#module 89 | */ 90 | module: { 91 | 92 | rules: [ 93 | 94 | /** 95 | * Tslint loader support for *.ts files 96 | * 97 | * See: https://github.com/wbuchwalter/tslint-loader 98 | */ 99 | { 100 | enforce: 'pre', 101 | test: /\.ts$/, 102 | loader: 'tslint-loader', 103 | exclude: [helpers.root('node_modules')] 104 | }, 105 | 106 | /** 107 | * Source map loader support for *.js files 108 | * Extracts SourceMaps for source files that as added as sourceMappingURL comment. 109 | * 110 | * See: https://github.com/webpack/source-map-loader 111 | */ 112 | { 113 | enforce: 'pre', 114 | test: /\.js$/, 115 | loader: 'source-map-loader', 116 | exclude: [ 117 | // these packages have problems with their sourcemaps 118 | helpers.root('node_modules/rxjs'), 119 | helpers.root('node_modules/@angular'), 120 | helpers.root('node_modules/ionic-angular'), 121 | helpers.root('node_modules/ionic-native') 122 | ] 123 | }, 124 | 125 | /** 126 | * Typescript loader support for .ts and Angular 2 async routes via .async.ts 127 | * 128 | * See: https://github.com/s-panferov/awesome-typescript-loader 129 | */ 130 | { 131 | test: /\.ts$/, 132 | 133 | loaders: [ 134 | `awesome-typescript-loader?sourceMap=${!coverageEnabled},inlineSourceMap=${coverageEnabled},module=commonjs,noEmitHelpers=true,compilerOptions{}=removeComments:true`, 135 | 'angular2-template-loader' 136 | ], 137 | // query: { 138 | // // use inline sourcemaps for "karma-remap-coverage" reporter 139 | // sourceMap: !coverageEnabled, 140 | // inlineSourceMap: coverageEnabled, 141 | // module: 'commonjs', 142 | // noEmitHelpers: true, 143 | // compilerOptions: { 144 | // removeComments: true 145 | // } 146 | // }, 147 | exclude: [/\.e2e\.ts$/] 148 | }, 149 | 150 | /** 151 | * Json loader support for *.json files. 152 | * 153 | * See: https://github.com/webpack/json-loader 154 | */ 155 | { 156 | test: /\.json$/, 157 | loader: 'json-loader', 158 | exclude: [helpers.root('src/index.html')] 159 | }, 160 | 161 | /** 162 | * Raw loader support for *.css files 163 | * Returns file content as string 164 | * 165 | * See: https://github.com/webpack/raw-loader 166 | */ 167 | { 168 | test: /\.css$/, 169 | loaders: ['to-string-loader', 'css-loader'], 170 | exclude: [helpers.root('src/index.html')] 171 | }, 172 | 173 | /** 174 | * Raw loader support for *.html 175 | * Returns file content as string 176 | * 177 | * See: https://github.com/webpack/raw-loader 178 | */ 179 | { 180 | test: /\.html$/, 181 | loader: 'raw-loader', 182 | exclude: [helpers.root('src/index.html')] 183 | }, 184 | 185 | postLoaders 186 | ] 187 | }, 188 | 189 | /** 190 | * Add additional plugins to the compiler. 191 | * 192 | * See: http://webpack.github.io/docs/configuration.html#plugins 193 | */ 194 | plugins: [ 195 | 196 | /** 197 | * Plugin: DefinePlugin 198 | * Description: Define free variables. 199 | * Useful for having development builds with debug logging or adding global constants. 200 | * 201 | * Environment helpers 202 | * 203 | * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin 204 | */ 205 | // NOTE: when adding more properties make sure you include them in custom-typings.d.ts 206 | new DefinePlugin({ 207 | 'ENV': JSON.stringify(ENV), 208 | 'process.env': { 209 | 'ENV': JSON.stringify(ENV), 210 | 'NODE_ENV': JSON.stringify(ENV) 211 | } 212 | }), 213 | 214 | /** 215 | * Plugin: ContextReplacementPlugin 216 | * Description: Provides context to Angular's use of System.import 217 | * 218 | * See: https://webpack.github.io/docs/list-of-plugins.html#contextreplacementplugin 219 | * See: https://github.com/angular/angular/issues/11580 220 | */ 221 | new ContextReplacementPlugin( 222 | // The (\\|\/) piece accounts for path separators in *nix and Windows 223 | /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, 224 | helpers.root('src') // location of your src 225 | ), 226 | 227 | /** 228 | * Plugin LoaderOptionsPlugin (experimental) 229 | * 230 | * See: https://gist.github.com/sokra/27b24881210b56bbaff7 231 | */ 232 | new LoaderOptionsPlugin({ 233 | debug: true, 234 | options: { 235 | 236 | /** 237 | * Static analysis linter for TypeScript advanced options configuration 238 | * Description: An extensible linter for the TypeScript language. 239 | * 240 | * See: https://github.com/wbuchwalter/tslint-loader 241 | */ 242 | tslint: { 243 | emitErrors: false, 244 | failOnHint: false, 245 | resourcePath: 'src' 246 | }, 247 | 248 | } 249 | }), 250 | 251 | ], 252 | 253 | /** 254 | * Disable performance hints 255 | * 256 | * See: https://github.com/a-tarasyuk/rr-boilerplate/blob/master/webpack/dev.config.babel.js#L41 257 | */ 258 | performance: { 259 | hints: false 260 | }, 261 | 262 | 263 | /** 264 | * Include polyfills or mocks for various node stuff 265 | * Description: Node configuration 266 | * 267 | * See: https://webpack.github.io/docs/configuration.html#node 268 | */ 269 | node: { 270 | global: true, 271 | process: false, 272 | crypto: 'empty', 273 | module: false, 274 | clearImmediate: false, 275 | setImmediate: false 276 | } 277 | 278 | }; 279 | }; 280 | -------------------------------------------------------------------------------- /coverage/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | !protractor 6 | -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic2-taxi-app", 3 | "app_id": "", 4 | "typescript": true, 5 | "v2": true 6 | } 7 | -------------------------------------------------------------------------------- /ionic.project: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic2-taxi-app", 3 | "app_id": "" 4 | } -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./config/karma.conf.js'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic2-taxi-app", 3 | "version": "0.1.0", 4 | "description": "Ionic 2 Taxi App - A simple starting point for building 'on-demand/rideshare taxi' ionic application ( something like Uber, Lyft or Sidecar .. )", 5 | "keywords": [ 6 | "ionic", 7 | "ionic2", 8 | "angular2", 9 | "google-maps", 10 | "webpack", 11 | "typescript", 12 | "yarn" 13 | ], 14 | "author": { 15 | "name": "Damien Dell'Amico", 16 | "email": "damien.dellamico@gmail.com", 17 | "url": "https://github.com/ddellamico" 18 | }, 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/ddellamico/ionic2-taxi-app.git" 23 | }, 24 | "scripts": { 25 | "dev": "bnr dev", 26 | "build": "bnr build", 27 | "build-dev": "bnr build-dev", 28 | "clean": "bnr clean", 29 | "test": "bnr test", 30 | "test:watch": "bnr test-watch", 31 | "ios:dev": "bnr ios:dev", 32 | "ios:release": "bnr ios:release", 33 | "android:dev": "bnr android:dev", 34 | "android:release": "bnr android:release", 35 | "commit": "git-cz", 36 | "release": "standard-version --no-verify", 37 | "push": "git push --follow-tags origin master", 38 | "lint": "tslint \"src/**/*.ts\" --exclude=src/**/*.d.ts", 39 | "docs": "typedoc --options typedoc.json --exclude '**/*+(e2e|spec|index).ts' ./src/", 40 | "outdated": "yarn outdated", 41 | "precommit": "npm run lint", 42 | "prepush": "npm test" 43 | }, 44 | "betterScripts": { 45 | "dev": { 46 | "command": "ionic-app-scripts serve", 47 | "env": { 48 | "NODE_ENV": "development", 49 | "PLATFORM": "web" 50 | } 51 | }, 52 | "build": { 53 | "command": "ionic-app-scripts build --prod", 54 | "env": { 55 | "NODE_ENV": "production" 56 | } 57 | }, 58 | "build-dev": { 59 | "command": "ionic-app-scripts build --dev", 60 | "env": { 61 | "NODE_ENV": "development" 62 | } 63 | }, 64 | "clean": { 65 | "command": "ionic-app-scripts clean" 66 | }, 67 | "test": { 68 | "command": "karma start", 69 | "env": { 70 | "NODE_ENV": "test" 71 | } 72 | }, 73 | "test-watch": { 74 | "command": "karma start --auto-watch --no-single-run", 75 | "env": { 76 | "NODE_ENV": "test", 77 | "NO_COVERAGE": true 78 | } 79 | }, 80 | "ios:dev": { 81 | "command": "ionic run ios --livereload", 82 | "env": { 83 | "NODE_ENV": "development", 84 | "PLATFORM": "ios" 85 | } 86 | }, 87 | "ios:release": { 88 | "command": "ionic build ios --prod", 89 | "env": { 90 | "NODE_ENV": "production", 91 | "PLATFORM": "ios" 92 | } 93 | }, 94 | "android:dev": { 95 | "command": "ionic run android --livereload", 96 | "env": { 97 | "NODE_ENV": "development", 98 | "PLATFORM": "android" 99 | } 100 | }, 101 | "android:release": { 102 | "command": "ionic build android --prod", 103 | "env": { 104 | "NODE_ENV": "production", 105 | "PLATFORM": "android" 106 | } 107 | } 108 | }, 109 | "dependencies": { 110 | "@angular/common": "2.4.8", 111 | "@angular/compiler": "2.4.8", 112 | "@angular/compiler-cli": "2.4.8", 113 | "@angular/core": "2.4.8", 114 | "@angular/forms": "2.4.8", 115 | "@angular/http": "2.4.8", 116 | "@angular/platform-browser": "2.4.8", 117 | "@angular/platform-browser-dynamic": "2.4.8", 118 | "@angular/platform-server": "2.4.8", 119 | "@angular/tsc-wrapped": "~0.5.2", 120 | "@ionic/storage": "2.0.0", 121 | "ionic-angular": "2.2.0", 122 | "ionic-native": "2.4.1", 123 | "ionicons": "3.0.0", 124 | "lodash-es": "^4.17.4", 125 | "node-uuid": "^1.4.7", 126 | "rxjs": "5.0.1", 127 | "sw-toolbox": "3.4.0", 128 | "uuid": "^2.0.2", 129 | "zone.js": "0.7.2" 130 | }, 131 | "devDependencies": { 132 | "@ionic/app-scripts": "1.1.4", 133 | "@types/googlemaps": "^3.26.2", 134 | "@types/jasmine": "2.5.41", 135 | "@types/lodash": "ts2.0", 136 | "@types/node-uuid": "0.0.28", 137 | "angular2-template-loader": "^0.6.0", 138 | "awesome-typescript-loader": "^3.0.8", 139 | "better-npm-run": "0.0.14", 140 | "codelyzer": "2.0.1", 141 | "commitizen": "^2.9.5", 142 | "css-loader": "^0.26.1", 143 | "cz-conventional-changelog": "^2.0.0", 144 | "finalhandler": "^1.0.0", 145 | "husky": "^0.13.1", 146 | "istanbul-instrumenter-loader": "0.2.0", 147 | "jasmine-core": "^2.5.2", 148 | "jasmine-spec-reporter": "^3.2.0", 149 | "json-loader": "^0.5.4", 150 | "karma": "^1.4.0", 151 | "karma-chrome-launcher": "^2.0.0", 152 | "karma-coverage": "^1.1.1", 153 | "karma-jasmine": "^1.1.0", 154 | "karma-mocha-reporter": "^2.2.2", 155 | "karma-phantomjs-launcher": "^1.0.2", 156 | "karma-remap-coverage": "0.1.2", 157 | "karma-sourcemap-loader": "^0.3.7", 158 | "karma-webpack": "2.0.2", 159 | "phantomjs-prebuilt": "^2.1.14", 160 | "raw-loader": "0.5.1", 161 | "serve-static": "^1.11.2", 162 | "source-map-loader": "^0.1.6", 163 | "standard-version": "^4.0.0", 164 | "to-string-loader": "^1.1.4", 165 | "ts-helpers": "^1.1.1", 166 | "ts-node": "^2.1.0", 167 | "tslint": "4.0.0", 168 | "tslint-loader": "^3.4.2", 169 | "typedoc": "^0.5.7", 170 | "typescript": "~2.0.10", 171 | "validate-commit-msg": "^2.11.1", 172 | "webpack": "2.2.1", 173 | "xml2js": "^0.4.17" 174 | }, 175 | "config": { 176 | "commitizen": { 177 | "path": "./node_modules/cz-conventional-changelog" 178 | }, 179 | "ionic_webpack": "./config/webpack.config.js" 180 | }, 181 | "engines": { 182 | "node": ">= 6.0.0", 183 | "npm": ">= 3" 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/icon.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-40@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-83.5@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon-small@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/resources/splash.png -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2017 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Component, ViewChild } from '@angular/core'; 8 | import { Platform, Nav } from 'ionic-angular'; 9 | import { StatusBar, Splashscreen } from 'ionic-native'; 10 | import { HomePage } from '../pages/home/home'; 11 | import { AboutPage } from '../pages/about/about'; 12 | import { RideListPage } from '../pages/ride-list/ride-list'; 13 | 14 | export interface PageInterface { 15 | title: string; 16 | component: any; 17 | icon: string; 18 | logsOut?: boolean; 19 | index?: number; 20 | tabComponent?: any; 21 | } 22 | 23 | @Component({ 24 | templateUrl: 'app.template.html', 25 | }) 26 | export class TaxiApp { 27 | 28 | appPages: PageInterface[] = [ 29 | {title: 'Map', component: HomePage, index: 1, icon: 'map'}, 30 | {title: 'Taxi rides', component: RideListPage, index: 2, icon: 'car'}, 31 | {title: 'About', component: AboutPage, index: 3, icon: 'information-circle'}, 32 | ]; 33 | 34 | rootPage: any = HomePage; 35 | 36 | // the root nav is a child of the root app component 37 | // @ViewChild(Nav) gets a reference to the app's root nav 38 | @ViewChild(Nav) private nav: Nav; 39 | 40 | constructor(private platform: Platform) { 41 | // Call any initial plugins when ready 42 | platform.ready().then(() => { 43 | // Okay, so the platform is ready and our plugins are available. 44 | // Here you can do any higher level native things you might need. 45 | StatusBar.styleDefault(); 46 | Splashscreen.hide(); 47 | // https://github.com/apache/cordova-plugin-inappbrowser 48 | // The cordova.InAppBrowser.open() function is defined to be a drop-in replacement for the window.open() 49 | // function. Existing window.open() calls can use the InAppBrowser window, by replacing window.open: 50 | if ((window).cordova && (window).cordova.InAppBrowser) { 51 | window.open = (window).cordova.InAppBrowser.open; 52 | } 53 | }); 54 | } 55 | 56 | openPage(page: PageInterface) { 57 | // the nav component was found using @ViewChild(Nav) 58 | // reset the nav to remove previous pages and only have this page 59 | // we wouldn't want the back button to show in this scenario 60 | if (page.index) { 61 | this.nav.setRoot(page.component, { tabIndex: page.index }); 62 | } else { 63 | this.nav.setRoot(page.component).catch(() => { 64 | console.log("Didn't set nav root"); 65 | }); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/app.mock.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Platform } from 'ionic-angular'; 8 | import { TaxiApp } from './app.component'; 9 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 10 | import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; 11 | import { PlatformMock } from '../ionic-mock'; 12 | import { } from 'jasmine'; 13 | 14 | describe('TaxiApp', () => { 15 | 16 | let comp: TaxiApp; 17 | let fixture: ComponentFixture; 18 | let de: DebugElement; 19 | let el: HTMLElement; 20 | 21 | beforeEach(async(() => { 22 | TestBed.configureTestingModule({ 23 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 24 | declarations: [TaxiApp], 25 | providers: [ 26 | {provide: Platform, useClass: PlatformMock} 27 | ] 28 | }).compileComponents(); 29 | })); 30 | 31 | beforeEach(() => { 32 | 33 | fixture = TestBed.createComponent(TaxiApp); 34 | comp = fixture.componentInstance; 35 | de = fixture.debugElement; 36 | 37 | // #trick 38 | // If you want to trigger ionViewWillEnter automatically de-comment the following line 39 | // fixture.componentInstance.ionViewWillEnter(); 40 | 41 | fixture.detectChanges(); 42 | }); 43 | 44 | afterEach(() => { 45 | fixture.destroy(); 46 | comp = null; 47 | de = null; 48 | el = null; 49 | }); 50 | 51 | it('is created', () => { 52 | expect(fixture).toBeTruthy(); 53 | expect(comp).toBeTruthy(); 54 | }); 55 | 56 | it('should initialize with an app', () => { 57 | expect(comp['app']).not.toBe(null); 58 | }); 59 | 60 | it('should have a root page', () => { 61 | expect(comp['rootPage']).not.toBe(null); 62 | }); 63 | 64 | it('should have 2 main pages', () => { 65 | expect(comp.appPages.length).toEqual(3); 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2017 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { NgModule } from '@angular/core'; 8 | import { IonicApp, IonicModule } from 'ionic-angular'; 9 | import { TaxiApp } from './app.component'; 10 | import { AboutPage } from '../pages/about/about'; 11 | import { HomePage } from '../pages/home/home'; 12 | import { RideListPage } from '../pages/ride-list/ride-list'; 13 | import { ConfirmationPage } from '../pages/confirmation/confirmation'; 14 | 15 | import { MapComponent } from '../components/map/map'; 16 | import { SearchPage } from '../pages/search/search'; 17 | 18 | import { RideService } from '../providers/ride/ride.service'; 19 | import { GeocoderService } from '../providers/map/geocoder.service'; 20 | import { MapService } from '../providers/map/map.service'; 21 | 22 | @NgModule({ 23 | declarations: [ 24 | TaxiApp, 25 | AboutPage, 26 | HomePage, 27 | RideListPage, 28 | MapComponent, 29 | SearchPage, 30 | ConfirmationPage 31 | ], 32 | imports: [ 33 | IonicModule.forRoot(TaxiApp), 34 | ], 35 | bootstrap: [IonicApp], 36 | entryComponents: [ 37 | TaxiApp, 38 | AboutPage, 39 | HomePage, 40 | RideListPage, 41 | SearchPage, 42 | ConfirmationPage 43 | ], 44 | providers: [RideService, GeocoderService, MapService], 45 | }) 46 | export class AppModule { 47 | } 48 | -------------------------------------------------------------------------------- /src/app/app.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Menu 5 | 6 | 7 | 8 | 9 | 10 | Navigate 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 8 | 9 | // RxJS 10 | import 'rxjs/add/observable/throw'; 11 | import 'rxjs/add/operator/catch'; 12 | import 'rxjs/add/operator/map'; 13 | import 'rxjs/add/operator/mergeMap'; 14 | import 'rxjs/add/operator/switchMap'; 15 | import 'rxjs/add/operator/debounceTime'; 16 | import 'rxjs/add/operator/retry'; 17 | import 'rxjs/add/operator/distinctUntilChanged'; 18 | import 'rxjs/add/operator/do'; 19 | import 'rxjs/add/observable/of'; 20 | import 'rxjs/add/observable/from'; 21 | 22 | import { AppModule } from './app.module'; 23 | 24 | platformBrowserDynamic().bootstrapModule(AppModule); 25 | -------------------------------------------------------------------------------- /src/assets/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/src/assets/icon/favicon.ico -------------------------------------------------------------------------------- /src/assets/img/taxi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddellamico/ionic2-taxi-app/a0f81c59270a5126c3fb527334dfca614d32e027/src/assets/img/taxi.png -------------------------------------------------------------------------------- /src/components/map/map.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { ViewController, AlertController } from 'ionic-angular'; 8 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 9 | import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; 10 | import { NavController } from 'ionic-angular'; 11 | import { TaxiApp } from '../../app/app.component'; 12 | import { MapComponent } from './map'; 13 | import { GeocoderServiceMock } from '../../providers/map/geocoder-mock.service'; 14 | import { GeocoderService } from '../../providers/map/geocoder.service'; 15 | import { MapService } from '../../providers/map/map.service'; 16 | import { MapServiceMock } from '../../providers/map/map-mock.service'; 17 | import { NavMock, ViewControllerMock, AlertControllerMock } from '../../ionic-mock'; 18 | import { RideService } from '../../providers/ride/ride.service'; 19 | import { RideServiceMock } from '../../providers/ride/ride-mock.service'; 20 | 21 | describe('Component: Map', () => { 22 | 23 | let comp: MapComponent; 24 | let fixture: ComponentFixture; 25 | let de: DebugElement; 26 | let el: HTMLElement; 27 | let mapService: MapService; 28 | let rideService: RideService; 29 | let geocoderService: GeocoderServiceMock; 30 | 31 | beforeEach(async(() => { 32 | TestBed.configureTestingModule({ 33 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 34 | declarations: [TaxiApp, MapComponent], 35 | providers: [ 36 | {provide: NavController, useClass: NavMock}, 37 | {provide: ViewController, useClass: ViewControllerMock}, 38 | {provide: AlertController, useClass: AlertControllerMock}, 39 | {provide: RideService, useClass: RideServiceMock}, 40 | {provide: MapService, useClass: MapServiceMock}, 41 | {provide: GeocoderService, useClass: GeocoderServiceMock} 42 | ] 43 | }).compileComponents(); 44 | })); 45 | 46 | beforeEach(() => { 47 | fixture = TestBed.createComponent(MapComponent); 48 | comp = fixture.componentInstance; 49 | de = fixture.debugElement; 50 | 51 | mapService = TestBed.get(MapService); 52 | rideService = TestBed.get(RideService); 53 | geocoderService = TestBed.get(GeocoderService); 54 | }); 55 | 56 | afterEach(() => { 57 | fixture.destroy(); 58 | comp = null; 59 | de = null; 60 | el = null; 61 | }); 62 | 63 | it('is created', () => { 64 | expect(fixture).toBeTruthy(); 65 | expect(comp).toBeTruthy(); 66 | }); 67 | 68 | it('should create a new map', () => { 69 | const _response = new Promise((resolve: Function) => { 70 | resolve(true); 71 | }); 72 | 73 | spyOn(mapService, 'createMap').and.returnValue(_response); 74 | 75 | const instance = fixture.componentInstance; 76 | instance.ngAfterViewInit().then(() => { 77 | expect(mapService.createMap).toHaveBeenCalled(); 78 | expect(instance.map).toBeTruthy(); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/components/map/map.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Component, Output, EventEmitter, AfterViewInit, ViewChild } from '@angular/core'; 8 | import { MapService } from '../../providers/map/map.service'; 9 | 10 | @Component({ 11 | selector: 'it-map', 12 | template: `
` 13 | }) 14 | 15 | export class MapComponent implements AfterViewInit { 16 | 17 | @Output() onMapReady = new EventEmitter(); 18 | @Output() onMapIdle = new EventEmitter(); 19 | @Output() onCenterChanged = new EventEmitter(); 20 | @Output() onDragStart = new EventEmitter(); 21 | 22 | @ViewChild('map') mapCanvas: any; 23 | map: any = null; 24 | 25 | constructor(private mapService: MapService) { 26 | } 27 | 28 | ngAfterViewInit() { 29 | const mapElem = this.mapCanvas.nativeElement; 30 | return this.mapService.createMap(mapElem).then((map) => { 31 | this.map = map; 32 | this.bindMapEvents(mapElem); 33 | }); 34 | } 35 | 36 | private bindMapEvents(mapEl: HTMLElement): void { 37 | // Stop the side bar from dragging when mousedown/tapdown on the map 38 | google.maps.event.addDomListener(mapEl, 'mousedown', (e: any) => { 39 | e.preventDefault(); 40 | }); 41 | 42 | google.maps.event.addListenerOnce(this.map, 'idle', () => { 43 | this.onMapReady.emit({ 44 | value: this.map 45 | }); 46 | }); 47 | 48 | google.maps.event.addListenerOnce(this.map, 'center_changed', () => { 49 | this.onCenterChanged.emit({ 50 | value: this.map 51 | }); 52 | }); 53 | 54 | google.maps.event.addListener(this.map, 'idle', () => { 55 | this.onMapIdle.emit({ 56 | value: this.map 57 | }); 58 | }); 59 | 60 | google.maps.event.addListener(this.map, 'dragstart', () => { 61 | this.onDragStart.emit({ 62 | value: this.map 63 | }); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ref: https://github.com/AngularClass/angular2-webpack-starter 3 | * Custom Type Definitions 4 | * When including 3rd party modules you also need to include the type definition for the module 5 | * if they don't provide one within the module. You can try to install it with @types 6 | * npm install @types/moment 7 | * npm install @types/lodash 8 | * If you can't find the type definition in the registry we can make an ambient/global definition in 9 | * this file for now. For example 10 | * declare module 'my-module' { 11 | * export function doesSomething(value: string): string; 12 | * } 13 | * If you are using a CommonJS module that is using module.exports then you will have to write your 14 | * types using export = yourObjectOrFunction with a namespace above it 15 | * notice how we have to create a namespace that is equal to the function we're 16 | * assigning the export to 17 | * declare module 'jwt-decode' { 18 | * function jwtDecode(token: string): any; 19 | * namespace jwtDecode {} 20 | * export = jwtDecode; 21 | * } 22 | * 23 | * If you're prototying and you will fix the types later you can also declare it as type any 24 | * 25 | * declare var assert: any; 26 | * declare var _: any; 27 | * declare var $: any; 28 | * 29 | * If you're importing a module that uses Node.js modules which are CommonJS you need to import as 30 | * in the files such as main.browser.ts or any file within app/ 31 | * 32 | * import * as _ from 'lodash' 33 | * You can include your type definitions in this file until you create one for the @types 34 | * 35 | */ 36 | 37 | declare module '*.json' 38 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ionic App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/ionic-mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | export class NavMock { 8 | 9 | public pop(): any { 10 | return new Promise(function (resolve: Function): void { 11 | resolve(); 12 | }); 13 | } 14 | 15 | public push(): any { 16 | return new Promise(function (resolve: Function): void { 17 | resolve(); 18 | }); 19 | } 20 | 21 | public getActive(): any { 22 | return { 23 | 'instance': { 24 | 'model': 'something', 25 | }, 26 | }; 27 | } 28 | 29 | public setRoot(): any { 30 | return true; 31 | } 32 | } 33 | 34 | export class PlatformMock { 35 | public ready(): any { 36 | return new Promise((resolve: Function) => { 37 | resolve(); 38 | }); 39 | } 40 | } 41 | 42 | export class MenuMock { 43 | public close(): any { 44 | return new Promise((resolve: Function) => { 45 | resolve(); 46 | }); 47 | } 48 | } 49 | 50 | export class LoadingControllerMock { 51 | create(opts?: any) { 52 | return { 53 | present: () => { 54 | }, 55 | dismiss: () => { 56 | } 57 | }; 58 | }; 59 | } 60 | 61 | export class ViewControllerMock { 62 | dismiss(data?: any, role?: any): Promise { 63 | return new Promise(function (resolve: Function): void { 64 | resolve(); 65 | }); 66 | } 67 | } 68 | 69 | export class AlertControllerMock { 70 | create(): any { 71 | return {}; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ionic Taxi App", 3 | "short_name": "Ionic Taxi", 4 | "start_url": "index.html", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "assets/img/taxi.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | }], 11 | "background_color": "#387ef5", 12 | "theme_color": "#387ef5" 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/about/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | About 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 |
15 |

Ionic Taxi

16 |

Author: Damien Dell'Amico

17 |

Email: damien.dellamico@gmail.com

18 |
19 |
20 | -------------------------------------------------------------------------------- /src/pages/about/about.scss: -------------------------------------------------------------------------------- 1 | .about-header { 2 | background-color: #434954; 3 | padding: 16px; 4 | width: 100%; 5 | min-height: 150px; 6 | text-align: center; 7 | } 8 | 9 | .about-header img { 10 | max-width: 200px; 11 | min-height: 115px; 12 | margin-left: -15px; 13 | padding: 25px 0 20px 0; 14 | } 15 | 16 | .about-info p { 17 | color: #697072; 18 | text-align: left; 19 | } 20 | 21 | .about-info ion-icon { 22 | color: color($colors, primary); 23 | width: 20px; 24 | } 25 | 26 | .md, 27 | .wp { 28 | .about-info [text-right] { 29 | margin-right: 0; 30 | } 31 | } 32 | 33 | .ios .about-info { 34 | text-align: center; 35 | } 36 | 37 | .ios .about-info ion-icon { 38 | width: auto; 39 | margin-right: 10px; 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/about/about.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Component } from '@angular/core'; 8 | 9 | @Component({ 10 | templateUrl: 'about.html' 11 | }) 12 | export class AboutPage { 13 | 14 | constructor() { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/base-page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { AlertController } from 'ionic-angular'; 8 | 9 | export class BasePage { 10 | 11 | constructor(protected alertCtrl: AlertController) { 12 | } 13 | 14 | displayErrorAlert(): void { 15 | const prompt = this.alertCtrl.create({ 16 | title: 'Ionic Taxi', 17 | message: 'Unknown error, please try again later', 18 | buttons: ['OK'] 19 | }); 20 | prompt.present(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/confirmation/confirmation.scss: -------------------------------------------------------------------------------- 1 | .confirmation { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/confirmation/confirmation.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { IonicModule, Platform } from 'ionic-angular'; 8 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 9 | import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; 10 | import { NavController } from 'ionic-angular'; 11 | import { TaxiApp } from '../../app/app.component'; 12 | import { ConfirmationPage } from './confirmation'; 13 | import { GeocoderServiceMock } from '../../providers/map/geocoder-mock.service'; 14 | import { GeocoderService } from '../../providers/map/geocoder.service'; 15 | import { MapService } from '../../providers/map/map.service'; 16 | import { MapServiceMock } from '../../providers/map/map-mock.service'; 17 | import { NavMock } from '../../ionic-mock'; 18 | import { RideService } from '../../providers/ride/ride.service'; 19 | import { RideServiceMock } from '../../providers/ride/ride-mock.service'; 20 | 21 | describe('Page: ConfirmationPage', () => { 22 | 23 | let comp: ConfirmationPage; 24 | let fixture: ComponentFixture; 25 | let de: DebugElement; 26 | let el: HTMLElement; 27 | let mapService: MapService; 28 | let rideService: RideService; 29 | let geocoderService: GeocoderServiceMock; 30 | 31 | beforeEach(async(() => { 32 | TestBed.configureTestingModule({ 33 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 34 | declarations: [TaxiApp, ConfirmationPage], 35 | providers: [ 36 | {provide: NavController, useClass: NavMock}, 37 | {provide: RideService, useClass: RideServiceMock}, 38 | {provide: MapService, useClass: MapServiceMock}, 39 | {provide: GeocoderService, useClass: GeocoderServiceMock} 40 | ], 41 | imports: [ 42 | IonicModule.forRoot(TaxiApp) 43 | ] 44 | }).compileComponents(); 45 | })); 46 | 47 | beforeEach(() => { 48 | 49 | fixture = TestBed.createComponent(ConfirmationPage); 50 | comp = fixture.componentInstance; 51 | de = fixture.debugElement; 52 | 53 | mapService = TestBed.get(MapService); 54 | rideService = TestBed.get(RideService); 55 | geocoderService = TestBed.get(GeocoderService); 56 | }); 57 | 58 | afterEach(() => { 59 | fixture.destroy(); 60 | comp = null; 61 | de = null; 62 | el = null; 63 | }); 64 | 65 | it('is created', () => { 66 | expect(fixture).toBeTruthy(); 67 | expect(comp).toBeTruthy(); 68 | }); 69 | 70 | it('should render `ion-content`', () => { 71 | fixture.detectChanges(); 72 | expect(de.nativeElement.querySelectorAll('ion-content').length).toBe(1); 73 | }); 74 | 75 | /* it('should get address when google map idle event is emitted', () => { 76 | spyOn(geocoderService, 'addressForlatLng').and.callThrough(); 77 | 78 | const _response = 'fake address'; 79 | geocoderService.setResponse(_response); 80 | 81 | const instance = fixture.componentInstance; 82 | fixture.detectChanges(); 83 | 84 | expect(geocoderService.addressForlatLng).toHaveBeenCalled(); 85 | expect(instance.departure).toBe(_response); 86 | });*/ 87 | }); 88 | -------------------------------------------------------------------------------- /src/pages/confirmation/confirmation.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Confirmation 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /src/pages/confirmation/confirmation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Component } from '@angular/core'; 8 | import { AlertController, NavController } from 'ionic-angular'; 9 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 10 | import { GeocoderService } from '../../providers/map/geocoder.service'; 11 | import { RideModel } from '../../providers/ride/ride.model'; 12 | import { RideService } from '../../providers/ride/ride.service'; 13 | import { BasePage } from '../base-page'; 14 | import { RideListPage } from '../ride-list/ride-list'; 15 | import { MapService } from '../../providers/map/map.service'; 16 | 17 | @Component({ 18 | templateUrl: 'confirmation.tpl.html' 19 | }) 20 | export class ConfirmationPage extends BasePage { 21 | form: FormGroup; 22 | position: google.maps.LatLng; 23 | 24 | departure: string = null; 25 | destination: string = null; 26 | 27 | constructor(private nav: NavController, 28 | private fb: FormBuilder, 29 | private rideService: RideService, 30 | private mapService: MapService, 31 | private geocoderService: GeocoderService, 32 | protected alertCtrl: AlertController) { 33 | super(alertCtrl); 34 | 35 | this.form = this.fb.group({ 36 | departure: ['', [Validators.required]], 37 | destination: ['', [Validators.required]] 38 | }); 39 | 40 | this.position = this.mapService.mapCenter; 41 | 42 | if (this.position) { 43 | this.geocoderService.addressForlatLng(this.position.lat(), this.position.lng()) 44 | .subscribe((address: string) => { 45 | this.departure = address; 46 | }, (error) => { 47 | this.displayErrorAlert(); 48 | console.error(error); 49 | }); 50 | } 51 | } 52 | 53 | onConfirm(model: RideModel, isValid: boolean): void { 54 | if (!isValid) { 55 | this.displayErrorAlert(); 56 | return; 57 | } 58 | const prompt = this.alertCtrl.create({ 59 | title: 'Confirmation', 60 | message: 'Taxi will pick you up within 5 minutes. Do you want to confirm ?', 61 | buttons: [{ 62 | text: 'Cancel' 63 | }, { 64 | text: 'Confirm', 65 | handler: () => { 66 | this.rideService.addRide(model.departure, model.destination); 67 | this.nav.setRoot(RideListPage); 68 | } 69 | }] 70 | }); 71 | prompt.present(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/pages/home/home.scss: -------------------------------------------------------------------------------- 1 | .map-page { 2 | background: rgb(229, 227, 223); 3 | 4 | #gmaps { 5 | width: 100%; 6 | height: 100%; 7 | opacity: 0; 8 | transition: opacity 150ms ease-in 9 | } 10 | 11 | #gmaps.show-map { 12 | opacity: 1; 13 | } 14 | 15 | #centerMarker { 16 | width: 22px; 17 | height: 40px; 18 | display: block; 19 | content: ' '; 20 | position: absolute; 21 | top: 50%; 22 | left: 50%; 23 | margin: -40px 0 0 -11px; 24 | background: url('https://maps.gstatic.com/mapfiles/api-3/images/spotlight-poi_hdpi.png'); 25 | background-size: 22px 40px; /* Since I used the HiDPI marker version this compensates for the 2x size */ 26 | pointer-events: none; /* This disables clicks on the marker. Not fully supported by all major browsers, though */ 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/home/home.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { IonicModule, Platform } from 'ionic-angular'; 8 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 9 | import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; 10 | import { NavController } from 'ionic-angular'; 11 | import { TaxiApp } from '../../app/app.component'; 12 | import { HomePage } from './home'; 13 | import { GeocoderServiceMock } from '../../providers/map/geocoder-mock.service'; 14 | import { GeocoderService } from '../../providers/map/geocoder.service'; 15 | import { MapService } from '../../providers/map/map.service'; 16 | import { MapServiceMock } from '../../providers/map/map-mock.service'; 17 | import { NavMock } from '../../ionic-mock'; 18 | 19 | describe('Page: HomePage', () => { 20 | 21 | let comp: HomePage; 22 | let fixture: ComponentFixture; 23 | let de: DebugElement; 24 | let el: HTMLElement; 25 | let mapService: MapService; 26 | let geocoderService: GeocoderServiceMock; 27 | 28 | beforeEach(async(() => { 29 | TestBed.configureTestingModule({ 30 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 31 | declarations: [TaxiApp, HomePage], 32 | providers: [ 33 | {provide: NavController, useClass: NavMock}, 34 | {provide: GeocoderService, useClass: GeocoderServiceMock}, 35 | {provide: MapService, useClass: MapServiceMock} 36 | ], 37 | imports: [ 38 | IonicModule.forRoot(TaxiApp) 39 | ] 40 | }).compileComponents(); 41 | })); 42 | 43 | beforeEach(() => { 44 | 45 | fixture = TestBed.createComponent(HomePage); 46 | comp = fixture.componentInstance; 47 | de = fixture.debugElement; 48 | 49 | mapService = TestBed.get(MapService); 50 | geocoderService = TestBed.get(GeocoderService); 51 | 52 | // #trick 53 | // If you want to trigger ionViewWillEnter automatically de-comment the following line 54 | // fixture.componentInstance.ionViewWillEnter(); 55 | }); 56 | 57 | afterEach(() => { 58 | fixture.destroy(); 59 | comp = null; 60 | de = null; 61 | el = null; 62 | }); 63 | 64 | it('is created', () => { 65 | expect(fixture).toBeTruthy(); 66 | expect(comp).toBeTruthy(); 67 | }); 68 | 69 | it('should render `ion-content`', () => { 70 | fixture.detectChanges(); 71 | expect(de.nativeElement.querySelectorAll('ion-content').length).toBe(1); 72 | }); 73 | 74 | it('should call platform ready when google map is ready', () => { 75 | const platform = de.injector.get(Platform); 76 | spyOn(platform, 'ready').and.callThrough(); 77 | 78 | const instance = fixture.componentInstance; 79 | instance.onMapReady().then(() => { 80 | expect(platform.ready).toHaveBeenCalled(); 81 | }); 82 | }); 83 | 84 | it('should localize after google map is ready', () => { 85 | const _response = new Promise((resolve: Function) => { 86 | resolve({ 87 | latitude: 0, 88 | longitude: 0 89 | }); 90 | }); 91 | 92 | spyOn(mapService, 'setPosition').and.returnValue(_response); 93 | 94 | const instance = fixture.componentInstance; 95 | fixture.detectChanges(); 96 | instance.onMapReady().then(() => { 97 | expect(mapService.setPosition).toHaveBeenCalled(); 98 | expect(instance.localized).toBe(true); 99 | }); 100 | }); 101 | 102 | it('should get address when google map idle event is emitted', () => { 103 | spyOn(geocoderService, 'addressForlatLng').and.callThrough(); 104 | 105 | const _response = 'fake address'; 106 | geocoderService.setResponse(_response); 107 | 108 | const instance = fixture.componentInstance; 109 | 110 | instance.localized = true; 111 | instance.onMapIdle(); 112 | fixture.detectChanges(); 113 | 114 | expect(geocoderService.addressForlatLng).toHaveBeenCalled(); 115 | }); 116 | 117 | it('should close info window when `onDragStart` is emitted', () => { 118 | spyOn(mapService, 'closeInfoWindow').and.callThrough(); 119 | 120 | const _response = 'fake address'; 121 | geocoderService.setResponse(_response); 122 | 123 | const instance = fixture.componentInstance; 124 | instance.onDragStart(); 125 | fixture.detectChanges(); 126 | 127 | expect(mapService.closeInfoWindow).toHaveBeenCalled(); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /src/pages/home/home.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Ionic Taxi 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | -------------------------------------------------------------------------------- /src/pages/home/home.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Component } from '@angular/core'; 8 | import { MapComponent } from '../../components/map/map'; 9 | import { Vibration } from 'ionic-native'; 10 | import { MapService } from '../../providers/map/map.service'; 11 | import { LoadingController, AlertController, ModalController, NavController, Platform } from 'ionic-angular'; 12 | import { GeocoderService } from '../../providers/map/geocoder.service'; 13 | import { SearchPage } from '../search/search'; 14 | import { ConfirmationPage } from '../confirmation/confirmation'; 15 | import { BasePage } from '../base-page'; 16 | 17 | @Component({ 18 | templateUrl: 'home.tpl.html' 19 | }) 20 | export class HomePage extends BasePage { 21 | 22 | localized: boolean = false; 23 | 24 | constructor(private platform: Platform, 25 | private nav: NavController, 26 | private geocoderService: GeocoderService, 27 | private mapService: MapService, 28 | private modalCtrl: ModalController, 29 | private loadingCtrl: LoadingController, 30 | protected alertCtrl: AlertController) { 31 | super(alertCtrl); 32 | } 33 | 34 | /*** 35 | * This event is fired when map has fully loaded 36 | */ 37 | onMapReady(): Promise { 38 | // I must wait platform.ready() to use plugins ( in this case Geolocation plugin ). 39 | return this.platform.ready().then(() => { 40 | return this.locate().then(() => { 41 | const mapElement: Element = this.mapService.mapElement; 42 | if (mapElement) { 43 | mapElement.classList.add('show-map'); 44 | this.mapService.resizeMap(); 45 | } 46 | }); 47 | }); 48 | } 49 | 50 | /*** 51 | * This event is fired when the map becomes idle after panning or zooming. 52 | */ 53 | onMapIdle(): void { 54 | if (!this.localized) return; 55 | const position = this.mapService.mapCenter; 56 | this.geocoderService.addressForlatLng(position.lat(), position.lng()) 57 | .subscribe((address: string) => { 58 | 59 | const content = `
${address}
`; 60 | this.mapService.createInfoWindow(content, position); 61 | 62 | }, (error) => { 63 | this.displayErrorAlert(); 64 | console.error(error); 65 | }); 66 | } 67 | 68 | /*** 69 | * This event is fired when the user starts dragging the map. 70 | */ 71 | onDragStart(): void { 72 | this.mapService.closeInfoWindow(); 73 | } 74 | 75 | openModal(): void { 76 | const searchModal = this.modalCtrl.create(SearchPage); 77 | searchModal.present(); 78 | } 79 | 80 | goToConfirmation(): void { 81 | this.nav.push(ConfirmationPage); 82 | } 83 | 84 | /** 85 | * Get the current position 86 | */ 87 | private locate(): Promise { 88 | const loader = this.loadingCtrl.create({ 89 | content: 'Please wait...', 90 | }); 91 | loader.present(); 92 | return this.mapService.setPosition().then(() => { 93 | this.localized = true; 94 | // Vibrate the device for a second 95 | Vibration.vibrate(1000); 96 | }).catch(error => { 97 | this.alertNoGps(); 98 | console.warn(error); 99 | }).then(() => { 100 | // TODO why dismiss not working without setTimeout ? 101 | setTimeout(() => { 102 | loader.dismiss(); 103 | }, 1000); 104 | }); 105 | } 106 | 107 | private alertNoGps() { 108 | const alert = this.alertCtrl.create({ 109 | title: 'Ionic Taxi', 110 | subTitle: 'Gps and network locations are unavailable. Click OK to retry', 111 | enableBackdropDismiss: false, 112 | buttons: [{ 113 | text: 'OK', 114 | handler: () => { 115 | setTimeout(() => this.locate(), 1500); 116 | } 117 | }], 118 | }); 119 | alert.present(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/pages/ride-list/ride-list.scss: -------------------------------------------------------------------------------- 1 | .ride-list { 2 | 3 | 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/ride-list/ride-list.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { IonicModule, Platform } from 'ionic-angular'; 8 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 9 | import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; 10 | import { NavController } from 'ionic-angular'; 11 | import { TaxiApp } from '../../app/app.component'; 12 | import { RideListPage } from './ride-list'; 13 | import { GeocoderServiceMock } from '../../providers/map/geocoder-mock.service'; 14 | import { GeocoderService } from '../../providers/map/geocoder.service'; 15 | import { MapService } from '../../providers/map/map.service'; 16 | import { MapServiceMock } from '../../providers/map/map-mock.service'; 17 | import { NavMock } from '../../ionic-mock'; 18 | import { RideService } from '../../providers/ride/ride.service'; 19 | import { RideServiceMock } from '../../providers/ride/ride-mock.service'; 20 | 21 | describe('Page: RideListPage', () => { 22 | 23 | let comp: RideListPage; 24 | let fixture: ComponentFixture; 25 | let de: DebugElement; 26 | let el: HTMLElement; 27 | let mapService: MapService; 28 | let rideService: RideService; 29 | let geocoderService: GeocoderServiceMock; 30 | 31 | beforeEach(async(() => { 32 | TestBed.configureTestingModule({ 33 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 34 | declarations: [TaxiApp, RideListPage], 35 | providers: [ 36 | {provide: NavController, useClass: NavMock}, 37 | {provide: RideService, useClass: RideServiceMock}, 38 | {provide: MapService, useClass: MapServiceMock}, 39 | {provide: GeocoderService, useClass: GeocoderServiceMock} 40 | ], 41 | imports: [ 42 | IonicModule.forRoot(TaxiApp) 43 | ] 44 | }).compileComponents(); 45 | })); 46 | 47 | beforeEach(() => { 48 | 49 | fixture = TestBed.createComponent(RideListPage); 50 | comp = fixture.componentInstance; 51 | de = fixture.debugElement; 52 | 53 | mapService = TestBed.get(MapService); 54 | rideService = TestBed.get(RideService); 55 | geocoderService = TestBed.get(GeocoderService); 56 | }); 57 | 58 | afterEach(() => { 59 | fixture.destroy(); 60 | comp = null; 61 | de = null; 62 | el = null; 63 | }); 64 | 65 | it('is created', () => { 66 | expect(fixture).toBeTruthy(); 67 | expect(comp).toBeTruthy(); 68 | }); 69 | 70 | it('should render `ion-content`', () => { 71 | fixture.detectChanges(); 72 | expect(de.nativeElement.querySelectorAll('ion-content').length).toBe(1); 73 | }); 74 | 75 | it('should fetch all taxi rides at page load', () => { 76 | spyOn(rideService, 'getRides').and.returnValue(Promise.resolve([])); 77 | 78 | const instance = fixture.componentInstance; 79 | instance.ionViewDidEnter(); 80 | 81 | expect(rideService.getRides).toHaveBeenCalled(); 82 | expect(instance.rides).not.toBeUndefined(); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/pages/ride-list/ride-list.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Taxi rides 7 | 8 | 9 | 10 | 11 |

No rides found

12 | 13 |

14 | 15 | {{ ride.departure }} 16 |

17 |

18 | 19 | {{ ride.destination }} 20 |

21 |

{{ ride.rideDate | date:'medium' }}

22 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/pages/ride-list/ride-list.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Component } from '@angular/core'; 8 | import { AlertController } from 'ionic-angular'; 9 | import { RideService } from '../../providers/ride/ride.service'; 10 | import { BasePage } from '../base-page'; 11 | import { RideModel } from '../../providers/ride/ride.model'; 12 | 13 | @Component({ 14 | templateUrl: 'ride-list.tpl.html' 15 | }) 16 | export class RideListPage extends BasePage { 17 | rides: Array = []; 18 | 19 | constructor(private rideService: RideService, 20 | protected alertCtrl: AlertController) { 21 | super(alertCtrl); 22 | } 23 | 24 | ionViewDidEnter() { 25 | this.rides = this.rideService.getRides(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/search/search.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { IonicModule, Platform, ViewController, AlertController } from 'ionic-angular'; 8 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 9 | import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; 10 | import { NavController } from 'ionic-angular'; 11 | import { TaxiApp } from '../../app/app.component'; 12 | import { SearchPage } from './search'; 13 | import { GeocoderServiceMock } from '../../providers/map/geocoder-mock.service'; 14 | import { GeocoderService } from '../../providers/map/geocoder.service'; 15 | import { MapService } from '../../providers/map/map.service'; 16 | import { MapServiceMock } from '../../providers/map/map-mock.service'; 17 | import { NavMock, ViewControllerMock, AlertControllerMock } from '../../ionic-mock'; 18 | import { RideService } from '../../providers/ride/ride.service'; 19 | import { RideServiceMock } from '../../providers/ride/ride-mock.service'; 20 | 21 | describe('Page: SearchPage', () => { 22 | 23 | let comp: SearchPage; 24 | let fixture: ComponentFixture; 25 | let de: DebugElement; 26 | let el: HTMLElement; 27 | let mapService: MapService; 28 | let rideService: RideService; 29 | let geocoderService: GeocoderServiceMock; 30 | 31 | beforeEach(async(() => { 32 | TestBed.configureTestingModule({ 33 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 34 | declarations: [TaxiApp, SearchPage], 35 | providers: [ 36 | {provide: NavController, useClass: NavMock}, 37 | {provide: ViewController, useClass: ViewControllerMock}, 38 | {provide: AlertController, useClass: AlertControllerMock}, 39 | {provide: RideService, useClass: RideServiceMock}, 40 | {provide: MapService, useClass: MapServiceMock}, 41 | {provide: GeocoderService, useClass: GeocoderServiceMock} 42 | ] 43 | }).compileComponents(); 44 | })); 45 | 46 | beforeEach(() => { 47 | fixture = TestBed.createComponent(SearchPage); 48 | comp = fixture.componentInstance; 49 | de = fixture.debugElement; 50 | 51 | mapService = TestBed.get(MapService); 52 | rideService = TestBed.get(RideService); 53 | geocoderService = TestBed.get(GeocoderService); 54 | }); 55 | 56 | afterEach(() => { 57 | fixture.destroy(); 58 | comp = null; 59 | de = null; 60 | el = null; 61 | }); 62 | 63 | it('is created', () => { 64 | expect(fixture).toBeTruthy(); 65 | expect(comp).toBeTruthy(); 66 | }); 67 | 68 | it('should render `ion-content`', () => { 69 | fixture.detectChanges(); 70 | expect(de.nativeElement.querySelectorAll('ion-content').length).toBe(1); 71 | }); 72 | 73 | it('should setup autocomplete and load nearby places', () => { 74 | spyOn(mapService, 'createAutocomplete').and.callThrough(); 75 | spyOn(mapService, 'loadNearbyPlaces').and.callThrough(); 76 | 77 | fixture.componentInstance.ionViewDidLoad(); 78 | 79 | fixture.detectChanges(); 80 | expect(mapService.createAutocomplete).toHaveBeenCalled(); 81 | expect(mapService.loadNearbyPlaces).toHaveBeenCalled(); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/pages/search/search.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

{{ place.name }}

20 |

{{ place.vicinity }}

21 |

Distance: {{ place.distance }} meters

22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /src/pages/search/search.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Component, ViewChild, ElementRef, NgZone } from '@angular/core'; 8 | import { ViewController, AlertController } from 'ionic-angular'; 9 | import { MapService } from '../../providers/map/map.service'; 10 | import { BasePage } from '../base-page'; 11 | 12 | @Component({ 13 | templateUrl: 'search.tpl.html' 14 | }) 15 | 16 | export class SearchPage extends BasePage { 17 | 18 | // reference : https://github.com/driftyco/ionic/issues/7223 19 | @ViewChild('searchbar', {read: ElementRef}) searchbar: ElementRef; 20 | 21 | private nearbyPlaces: Array = []; 22 | private addressElement: HTMLInputElement = null; 23 | 24 | constructor(private mapService: MapService, 25 | private zone: NgZone, 26 | protected alertCtrl: AlertController, 27 | private viewCtrl: ViewController) { 28 | super(alertCtrl); 29 | } 30 | 31 | ionViewDidLoad() { 32 | this.initAutocomplete(); 33 | this.loadNearbyPlaces(); 34 | } 35 | 36 | dismiss(location?: google.maps.LatLng) { 37 | if (location) { 38 | this.mapService.mapCenter = location; 39 | } 40 | if (this.addressElement) { 41 | this.addressElement.value = ''; 42 | } 43 | this.viewCtrl.dismiss(); 44 | } 45 | 46 | /*** 47 | * Place item has been selected 48 | */ 49 | selectPlace(place: any) { 50 | this.dismiss(place.geometry.location); 51 | } 52 | 53 | private initAutocomplete(): void { 54 | // reference : https://github.com/driftyco/ionic/issues/7223 55 | this.addressElement = this.searchbar.nativeElement.querySelector('.searchbar-input'); 56 | this.mapService.createAutocomplete(this.addressElement).subscribe((location) => { 57 | this.dismiss(location); 58 | }, (error) => { 59 | this.displayErrorAlert(); 60 | console.error(error); 61 | }); 62 | } 63 | 64 | private loadNearbyPlaces(): void { 65 | this.nearbyPlaces = []; 66 | this.mapService.loadNearbyPlaces().subscribe((_nearbyPlaces) => { 67 | // force NgZone to detect changes 68 | this.zone.run(() => { 69 | this.nearbyPlaces.push.apply(this.nearbyPlaces, _nearbyPlaces); 70 | }); 71 | }, (error) => { 72 | this.displayErrorAlert(); 73 | console.error(error); 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/providers/map/geocoder-mock.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Observable } from 'rxjs/Observable'; 8 | 9 | export class GeocoderServiceMock { 10 | 11 | public fakeResponse: any = null; 12 | 13 | public addressForlatLng(): Observable { 14 | return Observable.of(this.fakeResponse); 15 | } 16 | 17 | public setResponse(data: any): void { 18 | this.fakeResponse = data; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/providers/map/geocoder.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Observable } from 'rxjs/Observable'; 8 | import { Injectable } from '@angular/core'; 9 | 10 | import 'rxjs/add/observable/throw'; 11 | import 'rxjs/add/operator/catch'; 12 | import 'rxjs/add/operator/map'; 13 | import 'rxjs/add/operator/mergeMap'; 14 | import 'rxjs/add/operator/switchMap'; 15 | import 'rxjs/add/operator/debounceTime'; 16 | import 'rxjs/add/operator/retry'; 17 | import 'rxjs/add/operator/distinctUntilChanged'; 18 | import 'rxjs/add/operator/do'; 19 | import 'rxjs/add/observable/of'; 20 | import 'rxjs/add/observable/from'; 21 | 22 | // Amount of time (in milliseconds) to pause between each trip to the 23 | // Geocoding API, which places limits on frequency. 24 | const QUERY_PAUSE = 500; 25 | 26 | @Injectable() 27 | export class GeocoderService { 28 | 29 | geocoder: google.maps.Geocoder = null; 30 | 31 | constructor() { 32 | } 33 | 34 | /*** 35 | * Convert coordinates to address 36 | * @param lat 37 | * @param lng 38 | * @returns {Observable} 39 | */ 40 | public addressForlatLng(lat: number, lng: number): Observable { 41 | if (!this.geocoder) { 42 | this.geocoder = new google.maps.Geocoder(); 43 | } 44 | const latlng = new google.maps.LatLng(lat, lng); 45 | return this.geocode(latlng) 46 | .debounceTime(QUERY_PAUSE) 47 | .distinctUntilChanged() 48 | .map((res: any) => res.formatted_address) 49 | .retry(3); 50 | } 51 | 52 | private geocode(latlng: google.maps.LatLng): Observable { 53 | return new Observable((sub: any) => { 54 | this.geocoder.geocode({location: latlng}, (result: google.maps.GeocoderResult[], status: any) => { 55 | if (status === google.maps.GeocoderStatus.OK) { 56 | sub.next(result[0]); 57 | sub.complete(); 58 | } else if (status === google.maps.GeocoderStatus.ZERO_RESULTS) { 59 | sub.error({ 60 | type: 'ZERO', 61 | message: `Zero results for geocoding location: ${location}` 62 | }); 63 | } else if (status === google.maps.GeocoderStatus.OVER_QUERY_LIMIT) { 64 | sub.error({ 65 | type: 'OVER_QUERY_LIMIT', 66 | message: `OVER_QUERY_LIMIT. location: ${location}` 67 | }); 68 | } else if (status === google.maps.GeocoderStatus.REQUEST_DENIED) { 69 | sub.error({ 70 | type: 'DENIED', 71 | message: `Request denied for geocoding location: ${location}` 72 | }); 73 | } else { 74 | sub.error({ 75 | type: 'INVALID', 76 | message: `Invalid request for geocoding: status: ${status}, location: ${location}` 77 | }); 78 | } 79 | }); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/providers/map/map-mock.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Observable } from 'rxjs/Observable'; 8 | import { MapConst } from './map.constants'; 9 | 10 | interface IMapOptions { 11 | lat: number; 12 | lon: number; 13 | zoom: number; 14 | } 15 | 16 | export class MapServiceMock { 17 | 18 | constructor() { 19 | } 20 | 21 | public createMap(mapEl: Element, opts: IMapOptions = { 22 | lat: MapConst.DEFAULT_LAT, 23 | lon: MapConst.DEFAULT_LNG, 24 | zoom: MapConst.DEFAULT_ZOOM 25 | }): Promise { 26 | return Promise.resolve(new google.maps.Map(mapEl)); 27 | } 28 | 29 | public get mapCenter(): google.maps.LatLng { 30 | return new google.maps.LatLng(MapConst.DEFAULT_LAT, MapConst.DEFAULT_LNG); 31 | } 32 | 33 | public set mapCenter(location: google.maps.LatLng) { 34 | } 35 | 36 | public get mapElement(): Element { 37 | return new Element(); 38 | } 39 | 40 | public createInfoWindow(content: string, position: google.maps.LatLng): void { 41 | } 42 | 43 | public closeInfoWindow(): void { 44 | } 45 | 46 | public createAutocomplete(addressEl: HTMLInputElement): Observable { 47 | return Observable.of(new google.maps.LatLng(MapConst.DEFAULT_LAT, MapConst.DEFAULT_LNG)); 48 | } 49 | 50 | public setPosition(): Promise { 51 | return Promise.resolve(); 52 | } 53 | 54 | public resizeMap(): void { 55 | } 56 | 57 | public loadMap(): Promise { 58 | return Promise.resolve(); 59 | } 60 | 61 | public loadNearbyPlaces(): Observable { 62 | return Observable.of([]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/providers/map/map.constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | export class MapConst { 8 | public static get DEFAULT_LAT(): number { 9 | return 42; 10 | } 11 | 12 | public static get DEFAULT_LNG(): number { 13 | return 12.8; 14 | } 15 | 16 | public static get DEFAULT_ZOOM(): number { 17 | return 5; 18 | } 19 | 20 | public static get MAX_ZOOM(): number { 21 | return 18; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/providers/map/map.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import { Injectable } from '@angular/core'; 8 | import { Observable } from 'rxjs/Observable'; 9 | import { Geolocation, Geoposition } from 'ionic-native'; 10 | import { MapConst } from './map.constants'; 11 | import { } from '@types/googlemaps'; 12 | 13 | interface IMapOptions { 14 | lat: number; 15 | lon: number; 16 | zoom: number; 17 | } 18 | 19 | @Injectable() 20 | export class MapService { 21 | 22 | private map: google.maps.Map = null; 23 | private infoWindow: google.maps.InfoWindow = null; 24 | 25 | constructor() { 26 | } 27 | 28 | public createMap(mapEl: Element, opts: IMapOptions = { 29 | lat: MapConst.DEFAULT_LAT, 30 | lon: MapConst.DEFAULT_LNG, 31 | zoom: MapConst.DEFAULT_ZOOM 32 | }): Promise { 33 | 34 | return this.loadMap().then(() => { 35 | const myLatLng = new google.maps.LatLng(opts.lat, opts.lon); 36 | const styleArray = [ 37 | { 38 | featureType: 'poi', 39 | elementType: 'labels', 40 | stylers: [ 41 | {visibility: 'off'} 42 | ] 43 | } 44 | ]; 45 | 46 | const mapOptions: google.maps.MapOptions = { 47 | zoom: opts.zoom, 48 | minZoom: opts.zoom, 49 | center: myLatLng, 50 | mapTypeId: google.maps.MapTypeId.ROADMAP, 51 | disableDefaultUI: true, 52 | scaleControl: false, 53 | styles: styleArray, 54 | zoomControl: false, 55 | zoomControlOptions: { 56 | position: google.maps.ControlPosition.BOTTOM_CENTER 57 | } 58 | }; 59 | 60 | this.map = new google.maps.Map(mapEl, mapOptions); 61 | return this.map; 62 | }); 63 | } 64 | 65 | /** 66 | * return the coordinates of the center of map 67 | * @returns {LatLng} 68 | */ 69 | public get mapCenter(): google.maps.LatLng { 70 | return this.map.getCenter(); 71 | } 72 | 73 | public set mapCenter(location: google.maps.LatLng) { 74 | this.map.setCenter(location); 75 | } 76 | 77 | /*** 78 | * return map html element 79 | * @returns {Element} 80 | */ 81 | public get mapElement(): Element { 82 | return this.map.getDiv(); 83 | } 84 | 85 | /*** 86 | * create an infoWindow and display it in the map 87 | * @param content - the content to display inside the infoWindow 88 | * @param position 89 | */ 90 | public createInfoWindow(content: string, position: google.maps.LatLng): void { 91 | this.closeInfoWindow(); 92 | const opt: google.maps.InfoWindowOptions = { 93 | content, 94 | position, 95 | pixelOffset: new google.maps.Size(0, -50), 96 | disableAutoPan: true 97 | }; 98 | this.infoWindow = new google.maps.InfoWindow(opt); 99 | setTimeout(() => this.infoWindow.open(this.map), 100); 100 | } 101 | 102 | /*** 103 | * close the current infoWindow 104 | */ 105 | public closeInfoWindow(): void { 106 | if (this.infoWindow) { 107 | this.infoWindow.close(); 108 | } 109 | } 110 | 111 | /*** 112 | * create Place Autocomplete 113 | * ref: https://developers.google.com/maps/documentation/javascript/examples/places-autocomplete 114 | * @param addressEl 115 | * @returns {Observable} 116 | */ 117 | public createAutocomplete(addressEl: HTMLInputElement): Observable { 118 | const autocomplete = new google.maps.places.Autocomplete(addressEl); 119 | autocomplete.bindTo('bounds', this.map); 120 | return new Observable((sub: any) => { 121 | google.maps.event.addListener(autocomplete, 'place_changed', () => { 122 | const place = autocomplete.getPlace(); 123 | if (!place.geometry) { 124 | sub.error({ 125 | message: 'Autocomplete returned place with no geometry' 126 | }); 127 | } else { 128 | sub.next(place.geometry.location); 129 | sub.complete(); 130 | } 131 | }); 132 | }); 133 | } 134 | 135 | /*** 136 | * set map position and the relative center and zoom 137 | * @returns {Promise} 138 | */ 139 | public setPosition(): Promise { 140 | return this.getCurrentPosition().then((coords: Coordinates) => { 141 | if (!coords) { 142 | console.warn('invalid coordinates: ', coords); 143 | return null; 144 | } 145 | const myLatLng = new google.maps.LatLng(coords.latitude, coords.longitude); 146 | this.map.setCenter(myLatLng); 147 | this.map.setZoom(MapConst.MAX_ZOOM); 148 | return this.mapCenter; 149 | }); 150 | } 151 | 152 | /*** 153 | * trigger map resize event 154 | */ 155 | public resizeMap(): void { 156 | if (this.map) { 157 | google.maps.event.trigger(this.map, 'resize'); 158 | } 159 | } 160 | 161 | /*** 162 | * google map place searches 163 | * @returns {Observable} 164 | */ 165 | public loadNearbyPlaces(): Observable { 166 | const position: google.maps.LatLng = this.mapCenter; 167 | 168 | const placesService = new google.maps.places.PlacesService(this.map); 169 | const request: google.maps.places.PlaceSearchRequest = { 170 | location: position, 171 | radius: 500 172 | }; 173 | 174 | return new Observable((sub: any) => { 175 | placesService.nearbySearch(request, (results, status) => { 176 | const _nearbyPlaces: Array = []; 177 | if (status === google.maps.places.PlacesServiceStatus.OK) { 178 | for (let i = 0; i < results.length; i++) { 179 | const place: any = results[i]; 180 | const distance: number = 181 | google.maps.geometry.spherical.computeDistanceBetween(position, place.geometry.location); 182 | place.distance = distance.toFixed(2); 183 | _nearbyPlaces.push(place); 184 | } 185 | sub.next(_nearbyPlaces); 186 | sub.complete(); 187 | } else { 188 | sub.error({ 189 | message: `Invalid response status from nearbySearch : ${status}` 190 | }); 191 | } 192 | }); 193 | }); 194 | } 195 | 196 | /*** 197 | * Load Google Map Api in async mode 198 | * @returns {Promise} 199 | */ 200 | private loadMap(): Promise { 201 | return new Promise((resolve: Function, reject: Function) => { 202 | if ((window).google && (window).google.maps) { 203 | resolve(); 204 | } else { 205 | this.loadGoogleMapApi().then(() => resolve()).catch(reason => { 206 | reject(reason); 207 | }); 208 | } 209 | }); 210 | } 211 | 212 | /*** 213 | * get the current location using Geolocation cordova plugin 214 | * @param maximumAge 215 | * @returns {Promise} 216 | */ 217 | private getCurrentPosition(maximumAge: number = 10000): Promise { 218 | const options = { 219 | timeout: 10000, 220 | enableHighAccuracy: true, 221 | maximumAge 222 | }; 223 | return Geolocation.getCurrentPosition(options).then((pos: Geoposition) => { 224 | return pos.coords; 225 | }); 226 | } 227 | 228 | /*** 229 | * Create a script element to insert into the page 230 | * @returns {Promise} 231 | * @private 232 | */ 233 | private loadGoogleMapApi(): Promise { 234 | const _loadScript = () => { 235 | const script = document.createElement('script'); 236 | // tslint:disable-next-line 237 | script.src = `https://maps.googleapis.com/maps/api/js?libraries=places,geometry&language=it&components=country:IT&callback=initMap`; 238 | script.type = 'text/javascript'; 239 | script.async = true; 240 | const s = document.getElementsByTagName('script')[0]; 241 | s.parentNode.insertBefore(script, s); 242 | }; 243 | 244 | return new Promise((resolve: Function) => { 245 | (window).initMap = () => { 246 | return resolve(); 247 | }; 248 | _loadScript(); 249 | }); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/providers/ride/ride-mock.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | export class RideServiceMock { 8 | 9 | public fakeResponse: any = null; 10 | 11 | public getRides(): Array { 12 | return []; 13 | } 14 | 15 | public addRide(): void { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/providers/ride/ride.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | export class RideModel { 8 | constructor(public _id: string, 9 | public departure: string, 10 | public destination: string, 11 | public rideDate: Date = new Date()) { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/providers/ride/ride.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Damien Dell'Amico 3 | * @copyright Copyright (c) 2016 4 | * @license GPL-3.0 5 | */ 6 | 7 | import * as uuid from 'node-uuid'; 8 | import { Injectable } from '@angular/core'; 9 | import { Events } from 'ionic-angular'; 10 | import { RideModel } from './ride.model'; 11 | 12 | @Injectable() 13 | export class RideService { 14 | _rides: Array = []; 15 | 16 | constructor(public events: Events) { 17 | } 18 | 19 | addRide(departure: string, destination: string): void { 20 | const model = new RideModel(uuid.v4(), departure, destination); 21 | this._rides.push(model); 22 | } 23 | 24 | getRides(): Array { 25 | return this._rides; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check out https://googlechrome.github.io/sw-toolbox/docs/master/index.html for 3 | * more info on how to use sw-toolbox to custom configure your service worker. 4 | */ 5 | 6 | 7 | 'use strict'; 8 | importScripts('./build/sw-toolbox.js'); 9 | 10 | self.toolbox.options.cache = { 11 | name: 'ionic-cache' 12 | }; 13 | 14 | // pre-cache our key assets 15 | self.toolbox.precache( 16 | [ 17 | './build/main.js', 18 | './build/main.css', 19 | './build/polyfills.js', 20 | 'index.html', 21 | 'manifest.json' 22 | ] 23 | ); 24 | 25 | // dynamically cache any other local assets 26 | self.toolbox.router.any('/*', self.toolbox.cacheFirst); 27 | 28 | // for any other requests go to the network, cache, 29 | // and then only use that cached resource if your user goes offline 30 | self.toolbox.router.default = self.toolbox.networkFirst; -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/v2/theming/ 3 | 4 | // Font path is used to include ionicons, 5 | // roboto, and noto sans fonts 6 | $font-path: "../assets/fonts"; 7 | 8 | @import "ionic.globals"; 9 | 10 | 11 | // Shared Variables 12 | // -------------------------------------------------- 13 | // To customize the look and feel of this app, you can override 14 | // the Sass variables found in Ionic's source scss files. 15 | // To view all the possible Ionic variables, see: 16 | // http://ionicframework.com/docs/v2/theming/overriding-ionic-variables/ 17 | 18 | $headings-font-weight: 300; 19 | 20 | 21 | // Named Color Variables 22 | // -------------------------------------------------- 23 | // Named colors makes it easy to reuse colors on various components. 24 | // It's highly recommended to change the default colors 25 | // to match your app's branding. Ionic uses a Sass map of 26 | // colors so you can add, rename and remove colors as needed. 27 | // The "primary" color is the only required color in the map. 28 | 29 | $colors: ( 30 | // primary: #478aff, 31 | primary: #ffc900, 32 | secondary: #32db64, 33 | danger: #f53d3d, 34 | light: #f4f4f4, 35 | dark: #222, 36 | favorite: #69BB7B, 37 | twitter: #1da1f4, 38 | google: #dc4a38, 39 | vimeo: #23b6ea, 40 | facebook: #3b5998 41 | ); 42 | 43 | 44 | // App iOS Variables 45 | // -------------------------------------------------- 46 | // iOS only Sass variables can go here 47 | 48 | 49 | 50 | // App Material Design Variables 51 | // -------------------------------------------------- 52 | // Material Design only Sass variables can go here 53 | 54 | // Use the primary color as the background for the toolbar-md-background 55 | $toolbar-md-background: color($colors, primary); 56 | 57 | // Change the color of the segment button text 58 | $toolbar-md-active-color: #fff; 59 | 60 | 61 | // App Windows Variables 62 | // -------------------------------------------------- 63 | // Windows only Sass variables can go here 64 | 65 | 66 | 67 | // App Theme 68 | // -------------------------------------------------- 69 | // Ionic apps can have different themes applied, which can 70 | // then be future customized. This import comes last 71 | // so that the above variables are used and Ionic's 72 | // default are overridden. 73 | 74 | @import "ionic.theme.default"; 75 | 76 | 77 | // Ionicons 78 | // -------------------------------------------------- 79 | // The premium icon font for Ionic. For more info, please see: 80 | // http://ionicframework.com/docs/v2/ionicons/ 81 | 82 | @import "ionic.ionicons"; 83 | 84 | 85 | // Fonts 86 | // -------------------------------------------------- 87 | // Roboto font is used by default for Material Design. Noto sans 88 | // is used by default for Windows. 89 | 90 | @import "roboto"; 91 | @import "noto-sans"; 92 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "declaration": false, 10 | "sourceMap": true, 11 | "noUnusedParameters": false, 12 | "lib": [ 13 | "dom", 14 | "es2015" 15 | ] 16 | }, 17 | "types": [ 18 | "jasmine" 19 | ], 20 | "include": [ 21 | "src/**/*.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules" 25 | ], 26 | "compileOnSave": false, 27 | "atom": { 28 | "rewriteTsconfig": false 29 | }, 30 | "awesomeTypescriptLoaderOptions": { 31 | "forkChecker": true, 32 | "useWebpackText": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "align": [true, "statements"], 7 | "ban": false, 8 | "class-name": true, 9 | "comment-format": [true, "check-space"], 10 | "curly": false, 11 | "eofline": true, 12 | "forin": true, 13 | "indent": [true, "spaces"], 14 | "interface-name": false, 15 | "jsdoc-format": true, 16 | "label-position": true, 17 | "max-line-length": [true, 120], 18 | "member-access": false, 19 | "member-ordering": [true, "public-before-private", "static-before-instance", "variables-before-functions"], 20 | "no-any": false, 21 | "no-arg": true, 22 | "no-bitwise": true, 23 | "no-conditional-assignment": true, 24 | "no-consecutive-blank-lines": true, 25 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 26 | "no-construct": true, 27 | "no-debugger": true, 28 | "no-duplicate-variable": true, 29 | "no-empty": false, 30 | "no-eval": true, 31 | "no-inferrable-types": false, 32 | "no-internal-module": true, 33 | "no-null-keyword": false, 34 | "no-require-imports": true, 35 | "no-shadowed-variable": true, 36 | "no-string-literal": false, 37 | "no-switch-case-fall-through": true, 38 | "no-trailing-whitespace": true, 39 | "no-unused-expression": true, 40 | "no-use-before-declare": true, 41 | "no-var-keyword": true, 42 | "no-var-requires": false, 43 | "object-literal-sort-keys": false, 44 | "one-line": [true, "check-open-brace", "check-catch", "check-else", "check-finally", "check-whitespace"], 45 | "quotemark": [true, "single", "avoid-escape"], 46 | "radix": true, 47 | "semicolon": [true, "always"], 48 | "switch-default": true, 49 | "trailing-comma": false, 50 | "triple-equals": [true, "allow-null-check"], 51 | "typedef": false, 52 | "typedef-whitespace": [true, 53 | {"call-signature": "nospace", "index-signature": "nospace", "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace"}, 54 | {"call-signature": "space", "index-signature": "space", "parameter": "space", "property-declaration": "space", "variable-declaration": "space"}], 55 | "variable-name": [true, "check-format", "allow-leading-underscore", "ban-keywords"], 56 | "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"], 57 | 58 | "directive-selector": [true, "attribute", "it", "camelCase"], 59 | "component-selector": [true, "element", "it", "kebab-case"], 60 | "use-input-property-decorator": true, 61 | "use-output-property-decorator": true, 62 | "use-host-property-decorator": true, 63 | "no-input-rename": true, 64 | "no-output-rename": true, 65 | "use-life-cycle-interface": true, 66 | "use-pipe-transform-interface": true, 67 | "directive-class-suffix": true, 68 | "import-destructuring-spacing": true, 69 | "templates-use-public": true, 70 | "no-access-missing-member": true, 71 | "invoke-injectable": true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "modules", 3 | "out": "doc", 4 | "theme": "default", 5 | "ignoreCompilerErrors": "true", 6 | "experimentalDecorators": "true", 7 | "emitDecoratorMetadata": "true", 8 | "target": "ES5", 9 | "moduleResolution": "node", 10 | "preserveConstEnums": "true", 11 | "stripInternal": "true", 12 | "suppressExcessPropertyErrors": "true", 13 | "suppressImplicitAnyIndexErrors": "true", 14 | "module": "commonjs" 15 | } 16 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | --------------------------------------------------------------------------------