├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── docs ├── architecture.png ├── heatmap-with-data.png ├── initial-heatmap.png ├── initial-view.png ├── mongo-command.png └── xcode.png ├── heatmap-backend ├── app.js ├── manifest.yml ├── models │ ├── beacon.js │ ├── booth.js │ ├── event.js │ ├── page.js │ └── trigger.js ├── package-lock.json ├── package.json ├── public │ ├── css │ │ └── style.css │ └── index.html ├── routes │ ├── beacons.js │ ├── booths.js │ ├── events.js │ ├── pages.js │ ├── renderSvg.js │ ├── svg.js │ └── trigger.js ├── test │ ├── beaconTest.js │ ├── boothTest.js │ ├── eventTest.js │ ├── pageTest.js │ └── svgTest.js └── views │ └── main.ejs ├── heatmap.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── heatmap ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── CoordinateConverter.swift ├── CustomPolygons.swift ├── FloorplanOverlay.swift ├── FloorplanOverlayRenderer.swift ├── Floorplans │ └── floorplan_overlay_floor0.pdf ├── HideBackgroundOverlay.swift ├── Info.plist ├── MKMapRectRotated.swift ├── Utilities.swift ├── ViewController.swift └── VisibleMapRegionDelegate.swift ├── manual.js ├── package-lock.json ├── package.json ├── random.js └── real-data.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | 69 | # Logs 70 | logs 71 | *.log 72 | npm-debug.log* 73 | yarn-debug.log* 74 | yarn-error.log* 75 | 76 | # Runtime data 77 | pids 78 | *.pid 79 | *.seed 80 | *.pid.lock 81 | 82 | # Directory for instrumented libs generated by jscoverage/JSCover 83 | lib-cov 84 | 85 | # Coverage directory used by tools like istanbul 86 | coverage 87 | 88 | # nyc test coverage 89 | .nyc_output 90 | 91 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 92 | .grunt 93 | 94 | # Bower dependency directory (https://bower.io/) 95 | bower_components 96 | 97 | # node-waf configuration 98 | .lock-wscript 99 | 100 | # Compiled binary addons (https://nodejs.org/api/addons.html) 101 | build/Release 102 | 103 | # Dependency directories 104 | node_modules/ 105 | jspm_packages/ 106 | 107 | # Typescript v1 declaration files 108 | typings/ 109 | 110 | # Optional npm cache directory 111 | .npm 112 | 113 | # Optional eslint cache 114 | .eslintcache 115 | 116 | # Optional REPL history 117 | .node_repl_history 118 | 119 | # Output of 'npm pack' 120 | *.tgz 121 | 122 | # Yarn Integrity file 123 | .yarn-integrity 124 | 125 | # dotenv environment variables file 126 | .env 127 | 128 | # next.js build output 129 | .next -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This is an open source project, and we appreciate your help! 4 | 5 | We use the GitHub issue tracker to discuss new features and non-trivial bugs. 6 | 7 | In addition to the issue tracker, [#journeys on 8 | Slack](https://dwopen.slack.com) is the best way to get into contact with the 9 | project's maintainers. 10 | 11 | To contribute code, documentation, or tests, please submit a pull request to 12 | the GitHub repository. Generally, we expect two maintainers to review your pull 13 | request before it is approved for merging. For more details, see the 14 | [MAINTAINERS](MAINTAINERS.md) page. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers Guide 2 | 3 | This guide is intended for maintainers - anybody with commit access to one or 4 | more Code Pattern repositories. 5 | 6 | ## Methodology 7 | 8 | This repository does not have a traditional release management cycle, but 9 | should instead be maintained as as a useful, working, and polished reference at 10 | all times. While all work can therefore be focused on the master branch, the 11 | quality of this branch should never be compromised. 12 | 13 | The remainder of this document details how to merge pull requests to the 14 | repositories. 15 | 16 | ## Merge approval 17 | 18 | The project maintainers use LGTM (Looks Good To Me) in comments on the pull 19 | request to indicate acceptance prior to merging. A change requires LGTMs from 20 | two project maintainers. If the code is written by a maintainer, the change 21 | only requires one additional LGTM. 22 | 23 | ## Reviewing Pull Requests 24 | 25 | We recommend reviewing pull requests directly within GitHub. This allows a 26 | public commentary on changes, providing transparency for all users. When 27 | providing feedback be civil, courteous, and kind. Disagreement is fine, so long 28 | as the discourse is carried out politely. If we see a record of uncivil or 29 | abusive comments, we will revoke your commit privileges and invite you to leave 30 | the project. 31 | 32 | During your review, consider the following points: 33 | 34 | ### Does the change have positive impact? 35 | 36 | Some proposed changes may not represent a positive impact to the project. Ask 37 | whether or not the change will make understanding the code easier, or if it 38 | could simply be a personal preference on the part of the author (see 39 | [bikeshedding](https://en.wiktionary.org/wiki/bikeshedding)). 40 | 41 | Pull requests that do not have a clear positive impact should be closed without 42 | merging. 43 | 44 | ### Do the changes make sense? 45 | 46 | If you do not understand what the changes are or what they accomplish, ask the 47 | author for clarification. Ask the author to add comments and/or clarify test 48 | case names to make the intentions clear. 49 | 50 | At times, such clarification will reveal that the author may not be using the 51 | code correctly, or is unaware of features that accommodate their needs. If you 52 | feel this is the case, work up a code sample that would address the pull 53 | request for them, and feel free to close the pull request once they confirm. 54 | 55 | ### Does the change introduce a new feature? 56 | 57 | For any given pull request, ask yourself "is this a new feature?" If so, does 58 | the pull request (or associated issue) contain narrative indicating the need 59 | for the feature? If not, ask them to provide that information. 60 | 61 | Are new unit tests in place that test all new behaviors introduced? If not, do 62 | not merge the feature until they are! Is documentation in place for the new 63 | feature? (See the documentation guidelines). If not do not merge the feature 64 | until it is! Is the feature necessary for general use cases? Try and keep the 65 | scope of any given component narrow. If a proposed feature does not fit that 66 | scope, recommend to the user that they maintain the feature on their own, and 67 | close the request. You may also recommend that they see if the feature gains 68 | traction among other users, and suggest they re-submit when they can show such 69 | support. 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Using MapKit to create a Dynamic Indoor Map 2 | In this Code Pattern, we will create an indoor map using Apple's MapKit framework and a Cloud Foundry app for the iOS app's backend. The iOS app will use the existing framework MapKit to display the map while the Cloud Foundry app will generate a PDF file of an indoor map which is consumed by the iOS app. With the use of a backend data that is separate from the iOS app, developers would not need to keep updating their iOS app if they want to modify their indoor map's floor plan. This app extends the sample code in [Footprint: Indoor positioning](https://developer.apple.com/library/content/samplecode/footprint/Listings/Swift_README_md.html) from Apple. 3 | 4 | When the reader has completed this Code Pattern, they will understand how to: 5 | 6 | * Deploy a Cloud Foundry app 7 | * Build an iOS map using MapKit 8 | * Integrate the iOS map to get data from the Cloud Foundry app 9 | * Make custom overlays with MapKit 10 | 11 | ![](docs/architecture.png) 12 | 13 | ## Flow 14 | 15 | 1. The user opens the iOS application and can toggle the heatmap on or off. The user will also generate random data of events in the zones of the indoor map. This should update the colors of the heatmap. 16 | 2. The iOS app sends requests to the backend through RESTful API. 17 | 3. The backend retrieves or updates the documents in the database. The backend would also generate the PDF based on the data received. 18 | 4. The database gets or updates the data based on the request from the server. 19 | 20 | ## Included components 21 | 22 | * [Cloud Foundry](http://cloudfoundry.org/): Build, deploy, and run applications on an open source cloud platform. 23 | * [Compose for MongoDB](https://console.bluemix.net/catalog/services/compose-for-mongodb): MongoDB with its powerful indexing and querying, aggregation and wide driver support, has become the go-to JSON data store for many startups and enterprises. 24 | 25 | ## Featured technologies 26 | 27 | * [Mobile](https://mobilefirstplatform.ibmcloud.com/): Systems of engagement are increasingly using mobile technology as the platform for delivery. 28 | * [Node.js](https://nodejs.org/): An open-source JavaScript run-time environment for executing server-side JavaScript code. 29 | 30 | # Steps 31 | 32 | 1. [Clone the repo](#1-clone-the-repo) 33 | 2. [Create Compose for MongoDB](#2-create-compose-for-mongodb) 34 | 3. [Deploy the Cloud Foundry app](#3-deploy-the-cloud-foundry-app) 35 | 4. [Configure the iOS application](#4-configure-the-ios-application) 36 | 5. [Run the iOS application](#5-run-the-ios-application) 37 | 6. [Turn heatmap on](#6-turn-heatmap-on) 38 | 39 | ### 1. Clone the repo 40 | 41 | ``` 42 | $ git clone https://github.com/IBM/ios-mapkit-indoor-map 43 | cd ios-mapkit-indoor-map 44 | ``` 45 | 46 | ### 2. Create Compose for MongoDB 47 | 48 | Create Compose for MongoDB and name it `heatmap-compose-mongodb`: 49 | > It is important to have the Compose for Mongodb instance be named `heatmap-compose-mongodb` since the Cloud Foundry app will be binding to that instance name. If you have named it to a different one, modify the `heatmap-backend/manifest.yml` file. 50 | 51 | * [**Compose for MongoDB**](https://console.bluemix.net/catalog/services/compose-for-mongodb) 52 | 53 | After the provisioning is complete, you'll need to insert some documents in the database. 54 | In the IBM Cloud Dashboard of the compose instance you just created, copy the Mongo command line connection string 55 | ![connection string](docs/mongo-command.png) 56 | 57 | Generate the documents using `real-data.js`. Add this filename at the end of the command line connection string 58 | > You may need to install mongo shell if you don't have it yet 59 | 60 | ``` 61 | $ mongo --ssl --sslAllowInvalidCertificates -u -p --authenticationDatabase admin real-data.js 62 | ``` 63 | 64 | ### 3. Deploy the Cloud Foundry app 65 | 66 | Go to `heatmap-backend` folder and push the app in Cloud Foundry in your account. After the app is pushed, you'll need to copy the app url which you will need after this step. The url will be formatted like `heatmap-backend-.mybluemix.net` 67 | 68 |
 69 | $ cd heatmap-backend
 70 | $ bx cf push
 71 | ...
 72 | ...
 73 | Showing health and status for app heatmap-backend in org Developer Advocacy / space dev as Anthony.Amanse@ibm.com...
 74 | OK
 75 | 
 76 | requested state: started
 77 | instances: 1/1
 78 | usage: 128M x 1 instances
 79 | urls: heatmap-backend-unvillainous-washout.mybluemix.net
 80 | last uploaded: Mon Mar 12 00:02:39 UTC 2018
 81 | stack: cflinuxfs2
 82 | buildpack: SDK for Node.js(TM) (ibm-node.js-6.12.3, buildpack-v3.18-20180206-1137)
 83 | 
 84 |      state     since                    cpu    memory      disk      details
 85 | #0   running   2018-03-11 05:04:56 PM   0.0%   0 of 128M   0 of 1G
 86 | 
87 | 88 | ### 4. Configure the iOS application 89 | 90 | Open `heatmap.xcodeproj` with Xcode. This loads all the source which you need to build the iOS app. 91 | 92 | In `ViewController.swift`, modify the line (line 13) to use your own backend URL (CF_APP_URL in the swift file) which you just deployed in Cloud Foundry. 93 |
 94 | ...
 95 | let CF_APP_URL:String = "https://heatmap-backend-unvillainous-washout.mybluemix.net"
 96 | ...
 97 | 
98 | 99 | ### 5. Run the iOS application 100 | 101 | Once you have modified and saved the `ViewController.swift` to use your own backend, run the app using a simulator or your own iPhone. 102 | You should see Apple Maps using the PDF from your backend as an indoor map. 103 | 104 | 105 | The sample debugging annotations should show you the origin (0,0) of your pdf and the anchors. The anchors are there so that the MapView will know where to place the PDF. It needs the longitude and latitude of the location of the place of your indoor map. These data are in `real-data.js` which you have inserted in MongoDB. 106 | 107 | ### 6. Turn heatmap on 108 | 109 | With custom overlays and the data from the backend, you could create a heatmap over your indoor map. You can turn on the heatmap using the toggle at the bottom right corner of the app. 110 | 111 | 112 | 113 | You'll see that the app will render some overlays above the indoor map. To update the data from the backend, use `random.js` to generate random data which should change colors of the overlay depending on the number of events from each zone. The overlays will update every 5 seconds when the toggle is on. 114 | > random.js is in the root directory of this repo 115 | 116 |
117 | Use the URL of your own Cloud Foundry app
118 | 
119 | $ export CF_APP_URL="https://heatmap-backend-unvillainous-washout.mybluemix.net"
120 | $ npm install
121 | $ node random.js
122 | 
123 | Sending 6 number of events to zone: 8
124 | Sending 5 number of events to zone: 5
125 | Sending 9 number of events to zone: 1
126 | Sending 10 number of events to zone: 3
127 | ...
128 | 
129 | 130 | In a real setting, the iOS app can be used to trigger these events. 131 | 132 | 133 | 134 | # Links 135 | 136 | * [MapKit](https://developer.apple.com/documentation/mapkit) 137 | * [Footprint: Indoor positioning](https://developer.apple.com/library/content/samplecode/footprint/Listings/Swift_README_md.html) 138 | 139 | # Learn more 140 | 141 | * **Mobile Code Patterns**: Enjoyed this Code Pattern? Check out our other [Mobile Code Patterns](https://developer.ibm.com/code/technologies/mobile/). 142 | * **Node.js Code Patterns**: Enjoyed this Code Pattern? Check out our other [Node.js Code Patterns](https://developer.ibm.com/code/technologies/node-js/). 143 | 144 | # License 145 | This code pattern is licensed under the Apache Software License, Version 2. Separate third party code objects invoked within this code pattern are licensed by their respective providers pursuant to their own separate licenses. Contributions are subject to the [Developer Certificate of Origin, Version 1.1 (DCO)](https://developercertificate.org/) and the [Apache Software License, Version 2](http://www.apache.org/licenses/LICENSE-2.0.txt). 146 | 147 | [Apache Software License (ASL) FAQ](http://www.apache.org/foundation/license-faq.html#WhatDoesItMEAN) 148 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/ios-mapkit-indoor-map/dba86f92c64de7348bc4d189b7c0c337734c82b6/docs/architecture.png -------------------------------------------------------------------------------- /docs/heatmap-with-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/ios-mapkit-indoor-map/dba86f92c64de7348bc4d189b7c0c337734c82b6/docs/heatmap-with-data.png -------------------------------------------------------------------------------- /docs/initial-heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/ios-mapkit-indoor-map/dba86f92c64de7348bc4d189b7c0c337734c82b6/docs/initial-heatmap.png -------------------------------------------------------------------------------- /docs/initial-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/ios-mapkit-indoor-map/dba86f92c64de7348bc4d189b7c0c337734c82b6/docs/initial-view.png -------------------------------------------------------------------------------- /docs/mongo-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/ios-mapkit-indoor-map/dba86f92c64de7348bc4d189b7c0c337734c82b6/docs/mongo-command.png -------------------------------------------------------------------------------- /docs/xcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/ios-mapkit-indoor-map/dba86f92c64de7348bc4d189b7c0c337734c82b6/docs/xcode.png -------------------------------------------------------------------------------- /heatmap-backend/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const mongoose = require("mongoose"); 4 | const assert = require("assert"); 5 | const fs = require("fs"); 6 | const cors = require("cors"); 7 | 8 | const beaconRoute = require("./routes/beacons"); 9 | const boothRoute = require("./routes/booths"); 10 | const eventRoute = require("./routes/events"); 11 | const svgRoute = require("./routes/svg"); 12 | const pageRoute = require("./routes/pages"); 13 | const renderRoute = require("./routes/renderSvg"); 14 | const triggerRoute = require("./routes/trigger"); 15 | 16 | const cfenv = require('cfenv'); 17 | const util = require('util') 18 | 19 | // load local VCAP configuration and service credentials 20 | var vcapLocal; 21 | try { 22 | vcapLocal = require('./vcap-local.json'); 23 | console.log("Loaded local VCAP", vcapLocal); 24 | } catch (e) { 25 | console.log(e) 26 | } 27 | 28 | const appEnvOpts = vcapLocal ? { vcap: vcapLocal} : {} 29 | 30 | const appEnv = cfenv.getAppEnv(appEnvOpts); 31 | 32 | // Within the application environment (appenv) there's a services object 33 | var services = appEnv.services; 34 | 35 | // The services object is a map named by service so we extract the one for MongoDB 36 | var mongodb_services = services["compose-for-mongodb"]; 37 | 38 | // This check ensures there is a services for MongoDB databases 39 | assert(!util.isUndefined(mongodb_services), "Must be bound to compose-for-mongodb services"); 40 | 41 | // We now take the first bound MongoDB service and extract it's credentials object 42 | var credentials = mongodb_services[0].credentials; 43 | 44 | // Within the credentials, an entry ca_certificate_base64 contains the SSL pinning key 45 | // We convert that from a string into a Buffer entry in an array which we use when 46 | // connecting. 47 | var ca = [new Buffer(credentials.ca_certificate_base64, 'base64')]; 48 | 49 | let mongoDbOptions = { 50 | mongos: { 51 | useMongoClient: true, 52 | ssl: true, 53 | sslValidate: true, 54 | sslCA: ca, 55 | }, 56 | }; 57 | 58 | mongoose.connection.on("error", function(err) { 59 | console.log("Mongoose default connection error: " + err); 60 | }); 61 | 62 | mongoose.connection.on("open", function(err) { 63 | console.log("CONNECTED..."); 64 | assert.equal(null, err); 65 | }); 66 | 67 | if (process.env.UNIT_TEST == "test") { 68 | mongoose.connect("mongodb://localhost/myapp"); 69 | } 70 | else { 71 | mongoose.connect(credentials.uri, mongoDbOptions); 72 | } 73 | 74 | app.use(require("body-parser").json()); 75 | app.use(cors()); 76 | 77 | app.set('view engine', 'ejs'); 78 | 79 | app.use(express.static(__dirname + "/public")); 80 | 81 | app.use("/main", renderRoute); 82 | app.use("/beacons", beaconRoute); 83 | app.use("/booths", boothRoute); 84 | app.use("/events", eventRoute); 85 | app.use("/svg", svgRoute.main); 86 | app.use("/pages", pageRoute); 87 | app.use("/triggers", triggerRoute); 88 | 89 | let port = process.env.PORT || 8080; 90 | app.listen(port, function() { 91 | console.log("To view your app, open this link in your browser: http://localhost:" + port); 92 | }); 93 | 94 | module.exports = app; -------------------------------------------------------------------------------- /heatmap-backend/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: heatmap-backend 4 | random-route: true 5 | memory: 128M 6 | services: 7 | - heatmap-compose-mongodb -------------------------------------------------------------------------------- /heatmap-backend/models/beacon.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | // eslint-disable-next-line 4 | let beaconSchema = mongoose.Schema({ 5 | beaconId: {type: String, unique: true}, 6 | key: String, 7 | value: String, 8 | zone: Number, 9 | beaconid: String, 10 | color: String, 11 | x: Number, 12 | y: Number, 13 | width: Number, 14 | height: Number, 15 | }); 16 | 17 | module.exports = mongoose.model("Beacon", beaconSchema); 18 | -------------------------------------------------------------------------------- /heatmap-backend/models/booth.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | // eslint-disable-next-line 4 | let boothSchema = mongoose.Schema({ 5 | boothId: {type: String, unique: true}, 6 | unit: String, 7 | description: String, 8 | measurementUnit: String, 9 | shape: {type: Object, required: true}, 10 | contact: String, 11 | }); 12 | 13 | module.exports = mongoose.model("Booth", boothSchema); 14 | -------------------------------------------------------------------------------- /heatmap-backend/models/event.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | let Booths = require("../models/booth"); 4 | let Beacons = require("../models/beacon"); 5 | 6 | // eslint-disable-next-line 7 | let eventSchema = mongoose.Schema({ 8 | eventId: {type: String, unique: true}, 9 | eventName: String, 10 | location: String, 11 | x: Number, 12 | y: Number, 13 | startDate: Date, 14 | endDate: Date, 15 | fromAnchorLatitude: Number, 16 | fromAnchorLongitude: Number, 17 | fromAnchorSVGPointX: Number, 18 | fromAnchorSVGPointY: Number, 19 | toAnchorLatitude: Number, 20 | toAnchorLongitude: Number, 21 | toAnchorSVGPointX: Number, 22 | toAnchorSVGPointY: Number, 23 | map: [Booths.schema], 24 | beacons: [Beacons.schema], 25 | }); 26 | 27 | module.exports = mongoose.model("Event", eventSchema); 28 | -------------------------------------------------------------------------------- /heatmap-backend/models/page.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | // eslint-disable-next-line 4 | let pageSchema = mongoose.Schema({ 5 | page: {type: Number, unique: true}, 6 | title: String, 7 | subtitle: String, 8 | subtext: String, 9 | description: String, 10 | image: String, 11 | imageEncoded: String, 12 | }); 13 | 14 | module.exports = mongoose.model("Page", pageSchema); 15 | -------------------------------------------------------------------------------- /heatmap-backend/models/trigger.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | // eslint-disable-next-line 4 | let triggerSchema = mongoose.Schema({ 5 | zone: Number, 6 | event: String, 7 | timestamp: String 8 | }); 9 | 10 | module.exports = mongoose.model("Trigger", triggerSchema); 11 | -------------------------------------------------------------------------------- /heatmap-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secret-map-dashboard", 3 | "main": "app.js", 4 | "description": "secret-map-dashboard", 5 | "version": "1.0.0", 6 | "private": false, 7 | "engines": { 8 | "node": "6.*" 9 | }, 10 | "scripts": { 11 | "start": "node app.js", 12 | "test": "mocha --timeout 600000", 13 | "lint": "eslint ." 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/IBM/secret-map-dashboard" 18 | }, 19 | "dependencies": { 20 | "assert": "^1.4.x", 21 | "body-parser": "^1.17.x", 22 | "cfenv": "^1.0.4", 23 | "cors": "^2.8.x", 24 | "ejs": "^2.5.7", 25 | "express": "^4.15.x", 26 | "fs": "~0.0.1", 27 | "mongoose": "^4.13.x", 28 | "pdfkit": "^0.8.3", 29 | "router": "^1.3.2", 30 | "svg-to-pdfkit": "^0.1.6", 31 | "util": "^0.10.3" 32 | }, 33 | "devDependencies": { 34 | "chai": "^3.5.0", 35 | "chai-http": "^2.0.1", 36 | "eslint": "^4.18.2", 37 | "mocha": "^2.4.5" 38 | }, 39 | "author": "Anthony Amanse", 40 | "license": "Apache-2.0" 41 | } 42 | -------------------------------------------------------------------------------- /heatmap-backend/public/css/style.css: -------------------------------------------------------------------------------- 1 | #header { 2 | display: flex; 3 | justify-content: center; 4 | height: 150px; 5 | background-color: #68fce6; 6 | font-family: 'Open Sans'; 7 | color: #000 ; 8 | align-items: center; 9 | flex-direction: column; 10 | } 11 | 12 | #main svg { 13 | padding-top: 100px; 14 | display: block; 15 | margin: auto; 16 | } 17 | 18 | #main svg text { 19 | font-size: 14pt; 20 | font-family: 'Open Sans'; 21 | } 22 | 23 | #main { 24 | padding-top: 50px; 25 | display: block; 26 | font-family: 'Open Sans'; 27 | text-align: center; 28 | margin: auto; 29 | } 30 | 31 | #main ul { 32 | font-size: 12pt; 33 | display: inline; 34 | align-items: center; 35 | } 36 | 37 | #main li { 38 | margin-top: 12pt; 39 | } 40 | 41 | #main h2 { 42 | margin-bottom: -25px; 43 | } -------------------------------------------------------------------------------- /heatmap-backend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Main Page 4 | 5 | 6 | 7 | 8 | 16 |
17 |

GET

18 |
    19 |
  • /beacons
  • 20 |
  • /booths
  • 21 |
  • /events
  • 22 |
  • /beacons/:beaconId
  • 23 |
  • /booths/:boothId
  • 24 |
  • /events/:eventId
  • 25 |
  • /svg/:eventId
  • 26 |
  • /svg/<:eventId>.pdf
  • 27 |
28 |

POST

29 |
    30 |
  • /beacons/add
  • 31 |
  • /booths/add
  • 32 |
  • /events/add
  • 33 |
34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /heatmap-backend/routes/beacons.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const Beacons = require("../models/beacon"); 5 | 6 | // endpoints for beacon 7 | router.post("/add", function(req, res) { 8 | // JSON in req.body 9 | // Insert input validation 10 | let addBeacon = new Beacons(req.body); 11 | addBeacon.save(function(err) { 12 | if (err) { 13 | res.send(err); 14 | } else { 15 | res.send("Saved beacon."); 16 | } 17 | }); 18 | }); 19 | 20 | router.get("/", function(req, res) { 21 | Beacons.find(function(err, beacons) { 22 | if (err) { 23 | res.send(err); 24 | } else { 25 | res.send(beacons); 26 | } 27 | }); 28 | }); 29 | 30 | router.get("/:beaconId", function(req, res) { 31 | Beacons.findOne(req.params, function(err, beacon) { 32 | if (err) { 33 | res.send(err); 34 | } else if (beacon) { 35 | res.send(beacon); 36 | } else { 37 | res.send("Beacon not found..."); 38 | } 39 | }); 40 | }); 41 | 42 | module.exports = router; 43 | -------------------------------------------------------------------------------- /heatmap-backend/routes/booths.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const Booths = require("../models/booth"); 5 | 6 | // endpoints for booth 7 | router.post("/add", function(req, res) { 8 | // JSON in req.body 9 | // Insert input validation 10 | let addBooth = new Booths(req.body); 11 | addBooth.save(function(err) { 12 | if (err) { 13 | res.send(err); 14 | } else { 15 | res.send("Saved booth."); 16 | } 17 | }); 18 | }); 19 | 20 | router.get("/", function(req, res) { 21 | Booths.find(function(err, booths) { 22 | if (err) { 23 | res.send(err); 24 | } else { 25 | res.send(booths); 26 | } 27 | }); 28 | }); 29 | 30 | router.get("/:boothId", function(req, res) { 31 | Booths.findOne(req.params, function(err, booth) { 32 | if (err) { 33 | res.send(err); 34 | } else if (booth) { 35 | res.send(booth); 36 | } else { 37 | res.send("Booth not found..."); 38 | } 39 | }); 40 | }); 41 | 42 | module.exports = router; 43 | -------------------------------------------------------------------------------- /heatmap-backend/routes/events.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const Beacons = require("../models/beacon"); 5 | const Booths = require("../models/booth"); 6 | const Events = require("../models/event"); 7 | 8 | // endpoints for event 9 | router.post("/add", function(req, res) { 10 | // JSON in req.body 11 | // Insert input validation 12 | let boothIds = req.body.map; 13 | let beaconIds = req.body.beacons; 14 | 15 | let queryBooth = Booths.find({"boothId": {$in: boothIds}}, 16 | function(err, booths) { 17 | if (err) { 18 | res.send(err); 19 | } else { 20 | req.body.map = booths; 21 | } 22 | }); 23 | 24 | let queryBeacon = Beacons.find({"beaconId": {$in: beaconIds}}, 25 | function(err, beacons) { 26 | if (err) { 27 | res.send(err); 28 | } else { 29 | req.body.beacons = beacons; 30 | } 31 | }); 32 | 33 | queryBooth.then(queryBeacon) 34 | .then(function() { 35 | let addEvent = new Events(req.body); 36 | addEvent.save(function(err) { 37 | if (err) { 38 | res.send(err); 39 | } else { 40 | res.send("Saved event."); 41 | } 42 | }); 43 | }); 44 | }); 45 | 46 | router.get("/", function(req, res) { 47 | Events.find(function(err, events) { 48 | if (err) { 49 | res.send(err); 50 | } else { 51 | res.send(events); 52 | } 53 | }); 54 | }); 55 | 56 | router.get("/:eventId", function(req, res) { 57 | Events.findOne(req.params, function(err, event) { 58 | if (err) { 59 | res.send(err); 60 | } else if (event) { 61 | res.send(event); 62 | } else { 63 | res.send("Event not found..."); 64 | } 65 | }); 66 | }); 67 | 68 | module.exports = router; 69 | -------------------------------------------------------------------------------- /heatmap-backend/routes/pages.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const Pages = require("../models/page"); 5 | 6 | // endpoints for pages 7 | router.post("/add", function(req, res) { 8 | // JSON in req.body 9 | // Insert input validation 10 | let addPage = new Pages(req.body); 11 | addPage.save(function(err) { 12 | if (err) { 13 | res.send(err); 14 | } else { 15 | res.send("Saved booklet page."); 16 | } 17 | }); 18 | }); 19 | 20 | router.get("/", function(req, res) { 21 | Pages.find({},{},{sort: { page: 1 }},function(err, pages) { 22 | if (err) { 23 | res.send(err); 24 | } else { 25 | res.send(pages); 26 | } 27 | }); 28 | }); 29 | 30 | router.get("/:page", function(req, res) { 31 | Pages.findOne({"page": parseInt(req.params.page)}, function(err, page) { 32 | if (err) { 33 | res.send(err); 34 | } else if (page) { 35 | if (req.params.page.split(".").pop() == "png") { 36 | res.contentType("image/png"); 37 | res.send(new Buffer(page.imageEncoded, "base64")); 38 | } 39 | else { 40 | res.send(page); 41 | } 42 | } else { 43 | res.send("Page not found..."); 44 | } 45 | }); 46 | }); 47 | 48 | module.exports = router; 49 | -------------------------------------------------------------------------------- /heatmap-backend/routes/renderSvg.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const svgRoute = require("./svg"); 5 | 6 | const Events = require("../models/event"); 7 | 8 | router.get("/:eventId", function(req, res) { 9 | Events.findOne({"eventId": req.params.eventId.split(".")[0]}, function(err, event) { 10 | if (err) { 11 | res.send(err); 12 | } else if (event) { 13 | 14 | // Get SVG Content 15 | let SVGContent = svgRoute.functions.svgContent(event.map,1); 16 | let svg = svgRoute.functions.svgTemplate(event.x,event.y,SVGContent,1); 17 | 18 | // Send SVG 19 | res.render('main', {svg: svg, name: event.eventName, list: []}); 20 | 21 | } else { 22 | res.send("Event not found..."); 23 | } 24 | }); 25 | }); 26 | 27 | router.get("/", function(req, res) { 28 | Events.find(function(err, events) { 29 | if (err) { 30 | res.send(err); 31 | } else { 32 | res.render('main', { list: events, name: "Stored Events", svg: ""}); 33 | } 34 | }); 35 | }); 36 | 37 | module.exports = router; 38 | -------------------------------------------------------------------------------- /heatmap-backend/routes/svg.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const PDFDocument = require("pdfkit"); 4 | const SVGtoPDF = require("svg-to-pdfkit"); 5 | const stream = require("stream"); 6 | 7 | const Events = require("../models/event"); 8 | 9 | 10 | router.get("/:eventId", function(req, res) { 11 | Events.findOne({"eventId": req.params.eventId.split(".")[0]}, function(err, event) { 12 | if (err) { 13 | res.send(err); 14 | } else if (event) { 15 | 16 | // Get SVG Content 17 | let SVGContent = svgContent(event.map,1); 18 | let svg = svgTemplate(event.x,event.y,SVGContent,1); 19 | 20 | // If .pdf is present, send a pdf version of the svg 21 | if (req.params.eventId.split(".").pop() == "pdf") { 22 | 23 | // scale up for 1:1 svg point to pdf point 24 | SVGContent = svgContent(event.map,(4/3)); 25 | svg = svgTemplate(event.x,event.y,SVGContent,(4/3)); 26 | 27 | // we already scaled up svg points so no need to scale pdf points 28 | const pdfPointRatio = 1; 29 | let pdfX = pdfPointRatio * event.x * 1; 30 | let pdfY = pdfPointRatio * event.y * 1; 31 | 32 | // Start making PDF 33 | let doc = new PDFDocument({ size: [pdfX,pdfY]}); 34 | let echoStream = new stream.Writable(); 35 | let pdfBuffer = new Buffer(""); 36 | 37 | // Write to Buffer 38 | echoStream._write = function (chunk, encoding, done) { 39 | pdfBuffer = Buffer.concat([pdfBuffer, chunk]); 40 | done(); 41 | }; 42 | 43 | // Use svg-to-pdfkit 44 | SVGtoPDF(doc, svg, 0, 0, { fontCallback: () => 'Helvetica' }); 45 | doc.pipe(echoStream); 46 | doc.end(); 47 | 48 | // Set content type to pdf 49 | res.contentType("application/pdf"); 50 | 51 | // When stream is done, send pdf 52 | echoStream.on("finish", function () { 53 | // Make Buffer readable stream 54 | let bufferStream = new stream.PassThrough(); 55 | bufferStream.end(pdfBuffer); 56 | bufferStream.pipe(res); 57 | }); 58 | } else { 59 | 60 | // Send SVG 61 | res.send(svg); 62 | } 63 | } else { 64 | res.send("Event not found..."); 65 | } 66 | }); 67 | }); 68 | 69 | /** 70 | * Forms an SVG 71 | * @param {String} width is the width of SVG. 72 | * @param {String} height is the height of SVG. 73 | * @param {String} content contains the SVG elements. 74 | * @param {String} scale is used to scale the values: [width, height] 75 | * @return {String} an SVG in xml format 76 | */ 77 | function svgTemplate(width, height, content, scale) { 78 | let border = ""; 79 | let svg = "" + border + 80 | content + ""; 81 | return svg; 82 | } 83 | 84 | function svgContent(arrayOfElements,scale) { 85 | let svg = ""; 86 | for (let i = 0; i < arrayOfElements.length; i++) { 87 | let booth = arrayOfElements[i]; 88 | if(booth.shape.type == "rectangle") { 89 | svg += 90 | rectangleTemplate(booth,scale); 91 | } 92 | if(booth.shape.type == "circle") { 93 | svg += 94 | circleTemplate(booth,scale); 95 | } 96 | if(booth.shape.type == "ellipse") { 97 | svg += 98 | ellipseTemplate(booth,scale); 99 | } 100 | if(booth.shape.type == "polygon") { 101 | svg += 102 | polygonTemplate(booth,scale); 103 | } 104 | } 105 | return svg; 106 | } 107 | 108 | /** 109 | * Forms an SVG element of rectangle 110 | * @param {String} x is the x location of the rectangle. 111 | * @param {String} y is the y location of the rectangle. 112 | * @param {String} width is the width of the rectangle. 113 | * @param {String} height is the height of the rectangle. 114 | * @param {String} scale is used to scale the values above. 115 | * @return {String} an SVG element of rectangle in xml format 116 | */ 117 | function rectangleTemplate(booth, scale) { 118 | let elem = booth.shape; 119 | elem.x *= scale; 120 | elem.y *= scale; 121 | elem.width *= scale; 122 | elem.height *= scale; 123 | const xCentroid = (elem.width/2)+elem.x; 124 | const yCentroid = (elem.height/2)+elem.y; 125 | let boothTextLines = booth.unit.split(' '); 126 | 127 | let svg = ""; 129 | let tspans = ""; 130 | boothTextLines.forEach(function(value, index) { 131 | let dy = '1.2em'; 132 | if (index == 0 && boothTextLines.length == 3) { 133 | dy = '-1.2em'; 134 | } else if (index == 0 && boothTextLines.length == 2) { 135 | dy = '-0.6em'; 136 | } else if (index == 0 && boothTextLines.length == 1) { 137 | dy = '0'; 138 | } 139 | tspans += "" + value + ""; 140 | }); 141 | 142 | svg += "" + 145 | tspans + ""; 146 | return svg; 147 | } 148 | 149 | /** 150 | * Forms an SVG element of circle 151 | * @param {String} cx is the x location of the center of the circle. 152 | * @param {String} cy is the y location of the center of the circle. 153 | * @param {String} radius is the radius of the circle. 154 | * @param {String} scale is used to scale the values above. 155 | * @return {String} an SVG element of circle in xml format 156 | */ 157 | function circleTemplate(booth, scale) { 158 | let elem = booth.shape; 159 | elem.cx *= scale; 160 | elem.cy *= scale; 161 | elem.radius *= scale; 162 | 163 | let svg = ""; 165 | svg += "" + 168 | booth.unit + ""; 169 | return svg; 170 | } 171 | 172 | /** 173 | * Forms an SVG element of ellipse 174 | * @param {String} cx is the x location of the center of the ellipse. 175 | * @param {String} cy is the y location of the center of the ellipse. 176 | * @param {String} rx is the x radius of the ellipse. 177 | * @param {String} ry is the y radius of the ellipse. 178 | * @param {String} scale is used to scale the values above. 179 | * @return {String} an SVG element of rectangle in xml format 180 | */ 181 | function ellipseTemplate(booth, scale) { 182 | let elem = booth.shape; 183 | elem.cx *= scale; 184 | elem.cy *= scale; 185 | elem.rx *= scale; 186 | elem.ry *= scale; 187 | 188 | let svg = ""; 190 | svg += "" + 193 | booth.unit + ""; 194 | return svg; 195 | } 196 | 197 | /** 198 | * Forms an SVG element of polygon 199 | * @param {String} points contains the points of the polygon. 200 | * @param {String} scale is used to scale the values above. 201 | * @return {String} an SVG element of rectangle in xml format 202 | */ 203 | function polygonTemplate(booth, scale) { 204 | let elem = booth.shape; 205 | let integers = elem.points.split(/[\s,]+/); 206 | let scaledInt = ""; 207 | let xPoints = []; 208 | let yPoints = []; 209 | 210 | for (let i in integers) { 211 | if (i % 2 == 0) { 212 | scaledInt += integers[i]*scale + ","; 213 | xPoints.push(integers[i]*scale); 214 | } 215 | else { 216 | scaledInt += integers[i]*scale + " "; 217 | yPoints.push(integers[i]*scale); 218 | } 219 | } 220 | 221 | const xCentroid = xPoints.reduce((a,b) => (a+b)) / xPoints.length; 222 | const yCentroid = yPoints.reduce((a,b) => (a+b)) / yPoints.length; 223 | let svg = ""; 224 | svg += "" + 227 | booth.unit + ""; 228 | return svg; 229 | } 230 | 231 | module.exports = { 232 | main: router, 233 | functions: { svgTemplate, svgContent, rectangleTemplate, circleTemplate, ellipseTemplate, polygonTemplate } 234 | }; 235 | -------------------------------------------------------------------------------- /heatmap-backend/routes/trigger.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const Triggers = require("../models/trigger"); 5 | 6 | // endpoints for beacon 7 | router.post("/add", function(req, res) { 8 | // JSON in req.body 9 | // Insert input validation 10 | let addTrigger = new Triggers(req.body); 11 | addTrigger.save(function(err) { 12 | if (err) { 13 | res.send(err); 14 | } else { 15 | res.send("Saved trigger."); 16 | } 17 | }); 18 | }); 19 | 20 | router.get("/", function(req, res) { 21 | Triggers.find(function(err, triggers) { 22 | if (err) { 23 | res.send(err); 24 | } else { 25 | res.send(triggers); 26 | } 27 | }); 28 | }); 29 | 30 | router.get("/delete", function(req, res) { 31 | Triggers.remove({}, function(err) { 32 | if (err) { 33 | res.send(err); 34 | } else { 35 | res.send("Removed all trigger events"); 36 | } 37 | }); 38 | }); 39 | 40 | router.get("/total", function(req, res) { 41 | Triggers.find(function(err, triggers) { 42 | if (err) { 43 | res.send(err); 44 | } else { 45 | let jsonData = { 46 | zone_one: 0, 47 | zone_two: 0, 48 | zone_three: 0, 49 | zone_four: 0, 50 | zone_five: 0, 51 | zone_six: 0, 52 | zone_seven: 0, 53 | zone_eight: 0, 54 | zone_nine: 0, 55 | zone_ten: 0, 56 | zone_eleven: 0, 57 | zone_twelve: 0, 58 | zone_thirteen: 0, 59 | zone_fourteen: 0, 60 | zone_fifteen: 0 61 | }; 62 | triggers.forEach(function(trigger) { 63 | if (trigger.zone == 1 && trigger.event == "enter") { 64 | jsonData.zone_one += 1; 65 | } 66 | if (trigger.zone == 2 && trigger.event == "enter") { 67 | jsonData.zone_two += 1; 68 | } 69 | if (trigger.zone == 3 && trigger.event == "enter") { 70 | jsonData.zone_three += 1; 71 | } 72 | if (trigger.zone == 4 && trigger.event == "enter") { 73 | jsonData.zone_four += 1; 74 | } 75 | if (trigger.zone == 5 && trigger.event == "enter") { 76 | jsonData.zone_five += 1; 77 | } 78 | if (trigger.zone == 6 && trigger.event == "enter") { 79 | jsonData.zone_six += 1; 80 | } 81 | if (trigger.zone == 7 && trigger.event == "enter") { 82 | jsonData.zone_seven += 1; 83 | } 84 | if (trigger.zone == 8 && trigger.event == "enter") { 85 | jsonData.zone_eight += 1; 86 | } 87 | if (trigger.zone == 9 && trigger.event == "enter") { 88 | jsonData.zone_nine += 1; 89 | } 90 | if (trigger.zone == 10 && trigger.event == "enter") { 91 | jsonData.zone_ten += 1; 92 | } 93 | if (trigger.zone == 11 && trigger.event == "enter") { 94 | jsonData.zone_eleven += 1; 95 | } 96 | if (trigger.zone == 12 && trigger.event == "enter") { 97 | jsonData.zone_twelve += 1; 98 | } 99 | if (trigger.zone == 13 && trigger.event == "enter") { 100 | jsonData.zone_thirteen += 1; 101 | } 102 | if (trigger.zone == 14 && trigger.event == "enter") { 103 | jsonData.zone_fourteen += 1; 104 | } 105 | if (trigger.zone == 15 && trigger.event == "enter") { 106 | jsonData.zone_fifteen += 1; 107 | } 108 | }); 109 | res.send(jsonData); 110 | } 111 | }); 112 | }); 113 | 114 | module.exports = router; 115 | -------------------------------------------------------------------------------- /heatmap-backend/test/beaconTest.js: -------------------------------------------------------------------------------- 1 | process.env.UNIT_TEST = "test"; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | const server = require('../app'); 6 | const should = chai.should(); 7 | 8 | const Beacon = require('../models/beacon'); 9 | 10 | chai.use(chaiHttp); 11 | //Our parent block 12 | describe('Beacons', () => { 13 | beforeEach((done) => { //Before each test we empty the database 14 | Beacon.remove({}, (err) => { 15 | done(); 16 | }); 17 | }); 18 | /* 19 | * Test the /GET route 20 | */ 21 | describe('GET /beacons', () => { 22 | it('it should GET all the beacons', (done) => { 23 | chai.request(server) 24 | .get('/beacons') 25 | .end((err, res) => { 26 | res.should.have.status(200); 27 | res.body.should.be.a('array'); 28 | res.body.length.should.be.eql(0); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | /* 34 | * Test the /POST route 35 | */ 36 | describe('POST /beacons/add', () => { 37 | it('it should POST a beacon', (done) => { 38 | let beacon = { 39 | beaconId: "B01", 40 | x: 1, 41 | y: 1, 42 | minCount: 1, 43 | maxCount: 100 44 | }; 45 | chai.request(server) 46 | .post('/beacons/add') 47 | .send(beacon) 48 | .end((err, res) => { 49 | res.should.have.status(200); 50 | res.text.should.be.a('String'); 51 | res.text.should.be.eql('Saved beacon.'); 52 | done(); 53 | }); 54 | }); 55 | it('it should not POST a beacon when x coordinate is not a number', (done) => { 56 | let beacon = { 57 | beaconId: "B01", 58 | x: "1a", 59 | y: 1, 60 | minCount: 1, 61 | maxCount: 100 62 | }; 63 | chai.request(server) 64 | .post('/beacons/add') 65 | .send(beacon) 66 | .end((err, res) => { 67 | res.should.have.status(200); 68 | res.body.should.be.a('object'); 69 | res.body.should.have.a.property('errors'); 70 | res.body.errors.should.have.property('x'); 71 | res.body.errors.x.should.have.property('message').include('Cast to Number failed'); 72 | res.body.errors.x.should.have.property('kind').eql('Number'); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | 78 | /* 79 | * Test the /GET route 80 | */ 81 | describe('GET /beacons/:beaconId', () => { 82 | it('it should GET the beacon with the given beacon id ', (done) => { 83 | let addBeacon = new Beacon({ 84 | beaconId: "B01", 85 | x: 1, 86 | y: 1, 87 | minCount: 1, 88 | maxCount: 100 89 | }); 90 | addBeacon.save((err) => { 91 | chai.request(server) 92 | .get('/beacons/B01') 93 | .end((err, res) => { 94 | res.should.have.status(200); 95 | res.body.should.be.a('object'); 96 | res.body.should.have.property('beaconId'); 97 | res.body.should.have.property('beaconId').eql('B01'); 98 | res.body.should.have.property('x'); 99 | res.body.should.have.property('y'); 100 | // res.body.should.have.property('minCount'); 101 | // res.body.should.have.property('maxCount'); 102 | done(); 103 | }); 104 | }); 105 | }); 106 | }); 107 | }); -------------------------------------------------------------------------------- /heatmap-backend/test/boothTest.js: -------------------------------------------------------------------------------- 1 | process.env.UNIT_TEST = "test"; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | const server = require('../app'); 6 | const should = chai.should(); 7 | 8 | const Booth = require('../models/booth'); 9 | 10 | chai.use(chaiHttp); 11 | //Our parent block 12 | describe('Booths', () => { 13 | beforeEach((done) => { //Before each test we empty the database 14 | Booth.remove({}, (err) => { 15 | done(); 16 | }); 17 | }); 18 | /* 19 | * Test the /GET route 20 | */ 21 | describe('GET /booths', () => { 22 | it('it should GET all the booths', (done) => { 23 | chai.request(server) 24 | .get('/booths') 25 | .end((err, res) => { 26 | res.should.have.status(200); 27 | res.body.should.be.a('array'); 28 | res.body.length.should.be.eql(0); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | /* 34 | * Test the /POST route 35 | */ 36 | describe('POST /booths/add', () => { 37 | it('it should POST a booth', (done) => { 38 | let booth = { 39 | boothId: "A01", 40 | unit: "Node", 41 | description: "Node booth description here", 42 | measurementUnit: "metre", 43 | shape: {type: "rectangle", width : 3, height : 3, x : 0, y : 0}, 44 | contact: "John Doe" 45 | }; 46 | chai.request(server) 47 | .post('/booths/add') 48 | .send(booth) 49 | .end((err, res) => { 50 | res.should.have.status(200); 51 | res.text.should.be.a('String'); 52 | res.text.should.be.eql('Saved booth.'); 53 | done(); 54 | }); 55 | }); 56 | it('it should not POST a booth when shape is not defined', (done) => { 57 | let booth = { 58 | boothId: "A01", 59 | unit: "Node", 60 | description: "Node booth description here", 61 | measurementUnit: "metre", 62 | contact: "John Doe" 63 | }; 64 | chai.request(server) 65 | .post('/booths/add') 66 | .send(booth) 67 | .end((err, res) => { 68 | res.should.have.status(200); 69 | res.body.should.be.a('object'); 70 | res.body.should.have.a.property('errors'); 71 | res.body.errors.should.have.a.property('shape'); 72 | res.body.errors.shape.should.have.property('message').eql('Path `shape` is required.'); 73 | res.body.errors.shape.should.have.property('kind').eql('required'); 74 | done(); 75 | }); 76 | }); 77 | }); 78 | 79 | /* 80 | * Test the /GET route 81 | */ 82 | describe('GET /booths/:beaconId', () => { 83 | it('it should GET the booth with the given booth id ', (done) => { 84 | let addBooth = new Booth({ 85 | boothId: "A01", 86 | unit: "Node", 87 | description: "Node booth description here", 88 | measurementUnit: "metre", 89 | shape: {type: "rectangle", width : 3, height : 3, x : 0, y : 0}, 90 | contact: "John Doe" 91 | }); 92 | addBooth.save((err) => { 93 | chai.request(server) 94 | .get('/booths/A01') 95 | .end((err, res) => { 96 | res.should.have.status(200); 97 | res.body.should.be.a('object'); 98 | res.body.should.have.property('boothId'); 99 | res.body.should.have.property('boothId').eql('A01'); 100 | res.body.should.have.property('unit'); 101 | res.body.should.have.property('description'); 102 | res.body.should.have.property('measurementUnit'); 103 | res.body.should.have.property('contact'); 104 | res.body.should.have.property('shape'); 105 | res.body.shape.should.be.a('object'); 106 | res.body.shape.should.have.property('type'); 107 | res.body.shape.should.have.property('width'); 108 | res.body.shape.should.have.property('height'); 109 | res.body.shape.should.have.property('x'); 110 | res.body.shape.should.have.property('y'); 111 | done(); 112 | }); 113 | }); 114 | }); 115 | }); 116 | }); -------------------------------------------------------------------------------- /heatmap-backend/test/eventTest.js: -------------------------------------------------------------------------------- 1 | process.env.UNIT_TEST = "test"; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | const server = require('../app'); 6 | const should = chai.should(); 7 | 8 | const Beacon = require('../models/beacon'); 9 | const Booth = require('../models/booth'); 10 | const Event = require('../models/event'); 11 | 12 | chai.use(chaiHttp); 13 | //Our parent block 14 | describe('Events', () => { 15 | beforeEach((done) => { //Before each test we empty the database 16 | Beacon.remove({}, (err) => { 17 | Booth.remove({}, (err) => { 18 | Event.remove({}, (err) => { 19 | done(); 20 | }); 21 | }); 22 | }); 23 | }); 24 | /* 25 | * Test the /GET route 26 | */ 27 | describe('GET /events', () => { 28 | it('it should GET all the events', (done) => { 29 | chai.request(server) 30 | .get('/events') 31 | .end((err, res) => { 32 | res.should.have.status(200); 33 | res.body.should.be.a('array'); 34 | res.body.length.should.be.eql(0); 35 | done(); 36 | }); 37 | }); 38 | }); 39 | /* 40 | * Test the /POST route 41 | */ 42 | describe('POST /events/add', () => { 43 | it('it should POST an event', (done) => { 44 | let event = { 45 | eventId : "index", 46 | eventName : "Index", 47 | location : "San Francisco", 48 | startDate : "2018-02-20T00:00:00Z", 49 | endDate : "2018-02-24T00:00:00Z", 50 | beacons : [], 51 | map : [] 52 | }; 53 | chai.request(server) 54 | .post('/events/add') 55 | .send(event) 56 | .end((err, res) => { 57 | res.should.have.status(200); 58 | res.text.should.be.a('String'); 59 | res.text.should.be.eql('Saved event.'); 60 | done(); 61 | }); 62 | }); 63 | it('it should not POST an event when startDate is not a date', (done) => { 64 | let event = { 65 | eventId : "index", 66 | eventName : "Index", 67 | location : "San Francisco", 68 | startDate : "Tomorrow", 69 | endDate : "2018-02-24T00:00:00Z", 70 | beacons : [], 71 | map : [] 72 | }; 73 | chai.request(server) 74 | .post('/events/add') 75 | .send(event) 76 | .end((err, res) => { 77 | res.should.have.status(200); 78 | res.body.should.be.a('object'); 79 | res.body.should.have.a.property('errors'); 80 | res.body.errors.should.have.a.property('startDate'); 81 | res.body.errors.startDate.should.have.property('message').include('Cast to Date failed'); 82 | res.body.errors.startDate.should.have.property('kind').eql('Date'); 83 | done(); 84 | }); 85 | }); 86 | }); 87 | 88 | /* 89 | * Test the /GET route 90 | */ 91 | describe('GET /events/:eventId', () => { 92 | it('it should GET the event with the given event id ', (done) => { 93 | let addEvent = new Event({ 94 | eventId : "index", 95 | eventName : "Index", 96 | location : "San Francisco", 97 | startDate : "2018-02-20T00:00:00Z", 98 | endDate : "2018-02-24T00:00:00Z", 99 | beacons : [], 100 | map : [] 101 | }); 102 | addEvent.save((err) => { 103 | chai.request(server) 104 | .get('/events/index') 105 | .end((err, res) => { 106 | res.should.have.status(200); 107 | res.body.should.be.a('object'); 108 | res.body.should.have.property('eventId'); 109 | res.body.should.have.property('eventName'); 110 | res.body.should.have.property('location'); 111 | res.body.should.have.property('startDate'); 112 | res.body.should.have.property('endDate'); 113 | res.body.should.have.property('beacons'); 114 | res.body.should.have.property('map'); 115 | res.body.beacons.should.be.a('array'); 116 | res.body.beacons.length.should.be.eql(0); 117 | res.body.map.should.be.a('array'); 118 | res.body.map.length.should.be.eql(0); 119 | done(); 120 | }); 121 | }); 122 | }); 123 | }); 124 | }); -------------------------------------------------------------------------------- /heatmap-backend/test/pageTest.js: -------------------------------------------------------------------------------- 1 | process.env.UNIT_TEST = "test"; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | const server = require('../app'); 6 | const should = chai.should(); 7 | 8 | const Page = require('../models/page'); 9 | 10 | chai.use(chaiHttp); 11 | 12 | describe('Pages', () => { 13 | beforeEach((done) => { 14 | //Before each test we empty database 15 | Page.remove({}, (err) => { 16 | done(); 17 | }); 18 | }); 19 | 20 | describe('GET /pages', () => { 21 | it('it should GET all the Pages', (done) => { 22 | chai.request(server) 23 | .get('/pages') 24 | .end((err, res) => { 25 | res.should.have.status(200); 26 | res.body.should.be.an('array'); 27 | res.body.length.should.be.eql(0); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | 33 | describe('POST /pages/add', () => { 34 | it('it should POST a booklet page', (done) => { 35 | let _page = { 36 | page: 1, 37 | title: "think", 38 | subtitle: "2018", 39 | image: "boy pirate", 40 | subtext: "secret map", 41 | description: "an IBM Code experiment", 42 | imageEncoded: "" 43 | }; 44 | chai.request(server) 45 | .post('/pages/add') 46 | .send(_page) 47 | .end((err, res) => { 48 | res.should.have.status(200); 49 | res.text.should.be.a('String'); 50 | res.text.should.be.eql('Saved booklet page.'); 51 | done(); 52 | }); 53 | }); 54 | it('it should not POST a booklet page when page is not a number', (done) => { 55 | let _page = { 56 | page: "1A", 57 | title: "think", 58 | subtitle: "2018", 59 | image: "boy pirate", 60 | subtext: "secret map", 61 | description: "an IBM Code experiment", 62 | imageEncoded: "" 63 | }; 64 | chai.request(server) 65 | .post('/pages/add') 66 | .send(_page) 67 | .end((err, res) => { 68 | res.should.have.status(200); 69 | res.body.should.be.an('object'); 70 | res.body.should.have.a.property('errors'); 71 | res.body.errors.should.have.property('page'); 72 | res.body.errors.page.should.have.property('message').include('Cast to Number failed'); 73 | res.body.errors.page.should.have.property('kind').eql('Number'); 74 | done(); 75 | }); 76 | }); 77 | }); 78 | }); -------------------------------------------------------------------------------- /heatmap-backend/test/svgTest.js: -------------------------------------------------------------------------------- 1 | process.env.UNIT_TEST = "test"; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | const server = require('../app'); 6 | const should = chai.should(); 7 | 8 | const Beacon = require('../models/beacon'); 9 | const Booth = require('../models/booth'); 10 | const Event = require('../models/event'); 11 | 12 | chai.use(chaiHttp); 13 | 14 | describe('SVGs', () => { 15 | beforeEach((done) => { 16 | //Before each test we empty database AND add beacon, booth, and event. 17 | let addBeacon = new Beacon({ 18 | beaconId: "B01", 19 | x: 1, 20 | y: 1, 21 | minCount: 1, 22 | maxCount: 100 23 | }); 24 | let addBooth = new Booth({ 25 | boothId: "A01", 26 | unit: "Node", 27 | description: "Node booth description here", 28 | measurementUnit: "metre", 29 | shape: {type: "rectangle", width : 3, height : 3, x : 0, y : 0}, 30 | contact: "John Doe" 31 | }); 32 | let event = { 33 | eventId : "index", 34 | eventName : "Index", 35 | x : 5, 36 | y : 5, 37 | location : "San Francisco", 38 | startDate : "2018-02-20T00:00:00Z", 39 | endDate : "2018-02-24T00:00:00Z", 40 | beacons : [], 41 | map : [] 42 | }; 43 | Beacon.remove({}, (err) => { 44 | Booth.remove({}, (err) => { 45 | Event.remove({}, (err) => { 46 | addBeacon.save((err) => { 47 | addBooth.save((err) => { 48 | Beacon.find({}, (err,beacons) => { 49 | event.beacons = beacons; 50 | Booth.find({}, (err,booths) => { 51 | event.map = booths; 52 | let addEvent = new Event(event); 53 | addEvent.save((err) => { 54 | done(); 55 | }); 56 | }); 57 | }); 58 | }); 59 | }); 60 | }); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('GET /svg/:eventId', () => { 66 | it('it should GET an svg', (done) => { 67 | chai.request(server) 68 | .get('/svg/index') 69 | .end((err, res) => { 70 | res.should.have.status(200); 71 | res.text.should.be.a('String'); 72 | res.text.should.be.eql("Node"); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('GET /svg/<:eventId>.pdf', () => { 79 | it('it should GET a pdf version of the svg', (done) => { 80 | chai.request(server) 81 | .get('/svg/index.pdf') 82 | .end((err, res) => { 83 | res.should.have.status(200); 84 | res.should.have.property('type'); 85 | res.type.should.be.eql('application/pdf'); 86 | done(); 87 | }); 88 | }); 89 | }); 90 | }); -------------------------------------------------------------------------------- /heatmap-backend/views/main.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Map 4 | 5 | 6 | 7 | 8 | 17 |
18 | <% if (svg) { %> 19 | <%- svg %> 20 | <% } else if (list) %> 21 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /heatmap.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 920EAB2F2053AEA10054D05D /* CustomPolygons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920EAB2E2053AEA10054D05D /* CustomPolygons.swift */; }; 11 | 92A9AF612051E56F00163C30 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92A9AF602051E56F00163C30 /* AppDelegate.swift */; }; 12 | 92A9AF632051E56F00163C30 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92A9AF622051E56F00163C30 /* ViewController.swift */; }; 13 | 92A9AF662051E56F00163C30 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 92A9AF642051E56F00163C30 /* Main.storyboard */; }; 14 | 92A9AF682051E56F00163C30 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 92A9AF672051E56F00163C30 /* Assets.xcassets */; }; 15 | 92A9AF6B2051E56F00163C30 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 92A9AF692051E56F00163C30 /* LaunchScreen.storyboard */; }; 16 | 92A9AF742051EC7300163C30 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 92A9AF722051EC7300163C30 /* README.md */; }; 17 | 92A9AF752051EC7300163C30 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 92A9AF732051EC7300163C30 /* LICENSE */; }; 18 | 92A9AF7D2051F3A100163C30 /* HideBackgroundOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92A9AF762051F3A000163C30 /* HideBackgroundOverlay.swift */; }; 19 | 92A9AF7E2051F3A100163C30 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92A9AF772051F3A100163C30 /* Utilities.swift */; }; 20 | 92A9AF7F2051F3A100163C30 /* FloorplanOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92A9AF782051F3A100163C30 /* FloorplanOverlay.swift */; }; 21 | 92A9AF802051F3A100163C30 /* VisibleMapRegionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92A9AF792051F3A100163C30 /* VisibleMapRegionDelegate.swift */; }; 22 | 92A9AF812051F3A100163C30 /* FloorplanOverlayRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92A9AF7A2051F3A100163C30 /* FloorplanOverlayRenderer.swift */; }; 23 | 92A9AF822051F3A100163C30 /* CoordinateConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92A9AF7B2051F3A100163C30 /* CoordinateConverter.swift */; }; 24 | 92A9AF832051F3A100163C30 /* MKMapRectRotated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92A9AF7C2051F3A100163C30 /* MKMapRectRotated.swift */; }; 25 | 92A9AF852051F3DB00163C30 /* Floorplans in Resources */ = {isa = PBXBuildFile; fileRef = 92A9AF842051F3DB00163C30 /* Floorplans */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 920EAB2E2053AEA10054D05D /* CustomPolygons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPolygons.swift; sourceTree = ""; }; 30 | 92A9AF5D2051E56F00163C30 /* heatmap.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = heatmap.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | 92A9AF602051E56F00163C30 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 32 | 92A9AF622051E56F00163C30 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 33 | 92A9AF652051E56F00163C30 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 34 | 92A9AF672051E56F00163C30 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 35 | 92A9AF6A2051E56F00163C30 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 36 | 92A9AF6C2051E56F00163C30 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 37 | 92A9AF722051EC7300163C30 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 38 | 92A9AF732051EC7300163C30 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 39 | 92A9AF762051F3A000163C30 /* HideBackgroundOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HideBackgroundOverlay.swift; sourceTree = ""; }; 40 | 92A9AF772051F3A100163C30 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; 41 | 92A9AF782051F3A100163C30 /* FloorplanOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloorplanOverlay.swift; sourceTree = ""; }; 42 | 92A9AF792051F3A100163C30 /* VisibleMapRegionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisibleMapRegionDelegate.swift; sourceTree = ""; }; 43 | 92A9AF7A2051F3A100163C30 /* FloorplanOverlayRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloorplanOverlayRenderer.swift; sourceTree = ""; }; 44 | 92A9AF7B2051F3A100163C30 /* CoordinateConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoordinateConverter.swift; sourceTree = ""; }; 45 | 92A9AF7C2051F3A100163C30 /* MKMapRectRotated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MKMapRectRotated.swift; sourceTree = ""; }; 46 | 92A9AF842051F3DB00163C30 /* Floorplans */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Floorplans; path = heatmap/Floorplans; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | 92A9AF5A2051E56F00163C30 /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXFrameworksBuildPhase section */ 58 | 59 | /* Begin PBXGroup section */ 60 | 92A9AF542051E56F00163C30 = { 61 | isa = PBXGroup; 62 | children = ( 63 | 92A9AF722051EC7300163C30 /* README.md */, 64 | 92A9AF842051F3DB00163C30 /* Floorplans */, 65 | 92A9AF5F2051E56F00163C30 /* heatmap */, 66 | 92A9AF5E2051E56F00163C30 /* Products */, 67 | 92A9AF732051EC7300163C30 /* LICENSE */, 68 | ); 69 | sourceTree = ""; 70 | }; 71 | 92A9AF5E2051E56F00163C30 /* Products */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 92A9AF5D2051E56F00163C30 /* heatmap.app */, 75 | ); 76 | name = Products; 77 | sourceTree = ""; 78 | }; 79 | 92A9AF5F2051E56F00163C30 /* heatmap */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 92A9AF7B2051F3A100163C30 /* CoordinateConverter.swift */, 83 | 92A9AF782051F3A100163C30 /* FloorplanOverlay.swift */, 84 | 92A9AF7A2051F3A100163C30 /* FloorplanOverlayRenderer.swift */, 85 | 92A9AF762051F3A000163C30 /* HideBackgroundOverlay.swift */, 86 | 92A9AF7C2051F3A100163C30 /* MKMapRectRotated.swift */, 87 | 92A9AF772051F3A100163C30 /* Utilities.swift */, 88 | 92A9AF792051F3A100163C30 /* VisibleMapRegionDelegate.swift */, 89 | 92A9AF602051E56F00163C30 /* AppDelegate.swift */, 90 | 92A9AF622051E56F00163C30 /* ViewController.swift */, 91 | 920EAB2E2053AEA10054D05D /* CustomPolygons.swift */, 92 | 92A9AF642051E56F00163C30 /* Main.storyboard */, 93 | 92A9AF672051E56F00163C30 /* Assets.xcassets */, 94 | 92A9AF692051E56F00163C30 /* LaunchScreen.storyboard */, 95 | 92A9AF6C2051E56F00163C30 /* Info.plist */, 96 | ); 97 | path = heatmap; 98 | sourceTree = ""; 99 | }; 100 | /* End PBXGroup section */ 101 | 102 | /* Begin PBXNativeTarget section */ 103 | 92A9AF5C2051E56F00163C30 /* heatmap */ = { 104 | isa = PBXNativeTarget; 105 | buildConfigurationList = 92A9AF6F2051E56F00163C30 /* Build configuration list for PBXNativeTarget "heatmap" */; 106 | buildPhases = ( 107 | 92A9AF592051E56F00163C30 /* Sources */, 108 | 92A9AF5A2051E56F00163C30 /* Frameworks */, 109 | 92A9AF5B2051E56F00163C30 /* Resources */, 110 | ); 111 | buildRules = ( 112 | ); 113 | dependencies = ( 114 | ); 115 | name = heatmap; 116 | productName = heatmap; 117 | productReference = 92A9AF5D2051E56F00163C30 /* heatmap.app */; 118 | productType = "com.apple.product-type.application"; 119 | }; 120 | /* End PBXNativeTarget section */ 121 | 122 | /* Begin PBXProject section */ 123 | 92A9AF552051E56F00163C30 /* Project object */ = { 124 | isa = PBXProject; 125 | attributes = { 126 | LastSwiftUpdateCheck = 0920; 127 | LastUpgradeCheck = 0920; 128 | ORGANIZATIONNAME = "Joe Anthony Peter Amanse"; 129 | TargetAttributes = { 130 | 92A9AF5C2051E56F00163C30 = { 131 | CreatedOnToolsVersion = 9.2; 132 | ProvisioningStyle = Automatic; 133 | }; 134 | }; 135 | }; 136 | buildConfigurationList = 92A9AF582051E56F00163C30 /* Build configuration list for PBXProject "heatmap" */; 137 | compatibilityVersion = "Xcode 8.0"; 138 | developmentRegion = en; 139 | hasScannedForEncodings = 0; 140 | knownRegions = ( 141 | en, 142 | Base, 143 | ); 144 | mainGroup = 92A9AF542051E56F00163C30; 145 | productRefGroup = 92A9AF5E2051E56F00163C30 /* Products */; 146 | projectDirPath = ""; 147 | projectRoot = ""; 148 | targets = ( 149 | 92A9AF5C2051E56F00163C30 /* heatmap */, 150 | ); 151 | }; 152 | /* End PBXProject section */ 153 | 154 | /* Begin PBXResourcesBuildPhase section */ 155 | 92A9AF5B2051E56F00163C30 /* Resources */ = { 156 | isa = PBXResourcesBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | 92A9AF6B2051E56F00163C30 /* LaunchScreen.storyboard in Resources */, 160 | 92A9AF682051E56F00163C30 /* Assets.xcassets in Resources */, 161 | 92A9AF852051F3DB00163C30 /* Floorplans in Resources */, 162 | 92A9AF742051EC7300163C30 /* README.md in Resources */, 163 | 92A9AF662051E56F00163C30 /* Main.storyboard in Resources */, 164 | 92A9AF752051EC7300163C30 /* LICENSE in Resources */, 165 | ); 166 | runOnlyForDeploymentPostprocessing = 0; 167 | }; 168 | /* End PBXResourcesBuildPhase section */ 169 | 170 | /* Begin PBXSourcesBuildPhase section */ 171 | 92A9AF592051E56F00163C30 /* Sources */ = { 172 | isa = PBXSourcesBuildPhase; 173 | buildActionMask = 2147483647; 174 | files = ( 175 | 92A9AF812051F3A100163C30 /* FloorplanOverlayRenderer.swift in Sources */, 176 | 92A9AF802051F3A100163C30 /* VisibleMapRegionDelegate.swift in Sources */, 177 | 92A9AF832051F3A100163C30 /* MKMapRectRotated.swift in Sources */, 178 | 920EAB2F2053AEA10054D05D /* CustomPolygons.swift in Sources */, 179 | 92A9AF632051E56F00163C30 /* ViewController.swift in Sources */, 180 | 92A9AF7D2051F3A100163C30 /* HideBackgroundOverlay.swift in Sources */, 181 | 92A9AF822051F3A100163C30 /* CoordinateConverter.swift in Sources */, 182 | 92A9AF7E2051F3A100163C30 /* Utilities.swift in Sources */, 183 | 92A9AF612051E56F00163C30 /* AppDelegate.swift in Sources */, 184 | 92A9AF7F2051F3A100163C30 /* FloorplanOverlay.swift in Sources */, 185 | ); 186 | runOnlyForDeploymentPostprocessing = 0; 187 | }; 188 | /* End PBXSourcesBuildPhase section */ 189 | 190 | /* Begin PBXVariantGroup section */ 191 | 92A9AF642051E56F00163C30 /* Main.storyboard */ = { 192 | isa = PBXVariantGroup; 193 | children = ( 194 | 92A9AF652051E56F00163C30 /* Base */, 195 | ); 196 | name = Main.storyboard; 197 | sourceTree = ""; 198 | }; 199 | 92A9AF692051E56F00163C30 /* LaunchScreen.storyboard */ = { 200 | isa = PBXVariantGroup; 201 | children = ( 202 | 92A9AF6A2051E56F00163C30 /* Base */, 203 | ); 204 | name = LaunchScreen.storyboard; 205 | sourceTree = ""; 206 | }; 207 | /* End PBXVariantGroup section */ 208 | 209 | /* Begin XCBuildConfiguration section */ 210 | 92A9AF6D2051E56F00163C30 /* Debug */ = { 211 | isa = XCBuildConfiguration; 212 | buildSettings = { 213 | ALWAYS_SEARCH_USER_PATHS = NO; 214 | CLANG_ANALYZER_NONNULL = YES; 215 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 216 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 217 | CLANG_CXX_LIBRARY = "libc++"; 218 | CLANG_ENABLE_MODULES = YES; 219 | CLANG_ENABLE_OBJC_ARC = YES; 220 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 221 | CLANG_WARN_BOOL_CONVERSION = YES; 222 | CLANG_WARN_COMMA = YES; 223 | CLANG_WARN_CONSTANT_CONVERSION = YES; 224 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 225 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 226 | CLANG_WARN_EMPTY_BODY = YES; 227 | CLANG_WARN_ENUM_CONVERSION = YES; 228 | CLANG_WARN_INFINITE_RECURSION = YES; 229 | CLANG_WARN_INT_CONVERSION = YES; 230 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 231 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 232 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 233 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 234 | CLANG_WARN_STRICT_PROTOTYPES = YES; 235 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 236 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 237 | CLANG_WARN_UNREACHABLE_CODE = YES; 238 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 239 | CODE_SIGN_IDENTITY = "iPhone Developer"; 240 | COPY_PHASE_STRIP = NO; 241 | DEBUG_INFORMATION_FORMAT = dwarf; 242 | ENABLE_STRICT_OBJC_MSGSEND = YES; 243 | ENABLE_TESTABILITY = YES; 244 | GCC_C_LANGUAGE_STANDARD = gnu11; 245 | GCC_DYNAMIC_NO_PIC = NO; 246 | GCC_NO_COMMON_BLOCKS = YES; 247 | GCC_OPTIMIZATION_LEVEL = 0; 248 | GCC_PREPROCESSOR_DEFINITIONS = ( 249 | "DEBUG=1", 250 | "$(inherited)", 251 | ); 252 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 253 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 254 | GCC_WARN_UNDECLARED_SELECTOR = YES; 255 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 256 | GCC_WARN_UNUSED_FUNCTION = YES; 257 | GCC_WARN_UNUSED_VARIABLE = YES; 258 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 259 | MTL_ENABLE_DEBUG_INFO = YES; 260 | ONLY_ACTIVE_ARCH = YES; 261 | SDKROOT = iphoneos; 262 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 263 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 264 | }; 265 | name = Debug; 266 | }; 267 | 92A9AF6E2051E56F00163C30 /* Release */ = { 268 | isa = XCBuildConfiguration; 269 | buildSettings = { 270 | ALWAYS_SEARCH_USER_PATHS = NO; 271 | CLANG_ANALYZER_NONNULL = YES; 272 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 273 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 274 | CLANG_CXX_LIBRARY = "libc++"; 275 | CLANG_ENABLE_MODULES = YES; 276 | CLANG_ENABLE_OBJC_ARC = YES; 277 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 278 | CLANG_WARN_BOOL_CONVERSION = YES; 279 | CLANG_WARN_COMMA = YES; 280 | CLANG_WARN_CONSTANT_CONVERSION = YES; 281 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 282 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 283 | CLANG_WARN_EMPTY_BODY = YES; 284 | CLANG_WARN_ENUM_CONVERSION = YES; 285 | CLANG_WARN_INFINITE_RECURSION = YES; 286 | CLANG_WARN_INT_CONVERSION = YES; 287 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 288 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 289 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 290 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 291 | CLANG_WARN_STRICT_PROTOTYPES = YES; 292 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 293 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 294 | CLANG_WARN_UNREACHABLE_CODE = YES; 295 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 296 | CODE_SIGN_IDENTITY = "iPhone Developer"; 297 | COPY_PHASE_STRIP = NO; 298 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 299 | ENABLE_NS_ASSERTIONS = NO; 300 | ENABLE_STRICT_OBJC_MSGSEND = YES; 301 | GCC_C_LANGUAGE_STANDARD = gnu11; 302 | GCC_NO_COMMON_BLOCKS = YES; 303 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 304 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 305 | GCC_WARN_UNDECLARED_SELECTOR = YES; 306 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 307 | GCC_WARN_UNUSED_FUNCTION = YES; 308 | GCC_WARN_UNUSED_VARIABLE = YES; 309 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 310 | MTL_ENABLE_DEBUG_INFO = NO; 311 | SDKROOT = iphoneos; 312 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 313 | VALIDATE_PRODUCT = YES; 314 | }; 315 | name = Release; 316 | }; 317 | 92A9AF702051E56F00163C30 /* Debug */ = { 318 | isa = XCBuildConfiguration; 319 | buildSettings = { 320 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 321 | CODE_SIGN_STYLE = Automatic; 322 | DEVELOPMENT_TEAM = 24VUPDRF9D; 323 | INFOPLIST_FILE = heatmap/Info.plist; 324 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 325 | PRODUCT_BUNDLE_IDENTIFIER = com.anthonyamanse.heatmap; 326 | PRODUCT_NAME = "$(TARGET_NAME)"; 327 | SWIFT_VERSION = 4.0; 328 | TARGETED_DEVICE_FAMILY = "1,2"; 329 | }; 330 | name = Debug; 331 | }; 332 | 92A9AF712051E56F00163C30 /* Release */ = { 333 | isa = XCBuildConfiguration; 334 | buildSettings = { 335 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 336 | CODE_SIGN_STYLE = Automatic; 337 | DEVELOPMENT_TEAM = 24VUPDRF9D; 338 | INFOPLIST_FILE = heatmap/Info.plist; 339 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 340 | PRODUCT_BUNDLE_IDENTIFIER = com.anthonyamanse.heatmap; 341 | PRODUCT_NAME = "$(TARGET_NAME)"; 342 | SWIFT_VERSION = 4.0; 343 | TARGETED_DEVICE_FAMILY = "1,2"; 344 | }; 345 | name = Release; 346 | }; 347 | /* End XCBuildConfiguration section */ 348 | 349 | /* Begin XCConfigurationList section */ 350 | 92A9AF582051E56F00163C30 /* Build configuration list for PBXProject "heatmap" */ = { 351 | isa = XCConfigurationList; 352 | buildConfigurations = ( 353 | 92A9AF6D2051E56F00163C30 /* Debug */, 354 | 92A9AF6E2051E56F00163C30 /* Release */, 355 | ); 356 | defaultConfigurationIsVisible = 0; 357 | defaultConfigurationName = Release; 358 | }; 359 | 92A9AF6F2051E56F00163C30 /* Build configuration list for PBXNativeTarget "heatmap" */ = { 360 | isa = XCConfigurationList; 361 | buildConfigurations = ( 362 | 92A9AF702051E56F00163C30 /* Debug */, 363 | 92A9AF712051E56F00163C30 /* Release */, 364 | ); 365 | defaultConfigurationIsVisible = 0; 366 | defaultConfigurationName = Release; 367 | }; 368 | /* End XCConfigurationList section */ 369 | }; 370 | rootObject = 92A9AF552051E56F00163C30 /* Project object */; 371 | } 372 | -------------------------------------------------------------------------------- /heatmap.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /heatmap/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // heatmap 4 | // 5 | // Created by Joe Anthony Peter Amanse on 3/8/18. 6 | // Copyright © 2018 Joe Anthony Peter Amanse. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /heatmap/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /heatmap/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /heatmap/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 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 | -------------------------------------------------------------------------------- /heatmap/CoordinateConverter.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This class converts PDF coordinates of a floorplan to Geographic 7 | coordinates on Earth. 8 | NOTE: This class can also be used for any "right-handed" 9 | coordinate system (other than PDF) but should not be used as-is 10 | for "Raster image" coordinates (such as PNGs or JPEGs) because 11 | those require left-handed coordinate frames. 12 | There are other reasons we discourage the use of raster images 13 | as indoor floorplans. See the code comments inside 14 | FloorplanOverlay init for more info. 15 | */ 16 | 17 | import CoreLocation 18 | import Foundation 19 | import MapKit 20 | 21 | /** 22 | - note: In iOS, the term "pixel" usually refers to screen pixels whereas the 23 | term "point" is used to describe coordinates inside a visual/image asset. 24 | 25 | For more information, see: "Points Versus Pixels" on developer.apple.com 26 | 27 | This class matches a specific latitude & longitude (a coordinate on Earth) 28 | to a specifc x,y coordinate (a position on your floorplan PDF). 29 | 30 | PDFs are defined in a coordinate system where +y is counter-clockwise of +x 31 | (a.k.a. "a right handed coordinate system"). PDF coordinates 32 | 33 | - parameter latitudeLongitude: The lat-lon coordinate for this anchor 34 | - parameter pdfPoint: corresponding PDF coordinate 35 | */ 36 | struct GeoAnchor { 37 | var latitudeLongitudeCoordinate = CLLocationCoordinate2D() 38 | var pdfPoint = CGPoint.zero 39 | } 40 | 41 | /** 42 | Defines a pair of GeoAnchors 43 | 44 | - parameter fromAnchor: starting anchor 45 | - parameter toAnchor: ending anchor 46 | */ 47 | struct GeoAnchorPair { 48 | var fromAnchor = GeoAnchor() 49 | var toAnchor = GeoAnchor() 50 | } 51 | 52 | /** 53 | This class converts PDF coordinates of a floorplan to Geographic coordinates 54 | on Earth. 55 | 56 | **This class can also be used for any "right-handed" coordinate system 57 | (other than PDF) but should not be used as-is for "Raster image" coordinates 58 | (such as PNGs or JPEGs) because those require left-handed coordinate frames. 59 | There are other reasons we discourage the use of raster images as indoor 60 | floorplans. See the code & comments inside FloorplanOverlay init for more 61 | info.** 62 | */ 63 | class CoordinateConverter: NSObject { 64 | 65 | /// The GeoAnchorPair used to define this converter 66 | var anchors: GeoAnchorPair = GeoAnchorPair() 67 | 68 | /** 69 | This vector, expressed in points (PDF coordinates), has length one meter 70 | and direction due East. 71 | */ 72 | fileprivate var oneMeterEastwardVector: CGVector 73 | 74 | /** 75 | This vector, expressed in points (PDF coordinates), has length one meter 76 | and direction due South. 77 | */ 78 | fileprivate var oneMeterSouthwardVector: CGVector 79 | 80 | /** 81 | This coordinate, expressed in points (PDF coordinates), corresponds to 82 | exactly the same location as tangentLatLng 83 | */ 84 | fileprivate var tangentPDFPoint: CGPoint 85 | 86 | /** 87 | This coordinate, expressed in latitude & longitude (global coordinates), 88 | corresponds to exactly the same location as tangentPoint 89 | */ 90 | fileprivate var tangentLatitudeLongitudeCoordinate: CLLocationCoordinate2D 91 | 92 | /** 93 | Initializes this class from a given GeoAnchorPair 94 | 95 | - parameter Anchors: the anchors that this class will use for converting 96 | */ 97 | init(anchors: GeoAnchorPair) { 98 | self.anchors = anchors 99 | 100 | /* 101 | Next, to compute the direction between two geographical coordinates, 102 | we first need to convert to MapKit coordinates... 103 | */ 104 | let fromAnchorMercatorCoordinate = MKMapPointForCoordinate(anchors.fromAnchor.latitudeLongitudeCoordinate) 105 | let toAnchorMercatorCoordinate = MKMapPointForCoordinate(anchors.toAnchor.latitudeLongitudeCoordinate) 106 | 107 | let pdfDisplacement = CGPoint(x: anchors.toAnchor.pdfPoint.x - anchors.fromAnchor.pdfPoint.x, y: anchors.toAnchor.pdfPoint.y - anchors.fromAnchor.pdfPoint.y) 108 | 109 | /* 110 | ...so that we can use MapKit's Mercator coordinate system where +x 111 | is always eastward and +y is always southward. Imagine an arrow 112 | connecting fromAnchor to toAnchor... 113 | */ 114 | let anchorDisplacementMapKitX = (toAnchorMercatorCoordinate.x - fromAnchorMercatorCoordinate.x) 115 | let anchorDisplacementMapKitY = (toAnchorMercatorCoordinate.y - fromAnchorMercatorCoordinate.y) 116 | 117 | /* 118 | What is the angle of this arrow (geographically)? 119 | atan2 always returns: 120 | exactly 0.0 radians if the arrow is exactly in the +x direction 121 | ("MapKit's +x" is due East). 122 | positive radians as the arrow is rotated toward and through the +y 123 | direction ("MapKit's +y" is due South). 124 | In the case of MapKit, this is radians clockwise from due East. 125 | */ 126 | let radiansClockwiseOfDueEast = atan2(anchorDisplacementMapKitY, anchorDisplacementMapKitX) 127 | 128 | /* 129 | That means if we rotate pdfDisplacement COUNTER-clockwise by this 130 | value, it will be facing due east. In the CG coordinate frame, 131 | positive radians is counter-clockwise because in a PDF +x is 132 | rightward and +y is upward. 133 | */ 134 | let cgDueEast = CGVector(dx: pdfDisplacement.x, dy: pdfDisplacement.y).rotatedByRadians(CGFloat(radiansClockwiseOfDueEast)) 135 | 136 | // Now, get the distance (in meters) between the two anchors... 137 | let distanceBetweenAnchorsMeters = CLLocationDistance.distanceBetweenLocationCoordinates2D(anchors.fromAnchor.latitudeLongitudeCoordinate, b: anchors.toAnchor.latitudeLongitudeCoordinate) 138 | 139 | // ...and rescale so that it's exactly one meter in length. 140 | oneMeterEastwardVector = cgDueEast.scaledByFloat(CGFloat(1.0 / distanceBetweenAnchorsMeters)) 141 | 142 | /* 143 | Lastly, due south is PI/2 clockwise of due east. 144 | In the CG coordinate frame, clockwise rotation is NEGATIVE radians 145 | because in a PDF +x is rightward and +y is upward. 146 | */ 147 | oneMeterSouthwardVector = oneMeterEastwardVector.rotatedByRadians(CGFloat.pi / -2) 148 | 149 | /* 150 | We'll choose the midpoint between the two anchors to be our "tangent 151 | point". This is the MKMapPoint that will correspond to both 152 | tangentLatitudeLongitudeCoordinate on Earth and _tangentPDFPoint 153 | in the PDF. 154 | */ 155 | let tangentMercatorCoordinate = MKMapPoint.midpoint(fromAnchorMercatorCoordinate, b: toAnchorMercatorCoordinate) 156 | 157 | tangentLatitudeLongitudeCoordinate = MKCoordinateForMapPoint(tangentMercatorCoordinate) 158 | 159 | tangentPDFPoint = CGPoint.pointAverage(anchors.fromAnchor.pdfPoint, b: anchors.toAnchor.pdfPoint) 160 | 161 | } 162 | 163 | /** 164 | Calculate the MKMapPoint from a specific PDF coordinate 165 | 166 | - parameter pdfPoint: starting point in the PDF 167 | - returns: The corresponding MKMapPoint 168 | */ 169 | func MKMapPointFromPDFPoint(_ pdfPoint: CGPoint) -> MKMapPoint { 170 | /* 171 | To perform this conversion, we start by seeing how far we are from 172 | the tangentPoint. The tangentPoint is the one place on the PDF where 173 | we know exactly the corresponding Earth latitude & lontigude. 174 | */ 175 | let displacementFromTangentPoint = CGVector(dx: pdfPoint.x - tangentPDFPoint.x, dy: pdfPoint.y - tangentPDFPoint.y) 176 | 177 | // Now, let's figure out how far East & South we are from this point. 178 | let dotProductEast = displacementFromTangentPoint.dotProductWithVector(oneMeterEastwardVector) 179 | let dotProductSouth = displacementFromTangentPoint.dotProductWithVector(oneMeterSouthwardVector) 180 | 181 | let eastSouthDistanceMeters = ( 182 | east: CLLocationDistance(dotProductEast / oneMeterEastwardVector.dotProductWithVector(oneMeterEastwardVector)), 183 | south: CLLocationDistance(dotProductSouth / oneMeterSouthwardVector.dotProductWithVector(oneMeterSouthwardVector)) 184 | ) 185 | 186 | let metersPerMapPoint = MKMetersPerMapPointAtLatitude(tangentLatitudeLongitudeCoordinate.latitude) 187 | let tangentMercatorCoordinate = MKMapPointForCoordinate(tangentLatitudeLongitudeCoordinate) 188 | 189 | /* 190 | Each meter is about (1.0 / metersPerMapPoint) 'MKMapPoint's, as long 191 | as we are nearby _tangentLatLng. So just move this many meters East 192 | and South and we're done! 193 | */ 194 | return MKMapPoint(x: tangentMercatorCoordinate.x + eastSouthDistanceMeters.east / metersPerMapPoint, 195 | y: tangentMercatorCoordinate.y + eastSouthDistanceMeters.south / metersPerMapPoint) 196 | } 197 | 198 | /** 199 | - returns: a single CGAffineTransform that can transform any CGPoint in 200 | a PDF into its corresponding MKMapPoint. 201 | 202 | In theory, the following equalities should always hold: 203 | 204 | CGPointApplyAffineTransform(pdfPoint, transformerFromPDFToMk).x 205 | == MKMapPointFromPDFPoint(pdfPoint).x 206 | CGPointApplyAffineTransform(pdfPoint, transformerFromPDFToMk).y 207 | == MKMapPointFromPDFPoint(pdfPoint).y 208 | 209 | However, in practice we find that MKMapPointFromPDFPoint can be slightly 210 | more accurate than transformerFromPDFToMk due to hardware acceleration 211 | and/or numerical precision losses of CGAffineTransform operations. 212 | */ 213 | func transformerFromPDFToMk() -> CGAffineTransform { 214 | let metersPerMapPoint = MKMetersPerMapPointAtLatitude(tangentLatitudeLongitudeCoordinate.latitude) 215 | let tangentMercatorCoordinate = MKMapPointForCoordinate(tangentLatitudeLongitudeCoordinate) 216 | 217 | /* 218 | CGAffineTransform operations are easier to construct in reverse-order. 219 | Start with the last operation: 220 | */ 221 | let resultOfTangentMercatorCoordinate = CGAffineTransform(translationX: CGFloat(tangentMercatorCoordinate.x), y: CGFloat(tangentMercatorCoordinate.y)) 222 | 223 | /* 224 | Revise the AffineTransform to first scale by 225 | (1.0 / metersPerMapPoint), and then perform the above translation. 226 | */ 227 | let resultOfEastSouthDistanceMeters = resultOfTangentMercatorCoordinate.scaledBy(x: CGFloat(1.0 / metersPerMapPoint), y: CGFloat(1.0 / metersPerMapPoint)) 228 | 229 | /* 230 | Revise the AffineTransform to first scale by 231 | (1.0 / dotProduct(...)) before performing the transform so far. 232 | */ 233 | let resultOfDotProduct = resultOfEastSouthDistanceMeters.scaledBy(x: 1.0 / oneMeterEastwardVector.dotProductWithVector(oneMeterEastwardVector), 234 | y: 1.0 / oneMeterSouthwardVector.dotProductWithVector(oneMeterSouthwardVector)) 235 | 236 | /* 237 | Revise the AffineTransform to first perform dot products aginst our 238 | reference vectors before performing the transform so far. 239 | */ 240 | let resultOfDisplacementFromTangentPoint = CGAffineTransform( 241 | a: oneMeterEastwardVector.dx, b: oneMeterEastwardVector.dy, 242 | c: oneMeterSouthwardVector.dx, d: oneMeterSouthwardVector.dy, 243 | tx: 0.0, ty: 0.0 244 | ).concatenating(resultOfDotProduct 245 | ) 246 | 247 | /* 248 | Lastly, revise the AffineTransform to first perform the initial 249 | subtraction before performing the remaining operations. 250 | Each meter is about (1.0 / metersPerMapPoint) 'MKMapPoint's, as long 251 | as we are nearby tangentLatitudeLongitudeCoordinate. 252 | */ 253 | return resultOfDisplacementFromTangentPoint.translatedBy(x: -tangentPDFPoint.x, y: -tangentPDFPoint.y) 254 | } 255 | 256 | /// - returns: the size in meters of 1.0 CGPoint distance 257 | var unitSizeInMeters: CLLocationDistance { 258 | return CLLocationDistance(1.0 / hypot(oneMeterEastwardVector.dx, oneMeterEastwardVector.dy)) 259 | } 260 | 261 | /** 262 | Converts each corner of a PDF rectangle into an MKMapPoint (in MapKit 263 | space). The collection of MKMapPoints is returned as an MKPolygon 264 | overlay. 265 | 266 | - parameter pdfRect: A PDF rectangle 267 | - returns: the corners of the PDF in an MKPolygon (obviously there 268 | should be four points since it's a rectangle) 269 | */ 270 | func polygonFromPDFRectCorners(_ pdfRect: CGRect) -> MKPolygon { 271 | var corners = [ MKMapPointFromPDFPoint(CGPoint(x: pdfRect.maxX, y: pdfRect.maxY)), 272 | MKMapPointFromPDFPoint(CGPoint(x: pdfRect.minX, y: pdfRect.maxY)), 273 | MKMapPointFromPDFPoint(CGPoint(x: pdfRect.minX, y: pdfRect.minY)), 274 | MKMapPointFromPDFPoint(CGPoint(x: pdfRect.maxX, y: pdfRect.minY))] 275 | 276 | return MKPolygon(points: &corners, count: corners.count) 277 | } 278 | 279 | /** 280 | - returns: the smallest MKMapRect that can show all rotations of the 281 | given PDF rectangle. 282 | */ 283 | func boundingMapRectIncludingRotations(_ rect: CGRect) -> MKMapRect { 284 | // Start with the nominal rendering box for this rect is. 285 | let nominalRenderingRect = polygonFromPDFRectCorners(rect).boundingMapRect 286 | 287 | /* 288 | In order to account for all rotations, any bounding map rect must 289 | have diameter equal to the longest distance inside the rectangle. 290 | */ 291 | let boundsDiameter = hypot(nominalRenderingRect.size.width, nominalRenderingRect.size.height) 292 | 293 | let rectCenterPoints = CGPoint(x: rect.midX, y: rect.midY) 294 | 295 | let boundsCenter = MKMapPointFromPDFPoint(rectCenterPoints) 296 | 297 | /* 298 | Return a square MKMapRect centered at boundsCenterMercator with edge 299 | length diameterMercator 300 | */ 301 | return MKMapRectMake( 302 | boundsCenter.x - boundsDiameter / 2.0, 303 | boundsCenter.y - boundsDiameter / 2.0, 304 | boundsDiameter, boundsDiameter) 305 | } 306 | 307 | /** 308 | - returns: the MKMapCamera heading required to display your PDF (user 309 | space) coordinate system upright so that PDF +x is rightward and 310 | PDF +y is upward. 311 | */ 312 | func getUprightMKMapCameraHeading() -> CLLocationDirection { 313 | /* 314 | To make the floorplan upright, we want to rotate the floorplan +x 315 | vector toward due east. 316 | */ 317 | let resultRadians: CGFloat = atan2(oneMeterEastwardVector.dy, oneMeterEastwardVector.dx) 318 | let result = resultRadians * 180.0 / CGFloat.pi 319 | 320 | /* 321 | According to the CLLocationDirection documentation we must store a 322 | positive value if it is valid. 323 | */ 324 | if result < 0.0 { 325 | return CLLocationDirection(result + 360.0) 326 | } else { 327 | return CLLocationDirection(result) 328 | } 329 | } 330 | 331 | } 332 | -------------------------------------------------------------------------------- /heatmap/CustomPolygons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomPolygons.swift 3 | // heatmap 4 | // 5 | // Created by Joe Anthony Peter Amanse on 3/9/18. 6 | // Copyright © 2018 Joe Anthony Peter Amanse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | 12 | class HeatMapPolygon: MKPolygon { 13 | var identifier: String = "" 14 | var numberOfTriggers: Int? 15 | } 16 | 17 | class HeatMapCircle: MKCircle { 18 | var identifier: String = "" 19 | } 20 | -------------------------------------------------------------------------------- /heatmap/FloorplanOverlay.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This class describes a floorplan for an indoor venue. 7 | */ 8 | 9 | 10 | import Foundation 11 | import MapKit 12 | 13 | /// This class describes a floorplan for an indoor venue. 14 | @objc class FloorplanOverlay: NSObject, MKOverlay { 15 | 16 | /** 17 | Same as boundingMapRect but slightly larger to fit on-screen under 18 | any MKMapCamera rotation. 19 | */ 20 | var boundingMapRectIncludingRotations = MKMapRect() 21 | 22 | /** 23 | Cache the CGAffineTransform used to help draw the floorplan to the 24 | screen inside an MKMapView. 25 | */ 26 | var transformerFromPDFToMk = CGAffineTransform() 27 | 28 | /// Current floor level 29 | var floorLevel = 0 30 | 31 | /** 32 | Reference to the internal page data of the selected page of the PDF you 33 | are drawing. It is very likely that the PDF of your floorplan is a 34 | single page. 35 | */ 36 | var pdfPage: CGPDFPage 37 | 38 | /** 39 | Same as boundingMapRect, but more precise. 40 | The AAPLMapRectRotated you'll get here fits snugly accounting for the 41 | rotation of the floorplan (relative to North) whereas the 42 | boundingMapRect must be "North-aligned" since it's an MKMapRect. 43 | If you're still not 100% sure, toggle the "debug switch" in the sample 44 | code and look at the overlays that are drawn. 45 | */ 46 | var floorplanPDFBox: MKMapRectRotated 47 | 48 | /// The PDF document to be rendered. 49 | fileprivate var pdfDoc: CGPDFDocument 50 | 51 | /** 52 | The coordinate converter for converting between PDF coordinates (point) 53 | and MapKit coordinates (MKMapPoint). 54 | */ 55 | fileprivate var coordinateConverter: CoordinateConverter 56 | 57 | /// For debugging, remember the PDF page box selected at initialization. 58 | fileprivate var pdfBoxRectangle = CGRect.null 59 | 60 | /// MKOverlay protocol return values. 61 | var boundingMapRect = MKMapRect() 62 | var coordinate = CLLocationCoordinate2D() 63 | 64 | /** 65 | In this example, our floorplan is described by four things. 66 | 1. The URL of a PDF. This is the visual data for the floorplan. 67 | 2. The PDF page box to draw. This tells us which section of the PDF 68 | we will actually draw. 69 | 3. A pair of anchors. This tells us where the floorplan appears in 70 | the real world. 71 | 4. A floor level. This tells us which floor our floorplan represents 72 | 73 | - parameter floorplanUrl: the path to a PDF containing the drawing of 74 | the floorplan. 75 | - parameter pdfBox: which section of the PDF do we draw? 76 | - parameter andAnchors: real-world anchors of this floorplan 77 | -- opposite corners. 78 | - parameter forFloorLevel: which floor is it on? 79 | */ 80 | init(floorplanUrl: URL, withPDFBox pdfBox: CGPDFBox, andAnchors anchors: GeoAnchorPair, forFloorLevel level: NSInteger) { 81 | assert(floorplanUrl.absoluteString.hasSuffix("pdf"), "Sanity check: The URL should point to a PDF file") 82 | 83 | /* 84 | Using raster images (such as PNG or JPEG) would create a number of 85 | complications, such as: 86 | + you need multiple sizes of each image, and each would need its 87 | own GeoAnchorPair (see "Icon and Image Sizes" for iOS on 88 | developer.apple.com for more). 89 | + raster/bitmap images use a different coordinate system than PDFs 90 | do, so the code from CoordinateConverter could not be used 91 | out-of-the-box. Instead, you would need a separate 92 | implementation of CoordinateConverter that works for left-handed 93 | coordinate frames. PDFs use a right-handed coordinate frame. 94 | + text and fine details of raster images may not render as clearly 95 | as vector images when zoomed in. PDF is primarily a vector image 96 | format. 97 | + some raster image formats, such as JPEG, are designed for 98 | photographs and may suffer from loss of detail due to 99 | compression artifacts when being used for floorplans. 100 | */ 101 | coordinateConverter = CoordinateConverter(anchors: anchors) 102 | transformerFromPDFToMk = coordinateConverter.transformerFromPDFToMk() 103 | floorLevel = level 104 | 105 | /* 106 | Read the PDF file from disk into memory. Remember to CFRelease it 107 | when we dealloc. 108 | (see "The Create Rule" on developer.apple.com for more) 109 | */ 110 | pdfDoc = CGPDFDocument(floorplanUrl as CFURL)! 111 | 112 | /* 113 | In this example the floorplan PDF has only one page, so we pick 114 | "page 1" of the PDF. 115 | */ 116 | pdfPage = pdfDoc.page(at: 1)! 117 | 118 | // Figure out which region of the PDF is to be drawn. 119 | pdfBoxRectangle = pdfPage.getBoxRect(pdfBox) 120 | 121 | /* 122 | There is no need to display this floorplan if your MapView camera is 123 | beyond the four corners of the PDF page box. Thus, our 124 | boundingMapRect is based on the PDF page box corners in the 125 | MKMapPoint coordinate frame. 126 | */ 127 | let polygonFromPDFRectCorners = coordinateConverter.polygonFromPDFRectCorners(pdfBoxRectangle) 128 | boundingMapRect = polygonFromPDFRectCorners.boundingMapRect 129 | 130 | /* 131 | We need a quick way to check whether your screen is currently 132 | looking inside vs. outside the floorplan, in order to "clamp" your 133 | MKMapView. 134 | */ 135 | assert(polygonFromPDFRectCorners.pointCount == 4) 136 | let points = polygonFromPDFRectCorners.points() 137 | floorplanPDFBox = MKMapRectRotatedMake(points[0], corner2: points[1], corner3: points[2], corner4: points[3]) 138 | 139 | /* 140 | For the purposes of clamping MKMapCamera zoom, we need a slightly 141 | padded MKMapRect that allows the entire floorplan can be visible 142 | regardless of camera rotation. Otherwise, depending on the 143 | MKMapCamera rotation, auto-zoom might prevent the user from zooming 144 | out far enough to see the entire floorplan and/or auto-scroll might 145 | prevent the user from seeing the edge of the floorplan. 146 | */ 147 | boundingMapRectIncludingRotations = coordinateConverter.boundingMapRectIncludingRotations(pdfBoxRectangle) 148 | 149 | // For coordinate just return the centroid of boundingMapRect 150 | coordinate = MKCoordinateForMapPoint(boundingMapRect.getCenter()) 151 | } 152 | 153 | /** 154 | This is different from CoordinateConverter getUprightMKMapCameraHeading 155 | because here we also account for the PDF Page Dictionary's Rotate entry. 156 | 157 | - returns: the MKMapCamera heading required to display your *floorplan* 158 | upright. 159 | */ 160 | func getFloorplanUprightMKMapCameraHeading() -> CLLocationDirection { 161 | /* 162 | Applying this heading to the MKMapCamera will cause PDF +x to face 163 | MapKit +x. 164 | */ 165 | let rotatePDFXToMapKitX = coordinateConverter.getUprightMKMapCameraHeading() 166 | 167 | /* 168 | If a PDF Page Dictionary contains the "Rotate" entry, it is a 169 | request to the reader to rotate the _printed_ page *clockwise* by 170 | the given number of degrees before reading it. 171 | */ 172 | let pdfPageDictionaryRotationEntryDegrees = pdfPage.rotationAngle 173 | 174 | /* 175 | In the MapView world that is equivalent to subtracting that amount 176 | from the MKMapCamera heading. 177 | */ 178 | let result = CLLocationDirection(rotatePDFXToMapKitX) - CLLocationDirection(pdfPageDictionaryRotationEntryDegrees) 179 | 180 | /* 181 | According to the CLLocationDirection documentation we must store a 182 | positive value if it is valid. 183 | */ 184 | return ((result < CLLocationDirection(0.0)) ? (result + CLLocationDirection(360.0)) : result) 185 | } 186 | 187 | /** 188 | Create an MKPolygon overlay given a custom CGPath (whose coordinates 189 | are specified in the PDF points) 190 | - parameter pdfPath: an array of CGPoint, each element is a PDF 191 | coordinate along the path. 192 | - returns: A closed MapKit polygon made up of the points in PDF path. 193 | */ 194 | func polygonFromCustomPDFPath(_ pdfPath: [CGPoint]) -> MKPolygon { 195 | // Calculate the corresponding MKMapPoint for each PDF point. 196 | var coordinates = pdfPath.map { pathPoint in 197 | return coordinateConverter.MKMapPointFromPDFPoint(pathPoint) 198 | } 199 | 200 | return MKPolygon(points: &coordinates, count: coordinates.count) 201 | } 202 | 203 | func HeatMapPolygonFromCustomPDFPath(_ pdfPath: [CGPoint], value: Int? = nil, identifier: String) -> HeatMapPolygon { 204 | var coordinates = pdfPath.map { pathPoint in 205 | return coordinateConverter.MKMapPointFromPDFPoint(pathPoint) 206 | } 207 | 208 | let heatMapPolygon = HeatMapPolygon(points: &coordinates, count: coordinates.count) 209 | heatMapPolygon.identifier = identifier 210 | heatMapPolygon.numberOfTriggers = value 211 | 212 | return heatMapPolygon 213 | } 214 | 215 | /** 216 | Create an MKCircle overlay given a custom CGPoint (whose coordinates 217 | are specified in the PDF points) 218 | - parameter pdfPoint: a CGPoint. Coordinate is based on PDF points. 219 | - parameter radius: CGFloat. The distance is based on PDF points. 220 | - returns: A closed MapKit circle made up of the points in PDF path. 221 | */ 222 | func circleFromPDF(_ pdfPoint: CGPoint, radius: CGFloat) -> MKCircle { 223 | // Calculate the corresponding MKMapPoint for each PDF point. 224 | let centerCoordinate = coordinateConverter.MKMapPointFromPDFPoint(pdfPoint) 225 | 226 | var radiusDistance = pdfPoint 227 | radiusDistance.x = radiusDistance.x + radius 228 | let mapPointRadius = coordinateConverter.MKMapPointFromPDFPoint(radiusDistance) 229 | 230 | let distance = MKMetersBetweenMapPoints(centerCoordinate, mapPointRadius) 231 | 232 | return MKCircle(center: MKCoordinateForMapPoint(centerCoordinate), radius: distance) 233 | } 234 | 235 | func testCircle(_ pdfPoint: CGPoint, radius: CGFloat, identifier: String) -> HeatMapCircle { 236 | // Calculate the corresponding MKMapPoint for each PDF point. 237 | let centerCoordinate = coordinateConverter.MKMapPointFromPDFPoint(pdfPoint) 238 | 239 | var radiusDistance = pdfPoint 240 | radiusDistance.x = radiusDistance.x + radius 241 | let mapPointRadius = coordinateConverter.MKMapPointFromPDFPoint(radiusDistance) 242 | 243 | let distance = MKMetersBetweenMapPoints(centerCoordinate, mapPointRadius) 244 | let heatMapCircle = HeatMapCircle(center: MKCoordinateForMapPoint(centerCoordinate), radius: distance) 245 | heatMapCircle.identifier = identifier 246 | return heatMapCircle 247 | } 248 | 249 | /** 250 | For debugging, you may want to draw the reference anchors that define 251 | this floor's coordinate converter. 252 | */ 253 | var geoAnchorPair: GeoAnchorPair { 254 | return coordinateConverter.anchors 255 | } 256 | 257 | /// For debugging, you may want to draw the the (0.0, 0.0) point of the PDF. 258 | var pdfOrigin: MKMapPoint { 259 | return coordinateConverter.MKMapPointFromPDFPoint(CGPoint.zero) 260 | } 261 | 262 | /** 263 | For debugging, you may want to know the real-world coordinates of the 264 | PDF page box. 265 | */ 266 | var polygonFromFloorplanPDFBoxCorners: MKPolygon { 267 | return coordinateConverter.polygonFromPDFRectCorners(pdfBoxRectangle) 268 | } 269 | 270 | /** 271 | For debugging, you may want to have the boundingMapRect in the form of 272 | an MKPolygon overlay 273 | */ 274 | var polygonFromBoundingMapRect: MKPolygon { 275 | return boundingMapRect.polygonFromMapRect() 276 | } 277 | 278 | /** 279 | For debugging, you may want to have the 280 | boundingMapRectIncludingRotations in the form of an MKPolygon overlay 281 | */ 282 | var polygonFromBoundingMapRectIncludingRotations: MKPolygon { 283 | return boundingMapRectIncludingRotations.polygonFromMapRect() 284 | } 285 | 286 | /** 287 | For debugging, you may want to know the real-world meters size of one 288 | PDF "point" distance. 289 | */ 290 | var pdfPointSizeInMeters: CLLocationDistance { 291 | return coordinateConverter.unitSizeInMeters 292 | } 293 | 294 | } 295 | -------------------------------------------------------------------------------- /heatmap/FloorplanOverlayRenderer.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This class draws your FloorplanOverlay into an MKMapView. 7 | It is also capable of drawing diagnostic visuals to help with 8 | debugging, if needed. 9 | */ 10 | 11 | import Foundation 12 | import MapKit 13 | 14 | /** 15 | Should we show diagnostic visuals? Set this to false prior to compile to 16 | disable some of the diagnostic visuals 17 | */ 18 | let SHOW_DIAGNOSTIC_VISUALS = false 19 | 20 | /** 21 | This class draws your FloorplanOverlay into an MKMapView. 22 | It is also capable of drawing diagnostic visuals to help with debugging, 23 | if needed. 24 | */ 25 | class FloorplanOverlayRenderer: MKOverlayRenderer { 26 | 27 | override init(overlay: MKOverlay) { 28 | super.init(overlay: overlay) 29 | } 30 | 31 | /** 32 | - note: Overrides the drawMapRect method for MKOverlayRenderer. 33 | */ 34 | override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { 35 | assert(overlay.isKind(of: FloorplanOverlay.self), "Wrong overlay type") 36 | 37 | let floorplanOverlay = overlay as! FloorplanOverlay 38 | 39 | let boundingMapRect = overlay.boundingMapRect 40 | 41 | /* 42 | Mapkit converts to its own dynamic CGPoint frame, which we can read 43 | through rectForMapRect. 44 | */ 45 | let mapkitToGraphicsConversion = rect(for: boundingMapRect) 46 | 47 | let graphicsFloorplanCenter = CGPoint(x: mapkitToGraphicsConversion.midX, y: mapkitToGraphicsConversion.midY) 48 | let graphicsFloorplanWidth = mapkitToGraphicsConversion.width 49 | let graphicsFloorplanHeight = mapkitToGraphicsConversion.height 50 | 51 | // Now, how does this compare to MapKit coordinates? 52 | let mapkitFloorplanCenter = MKMapPoint(x: MKMapRectGetMidX(overlay.boundingMapRect), y: MKMapRectGetMidY(overlay.boundingMapRect)) 53 | 54 | let mapkitFloorplanWidth = MKMapRectGetWidth(overlay.boundingMapRect) 55 | let mapkitFloorplanHeight = MKMapRectGetHeight(overlay.boundingMapRect) 56 | 57 | /* 58 | Create the transformation that converts to Graphics coordinates from 59 | MapKit coordinates. 60 | 61 | graphics.x = (mapkit.x - mapkitFloorplanCenter.x) * 62 | graphicsFloorplanWidth / mapkitFloorplanWidth 63 | + graphicsFloorplanCenter.x 64 | */ 65 | var fromMapKitToGraphics = CGAffineTransform.identity as CGAffineTransform 66 | 67 | fromMapKitToGraphics = fromMapKitToGraphics.translatedBy(x: CGFloat(-mapkitFloorplanCenter.x), y: CGFloat(-mapkitFloorplanCenter.y)) 68 | fromMapKitToGraphics = fromMapKitToGraphics.scaledBy(x: graphicsFloorplanWidth / CGFloat(mapkitFloorplanWidth), 69 | y: graphicsFloorplanHeight / CGFloat(mapkitFloorplanHeight) 70 | ) 71 | fromMapKitToGraphics = fromMapKitToGraphics.translatedBy(x: graphicsFloorplanCenter.x, y: graphicsFloorplanCenter.y) 72 | 73 | /* 74 | Using this, we can send draw commands in MapKit coordinates and 75 | cause the equivalent drawing in (the correct) graphics coordinates 76 | For additional debugging, uncomment the following two lines to 77 | highlight the floorplan's boundingMapRect in cyan. 78 | */ 79 | if (SHOW_DIAGNOSTIC_VISUALS == true) { 80 | context.setFillColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 0.5) 81 | context.fill(mapkitToGraphicsConversion) 82 | } 83 | /* 84 | However, we want to be able to send draw commands in the original 85 | PDF coordinates though, so we'll also need the transformations that 86 | convert to MapKit coordinates from PDF coordinates. 87 | */ 88 | let fromPDFToMapKit = floorplanOverlay.transformerFromPDFToMk 89 | 90 | context.concatenate(fromPDFToMapKit.concatenating(fromMapKitToGraphics)) 91 | 92 | context.drawPDFPage(floorplanOverlay.pdfPage) 93 | 94 | /* 95 | The following diagnostic visuals are provided for debugging only. 96 | In production, you'll want to remove them. 97 | */ 98 | if (SHOW_DIAGNOSTIC_VISUALS == true) { 99 | drawDiagnosticVisuals(context, floorplanOverlay: floorplanOverlay) 100 | } 101 | } 102 | 103 | /** 104 | This draws directly in the PDF coordinate system. 105 | If drawing onto MapKit, the context object provided must already have 106 | the appropriate transforms applied. 107 | 108 | If you have the transform correct, you should see the following: 109 | [A] 1.0 m radius red square (50% alpha) centered on the 1st anchor pt. 110 | [B] 1.0 m radius green square (50% alpha) centered on the 2nd anchor pt. 111 | [C] a 1x1 point magenta square centered at the (0.0, 0.0) point of your 112 | PDF. This square is created by the precise overlap of the 113 | following two rectangles. 114 | [C.1] a 10x1 point red rectangle (50% alpha) that covers the 1x1 point 115 | square centered at PDF coordinate (0.0, 0.0) through the 1x1 116 | point square centered at PDF coordinate (10.0, 0.0). 117 | [C.2] a 1x10 point blue rectangle (50% alpha) that covers the 1x1 point 118 | square centered at PDF coordinate (0.0, 0.0) and the 1x1 point 119 | square centered at PDF coordinate (10.0, 1.0). 120 | 121 | Use [A] & [B] to verify that your anchor points have been set to the 122 | correct points on your PDF. If this does not match: 123 | + check your PDF reader and make sure it is giving you values in 124 | "points" and not "pixels" or some other unit of measure. 125 | + look for typos in the CGPoint values of your GeoAnchor structs. 126 | 127 | Use [C] to verify the location of (0.0, 0.0) on your PDF. If this does 128 | not match: 129 | + check your PDF reader and make sure it is showing you values of the 130 | underlying PDF coordinate system, and not its own internal display 131 | coordinate system. A proper PDF coordinate system should have +x be 132 | rightward and +y be upward. 133 | 134 | Use [C.1] & [C.2] to verify the sizes of "1.0 point" and "10.0 points" 135 | on your PDF. If this does not match: 136 | + check your PDF reader and make sure it is giving you values in 137 | "points" and not "pixels" or some other unit of measure. 138 | */ 139 | func drawDiagnosticVisuals(_ context: CGContext, floorplanOverlay: FloorplanOverlay) { 140 | // Draw a 1.0 meter radius square around each anchor point. 141 | let radiusPDFPoints = CGFloat(1.0) / CGFloat(floorplanOverlay.pdfPointSizeInMeters) 142 | let anchorMarkerSize = CGSize(width: radiusPDFPoints * 2.0, height: radiusPDFPoints * 2.0) 143 | 144 | let originPt = CGPoint(x: floorplanOverlay.geoAnchorPair.fromAnchor.pdfPoint.x - radiusPDFPoints, 145 | y: floorplanOverlay.geoAnchorPair.fromAnchor.pdfPoint.y - radiusPDFPoints) 146 | let destPt = CGPoint(x: floorplanOverlay.geoAnchorPair.toAnchor.pdfPoint.x - radiusPDFPoints, 147 | y: floorplanOverlay.geoAnchorPair.toAnchor.pdfPoint.y - radiusPDFPoints) 148 | let fromAnchorMarker = CGRect(origin: originPt, size: anchorMarkerSize) 149 | let toAnchorMarker = CGRect(origin: destPt, size: anchorMarkerSize) 150 | 151 | // Anchor 1: Red. 152 | context.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.75) 153 | context.fill(fromAnchorMarker) 154 | 155 | // Anchor 2: Green. 156 | context.setFillColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 0.75) 157 | context.fill(toAnchorMarker) 158 | 159 | /** 160 | Draw a 10pt x 1pt red rectangle that covers the square centered at 161 | (0.0, 0.0) through the square centered at (10.0, 0.0). 162 | */ 163 | context.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.5) 164 | context.fill(CGRect(x: -0.5, y: -0.5, width: 10.0, height: 1.0)) 165 | 166 | /** 167 | Draw a 1pt x 10pt blue rectangle that covers the square centered at 168 | (0.0, 0.0) through the square centered at (0.0, 10.0). 169 | */ 170 | context.setFillColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 0.5) 171 | context.fill(CGRect(x: -0.5, y: -0.5, width: 1.0, height: 10.0)) 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /heatmap/Floorplans/floorplan_overlay_floor0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/ios-mapkit-indoor-map/dba86f92c64de7348bc4d189b7c0c337734c82b6/heatmap/Floorplans/floorplan_overlay_floor0.pdf -------------------------------------------------------------------------------- /heatmap/HideBackgroundOverlay.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This class provides an MKOverlay that can be used to hide MapKit's 7 | underlaying map tiles. 8 | */ 9 | 10 | import Foundation 11 | import MapKit 12 | 13 | /** 14 | This class provides an MKOverlay that can be used to hide MapKit's 15 | underlaying map tiles. 16 | */ 17 | class HideBackgroundOverlay: MKPolygon { 18 | 19 | /// - returns: a HideBackgroundOverlay object that covers the world. 20 | class func hideBackgroundOverlay() -> HideBackgroundOverlay { 21 | var corners = [MKMapPointMake(MKMapRectGetMaxX(MKMapRectWorld), MKMapRectGetMaxY(MKMapRectWorld)), 22 | MKMapPointMake(MKMapRectGetMinX(MKMapRectWorld), MKMapRectGetMaxY(MKMapRectWorld)), 23 | MKMapPointMake(MKMapRectGetMinX(MKMapRectWorld), MKMapRectGetMinY(MKMapRectWorld)), 24 | MKMapPointMake(MKMapRectGetMaxX(MKMapRectWorld), MKMapRectGetMinY(MKMapRectWorld))] 25 | return HideBackgroundOverlay(points: &corners, count: corners.count) 26 | } 27 | 28 | /** 29 | - returns: true to tell MapKit to hide its underlying map tiles, as long 30 | as this overlay is visible (which, as you can see above, is 31 | everywhere in the world), effectively hiding all map tiles and 32 | replacing them with a solid colored MKPolygon. 33 | */ 34 | override func canReplaceMapContent() -> Bool { 35 | return true 36 | } 37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /heatmap/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /heatmap/MKMapRectRotated.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | In order to properly clamp the MKMapView (see 7 | VisibleMapRegionDelegate) to inside a floorplan (that may not be 8 | "North up", and therefore may not be aligned with the standard 9 | MKMapRect coordinate frames), 10 | we'll need a way to store and quickly compute whether a specific 11 | MKMapPoint is inside your floorplan or not, and the displacement to 12 | the nearest edge of the floorplan. 13 | 14 | Since all PDF bounding boxes are still PDFs, after all, in the case 15 | of this sample code we need only represent a "rotated" MKMapRect. 16 | If you have transparency in your PDF or need something fancier, 17 | consider an MKPolygon and some combination of CGPathContainsPoint() 18 | */ 19 | 20 | import Foundation 21 | import MapKit 22 | 23 | /** 24 | Represents a "direction vector" or a "unit vector" between MKMapPoints. 25 | 26 | It is intended to always have length 1.0, that is `hypot(eX, eY) === 1.0` 27 | 28 | - parameter eX: direction along x 29 | - parameter eY: direction along y 30 | */ 31 | struct MKMapDirection { 32 | var eX = 0.0 33 | var eY = 0.0 34 | } 35 | 36 | /** 37 | In order to properly clamp the MKMapView (see VisibleMapRegionDelegate) to 38 | inside a floorplan (that may not be "North up", and therefore may not be 39 | aligned with the standard MKMapRect coordinate frames), we'll need a way to 40 | store and quickly compute whether a specific MKMapPoint is inside your 41 | floorplan or not, and the displacement to the nearest edge of the floorplan. 42 | 43 | Since all PDF bounding boxes are still PDFs, after all, in the case of this 44 | sample code we need only represent a "rotated" MKMapRect. 45 | If you have transparency in your PDF or need something fancier, consider an 46 | MKPolygon and some combination of CGPathContainsPoint(), etc. 47 | 48 | - parameter rectCenter: The center of the rectangle in MK coordinates. 49 | - parameter rectSize: The size of the original rectangle in MK coordinates. 50 | - parameter widthDirection: The "direction vector" of the "width" dimension. 51 | This vector has length 1.0 and points in the direction of "width". 52 | - parameter heightDirection: The "direction vector" of the "height" 53 | dimension. This vector has length 1.0 and points in the direction of 54 | "width". 55 | */ 56 | struct MKMapRectRotated { 57 | var rectCenter = MKMapPoint() 58 | var rectSize = MKMapSize() 59 | var widthDirection = MKMapDirection() 60 | var heightDirection = MKMapDirection() 61 | } 62 | 63 | /** 64 | Displacement from two MKMapPoints -- a direction and distance. 65 | 66 | - parameter direction: The direction of displacement, a unit vector. 67 | - parameter distance: The magnitude of the displacement. 68 | */ 69 | struct MKMapPointDisplacement { 70 | var direction = MKMapDirection() 71 | var distance = 0.0 72 | } 73 | 74 | /** 75 | - parameter corner1: First corner. 76 | - parameter corner2: Next corner. 77 | - parameter corner3: Corner after corner2. 78 | - parameter corner4: Last corner. 79 | 80 | - note: The four corners MUST be in clockwise or counter-clockwise order 81 | (i.e. going around the rectangle, and not criss-crossing through it)! 82 | 83 | - returns: MKMapRect constructed from the four corners of a (probably 84 | rotated) rectangle. 85 | */ 86 | func MKMapRectRotatedMake(_ corner1: MKMapPoint, corner2: MKMapPoint, corner3: MKMapPoint, corner4: MKMapPoint) -> MKMapRectRotated{ 87 | 88 | // Average the points to get the center of the rect in MKMapPoint space. 89 | let averageX = (corner1.x + corner2.x + corner3.x + corner4.x) / 4.0 90 | let averageY = (corner1.y + corner2.y + corner3.y + corner4.y) / 4.0 91 | let center = MKMapPoint(x: averageX, y: averageY) 92 | 93 | // Figure out the "width direction" and "height direction"... 94 | let heightMax = MKMapPoint.midpoint(corner1, b: corner2) 95 | let heightMin = MKMapPoint.midpoint(corner4, b: corner3) 96 | let widthMax = MKMapPoint.midpoint(corner1, b: corner4) 97 | let widthMin = MKMapPoint.midpoint(corner2, b: corner3) 98 | 99 | // ...as well as the actual width and height. 100 | let width = widthMax.displacementToPoint(widthMin) 101 | let height = heightMax.displacementToPoint(heightMin) 102 | let rotatedRectSize = MKMapSize(width: width.distance, height: height.distance) 103 | 104 | return MKMapRectRotated(rectCenter: center, rectSize: rotatedRectSize, 105 | widthDirection: width.direction, heightDirection: height.direction) 106 | } 107 | 108 | /** 109 | Return the *nearest* MKMapPoint that is inside the MKMapRectRotated 110 | For an "upright" rectangle, getting the nearest point is simple. Just clamp 111 | the value to width and height! 112 | 113 | We'd love to have that simplicity too, so our underlying main strategy is to 114 | simplify the problem. 115 | 116 | If we can answer the following two questions: 117 | 1. how far away are you, from the rectangle, in the height direction? 118 | 2. how far away are you, from the rectangle, in the width direction? 119 | 120 | Then we can use these values to take the exact same (simple) approach! 121 | 122 | - parameter mapRectRotated: Your (likely rotated) MKMapRectRotated. 123 | - parameter point: An MKMapPoint. 124 | 125 | - returns: The MKMapPoint inside mapRectRotated that is closest to point 126 | */ 127 | func MKMapRectRotatedNearestPoint(_ mapRectRotated: MKMapRectRotated, point: MKMapPoint) -> MKMapPoint { 128 | let dxCenter = (point.x - mapRectRotated.rectCenter.x) 129 | let dyCenter = (point.y - mapRectRotated.rectCenter.y) 130 | 131 | /* 132 | We use a dot product against a unit vector (a.k.a. projection) to find 133 | distance "along a particular direction." 134 | */ 135 | let widthDistance = dxCenter * mapRectRotated.widthDirection.eX + dyCenter * mapRectRotated.widthDirection.eY 136 | 137 | /* 138 | We use a dot product against a unit vector (a.k.a. projection) to find 139 | distance "along a particular direction." 140 | */ 141 | let heightDistance = dxCenter * mapRectRotated.heightDirection.eX + dyCenter * mapRectRotated.heightDirection.eY 142 | 143 | // "If this rectangle _were_ upright, this would be the result." 144 | let widthNearestPoint = clamp(widthDistance, min: -0.5 * mapRectRotated.rectSize.width, max: 0.5 * mapRectRotated.rectSize.width) 145 | let heightNearestPoint = clamp(heightDistance, min: -0.5 * mapRectRotated.rectSize.height, max: 0.5 * mapRectRotated.rectSize.height) 146 | 147 | /* 148 | Since it's not upright, just combine the width and height in their 149 | corresponding directions! 150 | */ 151 | let mapPointX = mapRectRotated.rectCenter.x + widthNearestPoint * mapRectRotated.widthDirection.eX + heightNearestPoint * mapRectRotated.heightDirection.eX 152 | let mapPointY = mapRectRotated.rectCenter.y + widthNearestPoint * mapRectRotated.widthDirection.eY + heightNearestPoint * mapRectRotated.heightDirection.eY 153 | return MKMapPoint(x: mapPointX, y: mapPointY) 154 | } 155 | -------------------------------------------------------------------------------- /heatmap/Utilities.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This contains several utility methods and extensions 7 | */ 8 | import Foundation 9 | import MapKit 10 | 11 | /** 12 | - parameter a: 13 | - parameter b: 14 | - note: If numbers are the same, always chooses first 15 | - returns: the SMALLER of the two numbers (not the minimum) e.g. 16 | smallest(-5.0, 0.01) returns 0.01. 17 | */ 18 | func smallest(_ a: Double, b: Double) -> Double { 19 | return (fabs(a) <= fabs(b)) ? a: b 20 | } 21 | 22 | /** 23 | - parameter val: value to clamp. 24 | - parameter min: least possible value. 25 | - parameter max: greatest possible value. 26 | 27 | - returns: clamped version of val such that it falls between min and max. 28 | */ 29 | func clamp(_ val: Double, min: Double, max: Double) -> Double { 30 | return (val < min) ? min : ((val > max) ? max : val) 31 | } 32 | 33 | extension MKMapPoint { 34 | /** 35 | - parameter a: Point A. 36 | - parameter b: Point B. 37 | - returns: An MKMapPoint object representing the midpoints of a and b. 38 | */ 39 | static func midpoint(_ a: MKMapPoint, b: MKMapPoint) -> MKMapPoint { 40 | return MKMapPoint(x: (a.x + b.x) * 0.5, y: (a.y + b.y) * 0.5) 41 | } 42 | 43 | /** 44 | - parameter other: ending point. 45 | - returns: The MKMapPointDisplacement between two MKMapPoint objects. 46 | */ 47 | func displacementToPoint(_ other: MKMapPoint) -> MKMapPointDisplacement { 48 | let dx = (other.x - x) 49 | let dy = (other.y - y) 50 | let distance = hypot(dx, dy) 51 | 52 | return MKMapPointDisplacement(direction: MKMapDirection(eX: dx/distance, eY: dy/distance), distance: distance) 53 | } 54 | } 55 | 56 | extension CLLocationDistance { 57 | /** 58 | - parameter a: coordinate A. 59 | - parameter b: coordinate B. 60 | - returns: The distance between the two coordinates. 61 | */ 62 | static func distanceBetweenLocationCoordinates2D(_ a: CLLocationCoordinate2D, b: CLLocationCoordinate2D) -> CLLocationDistance { 63 | 64 | let locA: CLLocation = CLLocation(latitude: a.latitude, longitude: a.longitude) 65 | let locB: CLLocation = CLLocation(latitude: b.latitude, longitude: b.longitude) 66 | 67 | return locA.distance(from: locB) 68 | } 69 | } 70 | 71 | extension CGPoint { 72 | /** 73 | - parameter a: point A. 74 | - parameter b: point B. 75 | - returns: the mean point of the two CGPoint objects. 76 | */ 77 | static func pointAverage(_ a: CGPoint, b: CGPoint) -> CGPoint { 78 | return CGPoint(x:(a.x + b.x) * 0.5, y:(a.y + b.y) * 0.5) 79 | } 80 | } 81 | 82 | extension CGVector { 83 | /** 84 | - parameter other: a vector. 85 | - returns: the dot product of the other vector with this vector. 86 | */ 87 | func dotProductWithVector(_ other: CGVector) -> CGFloat { 88 | return dx * other.dx + dy * other.dy 89 | } 90 | 91 | 92 | /** 93 | - parameter scale: how much to scale (e.g. 1.0, 1.5, 0.2, etc). 94 | - returns: a copy of this vector, rescaled by the amount given. 95 | */ 96 | func scaledByFloat(_ scale: CGFloat) -> CGVector { 97 | return CGVector(dx: dx * scale, dy: dy * scale) 98 | } 99 | 100 | /** 101 | - parameter radians: how many radians you want to rotate by. 102 | - returns: a copy of this vector, after being rotated in the 103 | "positive radians" direction by the amount given. 104 | - note: If your coordinate frame is right-handed, positive radians 105 | is counter-clockwise. 106 | */ 107 | func rotatedByRadians(_ radians: CGFloat) -> CGVector { 108 | let cosRadians = cos(radians) 109 | let sinRadians = sin(radians) 110 | 111 | return CGVector(dx: cosRadians * dx - sinRadians * dy, dy: sinRadians * dx + cosRadians * dy) 112 | } 113 | } 114 | 115 | extension CGPoint { 116 | /** 117 | - parameter a: point A. 118 | - parameter b: point B. 119 | - returns: The hypotenuse defined by the two. 120 | */ 121 | static func hypotenuse(_ a: CGPoint, b: CGPoint) -> Double { 122 | return Double(hypot(b.x - a.x, b.y - a.y)) 123 | } 124 | } 125 | 126 | extension MKMapRect { 127 | /** 128 | - returns: The point at the center of the rectangle. 129 | - parameter rect: A rectangle. 130 | */ 131 | func getCenter() -> MKMapPoint { 132 | return MKMapPointMake(MKMapRectGetMidX(self), MKMapRectGetMidY(self)) 133 | } 134 | 135 | /** 136 | - parameter rect: a rectangle. 137 | - returns: an MKMapRect converted to an MKPolygon. 138 | */ 139 | func polygonFromMapRect() -> MKPolygon { 140 | var corners = [MKMapPointMake(MKMapRectGetMaxX(self), MKMapRectGetMaxY(self)), 141 | MKMapPointMake(MKMapRectGetMinX(self), MKMapRectGetMaxY(self)), 142 | MKMapPointMake(MKMapRectGetMinX(self), MKMapRectGetMinY(self)), 143 | MKMapPointMake(MKMapRectGetMaxX(self), MKMapRectGetMinY(self))] 144 | 145 | return MKPolygon(points: &corners, count: corners.count) 146 | } 147 | } 148 | 149 | extension MKMapSize { 150 | /// - returns: The area of this MKMapSize object 151 | func area() -> Double { 152 | return height * width 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /heatmap/VisibleMapRegionDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This class manages an MKMapView camera scroll zoom by implementing 7 | the typical MKMapViewDelegate regionDidChangeAnimated and 8 | regionWillChangeAnimated to add bounce-back when the user 9 | scrolls/zooms away from the floorplan. 10 | */ 11 | 12 | import CoreLocation 13 | import Foundation 14 | import MapKit 15 | 16 | /** 17 | This class manages an MKMapView camera scroll & zoom by implementing the 18 | typical MKMapViewDelegate regionDidChangeAnimated and 19 | regionWillChangeAnimated to add bounce-back when the user scrolls/zooms away 20 | from the floorplan. 21 | */ 22 | class VisibleMapRegionDelegate: NSObject { 23 | 24 | /** 25 | Set to true if you would want reset the MapCamera to center on the 26 | floorplan. 27 | */ 28 | var needResetCameraOrientation = true 29 | 30 | /** 31 | Keep track of changes to [mapView camera].altitude so that we know 32 | whether to auto-zoom or auto-scroll. 33 | */ 34 | fileprivate var lastAltitude: CLLocationDistance 35 | 36 | /** 37 | Properties of the floorplan. See FloorplanOverlay for more. 38 | */ 39 | fileprivate var boundingMapRectIncludingRotations: MKMapRect 40 | fileprivate var boundingPDFBox: MKMapRectRotated 41 | fileprivate var floorplanCenter: CLLocationCoordinate2D! 42 | fileprivate var floorplanUprightMKMapCameraHeading: CLLocationDirection! 43 | 44 | /// Initializes on floorplan bounds. 45 | init(floorplanBounds: MKMapRect, boundingPDFBox: MKMapRectRotated, floorplanCenter: CLLocationCoordinate2D, floorplanUprightMKMapCameraHeading heading: CLLocationDirection) { 46 | boundingMapRectIncludingRotations = floorplanBounds 47 | self.boundingPDFBox = boundingPDFBox 48 | self.floorplanCenter = floorplanCenter 49 | floorplanUprightMKMapCameraHeading = heading 50 | 51 | lastAltitude = Double.nan 52 | 53 | needResetCameraOrientation = true 54 | } 55 | 56 | /** 57 | Resets the camera orientation to the floorplan on our mapview. 58 | - parameter mapView: MKMapView upon which we reset. 59 | */ 60 | func mapViewResetCameraToFloorplan(_ mapView: MKMapView) { 61 | resetCameraOrientation(mapView, center: floorplanCenter, heading: floorplanUprightMKMapCameraHeading) 62 | } 63 | 64 | /// Handles zoom and floorplan autofit. 65 | func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { 66 | let camera = mapView.camera 67 | 68 | var didClampZoom = false 69 | 70 | // Has the zoom level stabilized? 71 | if (lastAltitude != camera.altitude) { 72 | // Not yet! Someone is changing the zoom! 73 | lastAltitude = camera.altitude 74 | 75 | // Auto-zoom the camera to fit the floorplan. 76 | didClampZoom = clampZoomToFloorplan(mapView, floorplanBoundingMapRect: boundingMapRectIncludingRotations, floorplanCenter: floorplanCenter) 77 | } 78 | 79 | if (!didClampZoom) { 80 | // Once the zoom level has stabilized, auto-scroll if needed. 81 | clampScrollToFloorplan(mapView, floorplanBoundingPDFBoxRect: boundingPDFBox, optionalCameraHeading: needResetCameraOrientation ? floorplanUprightMKMapCameraHeading : Double.nan) 82 | needResetCameraOrientation = false 83 | } 84 | } 85 | 86 | /** 87 | Resets the camera orientation to the given centerpoint with the given 88 | heading/orientation. 89 | - parameter mapView: MapView which needs to be re-centered. 90 | - parameter center: new centerpoint. 91 | - parameter heading: orientation to use. 92 | */ 93 | func resetCameraOrientation(_ mapView: MKMapView, center: CLLocationCoordinate2D, heading: CLLocationDirection) { 94 | let newCamera = mapView.camera.copy() as! MKMapCamera 95 | // Center the floorplan... 96 | newCamera.centerCoordinate = center 97 | // ...and rotate so the floorplan is upright. 98 | newCamera.heading = heading 99 | 100 | mapView.setCamera(newCamera, animated: true) 101 | } 102 | 103 | /** 104 | - returns: `true` if the floorplan doesn't fill the screen. 105 | - parameter mapView: MapView to check. 106 | - parameter floorplanBoundingMapRect: MKMapRect that defines the 107 | floorplan's boundaries. 108 | */ 109 | func floorplanDoesNotFillScreen(_ mapView: MKMapView, floorplanBoundingMapRect: MKMapRect) -> Bool { 110 | 111 | if (MKMapRectContainsRect(floorplanBoundingMapRect, mapView.visibleMapRect)) { 112 | // Your view is already entirely inside the floorplan. 113 | return false 114 | } 115 | 116 | // The specific part of the floorplan that is currently visible. 117 | let visiblePartOfFloorplan = MKMapRectIntersection(floorplanBoundingMapRect, mapView.visibleMapRect) 118 | 119 | // The floorplan does not fill your screen in either direction. 120 | return ( 121 | (visiblePartOfFloorplan.size.width < mapView.visibleMapRect.size.width) 122 | && 123 | (visiblePartOfFloorplan.size.height < mapView.visibleMapRect.size.height) 124 | ) 125 | } 126 | 127 | /** 128 | Helper function for clampZoomToFloorplan() 129 | - returns: the MapCamera altitude required to bounce back the MapCamera 130 | zoom back onto the floorplan. if no zoom adjustment is needed, 131 | returns NAN. 132 | - parameter mapView: The MKMapView we're looking at 133 | - parameter floorplanBoundingMapRect: floorplan's bounding rectangle. 134 | */ 135 | func getZoomAdjustment(_ mapView: MKMapView, floorplanBoundingMapRect: MKMapRect) -> Double { 136 | let mapViewVisibleMapRectArea: Double = mapView.visibleMapRect.size.area() 137 | 138 | let maxZoomedOut: MKMapRect = mapView.mapRectThatFits(floorplanBoundingMapRect) 139 | let maxZoomedOutArea: Double = maxZoomedOut.size.area() 140 | 141 | if (maxZoomedOutArea < mapViewVisibleMapRectArea) { 142 | // You have zoomed out too far? 143 | 144 | let zoomFactor: Double = sqrt(maxZoomedOutArea / mapViewVisibleMapRectArea) 145 | let currentAltitude: CLLocationDistance = mapView.camera.altitude 146 | let newAltitude: CLLocationDistance = currentAltitude * zoomFactor 147 | 148 | let newAltitudeUsable: CLLocationDistance = newAltitude 149 | 150 | /** 151 | NOTE: Supposedly MapKit's internal zoom level counter is by 152 | powers of two, so a 0.5x buffer here is safe and should 153 | prevent pulsing when we're near the maximum zoom level. 154 | 155 | Assumption: We will never see a lowestGoodAltitude smaller than 156 | 0.5x a stable MapKit altitude. 157 | */ 158 | if (newAltitudeUsable < currentAltitude) { 159 | // Zoom back in. 160 | return newAltitudeUsable 161 | } 162 | } 163 | 164 | // No change. Return NAN. 165 | return Double.nan 166 | } 167 | 168 | /** 169 | Detect whether the user has zoomed away from the floorplan and, if so, bounce back. 170 | - returns: `true` if we needed to bounce back 171 | - parameter mapView: mapview we're working on 172 | - parameter floorplanBoundingMapRect: bounds of the floorplan 173 | - parameter floorplanCenter: center of the floorplan 174 | */ 175 | func clampZoomToFloorplan(_ mapView: MKMapView, floorplanBoundingMapRect: MKMapRect, floorplanCenter: CLLocationCoordinate2D) -> Bool { 176 | 177 | if (floorplanDoesNotFillScreen(mapView, floorplanBoundingMapRect: floorplanBoundingMapRect)) { 178 | // Clamp! 179 | 180 | let newAltitude: CLLocationDistance = getZoomAdjustment(mapView, floorplanBoundingMapRect: floorplanBoundingMapRect) 181 | 182 | if (!newAltitude.isNaN) { 183 | // We have a zoom change to make! 184 | 185 | let newCamera: MKMapCamera = mapView.camera.copy() as! MKMapCamera 186 | 187 | newCamera.altitude = newAltitude 188 | 189 | /** 190 | Since we've zoomed out enough to see the entire floorplan 191 | anyway, let's re-center to make sure the entire floorplan is 192 | actually on-screen. 193 | */ 194 | newCamera.centerCoordinate = floorplanCenter 195 | 196 | mapView.setCamera(newCamera, animated: true) 197 | 198 | return true 199 | } 200 | } 201 | 202 | // No zoom correction took place. 203 | return false 204 | } 205 | 206 | /** 207 | Detect whether the user has scrolled away from the floorplan, and if so, 208 | bounce back. 209 | - parameter mapView: The MapView to scroll. 210 | - parameter floorplanBoundingMapRect: A map rect that must be "in view" 211 | when the scrolling is complete. We will only scroll until this map 212 | rect enters the view. 213 | - parameter optionalCameraHeading: If you give valid CLLocationDirection 214 | we will also adjust the camera heading. If you give an invalid 215 | CLLocationDirection (e.g. -1.0), we'll keep whatever heading the 216 | camera already has. 217 | */ 218 | func clampScrollToFloorplan(_ mapView: MKMapView, floorplanBoundingPDFBoxRect: MKMapRectRotated, optionalCameraHeading: CLLocationDirection) { 219 | 220 | let rotationNeeded: Bool = 0.0 <= optionalCameraHeading && optionalCameraHeading < 360.0 221 | 222 | /** 223 | Assuming we are zoomed at the correct level, we still can't see the 224 | floorplan. Maybe you have scrolled too far? 225 | */ 226 | 227 | let visibleMapRectMid = MKMapPoint(x: MKMapRectGetMidX(mapView.visibleMapRect), y: MKMapRectGetMidY(mapView.visibleMapRect)) 228 | 229 | let visibleMapRectOriginProposed = MKMapRectRotatedNearestPoint(floorplanBoundingPDFBoxRect, point: visibleMapRectMid) 230 | 231 | let dxOffset = visibleMapRectOriginProposed.x - visibleMapRectMid.x 232 | let dyOffset = visibleMapRectOriginProposed.y - visibleMapRectMid.y 233 | 234 | // Okay, now we know the "proposed" scroll adjustment... 235 | 236 | let visibleMapRectMidPixels = mapView.convert(MKCoordinateForMapPoint(visibleMapRectMid), toPointTo: mapView) 237 | let visibleMapRectProposedPixels = mapView.convert(MKCoordinateForMapPoint(visibleMapRectOriginProposed), toPointTo: mapView) 238 | 239 | let scrollDistancePixels = CGPoint.hypotenuse(visibleMapRectProposedPixels, b: visibleMapRectMidPixels) 240 | 241 | /** 242 | ...but is it more than 1.0 screen pixel worth? (Otherwise the user 243 | probably wouldn't even notice) 244 | 245 | NOTE: Due to rounding errors it's hard to get exactly 246 | scrollDistancePixels == 0.0 anyway, so doing a check like this 247 | improves general responsiveness overall. 248 | */ 249 | let scrollNeeded = scrollDistancePixels > 1.0 250 | 251 | if (rotationNeeded || scrollNeeded) { 252 | let newCamera = mapView.camera.copy() as! MKMapCamera 253 | if (rotationNeeded) { 254 | // Rotation the camera (e.g. to make the floorplan upright). 255 | newCamera.heading = optionalCameraHeading 256 | } 257 | if (scrollNeeded) { 258 | // Scroll back toward the floorplan. 259 | var cameraCenter = MKMapPointForCoordinate(mapView.camera.centerCoordinate) 260 | cameraCenter.x += dxOffset 261 | cameraCenter.y += dyOffset 262 | newCamera.centerCoordinate = MKCoordinateForMapPoint(cameraCenter) 263 | } 264 | mapView.setCamera(newCamera, animated: true) 265 | } 266 | 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /manual.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const prompt = require('prompt'); 3 | 4 | const schema = { 5 | properties: { 6 | zone: { 7 | pattern: /[123456789]/, 8 | message: 'Zone must be from 1 - 9', 9 | maxLength: 1, 10 | required: true 11 | }, 12 | numberOfTriggers: { 13 | pattern: /^([0-9]|([1-9][0-9])|100)$/, 14 | message: 'Triggers sent at this zone must be from 1 - 100', 15 | required: true 16 | } 17 | } 18 | }; 19 | 20 | prompt.start(); 21 | function askUser() { 22 | 23 | console.log("==========") 24 | console.log("Enter zone number to populate and number of trigger eventsto be sent at that zone") 25 | console.log("Zones are from 1 - 9. Trigger events are from 0 - 99") 26 | console.log("Ctrl + D to exit.") 27 | prompt.get(schema, function (err, result) { 28 | // 29 | // Log the results. 30 | // 31 | console.log('Command-line input received:'); 32 | console.log(' zone: ' + result.zone); 33 | generateData(result.zone, result.numberOfTriggers) 34 | askUser(); 35 | }); 36 | } 37 | 38 | function getRandomInt(min, max) { 39 | min = Math.ceil(min); 40 | max = Math.floor(max); 41 | return Math.floor(Math.random() * (max - min + 1)) + min; 42 | } 43 | 44 | function generateData(userInput, numberOfTriggers) { 45 | let zoneNumber = 0 46 | if (userInput == 0) { 47 | zoneNumber = getRandomInt(1,9) 48 | } else { 49 | zoneNumber = userInput 50 | } 51 | 52 | let input = { 53 | zone: zoneNumber, 54 | event: "enter" 55 | } 56 | 57 | let options = { 58 | url: process.env.CF_APP_URL + "/triggers/add", 59 | method: "POST", 60 | body: JSON.stringify(input), 61 | headers: { 62 | 'Content-Type': 'application/json' 63 | } 64 | } 65 | 66 | console.log(zoneNumber) 67 | var requestsSent = 0 68 | 69 | while (requestsSent < numberOfTriggers) { 70 | request(options, function (err, _res, body) { 71 | if (err) _res.send(err); 72 | // console.log(body) 73 | }); 74 | requestsSent++; 75 | } 76 | } 77 | 78 | if (process.env.CF_APP_URL) { 79 | askUser() 80 | } else { 81 | console.log("-------------------------") 82 | console.log("Please set environment varialbe CF_APP_URL to the heatmap-backend you just deployed"); 83 | console.log("example:\nexport CF_APP_URL=https://heatmap-backend.mybluemix.net") 84 | console.log("-------------------------") 85 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heat-map", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ajv": { 8 | "version": "5.5.2", 9 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", 10 | "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", 11 | "requires": { 12 | "co": "4.6.0", 13 | "fast-deep-equal": "1.1.0", 14 | "fast-json-stable-stringify": "2.0.0", 15 | "json-schema-traverse": "0.3.1" 16 | } 17 | }, 18 | "asn1": { 19 | "version": "0.2.3", 20 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", 21 | "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" 22 | }, 23 | "assert-plus": { 24 | "version": "1.0.0", 25 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 26 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 27 | }, 28 | "async": { 29 | "version": "0.9.2", 30 | "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", 31 | "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" 32 | }, 33 | "asynckit": { 34 | "version": "0.4.0", 35 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 36 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 37 | }, 38 | "aws-sign2": { 39 | "version": "0.7.0", 40 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 41 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 42 | }, 43 | "aws4": { 44 | "version": "1.6.0", 45 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", 46 | "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" 47 | }, 48 | "balanced-match": { 49 | "version": "1.0.0", 50 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 51 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 52 | }, 53 | "bcrypt-pbkdf": { 54 | "version": "1.0.1", 55 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", 56 | "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", 57 | "optional": true, 58 | "requires": { 59 | "tweetnacl": "0.14.5" 60 | } 61 | }, 62 | "boom": { 63 | "version": "4.3.1", 64 | "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", 65 | "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", 66 | "requires": { 67 | "hoek": "4.2.1" 68 | } 69 | }, 70 | "brace-expansion": { 71 | "version": "1.1.11", 72 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 73 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 74 | "requires": { 75 | "balanced-match": "1.0.0", 76 | "concat-map": "0.0.1" 77 | } 78 | }, 79 | "caseless": { 80 | "version": "0.12.0", 81 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 82 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 83 | }, 84 | "co": { 85 | "version": "4.6.0", 86 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 87 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 88 | }, 89 | "colors": { 90 | "version": "1.2.0", 91 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.0.tgz", 92 | "integrity": "sha512-lweugcX5nailCqZBttArTojZZpHGWhmFJX78KJHlxwhM8tLAy5QCgRgRxrubrksdvA+2Y3inWG5TToyyjL82BQ==" 93 | }, 94 | "combined-stream": { 95 | "version": "1.0.6", 96 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", 97 | "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", 98 | "requires": { 99 | "delayed-stream": "1.0.0" 100 | } 101 | }, 102 | "concat-map": { 103 | "version": "0.0.1", 104 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 105 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 106 | }, 107 | "core-util-is": { 108 | "version": "1.0.2", 109 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 110 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 111 | }, 112 | "cryptiles": { 113 | "version": "3.1.2", 114 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", 115 | "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", 116 | "requires": { 117 | "boom": "5.2.0" 118 | }, 119 | "dependencies": { 120 | "boom": { 121 | "version": "5.2.0", 122 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 123 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 124 | "requires": { 125 | "hoek": "4.2.1" 126 | } 127 | } 128 | } 129 | }, 130 | "cycle": { 131 | "version": "1.0.3", 132 | "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", 133 | "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" 134 | }, 135 | "dashdash": { 136 | "version": "1.14.1", 137 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 138 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 139 | "requires": { 140 | "assert-plus": "1.0.0" 141 | } 142 | }, 143 | "deep-equal": { 144 | "version": "0.2.2", 145 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.2.2.tgz", 146 | "integrity": "sha1-hLdFiW80xoTpjyzg5Cq69Du6AX0=" 147 | }, 148 | "delayed-stream": { 149 | "version": "1.0.0", 150 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 151 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 152 | }, 153 | "ecc-jsbn": { 154 | "version": "0.1.1", 155 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", 156 | "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", 157 | "optional": true, 158 | "requires": { 159 | "jsbn": "0.1.1" 160 | } 161 | }, 162 | "extend": { 163 | "version": "3.0.1", 164 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", 165 | "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" 166 | }, 167 | "extsprintf": { 168 | "version": "1.3.0", 169 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 170 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 171 | }, 172 | "eyes": { 173 | "version": "0.1.8", 174 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 175 | "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" 176 | }, 177 | "fast-deep-equal": { 178 | "version": "1.1.0", 179 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", 180 | "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" 181 | }, 182 | "fast-json-stable-stringify": { 183 | "version": "2.0.0", 184 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 185 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 186 | }, 187 | "forever-agent": { 188 | "version": "0.6.1", 189 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 190 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 191 | }, 192 | "form-data": { 193 | "version": "2.3.2", 194 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", 195 | "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", 196 | "requires": { 197 | "asynckit": "0.4.0", 198 | "combined-stream": "1.0.6", 199 | "mime-types": "2.1.18" 200 | } 201 | }, 202 | "fs.realpath": { 203 | "version": "1.0.0", 204 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 205 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 206 | }, 207 | "getpass": { 208 | "version": "0.1.7", 209 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 210 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 211 | "requires": { 212 | "assert-plus": "1.0.0" 213 | } 214 | }, 215 | "glob": { 216 | "version": "7.1.2", 217 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 218 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 219 | "requires": { 220 | "fs.realpath": "1.0.0", 221 | "inflight": "1.0.6", 222 | "inherits": "2.0.3", 223 | "minimatch": "3.0.4", 224 | "once": "1.4.0", 225 | "path-is-absolute": "1.0.1" 226 | } 227 | }, 228 | "har-schema": { 229 | "version": "2.0.0", 230 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 231 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 232 | }, 233 | "har-validator": { 234 | "version": "5.0.3", 235 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", 236 | "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", 237 | "requires": { 238 | "ajv": "5.5.2", 239 | "har-schema": "2.0.0" 240 | } 241 | }, 242 | "hawk": { 243 | "version": "6.0.2", 244 | "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", 245 | "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", 246 | "requires": { 247 | "boom": "4.3.1", 248 | "cryptiles": "3.1.2", 249 | "hoek": "4.2.1", 250 | "sntp": "2.1.0" 251 | } 252 | }, 253 | "hoek": { 254 | "version": "4.2.1", 255 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 256 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" 257 | }, 258 | "http-signature": { 259 | "version": "1.2.0", 260 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 261 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 262 | "requires": { 263 | "assert-plus": "1.0.0", 264 | "jsprim": "1.4.1", 265 | "sshpk": "1.13.1" 266 | } 267 | }, 268 | "i": { 269 | "version": "0.3.6", 270 | "resolved": "https://registry.npmjs.org/i/-/i-0.3.6.tgz", 271 | "integrity": "sha1-2WyScyB28HJxG2sQ/X1PZa2O4j0=" 272 | }, 273 | "inflight": { 274 | "version": "1.0.6", 275 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 276 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 277 | "requires": { 278 | "once": "1.4.0", 279 | "wrappy": "1.0.2" 280 | } 281 | }, 282 | "inherits": { 283 | "version": "2.0.3", 284 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 285 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 286 | }, 287 | "is-typedarray": { 288 | "version": "1.0.0", 289 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 290 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 291 | }, 292 | "isstream": { 293 | "version": "0.1.2", 294 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 295 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 296 | }, 297 | "jsbn": { 298 | "version": "0.1.1", 299 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 300 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 301 | "optional": true 302 | }, 303 | "json-schema": { 304 | "version": "0.2.3", 305 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 306 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 307 | }, 308 | "json-schema-traverse": { 309 | "version": "0.3.1", 310 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", 311 | "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" 312 | }, 313 | "json-stringify-safe": { 314 | "version": "5.0.1", 315 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 316 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 317 | }, 318 | "jsprim": { 319 | "version": "1.4.1", 320 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 321 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 322 | "requires": { 323 | "assert-plus": "1.0.0", 324 | "extsprintf": "1.3.0", 325 | "json-schema": "0.2.3", 326 | "verror": "1.10.0" 327 | } 328 | }, 329 | "mime-db": { 330 | "version": "1.33.0", 331 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 332 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 333 | }, 334 | "mime-types": { 335 | "version": "2.1.18", 336 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 337 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 338 | "requires": { 339 | "mime-db": "1.33.0" 340 | } 341 | }, 342 | "minimatch": { 343 | "version": "3.0.4", 344 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 345 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 346 | "requires": { 347 | "brace-expansion": "1.1.11" 348 | } 349 | }, 350 | "minimist": { 351 | "version": "0.0.8", 352 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 353 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 354 | }, 355 | "mkdirp": { 356 | "version": "0.5.1", 357 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 358 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 359 | "requires": { 360 | "minimist": "0.0.8" 361 | } 362 | }, 363 | "mute-stream": { 364 | "version": "0.0.7", 365 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", 366 | "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" 367 | }, 368 | "ncp": { 369 | "version": "1.0.1", 370 | "resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", 371 | "integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=" 372 | }, 373 | "oauth-sign": { 374 | "version": "0.8.2", 375 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", 376 | "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" 377 | }, 378 | "once": { 379 | "version": "1.4.0", 380 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 381 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 382 | "requires": { 383 | "wrappy": "1.0.2" 384 | } 385 | }, 386 | "path-is-absolute": { 387 | "version": "1.0.1", 388 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 389 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 390 | }, 391 | "performance-now": { 392 | "version": "2.1.0", 393 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 394 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 395 | }, 396 | "pkginfo": { 397 | "version": "0.4.1", 398 | "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz", 399 | "integrity": "sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=" 400 | }, 401 | "prompt": { 402 | "version": "1.0.0", 403 | "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.0.0.tgz", 404 | "integrity": "sha1-jlcSPDlquYiJf7Mn/Trtw+c15P4=", 405 | "requires": { 406 | "colors": "1.2.0", 407 | "pkginfo": "0.4.1", 408 | "read": "1.0.7", 409 | "revalidator": "0.1.8", 410 | "utile": "0.3.0", 411 | "winston": "2.1.1" 412 | } 413 | }, 414 | "punycode": { 415 | "version": "1.4.1", 416 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 417 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 418 | }, 419 | "qs": { 420 | "version": "6.5.1", 421 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 422 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 423 | }, 424 | "read": { 425 | "version": "1.0.7", 426 | "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", 427 | "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", 428 | "requires": { 429 | "mute-stream": "0.0.7" 430 | } 431 | }, 432 | "request": { 433 | "version": "2.83.0", 434 | "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", 435 | "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", 436 | "requires": { 437 | "aws-sign2": "0.7.0", 438 | "aws4": "1.6.0", 439 | "caseless": "0.12.0", 440 | "combined-stream": "1.0.6", 441 | "extend": "3.0.1", 442 | "forever-agent": "0.6.1", 443 | "form-data": "2.3.2", 444 | "har-validator": "5.0.3", 445 | "hawk": "6.0.2", 446 | "http-signature": "1.2.0", 447 | "is-typedarray": "1.0.0", 448 | "isstream": "0.1.2", 449 | "json-stringify-safe": "5.0.1", 450 | "mime-types": "2.1.18", 451 | "oauth-sign": "0.8.2", 452 | "performance-now": "2.1.0", 453 | "qs": "6.5.1", 454 | "safe-buffer": "5.1.1", 455 | "stringstream": "0.0.5", 456 | "tough-cookie": "2.3.4", 457 | "tunnel-agent": "0.6.0", 458 | "uuid": "3.2.1" 459 | } 460 | }, 461 | "revalidator": { 462 | "version": "0.1.8", 463 | "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", 464 | "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=" 465 | }, 466 | "rimraf": { 467 | "version": "2.6.2", 468 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", 469 | "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", 470 | "requires": { 471 | "glob": "7.1.2" 472 | } 473 | }, 474 | "safe-buffer": { 475 | "version": "5.1.1", 476 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 477 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 478 | }, 479 | "sntp": { 480 | "version": "2.1.0", 481 | "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", 482 | "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", 483 | "requires": { 484 | "hoek": "4.2.1" 485 | } 486 | }, 487 | "sshpk": { 488 | "version": "1.13.1", 489 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", 490 | "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", 491 | "requires": { 492 | "asn1": "0.2.3", 493 | "assert-plus": "1.0.0", 494 | "bcrypt-pbkdf": "1.0.1", 495 | "dashdash": "1.14.1", 496 | "ecc-jsbn": "0.1.1", 497 | "getpass": "0.1.7", 498 | "jsbn": "0.1.1", 499 | "tweetnacl": "0.14.5" 500 | } 501 | }, 502 | "stack-trace": { 503 | "version": "0.0.10", 504 | "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", 505 | "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" 506 | }, 507 | "stringstream": { 508 | "version": "0.0.5", 509 | "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", 510 | "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" 511 | }, 512 | "tough-cookie": { 513 | "version": "2.3.4", 514 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", 515 | "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", 516 | "requires": { 517 | "punycode": "1.4.1" 518 | } 519 | }, 520 | "tunnel-agent": { 521 | "version": "0.6.0", 522 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 523 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 524 | "requires": { 525 | "safe-buffer": "5.1.1" 526 | } 527 | }, 528 | "tweetnacl": { 529 | "version": "0.14.5", 530 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 531 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 532 | "optional": true 533 | }, 534 | "utile": { 535 | "version": "0.3.0", 536 | "resolved": "https://registry.npmjs.org/utile/-/utile-0.3.0.tgz", 537 | "integrity": "sha1-E1LDQOuCDk2N26A5pPv6oy7U7zo=", 538 | "requires": { 539 | "async": "0.9.2", 540 | "deep-equal": "0.2.2", 541 | "i": "0.3.6", 542 | "mkdirp": "0.5.1", 543 | "ncp": "1.0.1", 544 | "rimraf": "2.6.2" 545 | } 546 | }, 547 | "uuid": { 548 | "version": "3.2.1", 549 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", 550 | "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" 551 | }, 552 | "verror": { 553 | "version": "1.10.0", 554 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 555 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 556 | "requires": { 557 | "assert-plus": "1.0.0", 558 | "core-util-is": "1.0.2", 559 | "extsprintf": "1.3.0" 560 | } 561 | }, 562 | "winston": { 563 | "version": "2.1.1", 564 | "resolved": "https://registry.npmjs.org/winston/-/winston-2.1.1.tgz", 565 | "integrity": "sha1-PJNJ0ZYgf9G9/51LxD73JRDjoS4=", 566 | "requires": { 567 | "async": "1.0.0", 568 | "colors": "1.0.3", 569 | "cycle": "1.0.3", 570 | "eyes": "0.1.8", 571 | "isstream": "0.1.2", 572 | "pkginfo": "0.3.1", 573 | "stack-trace": "0.0.10" 574 | }, 575 | "dependencies": { 576 | "async": { 577 | "version": "1.0.0", 578 | "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", 579 | "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" 580 | }, 581 | "colors": { 582 | "version": "1.0.3", 583 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", 584 | "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" 585 | }, 586 | "pkginfo": { 587 | "version": "0.3.1", 588 | "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", 589 | "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=" 590 | } 591 | } 592 | }, 593 | "wrappy": { 594 | "version": "1.0.2", 595 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 596 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 597 | } 598 | } 599 | } 600 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heat-map", 3 | "main": "app.js", 4 | "description": "heat-map", 5 | "version": "1.0.0", 6 | "private": false, 7 | "engines": { 8 | "node": "6.*" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/IBM/ios-mapkit-heat-map" 13 | }, 14 | "dependencies": { 15 | "prompt": "^1.0.0", 16 | "request": "^2.83.0" 17 | }, 18 | "author": "Anthony Amanse", 19 | "license": "Apache-2.0" 20 | } 21 | -------------------------------------------------------------------------------- /random.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | 3 | function getRandomInt(min, max) { 4 | min = Math.ceil(min); 5 | max = Math.floor(max); 6 | return Math.floor(Math.random() * (max - min + 1)) + min; 7 | } 8 | 9 | function generateData() { 10 | let zoneNumber = getRandomInt(1,9) 11 | let numberOfTriggers = getRandomInt(1,10) 12 | let input = { 13 | zone: zoneNumber, 14 | event: "enter" 15 | } 16 | 17 | let options = { 18 | url: process.env.CF_APP_URL + "/triggers/add", 19 | method: "POST", 20 | body: JSON.stringify(input), 21 | headers: { 22 | 'Content-Type': 'application/json' 23 | } 24 | } 25 | 26 | console.log("Sending " + numberOfTriggers + " number of events to zone: " +zoneNumber) 27 | var requestsSent = 0 28 | 29 | while (requestsSent < numberOfTriggers) { 30 | request(options, function (err, _res, body) { 31 | if (err) _res.send(err); 32 | // console.log(body) 33 | }); 34 | requestsSent++; 35 | } 36 | } 37 | 38 | if (process.env.CF_APP_URL) { 39 | setInterval(generateData, 1000) 40 | } 41 | else { 42 | console.log("-------------------------") 43 | console.log("Please set environment varialbe CF_APP_URL to the heatmap-backend you just deployed"); 44 | console.log("example:\nexport CF_APP_URL=https://heatmap-backend.mybluemix.net") 45 | console.log("-------------------------") 46 | } 47 | -------------------------------------------------------------------------------- /real-data.js: -------------------------------------------------------------------------------- 1 | /*global db:false ISODate:false */ 2 | 3 | db.beacons.insert({"beaconId":"BEACON1","key":"zone_one", "value":"Small labs 1", "zone":1, "beaconid":"ef60e28ce1589d37be7ce20b74426705", "color":"lemon","x":0, "y":0,"width":400,"height":400}); 4 | db.beacons.insert({"beaconId":"BEACON2","key":"zone_two", "value":"Small labs 2", "zone":2, "beaconid":"5b5e1e2adbdec0d49f87637ddbf4dc21", "color":"beetroot","x":400, "y":0,"width":400,"height":400}); 5 | db.beacons.insert({"beaconId":"BEACON3","key":"zone_three", "value":"Small labs 3", "zone":3, "beaconid":"d7f6c128a30cae9ee58f42ca5177d525", "color":"candyfloss","x":800, "y":0,"width":400,"height":400}); 6 | db.beacons.insert({"beaconId":"BEACON4","key":"zone_four", "value":"Group labs", "zone":4, "beaconid":"1e6e643ca73c29c3aeaddac0b4c47e0a", "color":"lemon","x":0, "y":400,"width":400,"height":400}); 7 | db.beacons.insert({"beaconId":"BEACON5","key":"zone_five", "value":"Hall of fame", "zone":5, "beaconid":"39582382b7144c65f3fdaa4161163a07", "color":"candyfloss","x":400, "y":400,"width":400,"height":400}); 8 | db.beacons.insert({"beaconId":"BEACON6","key":"zone_six", "value":"Think Academy Media", "zone":6, "beaconid":"0d4fba2b6d28a63365fe91108907a51b", "color":"beetroot","x":800, "y":400,"width":400,"height":400}); 9 | db.beacons.insert({"beaconId":"BEACON7","key":"zone_seven", "value":"Open labs", "zone":7, "beaconid":"b2b8bc8f5587b4c94d5bab151815a505", "color":"candyfloss","x":0, "y":800,"width":400,"height":400}); 10 | db.beacons.insert({"beaconId":"BEACON8","key":"zone_eight", "value":"Watson area", "zone":8, "beaconid":"33f4af993cb3b4cde8f7d6b5b6d9e034", "color":"lemon","x":400, "y":800,"width":400,"height":400}); 11 | db.beacons.insert({"beaconId":"BEACON9","key":"zone_nine", "value":"Hangout", "zone":9, "beaconid":"a815cf0e8806d120f0335580b9b8ac35", "color":"beetroot","x":800, "y":800,"width":400,"height":400}); 12 | 13 | db.booths.insert({ "boothId" : "THINK1", "unit" : "Secret Map", "description" : "AR, Kubernetes, Blockchain, IoT, etc!", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x" : 843, "y" : 764, "width" : 140, "height" : 58}, "contact" : "John Doe" }); 14 | db.booths.insert({ "boothId" : "THINK2", "unit" : "Server Challenge", "description" : "Challenge the Server", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x" : 925, "y" : 832, "width" : 58, "height" : 58}, "contact" : "Mary Jane" }); 15 | db.booths.insert({ "boothId" : "THINK3", "unit" : "Infinity Portal", "description" : "To infinity and beyond!", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x" : 659, "y" : 761, "width" : 87, "height" : 87}, "contact" : "Jane Doe" }); 16 | db.booths.insert({ "boothId" : "THINK4", "unit" : "With Watson Studio", "description" : "Watson space", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x" : 419, "y" : 909, "width" : 146, "height" : 146}, "contact" : "Smith John" }); 17 | db.booths.insert({ "boothId" : "THINK5", "unit" : "Star Trek VR", "description" : "Star Trek meets Watson", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x" : 843, "y" : 993, "width" : 58, "height" : 58}, "contact" : "Catherine May" }); 18 | db.booths.insert({ "boothId" : "THINK6", "unit" : "TJ Bot", "description" : "Play with TJ Bot", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x" : 925, "y" : 993, "width" : 58, "height" : 58}, "contact" : "Ben Jerry" }); 19 | db.booths.insert({ "boothId" : "THINK7", "unit" : "Games", "description" : "Play the classic Connect 4", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x" : 615, "y" : 1122, "width" : 175, "height" : 58}, "contact" : "Joe Myers" }); 20 | db.booths.insert({ "boothId" : "THINK10", "unit" : "Food Beverages", "description" : "Have a break!", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 420, "y": 1113, "width": 146, "height": 58}, "contact" : "Joe Myers" }); 21 | db.booths.insert({ "boothId" : "THINK11", "unit" : "Watson Demos", "description" : "Some cool Watson Demos", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 419, "y": 761, "width": 146, "height": 118}, "contact" : "Joe Myers" }); 22 | db.booths.insert({ "boothId" : "THINK12", "unit" : "Softskills", "description" : "Softskills", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 869, "y": 640, "width": 105, "height": 46}, "contact" : "Joe Myers" }); 23 | db.booths.insert({ "boothId" : "THINK13", "unit" : "Games", "description" : "Some cool tabletop games", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 583, "y": 638, "width": 238, "height": 51}, "contact" : "Joe Myers" }); 24 | db.booths.insert({ "boothId" : "THINK14", "unit" : "Softskills", "description" : "Softskills", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 431, "y": 638, "width": 105, "height": 50}, "contact" : "Joe Myers" }); 25 | db.booths.insert({ "boothId" : "THINK15", "unit" : "Softskills", "description" : "Softskills", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 869, "y": 388, "width": 105, "height": 50}, "contact" : "Joe Myers" }); 26 | db.booths.insert({ "boothId" : "THINK16", "unit" : "Softskills", "description" : "Softskills", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 431, "y": 388, "width": 105, "height": 50}, "contact" : "Joe Myers" }); 27 | db.booths.insert({ "boothId" : "THINK17", "unit" : "Lounge", "description" : "Chill in the Lounge", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 580, "y": 388, "width": 242, "height": 53}, "contact" : "Joe Myers" }); 28 | db.booths.insert({ "boothId" : "THINK18", "unit" : "Hall of Fame", "description" : "Hall of Fame", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 583, "y": 480, "width": 238, "height": 117}, "contact" : "Joe Myers" }); 29 | db.booths.insert({ "boothId" : "THINK19", "unit" : "Think Academy Media", "description" : "Come here to learn", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 869, "y": 480, "width": 105, "height": 117}, "contact" : "Joe Myers" }); 30 | db.booths.insert({ "boothId" : "THINK20", "unit" : "Lunch and Learn", "description" : "Eat while you learn cool stuff", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 431, "y": 480, "width": 107, "height": 117}, "contact" : "Joe Myers" }); 31 | db.booths.insert({ "boothId" : "THINK21", "unit" : "Team Labs", "description" : "Lab with others", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 324, "y": 352, "width": 29, "height": 117}, "contact" : "Joe Myers" }); 32 | db.booths.insert({ "boothId" : "THINK22", "unit" : "Team Labs", "description" : "Lab with others", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 324, "y": 509, "width": 29, "height": 58}, "contact" : "Joe Myers" }); 33 | db.booths.insert({ "boothId" : "THINK23", "unit" : "Team Labs", "description" : "Lab with others", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 324, "y": 607, "width": 29, "height": 114}, "contact" : "Joe Myers" }); 34 | db.booths.insert({ "boothId" : "THINK24", "unit" : "Open Labs", "description" : "Lab in the open", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 106, "y": 761, "width": 246, "height": 248}, "contact" : "Joe Myers" }); 35 | db.booths.insert({ "boothId" : "THINK25", "unit" : "Ask Me Anything", "description" : "Ask me some interesting questions", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 615, "y": 878, "width": 175, "height": 175}, "contact" : "Joe Myers" }); 36 | db.booths.insert({ "boothId" : "THINK26", "unit" : "Certs Test Center", "description" : "Certifications", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 1676, "y": 716, "width": 363, "height": 337}, "contact" : "Joe Myers" }); 37 | db.booths.insert({ "boothId" : "THINK27", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 1483, "y": 397, "width": 93, "height": 231}, "contact" : "Joe Myers" }); 38 | db.booths.insert({ "boothId" : "THINK28", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 1577, "y": 397, "width": 93, "height": 231}, "contact" : "Joe Myers" }); 39 | db.booths.insert({ "boothId" : "THINK29", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 1702, "y": 397, "width": 93, "height": 231}, "contact" : "Joe Myers" }); 40 | db.booths.insert({ "boothId" : "THINK30", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 1796, "y": 397, "width": 93, "height": 231}, "contact" : "Joe Myers" }); 41 | db.booths.insert({ "boothId" : "THINK31", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 1921, "y": 397, "width": 93, "height": 231}, "contact" : "Joe Myers" }); 42 | db.booths.insert({ "boothId" : "THINK32", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 2014, "y": 397, "width": 93, "height": 231}, "contact" : "Joe Myers" }); 43 | db.booths.insert({ "boothId" : "THINK33", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 1402, "y": 748, "width": 231, "height": 96}, "contact" : "Joe Myers" }); 44 | db.booths.insert({ "boothId" : "THINK34", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 1402, "y": 839, "width": 231, "height": 96}, "contact" : "Joe Myers" }); 45 | db.booths.insert({ "boothId" : "THINK35", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 2084, "y": 799, "width": 94, "height": 231}, "contact" : "Joe Myers" }); 46 | db.booths.insert({ "boothId" : "THINK36", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 2179, "y": 799, "width": 94, "height": 231}, "contact" : "Joe Myers" }); 47 | db.booths.insert({ "boothId" : "THINK37", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 117, "y": 639, "width": 130, "height": 86}, "contact" : "Joe Myers" }); 48 | db.booths.insert({ "boothId" : "THINK38", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 117, "y": 554, "width": 130, "height": 86}, "contact" : "Joe Myers" }); 49 | db.booths.insert({ "boothId" : "THINK39", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 117, "y": 433, "width": 130, "height": 86}, "contact" : "Joe Myers" }); 50 | db.booths.insert({ "boothId" : "THINK40", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 117, "y": 347, "width": 130, "height": 86}, "contact" : "Joe Myers" }); 51 | db.booths.insert({ "boothId" : "THINK41", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 190, "y": 165, "width": 84, "height": 126}, "contact" : "Joe Myers" }); 52 | db.booths.insert({ "boothId" : "THINK42", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 316, "y": 165, "width": 84, "height": 126}, "contact" : "Joe Myers" }); 53 | db.booths.insert({ "boothId" : "THINK43", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 401, "y": 165, "width": 84, "height": 126}, "contact" : "Joe Myers" }); 54 | db.booths.insert({ "boothId" : "THINK44", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 530, "y": 165, "width": 84, "height": 126}, "contact" : "Joe Myers" }); 55 | db.booths.insert({ "boothId" : "THINK45", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 616, "y": 165, "width": 84, "height": 126}, "contact" : "Joe Myers" }); 56 | db.booths.insert({ "boothId" : "THINK46", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 744, "y": 165, "width": 84, "height": 126}, "contact" : "Joe Myers" }); 57 | db.booths.insert({ "boothId" : "THINK47", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 829, "y": 165, "width": 84, "height": 126}, "contact" : "Joe Myers" }); 58 | db.booths.insert({ "boothId" : "THINK48", "unit" : "Labs", "description" : "Lab in a slightly smaller room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 1007, "y": 165, "width": 84, "height": 126}, "contact" : "Joe Myers" }); 59 | db.booths.insert({ "boothId" : "THINK49", "unit" : "Labs", "description" : "Lab in a large room", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 1402, "y": 936, "width": 231, "height": 96}, "contact" : "Joe Myers" }); 60 | db.booths.insert({ "boothId" : "THINK50", "unit" : "Food Beverages", "description" : "Have a break!", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 840, "y": 1113, "width": 117, "height": 58}, "contact" : "Joe Myers" }); 61 | db.booths.insert({ "boothId" : "THINK51", "unit" : "Snack Tracker", "description" : "Have a snack!", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 902, "y": 923, "width": 23, "height": 36}, "contact" : "Joe Myers" }); 62 | db.booths.insert({ "boothId" : "THINK52", "unit" : "Hacked Two Minutes", "description" : "Hacked in two minutes!", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 843, "y": 832, "width": 58, "height": 58}, "contact" : "Joe Myers" }); 63 | db.booths.insert({ "boothId" : "THINK53", "unit" : "X-Force Command Center", "description" : "Command Center", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 840, "y": 1218, "width": 88, "height": 88}, "contact" : "Joe Myers" }); 64 | db.booths.insert({ "boothId" : "THINK8", "unit" : "Hands-on Labs", "description" : "Have a break!", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 447, "y": 1218, "width": 118, "height": 89}, "contact" : "Joe Myers" }); 65 | db.booths.insert({ "boothId" : "THINK9", "unit" : "Tech Talks", "description" : "Talks about cool technologies", "measurementUnit" : "metre", "shape" : {"type": "rectangle", "x": 615, "y": 1218, "width": 177, "height": 89}, "contact" : "Joe Myers" }); 66 | 67 | // add anchor points and latitude longitude 68 | db.events.insert({ "eventId" : "think-dev-area", "eventName" : "Think", "location" : "Las Vegas", "x" : 2388, "y" : 1461, "fromAnchorLatitude": 36.086811, "fromAnchorLongitude": -115.177325, "fromAnchorSVGPointX": 0, "fromAnchorSVGPointY": 7, "toAnchorLatitude": 36.086811, "toAnchorLongitude": -115.178535, "toAnchorSVGPointX": 2213, "toAnchorSVGPointY":7, "startDate" : ISODate("2018-02-19T00:00:00Z"), "endDate" : ISODate("2018-02-22T00:00:00Z"), "beacons" : [], "map" : [] }); 69 | 70 | let booths = db.booths.find({"boothId": {$in: ["THINK1","THINK2","THINK3","THINK4","THINK5","THINK6","THINK7","THINK8","THINK9","THINK10","THINK11","THINK12","THINK13","THINK14","THINK15","THINK16","THINK17","THINK18","THINK19","THINK20","THINK21","THINK22","THINK23","THINK24","THINK25","THINK26","THINK27","THINK28","THINK29","THINK30","THINK31","THINK32","THINK33","THINK34","THINK35","THINK36","THINK37","THINK38","THINK39","THINK40","THINK41","THINK42","THINK43","THINK44","THINK45","THINK46","THINK47","THINK48","THINK49","THINK50","THINK51","THINK52","THINK53"]}}).toArray(); 71 | let beacons = db.beacons.find({"beaconId": {$in: ["BEACON1","BEACON2","BEACON3","BEACON4","BEACON5","BEACON6","BEACON7","BEACON8","BEACON9"]}}).toArray(); 72 | 73 | db.events.update({eventId: "think-dev-area"}, {$set: {"beacons": beacons, "map": booths}}); 74 | 75 | --------------------------------------------------------------------------------