├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── .npmignore ├── LICENSE.txt ├── README.md ├── dist ├── L.Deflate.js └── L.Deflate.js.map ├── example ├── circle-marker.html ├── custom-marker-function.html ├── custom-marker.html ├── event.html ├── geojson-event.html ├── geojson-two-layers-popup.html ├── geojson-two-layers-tooltip.html ├── geojson-two-layers.html ├── geojson.html ├── greedy.html ├── hamburch.js ├── huge.html ├── img │ └── marker.png ├── leaflet-draw.html ├── markercluster-freezable.html ├── markercluster-geojson.html ├── markercluster.html ├── polyline-decorator.html ├── simple-tooltip.html └── simple.html ├── package-lock.json ├── package.json ├── src └── L.Deflate.js ├── tests ├── L.Deflate.test.js ├── fixtures.js └── types.ts ├── tsconfig.json └── types └── index.d.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "parserOptions": { 4 | "ecmaVersion": 2018, 5 | "sourceType": "script" 6 | }, 7 | "globals": { 8 | "L": true, 9 | "describe": false, 10 | "beforeEach": false, 11 | "afterEach": false, 12 | "jest": false, 13 | "test": false, 14 | "expect": false, 15 | "document": false 16 | }, 17 | "rules": { 18 | "no-underscore-dangle": "off", 19 | "no-var": "off", 20 | "func-names": "off", 21 | "object-shorthand": "off", 22 | "prefer-destructuring": "off", 23 | "no-param-reassign": "off", 24 | "prefer-arrow-callback": "off", 25 | "strict": 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at oliver.roick@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Leaflet.Deflate is a small project; contributing to the project is simple. To get your changes accepted, follow a few guidelines: 4 | 5 | - Make sure your commit messages are in the correct format. Use [Conventional Commits](https://www.conventionalcommits.org/). 6 | - Add the necessary tests for your changes. 7 | - For modifications to Leaflet.Deflate's API, add necessary changes to the TypeScript declaration. 8 | - Add documentation to the README if necessary. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Example** 27 | If applicable, provide example code demonstrating the issue; for example, in a JSFiddle. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Browser [e.g. chrome, safari] 32 | - Version [e.g. 22] 33 | 34 | **Smartphone (please complete the following information):** 35 | - Device: [e.g. iPhone6] 36 | - OS: [e.g. iOS8.1] 37 | - Browser [e.g. stock browser, safari] 38 | - Version [e.g. 22] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Describe the change** 2 | 3 | A clear and concise description of the change. Include why you made the change. If the change resolves a bug report or feature request, include a link to the issue. 4 | 5 | **Checklist** 6 | 7 | - [ ] Lint and unit tests pass locally with my changes 8 | - [ ] I have added tests that prove my fix is effective or that my feature works 9 | - [ ] I have made added necessary changes to the TypeScript declaration (if appropriate) 10 | - [ ] I have added necessary documentation (if appropriate) 11 | 12 | **Further comments** 13 | 14 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered. 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | env: 12 | NODE: 16 13 | 14 | jobs: 15 | prep: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Cancel Previous Runs 20 | uses: styfle/cancel-workflow-action@0.8.0 21 | with: 22 | access_token: ${{ github.token }} 23 | 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | 27 | - name: Setup node ${{ env.NODE }} 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ env.NODE }} 31 | 32 | - name: Cache Node Modules 33 | uses: actions/cache@v2 34 | id: cache-node-modules 35 | with: 36 | path: node_modules 37 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 38 | restore-keys: | 39 | ${{ runner.os }}-node- 40 | 41 | - name: Install 42 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 43 | run: npm ci 44 | test: 45 | name: Test 46 | runs-on: ubuntu-latest 47 | needs: prep 48 | steps: 49 | - name: Checkout repository 50 | uses: actions/checkout@v2 51 | - name: Setup node ${{ env.NODE }} 52 | uses: actions/setup-node@v1 53 | with: 54 | node-version: ${{ env.NODE }} 55 | - name: Cache Node Modules 56 | uses: actions/cache@v2 57 | id: cache-node-modules 58 | with: 59 | path: node_modules 60 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 61 | restore-keys: | 62 | ${{ runner.os }}-node- 63 | - name: Install 64 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 65 | run: npm ci 66 | - name: test 67 | run: npm test 68 | env: 69 | CI: true 70 | lint: 71 | name: Lint 72 | runs-on: ubuntu-latest 73 | needs: prep 74 | steps: 75 | - name: Checkout repository 76 | uses: actions/checkout@v2 77 | - name: Setup node ${{ env.NODE }} 78 | uses: actions/setup-node@v1 79 | with: 80 | node-version: ${{ env.NODE }} 81 | - name: Cache Node Modules 82 | uses: actions/cache@v2 83 | id: cache-node-modules 84 | with: 85 | path: node_modules 86 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 87 | restore-keys: | 88 | ${{ runner.os }}-node- 89 | - name: Install 90 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 91 | run: npm ci 92 | - name: lint 93 | run: npm run lint 94 | env: 95 | CI: true 96 | typescript: 97 | name: TypeScript compile 98 | runs-on: ubuntu-latest 99 | needs: prep 100 | steps: 101 | - name: Checkout repository 102 | uses: actions/checkout@v2 103 | - name: Setup node ${{ env.NODE }} 104 | uses: actions/setup-node@v1 105 | with: 106 | node-version: ${{ env.NODE }} 107 | - name: Cache Node Modules 108 | uses: actions/cache@v2 109 | id: cache-node-modules 110 | with: 111 | path: node_modules 112 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 113 | restore-keys: | 114 | ${{ runner.os }}-node- 115 | - name: Install 116 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 117 | run: npm ci 118 | - name: lint 119 | run: tsc 120 | env: 121 | CI: true 122 | build-test: 123 | name: Build and test 124 | runs-on: ubuntu-latest 125 | needs: prep 126 | steps: 127 | - name: Checkout repository 128 | uses: actions/checkout@v2 129 | - name: Setup node ${{ env.NODE }} 130 | uses: actions/setup-node@v1 131 | with: 132 | node-version: ${{ env.NODE }} 133 | - name: Cache Node Modules 134 | uses: actions/cache@v2 135 | id: cache-node-modules 136 | with: 137 | path: node_modules 138 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 139 | restore-keys: | 140 | ${{ runner.os }}-node- 141 | - name: Install 142 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 143 | run: npm ci 144 | - name: build and test 145 | run: | 146 | npm run dist 147 | TARGET=dist npm test 148 | env: 149 | CI: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | test-output 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | tests 3 | tsconfig.json 4 | .github 5 | .eslintrc.json 6 | .travis.yml 7 | test-output 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leaflet.Deflate 2 | 3 | [![npm version](https://badge.fury.io/js/Leaflet.Deflate.svg)](https://badge.fury.io/js/Leaflet.Deflate) 4 | 5 | Leaflet.Deflate is a plugin for [Leaflet](https://leafletjs.com/) that improves the readability of large-scale web maps. It substitutes polygons and lines with markers when their screen size falls below a defined threshold. 6 | 7 | ![Example](https://user-images.githubusercontent.com/159510/52898557-a491d700-31df-11e9-86c4-7a585dc50372.gif) 8 | 9 | **Note:** The documentation and examples below are for Leaflet.Deflate's latest release. [Documentation of older releases is available](#previous-releases). 10 | 11 | ## Installation 12 | 13 | ### Using a hosted version 14 | 15 | Include the source into the `head` section of your document. 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | ### Install via NPM 22 | 23 | If you use the [npm package manager](https://www.npmjs.com/), you can fetch a local copy by running: 24 | 25 | ```bash 26 | npm install Leaflet.Deflate 27 | ``` 28 | 29 | You will find a copy of the release files in `node_modules/Leaflet.Deflate/dist`. 30 | 31 | ## API 32 | 33 | ### `L.deflate` 34 | 35 | `L.deflate` is the main class of `Leaflet.Deflate`. Use it to create a feature group that deflates all layers added to the group. 36 | 37 | #### Usage example 38 | 39 | Initialize `L.deflate` and add it to your map. Then add layers you want to deflate. 40 | 41 | ```javascript 42 | const map = L.map("map"); 43 | const features = L.deflate({minSize: 10}) 44 | features.addTo(map); 45 | 46 | // add layers 47 | const polygon = L.polygon([ 48 | [51.509, -0.08], 49 | [51.503, -0.06], 50 | [51.51, -0.047] 51 | ]) 52 | .addTo(features); 53 | 54 | // works with GeoJSONLayer too 55 | L.geoJson(json).addTo(features); 56 | ``` 57 | 58 | #### Creation 59 | 60 | Factory | Description 61 | ---------------------------- | ------------- 62 | `L.deflate( options)` | Creates a new deflatable feature group, optionally given an options object. 63 | 64 | #### Options 65 | 66 | Option | Type | Default | Description 67 | --------------- | --------- | ------- | ------------- 68 | `minSize` | `int` | `20` | Optional. Defines the minimum width and height in pixels for a path to be displayed in its actual shape. Anything smaller than the defined `minSize` will be deflated. 69 | `markerType` | `object` | `L.marker` | Optional. Specifies the marker type to use for deflated features. Must be either `L.marker` or `L.circleMarker`. 70 | `markerOptions` | `object` or `function` | `{}` | Optional. Customize the markers of deflated features using [Leaflet marker options](http://leafletjs.com/reference-1.3.0.html#marker). If you specify `L.circleMarker` as `markerType` use [Leaflet circleMarker options](https://leafletjs.com/reference-1.3.0.html#circlemarker) instead. 71 | `markerLayer` | `L.featureGroup` | `L.featureGroup` | A `L.FeatureGroup` instance used to display deflate markers. Use this to realise special behaviours, such as clustering markers. 72 | `greedyCollapse`| `boolean` | `true` | Specify false if you would like that features would be deflated only if both of their width and height are less than `minSize`. 73 | 74 | ## Examples 75 | 76 | ### Basic 77 | 78 | To create a basic deflatable layer, you have to 79 | 80 | 1. Create an `L.deflate` feature group and add it to your map. 81 | 2. Add features to the `L.Deflate` feature group. 82 | 83 | ```javascript 84 | const map = L.map("map").setView([51.505, -0.09], 12); 85 | 86 | const deflate_features = L.deflate({minSize: 20}); 87 | deflate_features.addTo(map); 88 | 89 | const polygon = L.polygon([ 90 | [51.509, -0.08], 91 | [51.503, -0.06], 92 | [51.51, -0.047] 93 | ]); 94 | polygon.addTo(deflate_features); 95 | 96 | const polyline = L.polyline([ 97 | [51.52, -0.05], 98 | [51.53, -0.10], 99 | ], {color: 'red'}); 100 | polyline.addTo(deflate_features); 101 | ``` 102 | 103 | ### GeoJSON 104 | 105 | [`GeoJSON` layers](http://leafletjs.com/reference-1.3.0.html#geojson) can be added in the same way: 106 | 107 | ```javascript 108 | const map = L.map("map").setView([51.505, -0.09], 12); 109 | 110 | const deflate_features = L.deflate({minSize: 20}); 111 | deflate_features.addTo(map); 112 | 113 | const json = { 114 | "type": "FeatureCollection", 115 | "features": [{}] 116 | } 117 | 118 | L.geoJson(json, {style: {color: '#0000FF'}}).addTo(deflate_features); 119 | ``` 120 | 121 | ### Custom markers 122 | 123 | You can change the appearance of markers representing deflated features by providing: 124 | 125 | - A [marker-options object](http://leafletjs.com/reference-1.3.0.html#marker-option), or 126 | - A function that returns a marker-options object. 127 | 128 | Providing a marker-options object is usually sufficient. You would typically choose to provide a function if you want to base the marker appearance on the feature's properties. 129 | 130 | Provide the object or function to the `markerOptions` property when initializing `L.deflate`. 131 | 132 | #### Define custom markers using a marker options object 133 | 134 | ```javascript 135 | const map = L.map("map").setView([51.550406, -0.140765], 16); 136 | 137 | const myIcon = L.icon({ 138 | iconUrl: 'img/marker.png', 139 | iconSize: [24, 24] 140 | }); 141 | 142 | const features = L.deflate({minSize: 20, markerOptions: {icon: myIcon}}); 143 | features.addTo(map); 144 | ``` 145 | 146 | #### Define custom markers using a function 147 | 148 | ```javascript 149 | const map = L.map("map").setView([51.550406, -0.140765], 16); 150 | 151 | function options(f) { 152 | // Use custom marker only for buildings 153 | if (f.feature.properties.type === 'building') { 154 | return { 155 | icon: L.icon({ 156 | iconUrl: 'img/marker.png', 157 | iconSize: [24, 24] 158 | }) 159 | } 160 | } 161 | 162 | return {}; 163 | } 164 | 165 | const features = L.deflate({minSize: 20, markerOptions: options}); 166 | features.addTo(map); 167 | ``` 168 | 169 | ### CircleMarkers 170 | 171 | Alternatively to standard markers, you can use [`CircleMarker`](https://leafletjs.com/reference-1.6.0.html#circlemarker) objects to represent deflated features on the map. 172 | 173 | To use default circle markers, specify the `markerType` option. 174 | 175 | ```javascript 176 | const map = L.map("map").setView([51.550406, -0.140765], 16); 177 | 178 | const features = L.deflate({ 179 | minSize: 20, 180 | markerType: L.circleMarker 181 | }); 182 | features.addTo(map); 183 | ``` 184 | 185 | #### Customise CircleMarker 186 | 187 | Similar to standard markers, you can customise how circle markers are displayed using the `markerOptions` property. There are to options to provide the options for circle markers: 188 | 189 | - A [CircleMarker-options object](https://leafletjs.com/reference-1.6.0.html#circlemarker-option), or 190 | - A function that returns a CircleMarker-options object. 191 | 192 | ##### Define custom circle markers using a CircleMarker options object 193 | 194 | ```javascript 195 | const map = L.map("map").setView([51.550406, -0.140765], 16); 196 | 197 | const features = L.deflate({ 198 | minSize: 20, 199 | markerType: L.circleMarker, 200 | markerOptions: { 201 | radius: 3, 202 | color: '#ff0000' 203 | } 204 | }); 205 | features.addTo(map); 206 | ``` 207 | 208 | ##### Define custom markers using a function 209 | 210 | ```javascript 211 | const map = L.map("map").setView([51.550406, -0.140765], 16); 212 | 213 | function options(f) { 214 | // Use custom marker only for buildings 215 | if (f.feature.properties.type === 'building') { 216 | return { 217 | radius: 3, 218 | color: '#ff0000' 219 | } 220 | } 221 | 222 | return {}; 223 | } 224 | 225 | const features = L.deflate({ 226 | minSize: 20, 227 | markerType: L.circleMarker, 228 | markerOptions: options 229 | }); 230 | features.addTo(map); 231 | ``` 232 | 233 | ### Cluster Markers 234 | 235 | Using [Leaflet.Markercluster](https://github.com/Leaflet/Leaflet.markercluster>), you can cluster markers. To enable clustered markers on a map: 236 | 237 | 1. Add the `Leaflet.Markercluster` libraries to the `head` section of your document as [described in the MarkerCluster documentation](https://github.com/Leaflet/Leaflet.markercluster#using-the-plugin>). 238 | 2. Inject a `MarkerClusterGroup` instance via the `markerLayer` option when initializing `L.deflate`. 239 | 240 | ```javascript 241 | const map = L.map("map").setView([51.505, -0.09], 12); 242 | 243 | const markerLayer = L.markerClusterGroup(); 244 | const deflate_features = L.deflate({minSize: 20, markerLayer: markerLayer}); 245 | deflate_features.addTo(map); 246 | 247 | const polygon = L.polygon([ 248 | [51.509, -0.08], 249 | [51.503, -0.06], 250 | [51.51, -0.047] 251 | ]); 252 | polygon.addTo(deflate_features) 253 | 254 | const polyline = L.polyline([ 255 | [51.52, -0.05], 256 | [51.53, -0.10], 257 | ], {color: 'red'}); 258 | polyline.addTo(deflate_features) 259 | ``` 260 | 261 | ### Leaflet.Draw 262 | 263 | [`Leaflet.Draw`](https://github.com/Leaflet/Leaflet.draw) is a plugin that adds support for drawing and editing vector features on Leaflet maps. `Leaflet.Deflate` integrates with `Leaflet.Draw`. 264 | 265 | Initialize the [`Leaflet.draw` control](https://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest#l-draw). Use the `L.deflate` instance to draw and edit features and add it the map. 266 | 267 | To ensure that newly added or edited features are deflated at the correct zoom level and show the marker at the correct location, you need to call `prepLayer` with the edited layer on every change. In the example below, we call `prepLayer` inside the handler function for the [`L.Draw.Event.EDITED`](https://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest#l-draw-event-draw:editstop) event. 268 | 269 | ```javascript 270 | const map = L.map("map").setView([51.505, -0.09], 12); 271 | 272 | const deflate_features = L.deflate({minSize: 20, markerCluster: true}); 273 | deflate_features.addTo(map); 274 | 275 | const drawControl = new L.Control.Draw({ 276 | edit: { 277 | featureGroup: deflate_features 278 | } 279 | }); 280 | map.addControl(drawControl); 281 | 282 | map.on(L.Draw.Event.CREATED, function (event) { 283 | const layer = event.layer; 284 | deflate_features.addLayer(layer); 285 | }); 286 | 287 | map.on(L.Draw.Event.EDITED, function(event) { 288 | const editedLayers = event.layers; 289 | editedLayers.eachLayer(function(l) { 290 | deflate_features.prepLayer(l); 291 | }); 292 | }); 293 | 294 | ``` 295 | 296 | ## Previous releases 297 | 298 | Documentation for older releases is available: 299 | 300 | - [2.0.x](https://github.com/oliverroick/Leaflet.Deflate/tree/v2.0.0#leafletdeflate) 301 | - [1.4.x](https://github.com/oliverroick/Leaflet.Deflate/tree/v1.4.0#leafletdeflate) 302 | - [1.3.x](https://github.com/oliverroick/Leaflet.Deflate/tree/v1.3.0#leafletdeflate) 303 | - [1.2.x](https://github.com/oliverroick/Leaflet.Deflate/tree/v1.2.0#leafletdeflate) 304 | - [1.1.x](https://github.com/oliverroick/Leaflet.Deflate/tree/1.1.0#leafletdeflate) 305 | - [1.0.x](https://github.com/oliverroick/Leaflet.Deflate/tree/1.0.0#leafletdeflate) 306 | - [0.3](https://github.com/oliverroick/Leaflet.Deflate/tree/v0.3#leafletdeflate) 307 | - [0.2](https://github.com/oliverroick/Leaflet.Deflate/tree/v0.2#leafletdeflate) 308 | - [0.1](https://github.com/oliverroick/Leaflet.Deflate/tree/v0.1#leafletdeflate) 309 | 310 | ## Developing 311 | 312 | You'll need to install the dev dependencies to test and write the distribution file. 313 | 314 | ``` 315 | npm install 316 | ``` 317 | 318 | To run tests: 319 | 320 | ``` 321 | npm test 322 | ``` 323 | 324 | To run eslint on source and test code: 325 | 326 | ``` 327 | npm run lint 328 | ``` 329 | 330 | To write a minified JS into dist: 331 | 332 | ``` 333 | npm run dist 334 | ``` 335 | 336 | ## Authors 337 | 338 | - [Lindsey Jacks](https://github.com/linzjax) 339 | - [Loic Lacroix](https://github.com/loclac) 340 | - [Oliver Roick](http://github.com/oliverroick) 341 | 342 | ## License 343 | 344 | Apache 2.0 345 | -------------------------------------------------------------------------------- /dist/L.Deflate.js: -------------------------------------------------------------------------------- 1 | "use strict";L.Layer.include({_originalRemove:L.Layer.prototype.remove,remove:function(){if(this.marker){this.marker.remove()}return this._originalRemove()}});L.Map.include({_originalRemoveLayer:L.Map.prototype.removeLayer,removeLayer:function(layer){if(layer.marker){layer.marker.remove()}return this._originalRemoveLayer(layer)}});L.Deflate=L.FeatureGroup.extend({options:{minSize:10,markerOptions:{},markerType:L.marker,greedyCollapse:true},initialize:function(options){L.Util.setOptions(this,options);this._layers=[];this._needsPrepping=[];this._featureLayer=this._getFeatureLayer(options)},_getFeatureLayer:function(){if(this.options.markerLayer){return this.options.markerLayer}return L.featureGroup(this.options)},_getBounds:function(path){if(path instanceof L.Circle){path.addTo(this._map);const bounds=path.getBounds();this._map.removeLayer(path);return bounds}return path.getBounds()},_isCollapsed:function(path,zoom){const bounds=path.computedBounds;const northEastPixels=this._map.project(bounds.getNorthEast(),zoom);const southWestPixels=this._map.project(bounds.getSouthWest(),zoom);const width=Math.abs(northEastPixels.x-southWestPixels.x);const height=Math.abs(southWestPixels.y-northEastPixels.y);if(this.options.greedyCollapse){return height 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /example/custom-marker-function.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /example/custom-marker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /example/event.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /example/geojson-event.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /example/geojson-two-layers-popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /example/geojson-two-layers-tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /example/geojson-two-layers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /example/geojson.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /example/greedy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 20 |
21 | 22 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /example/huge.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 |
22 | 23 | 24 | 36 | 37 | -------------------------------------------------------------------------------- /example/img/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliverroick/Leaflet.Deflate/561501028efdf1a399398011dab93041662fc774/example/img/marker.png -------------------------------------------------------------------------------- /example/leaflet-draw.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | 18 | 19 |
20 | 21 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /example/markercluster-freezable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 |
23 | 24 | 96 | 97 | -------------------------------------------------------------------------------- /example/markercluster-geojson.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 |
22 | 23 | 117 | 118 | -------------------------------------------------------------------------------- /example/markercluster.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 |
22 | 23 | 87 | 88 | -------------------------------------------------------------------------------- /example/polyline-decorator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | 17 | 18 |
19 | 20 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /example/simple-tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /example/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 | 19 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Leaflet.Deflate", 3 | "author": "Oliver Roick", 4 | "version": "2.1.0", 5 | "description": "Deflates lines and polygons to a marker when their screen size becomes too small in lower zoom levels.", 6 | "license": "Apache-2.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/oliverroick/Leaflet.Deflate" 10 | }, 11 | "homepage": "https://github.com/oliverroick/Leaflet.Deflate", 12 | "bugs": "https://github.com/oliverroick/Leaflet.Deflate/issues", 13 | "main": "dist/L.Deflate.js", 14 | "source": "src/L.Deflate.js", 15 | "types": "types/index.d.ts", 16 | "directories": { 17 | "test": "tests", 18 | "lib": "src", 19 | "example": "example" 20 | }, 21 | "devDependencies": { 22 | "@types/leaflet": "^1.5.12", 23 | "@types/leaflet.markercluster": "^1.4.2", 24 | "eslint": "8.17.0", 25 | "eslint-config-airbnb-base": "^15.0.0", 26 | "eslint-plugin-import": "^2.18.2", 27 | "jest": "^29.2.2", 28 | "jest-environment-jsdom": "^29.2.2", 29 | "leaflet": "^1.7.1", 30 | "terser": "^5.2.1", 31 | "typescript": "^4.0.5" 32 | }, 33 | "peerDependencies": { 34 | "leaflet": "^1.0.0" 35 | }, 36 | "scripts": { 37 | "test": "jest", 38 | "dist": "terser --output dist/L.Deflate.js src/L.Deflate.js --source-map", 39 | "lint": "eslint src tests", 40 | "preversion": "if [[ \"$(git rev-parse --abbrev-ref HEAD)\" != \"master\" ]]; then echo \"Not on master\" && exit 1; fi", 41 | "version": "npm run dist && git add -A ./dist", 42 | "postversion": "git push && git push --tags" 43 | }, 44 | "jest": { 45 | "testEnvironment": "jsdom" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/L.Deflate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | L.Layer.include({ 4 | _originalRemove: L.Layer.prototype.remove, 5 | remove: function () { 6 | if (this.marker) { this.marker.remove(); } 7 | return this._originalRemove(); 8 | }, 9 | }); 10 | 11 | L.Map.include({ 12 | _originalRemoveLayer: L.Map.prototype.removeLayer, 13 | removeLayer: function (layer) { 14 | if (layer.marker) { layer.marker.remove(); } 15 | return this._originalRemoveLayer(layer); 16 | }, 17 | }); 18 | 19 | L.Deflate = L.FeatureGroup.extend({ 20 | options: { 21 | minSize: 10, 22 | markerOptions: {}, 23 | markerType: L.marker, 24 | greedyCollapse: true, 25 | }, 26 | 27 | initialize: function (options) { 28 | L.Util.setOptions(this, options); 29 | this._layers = []; 30 | this._needsPrepping = []; 31 | this._featureLayer = this._getFeatureLayer(options); 32 | }, 33 | 34 | _getFeatureLayer: function () { 35 | if (this.options.markerLayer) { 36 | return this.options.markerLayer; 37 | } 38 | 39 | return L.featureGroup(this.options); 40 | }, 41 | 42 | _getBounds: function (path) { 43 | // L.Circle defines the radius in metres. If you want to calculate 44 | // the bounding box of a circle, it needs to be projected on the map. 45 | // The only way to do that at present is to add it to the map. We're 46 | // removing the circle after computing the bounds because we haven't 47 | // figured out wether to display the circle or the deflated marker. 48 | // It's a terribly ugly solution but ¯\_(ツ)_/¯ 49 | if (path instanceof L.Circle) { 50 | path.addTo(this._map); 51 | const bounds = path.getBounds(); 52 | this._map.removeLayer(path); 53 | return bounds; 54 | } 55 | return path.getBounds(); 56 | }, 57 | 58 | _isCollapsed: function (path, zoom) { 59 | const bounds = path.computedBounds; 60 | 61 | const northEastPixels = this._map.project(bounds.getNorthEast(), zoom); 62 | const southWestPixels = this._map.project(bounds.getSouthWest(), zoom); 63 | 64 | const width = Math.abs(northEastPixels.x - southWestPixels.x); 65 | const height = Math.abs(southWestPixels.y - northEastPixels.y); 66 | 67 | if (this.options.greedyCollapse) { 68 | return (height < this.options.minSize || width < this.options.minSize); 69 | } 70 | 71 | return (height < this.options.minSize && width < this.options.minSize); 72 | }, 73 | 74 | _getZoomThreshold: function (path) { 75 | let zoomThreshold; 76 | let zoom = this._map.getZoom(); 77 | if (this._isCollapsed(path, this._map.getZoom())) { 78 | while (!zoomThreshold) { 79 | zoom += 1; 80 | if (!this._isCollapsed(path, zoom)) { 81 | zoomThreshold = zoom - 1; 82 | } 83 | } 84 | } else { 85 | while (!zoomThreshold) { 86 | zoom -= 1; 87 | if (this._isCollapsed(path, zoom)) { 88 | zoomThreshold = zoom; 89 | } 90 | } 91 | } 92 | return zoomThreshold; 93 | }, 94 | 95 | _bindInfoTools: function (marker, parentLayer) { 96 | if (parentLayer._popupHandlersAdded) { 97 | marker.bindPopup(parentLayer._popup._content, parentLayer._popup.options); 98 | } 99 | 100 | if (parentLayer._tooltipHandlersAdded) { 101 | marker.bindTooltip(parentLayer._tooltip._content, parentLayer._tooltip.options); 102 | } 103 | }, 104 | 105 | _bindEvents: function _bindEvents(marker, parentLayer) { 106 | const events = parentLayer._events; 107 | const eventKeys = events ? Object.getOwnPropertyNames(events) : []; 108 | const eventParents = parentLayer._eventParents; 109 | const eventParentKeys = eventParents ? Object.getOwnPropertyNames(eventParents) : []; 110 | 111 | this._bindInfoTools(marker, parentLayer); 112 | 113 | for (let i = 0, lenI = eventKeys.length; i < lenI; i += 1) { 114 | const listeners = events[eventKeys[i]]; 115 | for (let j = 0, lenJ = listeners.length; j < lenJ; j += 1) { 116 | marker.on(eventKeys[i], listeners[j].fn); 117 | } 118 | } 119 | 120 | // For FeatureGroups we need to bind all events, tooltips and popups 121 | // from the FeatureGroup to each marker 122 | if (!parentLayer._eventParents) { return; } 123 | 124 | for (let i = 0, lenI = eventParentKeys.length; i < lenI; i += 1) { 125 | if (!parentLayer._eventParents[eventParentKeys[i]]._map) { 126 | this._bindEvents(marker, parentLayer._eventParents[eventParentKeys[i]]); 127 | 128 | // We're copying all layers of a FeatureGroup, so we need to bind 129 | // all tooltips and popups to the original feature. 130 | this._bindInfoTools(parentLayer, parentLayer._eventParents[eventParentKeys[i]]); 131 | } 132 | } 133 | }, 134 | 135 | _makeMarker: function (layer) { 136 | const allowedMarkerTypes = [L.marker, L.circleMarker]; 137 | if (allowedMarkerTypes.indexOf(this.options.markerType) === -1) { 138 | throw new Error('Invalid markerType provided. Allowed markerTypes are: L.marker and L.circleMarker'); 139 | } 140 | const markerOptions = typeof this.options.markerOptions === 'function' 141 | ? this.options.markerOptions(layer) 142 | : this.options.markerOptions; 143 | const marker = this.options.markerType(layer.computedBounds.getCenter(), markerOptions); 144 | const markerFeature = layer.feature ? marker.toGeoJSON() : undefined; 145 | 146 | this._bindEvents(marker, layer); 147 | 148 | if (markerFeature) { 149 | markerFeature.properties = layer.feature.properties; 150 | marker.feature = markerFeature; 151 | } 152 | 153 | return marker; 154 | }, 155 | 156 | prepLayer: function (layer) { 157 | if (layer.getBounds) { 158 | layer.computedBounds = this._getBounds(layer); 159 | 160 | layer.zoomThreshold = this._getZoomThreshold(layer); 161 | layer.marker = this._makeMarker(layer); 162 | layer.zoomState = this._map.getZoom(); 163 | } 164 | }, 165 | 166 | _addToMap: function (layer) { 167 | const layerToAdd = this._map.getZoom() <= layer.zoomThreshold ? layer.marker : layer; 168 | this._featureLayer.addLayer(layerToAdd); 169 | }, 170 | 171 | addLayer: function (layer) { 172 | const layers = layer instanceof L.FeatureGroup ? Object.getOwnPropertyNames(layer._layers) : []; 173 | if (layers.length) { 174 | for (let i = 0, len = layers.length; i < len; i += 1) { 175 | this.addLayer(layer._layers[layers[i]]); 176 | } 177 | } else { 178 | if (this._map) { 179 | this.prepLayer(layer); 180 | this._addToMap(layer); 181 | } else { 182 | this._needsPrepping.push(layer); 183 | } 184 | this._layers[this.getLayerId(layer)] = layer; 185 | } 186 | }, 187 | 188 | removeLayer: function (layer) { 189 | const layers = layer instanceof L.FeatureGroup ? Object.getOwnPropertyNames(layer._layers) : []; 190 | 191 | if (layers.length) { 192 | for (let i = 0, len = layers.length; i < len; i += 1) { 193 | this.removeLayer(layer._layers[layers[i]]); 194 | } 195 | } else { 196 | const layerId = layer in this._layers ? layer : this.getLayerId(layer); 197 | 198 | this._featureLayer.removeLayer(this._layers[layerId]); 199 | if (this._layers[layerId].marker) { 200 | this._featureLayer.removeLayer(this._layers[layerId].marker); 201 | } 202 | 203 | delete this._layers[layerId]; 204 | 205 | const layerIndex = this._needsPrepping.indexOf(this._layers[layerId]); 206 | if (layerIndex !== -1) { this._needsPrepping.splice(layerIndex, 1); } 207 | } 208 | }, 209 | 210 | clearLayers: function () { 211 | this._featureLayer.clearLayers(); 212 | this._layers = []; 213 | }, 214 | 215 | _switchDisplay: function (layer, showMarker) { 216 | if (showMarker) { 217 | this._featureLayer.removeLayer(layer); 218 | this._featureLayer.addLayer(layer.marker); 219 | } else { 220 | this._featureLayer.removeLayer(layer.marker); 221 | this._featureLayer.addLayer(layer); 222 | } 223 | }, 224 | 225 | _deflate: function () { 226 | const bounds = this._map.getBounds(); 227 | const endZoom = this._map.getZoom(); 228 | 229 | this.eachLayer(function (layer) { 230 | if (layer.marker && layer.zoomState !== endZoom && layer.computedBounds.intersects(bounds)) { 231 | this._switchDisplay(layer, endZoom <= layer.zoomThreshold); 232 | layer.zoomState = endZoom; 233 | } 234 | }, this); 235 | }, 236 | 237 | onAdd: function (map) { 238 | this._featureLayer.addTo(map); 239 | this._map.on('zoomend', this._deflate, this); 240 | this._map.on('moveend', this._deflate, this); 241 | 242 | for (let i = 0, len = this._needsPrepping.length; i < len; i += 1) { 243 | this.addLayer(this._needsPrepping[i]); 244 | } 245 | this._needsPrepping = []; 246 | this._deflate(); 247 | }, 248 | 249 | onRemove: function (map) { 250 | map.removeLayer(this._featureLayer); 251 | this._map.off('zoomend', this._deflate, this); 252 | this._map.off('moveend', this._deflate, this); 253 | }, 254 | }); 255 | 256 | L.deflate = function (options) { 257 | return new L.Deflate(options); 258 | }; 259 | -------------------------------------------------------------------------------- /tests/L.Deflate.test.js: -------------------------------------------------------------------------------- 1 | const L = require('leaflet'); 2 | const fixtures = require('./fixtures'); 3 | 4 | const target = process.env.TARGET || 'src'; 5 | 6 | require(`../${target}/L.Deflate`); // eslint-disable-line import/no-dynamic-require 7 | 8 | const tests = (options) => () => { 9 | let map; 10 | let deflateLayer; 11 | let polygon; 12 | let longPolygon; 13 | let marker; 14 | let circle; 15 | let json; 16 | 17 | beforeEach(() => { 18 | const container = document.createElement('div'); 19 | Object.defineProperty(container, 'clientHeight', { configurable: true, value: 600 }); 20 | Object.defineProperty(container, 'clientWidth', { configurable: true, value: 800 }); 21 | 22 | map = L.map(container, { renderer: new L.SVG(), center: [51.505, -0.09], zoom: 10 }); 23 | 24 | polygon = fixtures.polygon; 25 | longPolygon = fixtures.longPolygon; 26 | marker = fixtures.polygon; 27 | circle = fixtures.circle; 28 | json = fixtures.geojson; 29 | }); 30 | 31 | afterEach(() => { 32 | map.remove(); 33 | }); 34 | 35 | describe('add layer', () => { 36 | beforeEach(() => { 37 | deflateLayer = L.deflate(options); 38 | }); 39 | 40 | test('using Map.addLayer', () => { 41 | polygon.addTo(deflateLayer); 42 | map.addLayer(deflateLayer); 43 | map.setZoom(13, { animate: false }); 44 | expect(map.hasLayer(polygon)).toBeTruthy(); 45 | }); 46 | 47 | test('using L.Deflate.addTo(Map)', () => { 48 | polygon.addTo(deflateLayer); 49 | deflateLayer.addTo(map); 50 | map.setZoom(13, { animate: false }); 51 | expect(map.hasLayer(polygon)).toBeTruthy(); 52 | }); 53 | 54 | test('using L.Deflate.addLayer(Layer)', () => { 55 | deflateLayer.addTo(map); 56 | deflateLayer.addLayer(polygon); 57 | map.setZoom(13, { animate: false }); 58 | expect(map.hasLayer(polygon)).toBeTruthy(); 59 | }); 60 | 61 | test('using Layer.addTo(L.Deflate)', () => { 62 | deflateLayer.addTo(map); 63 | polygon.addTo(deflateLayer); 64 | map.setZoom(13, { animate: false }); 65 | expect(map.hasLayer(polygon)).toBeTruthy(); 66 | }); 67 | }); 68 | 69 | describe('remove layer', () => { 70 | beforeEach(() => { 71 | deflateLayer = L.deflate(options); 72 | deflateLayer.addTo(map); 73 | deflateLayer.addLayer(polygon); 74 | }); 75 | 76 | test('Map.removeLayer(L.deflate) removes layer', () => { 77 | map.setZoom(13, { animate: false }); 78 | map.removeLayer(deflateLayer); 79 | expect(map.hasLayer(polygon)).toBeFalsy(); 80 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 81 | }); 82 | 83 | test('Map.removeLayer(L.deflate) removes marker', () => { 84 | map.setZoom(10, { animate: false }); 85 | map.removeLayer(deflateLayer); 86 | expect(map.hasLayer(polygon)).toBeFalsy(); 87 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 88 | }); 89 | 90 | test('Map.removeLayer(Layer) removes layer', () => { 91 | map.setZoom(13, { animate: false }); 92 | map.removeLayer(polygon); 93 | expect(map.hasLayer(polygon)).toBeFalsy(); 94 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 95 | }); 96 | 97 | test('Map.removeLayer(Layer) removes marker', () => { 98 | map.setZoom(10, { animate: false }); 99 | map.removeLayer(polygon); 100 | expect(map.hasLayer(polygon)).toBeFalsy(); 101 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 102 | }); 103 | 104 | test('L.Deflate.removeFrom(L.Map) removes layer', () => { 105 | map.setZoom(13, { animate: false }); 106 | deflateLayer.removeFrom(map); 107 | expect(map.hasLayer(polygon)).toBeFalsy(); 108 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 109 | }); 110 | 111 | test('L.Deflate.removeFrom(L.Map) removes marker', () => { 112 | map.setZoom(10, { animate: false }); 113 | deflateLayer.removeFrom(map); 114 | expect(map.hasLayer(polygon)).toBeFalsy(); 115 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 116 | }); 117 | 118 | test('L.Deflate.removeLayer(Layer) removes layer', () => { 119 | map.setZoom(13, { animate: false }); 120 | deflateLayer.removeLayer(polygon); 121 | expect(map.hasLayer(polygon)).toBeFalsy(); 122 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 123 | }); 124 | 125 | test('L.Deflate.removeLayer(Layer) removes marker', () => { 126 | map.setZoom(10, { animate: false }); 127 | deflateLayer.removeLayer(polygon); 128 | expect(map.hasLayer(polygon)).toBeFalsy(); 129 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 130 | }); 131 | 132 | test('Layer.removeFrom(L.Deflate) removes layer', () => { 133 | map.setZoom(13, { animate: false }); 134 | polygon.removeFrom(deflateLayer); 135 | expect(map.hasLayer(polygon)).toBeFalsy(); 136 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 137 | }); 138 | 139 | test('Layer.removeFrom(L.Deflate) removes marker', () => { 140 | map.setZoom(10, { animate: false }); 141 | polygon.removeFrom(deflateLayer); 142 | expect(map.hasLayer(polygon)).toBeFalsy(); 143 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 144 | }); 145 | 146 | test('Layer.remove() removes layer', () => { 147 | map.setZoom(13, { animate: false }); 148 | polygon.remove(); 149 | expect(map.hasLayer(polygon)).toBeFalsy(); 150 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 151 | }); 152 | 153 | test('Layer.remove() removes polygon marker', () => { 154 | map.setZoom(10, { animate: false }); 155 | polygon.remove(); 156 | expect(map.hasLayer(polygon)).toBeFalsy(); 157 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 158 | }); 159 | }); 160 | 161 | describe('clear layers', () => { 162 | beforeEach(() => { 163 | deflateLayer = L.deflate(options); 164 | deflateLayer.addTo(map); 165 | deflateLayer.addLayer(polygon); 166 | }); 167 | 168 | test('should remove polygon', function () { 169 | map.setZoom(13, { animate: false }); 170 | deflateLayer.clearLayers(); 171 | 172 | expect(map.hasLayer(polygon)).toBeFalsy(); 173 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 174 | }); 175 | 176 | test('should remove polygon marker', function () { 177 | map.setZoom(8, { animate: false }); 178 | deflateLayer.clearLayers(); 179 | 180 | expect(map.hasLayer(polygon)).toBeFalsy(); 181 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 182 | }); 183 | 184 | test('should remove point', function () { 185 | map.setZoom(13, { animate: false }); 186 | deflateLayer.clearLayers(); 187 | expect(map.hasLayer(marker)).toBeFalsy(); 188 | }); 189 | }); 190 | 191 | describe('Event listeners', () => { 192 | beforeEach(() => { 193 | deflateLayer = L.deflate(options); 194 | deflateLayer.addTo(map); 195 | }); 196 | 197 | test('passed from layer to marker', () => { 198 | const callback = jest.fn(); 199 | polygon.on('click', callback); 200 | deflateLayer.addLayer(polygon); 201 | 202 | expect(polygon.marker).toHaveProperty(['_events', 'click', 0, 'fn'], callback); 203 | }); 204 | 205 | test('passed from GeoJSON to marker', function () { 206 | const callback = jest.fn(); 207 | json.on('click', callback); 208 | deflateLayer.addLayer(json); 209 | 210 | map.eachLayer(function (layer) { 211 | if (layer.marker) { 212 | expect(layer.marker).toHaveProperty(['_events', 'click', 0, 'fn'], callback); 213 | } 214 | }); 215 | }); 216 | }); 217 | 218 | describe('Popup', () => { 219 | beforeEach(() => { 220 | deflateLayer = L.deflate(options); 221 | deflateLayer.addTo(map); 222 | }); 223 | 224 | test('passed from layer to marker', () => { 225 | polygon.bindPopup('Click', { closeButton: false }); 226 | deflateLayer.addLayer(polygon); 227 | 228 | expect(polygon.marker).toHaveProperty('_popupHandlersAdded', true); 229 | expect(polygon.marker).toHaveProperty('_popup._content', 'Click'); 230 | expect(polygon.marker).toHaveProperty('_popup.options.closeButton', false); 231 | }); 232 | 233 | test('passed from GeoJson to marker', () => { 234 | json.bindPopup('Click', { closeButton: false }); 235 | deflateLayer.addLayer(json); 236 | 237 | map.eachLayer(function (layer) { 238 | if (layer.marker) { 239 | expect(layer).toHaveProperty('_popupHandlersAdded', true); 240 | expect(layer).toHaveProperty('_popup._content', 'Click'); 241 | expect(layer).toHaveProperty('_popup.options.closeButton', false); 242 | expect(layer.marker).toHaveProperty('_popupHandlersAdded', true); 243 | expect(layer.marker).toHaveProperty('_popup._content', 'Click'); 244 | expect(layer.marker).toHaveProperty('_popup.options.closeButton', false); 245 | } 246 | }); 247 | }); 248 | }); 249 | 250 | describe('Tooltip', () => { 251 | beforeEach(() => { 252 | deflateLayer = L.deflate(options); 253 | deflateLayer.addTo(map); 254 | }); 255 | 256 | test('passed from layer to marker', () => { 257 | polygon.bindTooltip('Click', { direction: 'bottom' }); 258 | deflateLayer.addLayer(polygon); 259 | 260 | expect(polygon.marker).toHaveProperty('_tooltipHandlersAdded', true); 261 | expect(polygon.marker).toHaveProperty('_tooltip._content', 'Click'); 262 | expect(polygon.marker).toHaveProperty('_tooltip.options.direction', 'bottom'); 263 | }); 264 | 265 | test('passed from GeoJson to marker', () => { 266 | json.bindTooltip('Click', { direction: 'bottom' }); 267 | deflateLayer.addLayer(json); 268 | 269 | map.eachLayer(function (layer) { 270 | if (layer.marker) { 271 | expect(layer).toHaveProperty('_tooltipHandlersAdded', true); 272 | expect(layer).toHaveProperty('_tooltip._content', 'Click'); 273 | expect(layer).toHaveProperty('_tooltip.options.direction', 'bottom'); 274 | expect(layer.marker).toHaveProperty('_tooltipHandlersAdded', true); 275 | expect(layer.marker).toHaveProperty('_tooltip._content', 'Click'); 276 | expect(layer.marker).toHaveProperty('_tooltip.options.direction', 'bottom'); 277 | } 278 | }); 279 | }); 280 | }); 281 | 282 | describe('Deflate marker', () => { 283 | const iconPath = '../example/img/marker.png'; 284 | 285 | test('can be L.marker', () => { 286 | deflateLayer = L.deflate({ ...options, markerType: L.marker }); 287 | deflateLayer.addTo(map); 288 | expect(() => polygon.addTo(deflateLayer)).not.toThrow(); 289 | }); 290 | 291 | test('can be L.circleMarker', () => { 292 | deflateLayer = L.deflate({ ...options, markerType: L.circleMarker }); 293 | deflateLayer.addTo(map); 294 | expect(() => polygon.addTo(deflateLayer)).not.toThrow(); 295 | }); 296 | 297 | test('can be L.polygon', () => { 298 | deflateLayer = L.deflate({ ...options, markerType: L.polygon }); 299 | deflateLayer.addTo(map); 300 | expect(() => polygon.addTo(deflateLayer)).toThrow(); 301 | }); 302 | 303 | test('uses icon', () => { 304 | const myIcon = L.icon({ 305 | iconUrl: iconPath, 306 | iconSize: [24, 24], 307 | }); 308 | deflateLayer = L.deflate({ ...options, markerOptions: { icon: myIcon } }); 309 | deflateLayer.addTo(map); 310 | polygon.addTo(deflateLayer); 311 | 312 | expect(polygon.marker).toHaveProperty('options.icon.options.iconUrl', iconPath); 313 | }); 314 | 315 | test('uses function', () => { 316 | const markerOptions = () => ({ 317 | icon: L.icon({ 318 | iconUrl: iconPath, 319 | iconSize: [24, 24], 320 | }), 321 | }); 322 | deflateLayer = L.deflate({ ...options, markerOptions }); 323 | deflateLayer.addTo(map); 324 | polygon.addTo(deflateLayer); 325 | 326 | expect(polygon.marker).toHaveProperty('options.icon.options.iconUrl', iconPath); 327 | }); 328 | }); 329 | 330 | describe('Deflating polygon', () => { 331 | beforeEach(() => { 332 | deflateLayer = L.deflate(options); 333 | deflateLayer.addTo(map); 334 | deflateLayer.addLayer(polygon); 335 | }); 336 | 337 | test('polygon is not deflated', () => { 338 | map.setZoom(13, { animate: false }); 339 | expect(map.hasLayer(polygon)).toBeTruthy(); 340 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 341 | }); 342 | 343 | test('polygon is deflated after zooming out', () => { 344 | map.setZoom(10, { animate: false }); 345 | expect(map.hasLayer(polygon)).toBeFalsy(); 346 | expect(map.hasLayer(polygon.marker)).toBeTruthy(); 347 | }); 348 | 349 | test('polygon is not inflated after zooming in when outside map bbox', () => { 350 | map.setZoom(10, { animate: false }); 351 | map.panTo([0, 0]); 352 | map.setZoom(13, { animate: false }); 353 | expect(map.hasLayer(polygon)).toBeFalsy(); 354 | expect(map.hasLayer(polygon.marker)).toBeTruthy(); 355 | }); 356 | 357 | test('polygon is inflated after zooming in when dragged inside map bbox', () => { 358 | map.setZoom(10, { animate: false }); 359 | map.panTo([0, 0]); 360 | map.setZoom(13, { animate: false }); 361 | map.panTo([51.505, -0.09], { animate: false }); 362 | expect(map.hasLayer(polygon)).toBeTruthy(); 363 | expect(map.hasLayer(polygon.marker)).toBeFalsy(); 364 | }); 365 | }); 366 | 367 | describe('Deflating long polygon', () => { 368 | beforeEach(() => { 369 | deflateLayer = L.deflate(options); 370 | deflateLayer.addTo(map); 371 | deflateLayer.addLayer(longPolygon); 372 | }); 373 | 374 | test('polygon is not deflated', () => { 375 | map.setZoom(13, { animate: false }); 376 | expect(map.hasLayer(longPolygon)).toBeTruthy(); 377 | expect(map.hasLayer(longPolygon.marker)).toBeFalsy(); 378 | }); 379 | 380 | test('long polygon is deflated after zooming out', () => { 381 | map.setZoom(10, { animate: false }); 382 | expect(map.hasLayer(longPolygon)).toBeFalsy(); 383 | expect(map.hasLayer(longPolygon.marker)).toBeTruthy(); 384 | }); 385 | 386 | test('long polygon is not inflated after zooming in when outside map bbox', () => { 387 | map.setZoom(10, { animate: false }); 388 | map.panTo([0, 0]); 389 | map.setZoom(16, { animate: false }); 390 | expect(map.hasLayer(longPolygon)).toBeFalsy(); 391 | expect(map.hasLayer(longPolygon.marker)).toBeTruthy(); 392 | }); 393 | 394 | test('long polygon is inflated after zooming in when dragged inside map bbox', () => { 395 | map.setZoom(10, { animate: false }); 396 | map.panTo([0, 0]); 397 | map.setZoom(16, { animate: false }); 398 | map.panTo([51.505, -0.09], { animate: false }); 399 | expect(map.hasLayer(longPolygon)).toBeTruthy(); 400 | expect(map.hasLayer(longPolygon.marker)).toBeFalsy(); 401 | }); 402 | }); 403 | 404 | describe('Deflating long polygon with greedyCollapse = false', () => { 405 | beforeEach(() => { 406 | deflateLayer = L.deflate({ ...options, greedyCollapse: false }); 407 | deflateLayer.addTo(map); 408 | deflateLayer.addLayer(longPolygon); 409 | }); 410 | 411 | test('polygon is not deflated', () => { 412 | map.setZoom(13, { animate: false }); 413 | expect(map.hasLayer(longPolygon)).toBeTruthy(); 414 | expect(map.hasLayer(longPolygon.marker)).toBeFalsy(); 415 | }); 416 | 417 | test('long polygon not is deflated after zooming out', () => { 418 | map.setZoom(10, { animate: false }); 419 | expect(map.hasLayer(longPolygon)).toBeTruthy(); 420 | expect(map.hasLayer(longPolygon.marker)).toBeFalsy(); 421 | }); 422 | 423 | test('long polygon is deflated after zooming out', () => { 424 | map.setZoom(8, { animate: false }); 425 | expect(map.hasLayer(longPolygon)).toBeFalsy(); 426 | expect(map.hasLayer(longPolygon.marker)).toBeTruthy(); 427 | }); 428 | }); 429 | 430 | describe('Deflating circle', () => { 431 | beforeEach(() => { 432 | deflateLayer = L.deflate(options); 433 | deflateLayer.addTo(map); 434 | deflateLayer.addLayer(circle); 435 | }); 436 | 437 | test('circle is not deflated', () => { 438 | map.setZoom(14, { animate: false }); 439 | expect(map.hasLayer(circle)).toBeTruthy(); 440 | expect(map.hasLayer(circle.marker)).toBeFalsy(); 441 | }); 442 | 443 | test('circle is deflated after zooming out', () => { 444 | map.setZoom(10, { animate: false }); 445 | expect(map.hasLayer(circle)).toBeFalsy(); 446 | expect(map.hasLayer(circle.marker)).toBeTruthy(); 447 | }); 448 | 449 | test('circle is not inflated after zooming in when outside map bbox', () => { 450 | map.setZoom(10, { animate: false }); 451 | map.panTo([0, 0]); 452 | map.setZoom(14, { animate: false }); 453 | expect(map.hasLayer(circle)).toBeFalsy(); 454 | expect(map.hasLayer(circle.marker)).toBeTruthy(); 455 | }); 456 | 457 | test('circle is inflated after zooming in when dragged inside map bbox', () => { 458 | map.setZoom(10, { animate: false }); 459 | map.panTo([0, 0]); 460 | map.setZoom(14, { animate: false }); 461 | map.panTo([51.505, -0.09], { animate: false }); 462 | expect(map.hasLayer(circle)).toBeTruthy(); 463 | expect(map.hasLayer(circle.marker)).toBeFalsy(); 464 | }); 465 | }); 466 | 467 | describe('Deflating GeoJSON', () => { 468 | const countLayers = (m) => { 469 | let numberLayers = 0; 470 | m.eachLayer((layer) => { 471 | if (layer.zoomThreshold && layer.getBounds) { numberLayers += 1; } 472 | }); 473 | return numberLayers; 474 | }; 475 | 476 | beforeEach(() => { 477 | deflateLayer = L.deflate(options); 478 | deflateLayer.addTo(map); 479 | deflateLayer.addLayer(json); 480 | }); 481 | 482 | test('2 polygons inflated at zoom 10', () => { 483 | map.setZoom(10, { animate: false }); 484 | expect(countLayers(map)).toEqual(2); 485 | }); 486 | 487 | test('3 polygons inflated at zoom 11', () => { 488 | map.setZoom(11, { animate: false }); 489 | expect(countLayers(map)).toEqual(3); 490 | }); 491 | }); 492 | 493 | describe('GeoJSON', () => { 494 | beforeEach(() => { 495 | deflateLayer = L.deflate(options); 496 | deflateLayer.addTo(map); 497 | deflateLayer.addLayer(json); 498 | }); 499 | 500 | test('passed feature properties to marker', () => { 501 | const layer = deflateLayer.getLayers()[0]; 502 | expect(layer.marker.feature.geometry).toEqual(layer.marker.toGeoJSON().geometry); 503 | expect(layer.marker.feature.properties).toEqual(layer.feature.properties); 504 | }); 505 | }); 506 | }; 507 | 508 | describe(`Test ${target}`, () => { 509 | describe('using internal deflate layer', tests({ minSize: 20 })); 510 | describe('using injected deflate layer', tests({ minSize: 20, deflateLayer: L.featureGroup() })); 511 | }); 512 | -------------------------------------------------------------------------------- /tests/fixtures.js: -------------------------------------------------------------------------------- 1 | const geojson = { 2 | type: 'FeatureCollection', 3 | features: [ 4 | { 5 | type: 'Feature', 6 | properties: { 7 | id: 1, 8 | }, 9 | geometry: { 10 | type: 'Polygon', 11 | coordinates: [ 12 | [ 13 | [ 14 | -0.273284912109375, 15 | 51.60437164681676, 16 | ], 17 | [ 18 | -0.30212402343749994, 19 | 51.572802100290254, 20 | ], 21 | [ 22 | -0.276031494140625, 23 | 51.57194856482396, 24 | ], 25 | [ 26 | -0.267791748046875, 27 | 51.587309751245456, 28 | ], 29 | [ 30 | -0.273284912109375, 31 | 51.60437164681676, 32 | ], 33 | ], 34 | ], 35 | }, 36 | }, 37 | { 38 | type: 'Feature', 39 | properties: { 40 | id: 4, 41 | }, 42 | geometry: { 43 | type: 'Polygon', 44 | coordinates: [ 45 | [ 46 | [ 47 | -0.25543212890625, 48 | 51.600959780448626, 49 | ], 50 | [ 51 | -0.2581787109375, 52 | 51.57621608189101, 53 | ], 54 | [ 55 | -0.22247314453125, 56 | 51.6001067737997, 57 | ], 58 | [ 59 | -0.24032592773437497, 60 | 51.613752957501, 61 | ], 62 | [ 63 | -0.25543212890625, 64 | 51.600959780448626, 65 | ], 66 | ], 67 | ], 68 | }, 69 | }, 70 | { 71 | type: 'Feature', 72 | properties: { 73 | id: 2, 74 | }, 75 | geometry: { 76 | type: 'Point', 77 | coordinates: [ 78 | 0.031585693359375, 79 | 51.4428807236673, 80 | ], 81 | }, 82 | }, 83 | { 84 | type: 'Feature', 85 | properties: { 86 | id: 3, 87 | }, 88 | geometry: { 89 | type: 'Polygon', 90 | coordinates: [ 91 | [ 92 | [ 93 | 0.06866455078125, 94 | 51.59584150020809, 95 | ], 96 | [ 97 | 0.06866455078125, 98 | 51.61034179610212, 99 | ], 100 | [ 101 | 0.10162353515625, 102 | 51.61034179610212, 103 | ], 104 | [ 105 | 0.10162353515625, 106 | 51.59584150020809, 107 | ], 108 | [ 109 | 0.06866455078125, 110 | 51.59584150020809, 111 | ], 112 | ], 113 | ], 114 | }, 115 | }, 116 | ], 117 | }; 118 | 119 | const fixtures = { 120 | get polygon() { 121 | return L.polygon([ 122 | [51.509, -0.08], 123 | [51.503, -0.06], 124 | [51.51, -0.047], 125 | ]); 126 | }, 127 | 128 | get longPolygon() { 129 | return L.polygon([ 130 | [51.503, -0.06], 131 | [51.509, -0.06], 132 | [51.509, -0.12], 133 | [51.503, -0.12], 134 | ]); 135 | }, 136 | 137 | get marker() { 138 | return L.marker([51.509, -0.08]); 139 | }, 140 | 141 | get circle() { 142 | return L.circle([51.505, -0.09], { radius: 100 }); 143 | }, 144 | 145 | get geojson() { 146 | return L.geoJson(geojson); 147 | }, 148 | }; 149 | 150 | module.exports = fixtures; 151 | -------------------------------------------------------------------------------- /tests/types.ts: -------------------------------------------------------------------------------- 1 | import * as L from 'leaflet'; 2 | import 'L.Deflate'; 3 | import 'leaflet.markercluster'; 4 | 5 | const l = L.deflate({minSize: 20}); 6 | 7 | // Polygon 8 | const polygon = L.polygon([ 9 | [51.509, -0.08], 10 | [51.503, -0.06], 11 | [51.51, -0.047], 12 | ]); 13 | 14 | l.addLayer(polygon); 15 | l.prepLayer(polygon); 16 | 17 | // GeoJSON 18 | const json: GeoJSON.FeatureCollection = { 19 | "type": "FeatureCollection", 20 | "features": [{ 21 | "type": "Feature", 22 | "properties": { 23 | "id": 1 24 | }, 25 | "geometry": { 26 | "type": "Polygon", 27 | "coordinates": [ 28 | [ 29 | [-0.144624, 51.551088], 30 | [-0.143648, 51.550818], 31 | [-0.143718, 51.550701], 32 | [-0.142398, 51.550351], 33 | [-0.142060, 51.550861], 34 | [-0.143160, 51.551155], 35 | [-0.143213, 51.551091], 36 | [-0.144410, 51.551408] 37 | ] 38 | ] 39 | } 40 | }] 41 | }; 42 | const geoJson = L.geoJSON(json); 43 | l.addLayer(geoJson); 44 | 45 | // Marker 46 | L.deflate({ 47 | markerOptions: { 48 | icon: L.icon({ 49 | iconUrl: 'img/marker.png', 50 | iconSize: [24, 24] 51 | }) 52 | } 53 | }); 54 | 55 | 56 | // CircleMarker 57 | L.deflate({ 58 | markerType: L.circleMarker, 59 | markerOptions: { 60 | radius: 12 61 | } 62 | }); 63 | 64 | // Custom marker function 65 | L.deflate({ 66 | markerOptions: () => { 67 | return { 68 | icon: L.icon({ 69 | iconUrl: 'img/marker.png', 70 | iconSize: [24, 24] 71 | }) 72 | }; 73 | } 74 | }); 75 | 76 | L.deflate({ 77 | markerOptions: () => { 78 | return { 79 | radius: 12 80 | }; 81 | } 82 | }); 83 | 84 | 85 | // Layer Injection 86 | L.deflate({ 87 | markerLayer: L.featureGroup() 88 | }); 89 | 90 | L.deflate({ 91 | markerLayer: L.markerClusterGroup() 92 | }); 93 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "system", 4 | "noImplicitAny": true, 5 | "removeComments": true, 6 | "preserveConstEnums": true, 7 | "outFile": "./test-output/types.js", 8 | "moduleResolution": "node" 9 | }, 10 | "files": [ 11 | "./types/index.d.ts", 12 | "./tests/types.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as L from 'leaflet'; 2 | 3 | type MarkerFactory = (latlng: L.LatLngExpression, options?: L.MarkerOptions) => L.Marker; 4 | type CircleMarkerFactory = (latlng: L.LatLngExpression, options?: L.CircleMarkerOptions) => L.CircleMarker; 5 | type MarkerOptionsFunction = (layer: L.Layer) => L.MarkerOptions | L.CircleMarkerOptions; 6 | 7 | declare module 'leaflet' { 8 | interface DeflateOptions extends LayerOptions { 9 | minSize?: number; 10 | markerOptions?: MarkerOptions | CircleMarkerOptions | MarkerOptionsFunction; 11 | markerType?: MarkerFactory | CircleMarkerFactory; 12 | markerLayer?: FeatureGroup; 13 | } 14 | 15 | class DeflateLayer extends FeatureGroup { 16 | constructor(options: DeflateOptions); 17 | prepLayer(layer: Layer): void; 18 | } 19 | 20 | function deflate(options: DeflateOptions): DeflateLayer; 21 | } 22 | export {}; 23 | --------------------------------------------------------------------------------