├── docs └── images │ └── geofences_reduced.png ├── config ├── tsconfig.json ├── jest.unit.config.ts └── jest.performance.config.ts ├── .github ├── workflows │ ├── test.yml │ ├── repolint.yml │ └── release.yml ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── index.d.ts ├── package.json ├── .gitignore ├── tests ├── performance │ ├── cases │ │ ├── geofences-case-2.json │ │ └── geofences-case-6.json │ └── index.test.ts └── unit │ ├── index.test.ts │ └── geofences.json ├── CHANGELOG.md ├── src └── index.ts ├── README.md └── LICENSE /docs/images/geofences_reduced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klarna/geofences-reducer/HEAD/docs/images/geofences_reduced.png -------------------------------------------------------------------------------- /config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | // "esModuleInterop": true, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "outDir": "../dist", 8 | "sourceMap": true, 9 | "strict": true, 10 | "target": "es2019" 11 | }, 12 | "include": [ "../src/*.ts" ] 13 | } -------------------------------------------------------------------------------- /config/jest.unit.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types' 2 | 3 | const config: Config.InitialOptions = { 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: 'config/tsconfig.json' 7 | } 8 | }, 9 | preset: 'ts-jest', 10 | rootDir: '../', 11 | testEnvironment: 'node', 12 | testMatch: ['/tests/unit/*.test.ts'], 13 | collectCoverage: false, 14 | } 15 | 16 | export default config 17 | -------------------------------------------------------------------------------- /config/jest.performance.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const performanceConfig: Config.InitialOptions = { 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: 'config/tsconfig.json', 7 | }, 8 | }, 9 | preset: 'ts-jest', 10 | rootDir: '../', 11 | testEnvironment: 'node', 12 | testMatch: ['/tests/performance/*.test.ts'], 13 | collectCoverage: false, 14 | verbose: true, 15 | maxConcurrency: 4, 16 | }; 17 | 18 | export default performanceConfig; 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | 7 | jobs: 8 | test: 9 | name: Node.js 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [ 12.x, 14.x, 16.x, 18.x ] 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Setup Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: Install modules 22 | run: npm ci 23 | - name: Build 24 | run: npm run build --if-present 25 | - name: Test 26 | run: npm test 27 | -------------------------------------------------------------------------------- /.github/workflows/repolint.yml: -------------------------------------------------------------------------------- 1 | name: Klarna repolint 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Checks-out the repository under $GITHUB_WORKSPACE 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install dependencies 18 | run: npm install repolinter log-symbols 19 | 20 | # @TODO Remove when fixed 21 | - name: Fix missing dependency in repolint 22 | run: npm install is-windows 23 | 24 | - name: Use custom rules 25 | run: wget https://raw.githubusercontent.com/klarna-incubator/meta/master/repolint.json 26 | 27 | - name: Run repolint 28 | run: ./node_modules/.bin/repolinter $GITHUB_WORKSPACE 29 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Klarna Bank AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export function reduce( 18 | geofences: { 19 | latitude: number, 20 | longitude: number, 21 | radius: number 22 | }[], 23 | config?: { 24 | precision?: number 25 | } 26 | ): { 27 | latitude: number, 28 | longitude: number, 29 | radius: number 30 | }[] 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | outputs: 12 | created: ${{ steps.release.outputs.release_created }} 13 | steps: 14 | - name: Release 15 | uses: google-github-actions/release-please-action@v3 16 | id: release 17 | with: 18 | release-type: node 19 | package-name: '@klarna/geofences-reducer' 20 | publish: 21 | name: Publish 22 | needs: release 23 | runs-on: ubuntu-latest 24 | if: ${{ needs.release.outputs.created }} 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: '14.x' 32 | registry-url: 'https://registry.npmjs.org' 33 | - name: Install modules 34 | run: npm ci 35 | - name: Build 36 | run: npm run build --if-present 37 | - name: Publish 38 | run: npm publish --access public 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@klarna/geofences-reducer", 3 | "version": "1.3.1", 4 | "description": "Reduces overlapping geofences", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "test": "npm run test:unit", 12 | "test:unit": "jest --config config/jest.unit.config.ts", 13 | "test:performance": "jest --config config/jest.performance.config.ts", 14 | "test:coverage": "jest --config config/jest.performance.config.ts --coverage", 15 | "build": "tsc -p config/tsconfig.json" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/klarna/geofences-reducer.git" 20 | }, 21 | "keywords": [ 22 | "geofence", 23 | "reducer", 24 | "overlap", 25 | "duplicate" 26 | ], 27 | "author": "Filipe Correa (http://github.com/filipecorrea)", 28 | "license": "Apache-2.0", 29 | "dependencies": { 30 | "vicinityhash": "^2.4.0" 31 | }, 32 | "devDependencies": { 33 | "@types/jest": "^28.1.6", 34 | "jest": "^28.1.3", 35 | "ts-jest": "^28.0.7", 36 | "ts-node": "^10.9.1", 37 | "typescript": "^4.7.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this Klarna project 2 | 3 | Are you here to help with this Klarna project? Welcome! Please read the following to better understand how to ask questions or work on something. 4 | 5 | All members of our community are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md). Please make sure you are welcoming and friendly in all of our spaces. 6 | 7 | ## Get in touch 8 | 9 | - Report bugs, suggest features or view the source code on GitHub. 10 | - If you have any questions concerning this product, please contact developers@klarna.com. 11 | 12 | ## Contributing to development 13 | 14 | At Klarna, we strive toward achieving the highest possible quality for our 15 | products. Therefore, we require you to follow these guidelines if you wish 16 | to contribute. 17 | 18 | Your contribution has to meet the following criteria: 19 | 20 | - It is accompanied by a description regarding what has been changed and why. 21 | - Pull requests should implement a boxed change, meaning they should optimally not try to address many things at once. 22 | - All code and documentation must follow the style specified by 23 | the included configuration. 24 | - New features and bug fixes must have accompanying unit tests. 25 | - All unit tests should pass. 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port -------------------------------------------------------------------------------- /tests/performance/cases/geofences-case-2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "latitude": -39.8769779205322, 4 | "longitude": 150.327861785889, 5 | "radius": 15000 6 | }, 7 | { 8 | "latitude": -38.4306203, 9 | "longitude": 150.1297996, 10 | "radius": 15000 11 | }, 12 | { 13 | "latitude": -28.9015520962416, 14 | "longitude": 118.89958190918, 15 | "radius": 15000 16 | }, 17 | { 18 | "latitude": -29.021406630833198, 19 | "longitude": 118.936059951782, 20 | "radius": 15000 21 | }, 22 | { 23 | "latitude": -29.058072741053003, 24 | "longitude": 118.792497396469, 25 | "radius": 15000 26 | }, 27 | { 28 | "latitude": -29.5091374475383, 29 | "longitude": 118.740065574646, 30 | "radius": 15000 31 | }, 32 | { 33 | "latitude": -29.125570959343797, 34 | "longitude": 118.868425369263, 35 | "radius": 15000 36 | }, 37 | { 38 | "latitude": -28.9917583465576, 39 | "longitude": 118.911769866943, 40 | "radius": 15000 41 | }, 42 | { 43 | "latitude": -28.7402744293213, 44 | "longitude": 118.765171051025, 45 | "radius": 15000 46 | }, 47 | { 48 | "latitude": -28.8516826629639, 49 | "longitude": 118.878124237061, 50 | "radius": 15000 51 | }, 52 | { 53 | "latitude": -28.9469547271729, 54 | "longitude": 118.832805633545, 55 | "radius": 15000 56 | }, 57 | { 58 | "latitude": -28.8966579437256, 59 | "longitude": 119.010646820068, 60 | "radius": 15000 61 | }, 62 | { 63 | "latitude": -29.0449733734131, 64 | "longitude": 118.752811431885, 65 | "radius": 15000 66 | }, 67 | { 68 | "latitude": -32.0162162, 69 | "longitude": 120.8776779, 70 | "radius": 15000 71 | }, 72 | { 73 | "latitude": -28.9431668, 74 | "longitude": 118.9243482, 75 | "radius": 15000 76 | }, 77 | { 78 | "latitude": -30.3228704, 79 | "longitude": 118.6351306, 80 | "radius": 15000 81 | }, 82 | { 83 | "latitude": -28.9532111, 84 | "longitude": 118.8564191, 85 | "radius": 15000 86 | }, 87 | { 88 | "latitude": -28.9093422, 89 | "longitude": 118.8107761, 90 | "radius": 15000 91 | } 92 | ] -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.3.1](https://github.com/klarna/geofences-reducer/compare/v1.3.0...v1.3.1) (2025-01-17) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * reduce test suite resource usage ([56c5cf0](https://github.com/klarna/geofences-reducer/commit/56c5cf02444b9ccab20f25f5904f4fee25e30edf)) 14 | 15 | ## [1.3.0](https://github.com/klarna/geofences-reducer/compare/v1.2.1...v1.3.0) (2025-01-17) 16 | 17 | 18 | ### Features 19 | 20 | * improve uniqueness algorithm in geofence reducer and performance tests ([a561df0](https://github.com/klarna/geofences-reducer/commit/a561df0ff1eb6034bba1164509807c1dbcd46334)) 21 | 22 | ## [1.2.1](https://github.com/klarna/geofences-reducer/compare/v1.2.0...v1.2.1) (2023-09-29) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * github org ([1cfa3a0](https://github.com/klarna/geofences-reducer/commit/1cfa3a0d9f7e25b9f3d3fa96bc9e523a849681d3)) 28 | * repolint path ([d8186fa](https://github.com/klarna/geofences-reducer/commit/d8186fa61d43722c639364c7cbd79a01c3328cb3)) 29 | 30 | ## [1.2.0](https://github.com/klarna/geofences-reducer/compare/v1.1.0...v1.2.0) (2023-09-28) 31 | 32 | 33 | ### Features 34 | 35 | * upgrade vicinityhash ([28e2d5a](https://github.com/klarna/geofences-reducer/commit/28e2d5ad9216d7859a9e4fc73e3a160ea6b00c15)) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * node 12 ([05dc0d9](https://github.com/klarna/geofences-reducer/commit/05dc0d9e6a39fa69da5ff7b92a74260aec83643e)) 41 | * npm 6 ([fb2777d](https://github.com/klarna/geofences-reducer/commit/fb2777d3e7e29422915163d06d2dc63fe4e5eedd)) 42 | * npm 6 ([b92a6a2](https://github.com/klarna/geofences-reducer/commit/b92a6a21a494cc4a1dea29831d82129fa052ef49)) 43 | * remove unnecessary deduplication ([5bcb981](https://github.com/klarna/geofences-reducer/commit/5bcb98161799677b8108882238afb922d8643f59)) 44 | 45 | ## [1.1.0](https://github.com/klarna/geofences-reducer/compare/v1.0.2...v1.1.0) (2023-03-06) 46 | 47 | 48 | ### Features 49 | 50 | * release ([4f910ef](https://github.com/klarna/geofences-reducer/commit/4f910efe0c081f0f4c1f43ee28dd906720cd1f76)) 51 | 52 | ## [unreleased] - 2023-01-02 53 | 54 | ## [1.0.2] - 2023-01-02 55 | 56 | ### Changed 57 | 58 | - Upgrade dependencies. 59 | - Return early after deduplicating geofences. 60 | 61 | ## [1.0.0] - 2022-07-20 62 | 63 | ### Added 64 | 65 | - Initial implementation. 66 | 67 | 68 | [unreleased]: https://github.com/klarna/geofences-reducer/compare/v1.0.2...HEAD 69 | [1.0.2]: https://github.com/klarna/geofences-reducer/compare/v1.0.0...v1.0.2 70 | [1.0.0]: https://github.com/klarna/geofences-reducer/releases/tag/v1.0.0 71 | -------------------------------------------------------------------------------- /tests/performance/index.test.ts: -------------------------------------------------------------------------------- 1 | import { performance } from 'perf_hooks'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { Geofence, reduce } from '../../src'; 5 | 6 | interface Scenario { 7 | label: string; 8 | geofences: Geofence[]; 9 | } 10 | 11 | interface MeasurePerformanceResult { 12 | avgTime: number; 13 | lastResultSize: number; 14 | times: number[]; 15 | } 16 | 17 | const FOLDER_PATH = './tests/performance/cases'; 18 | const ITERATIONS = 10; 19 | const MAX_ALLOWED_TIME = 3500; 20 | 21 | describe('Performance Tests', () => { 22 | const loadScenarios = (folderPath: string): Scenario[] => { 23 | return fs 24 | .readdirSync(folderPath) 25 | .filter((file) => file.toLowerCase().endsWith('.json')) 26 | .map((file) => { 27 | const fullPath = path.join(folderPath, file); 28 | try { 29 | const raw = fs.readFileSync(fullPath, 'utf8'); 30 | const geofences = JSON.parse(raw); 31 | if (Array.isArray(geofences)) { 32 | return { label: file, geofences }; 33 | } 34 | console.warn(`File "${file}" does not contain a valid geofence array.`); 35 | } catch (err) { 36 | console.error(`Failed to parse file "${file}":`, err); 37 | } 38 | return null; 39 | }) 40 | .filter((scenario): scenario is Scenario => scenario !== null); 41 | }; 42 | 43 | const measurePerformance = ( 44 | reduceFn: (geofences: Geofence[], options: { precision: number }) => Geofence[], 45 | geofences: Geofence[], 46 | precision: number, 47 | iterations: number 48 | ): MeasurePerformanceResult => { 49 | const times: number[] = []; 50 | let lastResult: Geofence[] = []; 51 | 52 | for (let i = 0; i < iterations; i++) { 53 | const start = performance.now(); 54 | lastResult = reduceFn(geofences, { precision }); 55 | const end = performance.now(); 56 | times.push(end - start); 57 | } 58 | 59 | const avgTime = times.reduce((sum, t) => sum + t, 0) / times.length; 60 | 61 | return { avgTime, lastResultSize: lastResult.length, times }; 62 | }; 63 | 64 | const scenarios = loadScenarios(FOLDER_PATH); 65 | 66 | if (scenarios.length === 0) { 67 | console.error('No valid geofence files found. Exiting.'); 68 | return; 69 | } 70 | 71 | const testCases = scenarios.flatMap(({ label, geofences }) => 72 | [4, 5, 6].map((precision) => ({ label, geofences, precision })) 73 | ); 74 | 75 | test.concurrent.each(testCases)( 76 | 'Scenario %s', 77 | async ({ label, geofences, precision }) => { 78 | const { avgTime, lastResultSize, times } = measurePerformance(reduce, geofences, precision, ITERATIONS); 79 | 80 | expect(avgTime).toBeLessThan(MAX_ALLOWED_TIME); 81 | expect(lastResultSize).toBeGreaterThan(0); 82 | 83 | console.log( 84 | ` > Scenario: ${label}, Precision: ${precision}, Avg Time: ${avgTime.toFixed(2)}ms, Min Time: ${Math.min( 85 | ...times 86 | ).toFixed(2)}ms, Max Time: ${Math.max(...times).toFixed(2)}ms, Result Size: ${lastResultSize}` 87 | ); 88 | } 89 | ); 90 | }); 91 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Klarna Bank AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as vicinityhash from 'vicinityhash' 18 | 19 | export type Geofence = { 20 | latitude: number 21 | longitude: number 22 | radius: number 23 | } 24 | 25 | const defaultPrecision = 6 26 | 27 | export function reduce ( 28 | geofences: Geofence[], 29 | config = { precision: defaultPrecision } 30 | ): Geofence[] { 31 | validateGeofences(geofences) 32 | if (config) validateConfig(config) 33 | 34 | if (geofences.length === 0) return geofences 35 | 36 | const geohashesList: string[][] = convertToGeohash(geofences, config.precision).map((geohashes) => [ 37 | ...new Set(geohashes) 38 | ]) 39 | const geohashFrequency = geohashesList.flat().reduce((geohashFrequencyMap, geohash) => { 40 | geohashFrequencyMap.set(geohash, (geohashFrequencyMap.get(geohash) || 0) + 1); 41 | return geohashFrequencyMap; 42 | }, new Map()); 43 | 44 | const selectedGeofences: Geofence[] = [] 45 | const coveredGeohashes: Set = new Set() 46 | const remainingGeofences: { geofence: Geofence; geohashes: string[] }[] = [] 47 | 48 | geofences.forEach((geofence, index) => { 49 | const geohashes = geohashesList[index] 50 | const hasUniqueGeohash = geohashes.some((geohash) => geohashFrequency.get(geohash) === 1) 51 | if (hasUniqueGeohash) { 52 | selectedGeofences.push(geofence) 53 | geohashes.forEach((geohash) => coveredGeohashes.add(geohash)) 54 | } else { 55 | remainingGeofences.push({ geofence, geohashes }) 56 | } 57 | }) 58 | 59 | remainingGeofences.forEach(({ geofence, geohashes }) => { 60 | const newGeohashes = geohashes.filter((geohash) => !coveredGeohashes.has(geohash)) 61 | if (newGeohashes.length > 0) { 62 | selectedGeofences.push(geofence) 63 | newGeohashes.forEach((geohash) => coveredGeohashes.add(geohash)) 64 | } 65 | }) 66 | 67 | return selectedGeofences 68 | } 69 | 70 | function validateGeofences(geofences: Geofence[]) { 71 | if (!Array.isArray(geofences)) { 72 | throw new Error('Geofences must be an array') 73 | } 74 | 75 | geofences.forEach(geofence => validateGeofence(geofence)) 76 | } 77 | 78 | function validateGeofence(geofence: Geofence) { 79 | const { latitude, longitude, radius } = geofence 80 | 81 | if (isNaN(latitude) || latitude < -90 || latitude > 90) { 82 | throw new Error('Latitude must be a number between -90 and 90') 83 | } 84 | if (isNaN(longitude) || longitude < -180 || longitude > 180) { 85 | throw new Error('Longitude must be a number between -180 and 180') 86 | } 87 | if (isNaN(radius) || !Number.isInteger(radius) || radius <= 0) { 88 | throw new Error('Radius must be a positive integer') 89 | } 90 | } 91 | 92 | function validateConfig(config: { precision: number }) { 93 | const { precision = defaultPrecision } = config 94 | 95 | if (isNaN(precision) || !Number.isInteger(precision) || precision < 1 || precision > 12) { 96 | throw new Error('Precision level must be a number between 1 and 12') 97 | } 98 | } 99 | 100 | function convertToGeohash(geofences: Geofence[], precision: number) { 101 | return geofences.map(geofence => 102 | vicinityhash.convert( 103 | geofence, 104 | { 105 | precision, 106 | compress: false 107 | } 108 | )) 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Geofences Reducer 2 | > Reduces overlapping geofences. 3 | 4 | [![Build Status][ci-image]][ci-url] 5 | [![License][license-image]][license-url] 6 | [![Developed at Klarna][klarna-image]][klarna-url] 7 | 8 | 9 | Given a list of circular geofences, this library removes the overlapping items and returns a reduced list that could be used to represent the same area. 10 | 11 | ## Usage example 12 | 13 | ### Installation 14 | 15 | Via npm: 16 | 17 | ```sh 18 | npm install @klarna/geofences-reducer --save 19 | ``` 20 | 21 | Via yarn: 22 | 23 | ```sh 24 | yarn add @klarna/geofences-reducer 25 | ``` 26 | 27 | ### Usage 28 | 29 | With JavaScript: 30 | 31 | ```javascript 32 | const reducer = require('@klarna/geofences-reducer') 33 | 34 | const geofences = [ 35 | { 36 | latitude: 51.51, 37 | longitude: -0.36, 38 | radius: 20000 // in meters 39 | }, 40 | { 41 | latitude: 51.51, 42 | longitude: -0.07, 43 | radius: 20000 // in meters 44 | }, 45 | { 46 | latitude: 51.6, 47 | longitude: -0.07, 48 | radius: 20000 // in meters 49 | }, 50 | { 51 | latitude: 51.51, 52 | longitude: 0.12, 53 | radius: 20000 // in meters 54 | }, 55 | { 56 | latitude: 51.15, 57 | longitude: -0.07, 58 | radius: 20000 // in meters 59 | }, 60 | { 61 | latitude: 51.72, 62 | longitude: -0.07, 63 | radius: 20000 // in meters 64 | } 65 | ] 66 | 67 | const geofencesReduced = reducer.reduce(geofences) 68 | ``` 69 | 70 | With TypeScript: 71 | 72 | ```typescript 73 | import * as reducer from '@klarna/geofences-reducer' 74 | 75 | type Geofence = { 76 | latitude: number 77 | longitude: number 78 | radius: number 79 | } 80 | 81 | const geofences: Geofence[] = [ 82 | { 83 | latitude: 51.51, 84 | longitude: -0.36, 85 | radius: 20000 // in meters 86 | }, 87 | { 88 | latitude: 51.51, 89 | longitude: -0.07, 90 | radius: 20000 // in meters 91 | }, 92 | { 93 | latitude: 51.6, 94 | longitude: -0.07, 95 | radius: 20000 // in meters 96 | }, 97 | { 98 | latitude: 51.51, 99 | longitude: 0.12, 100 | radius: 20000 // in meters 101 | }, 102 | { 103 | latitude: 51.15, 104 | longitude: -0.07, 105 | radius: 20000 // in meters 106 | }, 107 | { 108 | latitude: 51.72, 109 | longitude: -0.07, 110 | radius: 20000 // in meters 111 | } 112 | ] 113 | 114 | const geofencesReduced: Geofence[] = reducer.reduce(geofences) 115 | ``` 116 | 117 | Result: 118 | 119 | ![Geohashes](./docs/images/geofences_reduced.png) 120 | 121 | ### Optional Configuration 122 | 123 | #### Precision 124 | 125 | Precision can be customized as follows: 126 | 127 | ```javascript 128 | const config = { 129 | precision: 5 // 6 by default, accepts 1 to 12 130 | } 131 | 132 | const geofencesReduced = reducer.reduce(geofences, config) 133 | ``` 134 | 135 | ## Development setup 136 | 137 | Install project dependencies: 138 | 139 | ```sh 140 | npm install 141 | ``` 142 | 143 | Run automated test-suite: 144 | 145 | ```sh 146 | npm test 147 | ``` 148 | 149 | ## How to contribute 150 | 151 | See our guide on [contributing](.github/CONTRIBUTING.md). 152 | 153 | ## Release History 154 | 155 | See our [changelog](CHANGELOG.md). 156 | 157 | ## License 158 | 159 | Copyright © 2021 Klarna Bank AB 160 | 161 | For license details, see the [LICENSE](LICENSE) file in the root of this project. 162 | 163 | 164 | 165 | [ci-image]: https://img.shields.io/badge/build-passing-brightgreen?style=flat-square 166 | [ci-url]: https://github.com/klarna/geofences-reducer/actions 167 | [license-image]: https://img.shields.io/badge/license-Apache%202-blue?style=flat-square 168 | [license-url]: http://www.apache.org/licenses/LICENSE-2.0 169 | [klarna-image]: https://img.shields.io/badge/%20-Developed%20at%20Klarna-black?labelColor=ffb3c7&style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAYAAAAmL5yKAAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAALQAAAAAQAAAtAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAABCgAwAEAAAAAQAAAA4AAAAA0LMKiwAAAAlwSFlzAABuugAAbroB1t6xFwAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAAVBJREFUKBVtkz0vREEUhsdXgo5qJXohkUgQ0fgFNFpR2V5ClP6CQu9PiB6lEL1I7B9A4/treZ47c252s97k2ffMmZkz5869m1JKL/AFbzAHaiRbmsIf4BdaMAZqMFsOXNxXkroKbxCPV5l8yHOJLVipn9/vEreLa7FguSN3S2ynA/ATeQuI8tTY6OOY34DQaQnq9mPCDtxoBwuRxPfAvPMWnARlB12KAi6eLTPruOOP4gcl33O6+Sjgc83DJkRH+h2MgorLzaPy68W48BG2S+xYnmAa1L+nOxEduMH3fgjGFvZeVkANZau68B6CrgJxWosFFpF7iG+h5wKZqwt42qIJtARu/ix+gqsosEq8D35o6R3c7OL4lAnTDljEe9B3Qa2BYzmHemDCt6Diwo6JY7E+A82OnN9HuoBruAQvUQ1nSxP4GVzBDRyBfygf6RW2/gD3NmEv+K/DZgAAAABJRU5ErkJggg== 170 | [klarna-url]: https://klarna.github.io 171 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, political beliefs, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | developers@klarna.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /tests/unit/index.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Klarna Bank AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as reducer from '../../src/index' 18 | import * as vicinityhash from 'vicinityhash'; 19 | import { Geofence } from '../../src/index'; 20 | 21 | const geofences = require('./geofences.json') 22 | 23 | test('reduces geofences', () => { 24 | expect(reducer.reduce([])).toEqual([]) 25 | 26 | expect(reducer.reduce([ 27 | { latitude: 90, longitude: 180, radius: 500 } 28 | ])).toEqual([ 29 | { latitude: 90, longitude: 180, radius: 500 } 30 | ]) 31 | 32 | expect(reducer.reduce(geofences)).toEqual( [ 33 | { latitude: 48.64989, longitude: 13.96389, radius: 40000 }, 34 | { latitude: 48.4043, longitude: 14.836, radius: 40000 }, 35 | { latitude: 47.86719, longitude: 13.3261, radius: 40000 }, 36 | { latitude: 47.86259, longitude: 14.6709, radius: 40000 }, 37 | { latitude: 48.4611, longitude: 13.43369, radius: 40000 }, 38 | { latitude: 47.87969, longitude: 14.13269, radius: 40000 }, 39 | { latitude: 48, longitude: 13.2318, radius: 40000 }, 40 | { latitude: 48.51739, longitude: 14.2951, radius: 40000 }, 41 | { latitude: 48.51129, longitude: 14.5008, radius: 40000 }, 42 | { latitude: 47.65489, longitude: 13.60929, radius: 40000 }, 43 | { latitude: 47.85749, longitude: 13.34189, radius: 40000 }, 44 | { latitude: 48.678, longitude: 13.90909, radius: 40000 }, 45 | { latitude: 48.2285, longitude: 13.02369, radius: 40000 }, 46 | { latitude: 48.25849, longitude: 13.03559, radius: 40000 }, 47 | { latitude: 48.22809, longitude: 14.8465, radius: 40000 }, 48 | { latitude: 48.18239, longitude: 13.78149, radius: 40000 } 49 | ]) 50 | }) 51 | 52 | test('reduces geofences with custom precision', () => { 53 | const config = { precision: 5 } 54 | 55 | expect(reducer.reduce(geofences, config)).toEqual([ 56 | { latitude: 48.64989, longitude: 13.96389, radius: 40000 }, 57 | { latitude: 48.4043, longitude: 14.836, radius: 40000 }, 58 | { latitude: 47.86719, longitude: 13.3261, radius: 40000 }, 59 | { latitude: 47.86259, longitude: 14.6709, radius: 40000 }, 60 | { latitude: 48.4611, longitude: 13.43369, radius: 40000 }, 61 | { latitude: 47.87969, longitude: 14.13269, radius: 40000 }, 62 | { latitude: 48, longitude: 13.2318, radius: 40000 }, 63 | { latitude: 48.51129, longitude: 14.5008, radius: 40000 }, 64 | { latitude: 47.65489, longitude: 13.60929, radius: 40000 }, 65 | { latitude: 48.678, longitude: 13.90909, radius: 40000 }, 66 | { latitude: 48.2285, longitude: 13.02369, radius: 40000 }, 67 | { latitude: 48.22809, longitude: 14.8465, radius: 40000 }, 68 | { latitude: 48.256, longitude: 13.0367, radius: 40000 } 69 | ]) 70 | }) 71 | 72 | test.each([3, 4, 5])( 73 | 'ensures unique geohashes list of input matches output geofences at precision %i', 74 | (precision) => { 75 | const config = { precision }; 76 | const reducedGeofences = reducer.reduce(geofences, config); 77 | 78 | const inputGeohashes = new Set( 79 | geofences 80 | .flatMap((geofence: Geofence) => 81 | vicinityhash.convert(geofence, { precision }) 82 | ) 83 | ); 84 | 85 | const outputGeohashes = new Set( 86 | reducedGeofences 87 | .flatMap((geofence: Geofence) => 88 | vicinityhash.convert(geofence, { precision }) 89 | ) 90 | ); 91 | 92 | expect(inputGeohashes).toEqual(outputGeohashes); 93 | } 94 | ); 95 | 96 | test('throws error if geofences are invalid', () => { 97 | expect(() => { reducer.reduce({} as unknown as []) }).toThrow('Geofences must be an array') 98 | }) 99 | 100 | test('throws error if latitude is invalid', () => { 101 | const originalGeofence = geofences[0] 102 | 103 | geofences[0] = { latitude: 'foo' } 104 | expect(() => { reducer.reduce(geofences) }).toThrow('Latitude must be a number between -90 and 90') 105 | 106 | geofences[0] = { latitude: -91 } 107 | expect(() => { reducer.reduce(geofences) }).toThrow('Latitude must be a number between -90 and 90') 108 | 109 | geofences[0] = { latitude: 91 } 110 | expect(() => { reducer.reduce(geofences) }).toThrow('Latitude must be a number between -90 and 90') 111 | 112 | geofences[0] = originalGeofence 113 | }) 114 | 115 | test('throws error if longitude is invalid', () => { 116 | const originalGeofence = geofences[0] 117 | 118 | geofences[0] = { latitude: 90, longitude: 'foo' } 119 | expect(() => { reducer.reduce(geofences) }).toThrow('Longitude must be a number between -180 and 180') 120 | 121 | geofences[0] = { latitude: 90, longitude: -181 } 122 | expect(() => { reducer.reduce(geofences) }).toThrow('Longitude must be a number between -180 and 180') 123 | 124 | geofences[0] = { latitude: 90, longitude: 181 } 125 | expect(() => { reducer.reduce(geofences) }).toThrow('Longitude must be a number between -180 and 180') 126 | 127 | geofences[0] = originalGeofence 128 | }) 129 | 130 | test('throws error if radius is invalid', () => { 131 | const originalGeofence = geofences[0] 132 | 133 | geofences[0] = { latitude: 90, longitude: 180, radius: 'foo' } 134 | expect(() => { reducer.reduce(geofences) }).toThrow('Radius must be a positive integer') 135 | 136 | geofences[0] = { latitude: 90, longitude: 180, radius: 0 } 137 | expect(() => { reducer.reduce(geofences) }).toThrow('Radius must be a positive integer') 138 | 139 | geofences[0] = { latitude: 90, longitude: 180, radius: -1 } 140 | expect(() => { reducer.reduce(geofences) }).toThrow('Radius must be a positive integer') 141 | 142 | geofences[0] = originalGeofence 143 | }) 144 | 145 | test('throws error if precision is invalid', () => { 146 | const config = { precision: 0 } 147 | 148 | config.precision = 'foo' as unknown as number 149 | expect(() => { reducer.reduce(geofences, config) }).toThrow('Precision level must be a number between 1 and 12') 150 | 151 | config.precision = 0 152 | expect(() => { reducer.reduce(geofences, config) }).toThrow('Precision level must be a number between 1 and 12') 153 | 154 | config.precision = 13 155 | expect(() => { reducer.reduce(geofences, config) }).toThrow('Precision level must be a number between 1 and 12') 156 | }) 157 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /tests/unit/geofences.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "latitude": 48.18239, 4 | "longitude": 13.78149, 5 | "radius": 40000 6 | }, 7 | { 8 | "latitude": 48.18239, 9 | "longitude": 13.78149, 10 | "radius": 40000 11 | }, 12 | { 13 | "latitude": 48.21419, 14 | "longitude": 13.4861, 15 | "radius": 40000 16 | }, 17 | { 18 | "latitude": 48.2946, 19 | "longitude": 14.2868, 20 | "radius": 40000 21 | }, 22 | { 23 | "latitude": 48.13899, 24 | "longitude": 14.2299, 25 | "radius": 40000 26 | }, 27 | { 28 | "latitude": 48.2638, 29 | "longitude": 14.45419, 30 | "radius": 40000 31 | }, 32 | { 33 | "latitude": 48.24319, 34 | "longitude": 14.3371, 35 | "radius": 40000 36 | }, 37 | { 38 | "latitude": 48.02899, 39 | "longitude": 14.40999, 40 | "radius": 40000 41 | }, 42 | { 43 | "latitude": 48.34669, 44 | "longitude": 14.059, 45 | "radius": 40000 46 | }, 47 | { 48 | "latitude": 48.17509, 49 | "longitude": 14.05519, 50 | "radius": 40000 51 | }, 52 | { 53 | "latitude": 48.0483, 54 | "longitude": 14.4153, 55 | "radius": 40000 56 | }, 57 | { 58 | "latitude": 48.2181, 59 | "longitude": 14.42189, 60 | "radius": 40000 61 | }, 62 | { 63 | "latitude": 48.64989, 64 | "longitude": 13.96389, 65 | "radius": 40000 66 | }, 67 | { 68 | "latitude": 48.64989, 69 | "longitude": 13.96389, 70 | "radius": 10000 71 | }, 72 | { 73 | "latitude": 48.32279, 74 | "longitude": 14.2888, 75 | "radius": 40000 76 | }, 77 | { 78 | "latitude": 48.04909, 79 | "longitude": 14.4236, 80 | "radius": 40000 81 | }, 82 | { 83 | "latitude": 48.27099, 84 | "longitude": 14.2985, 85 | "radius": 40000 86 | }, 87 | { 88 | "latitude": 47.7181, 89 | "longitude": 13.59519, 90 | "radius": 40000 91 | }, 92 | { 93 | "latitude": 48.34059, 94 | "longitude": 14.1674, 95 | "radius": 40000 96 | }, 97 | { 98 | "latitude": 48.21709, 99 | "longitude": 14.47789, 100 | "radius": 40000 101 | }, 102 | { 103 | "latitude": 48.19449, 104 | "longitude": 14.1152, 105 | "radius": 40000 106 | }, 107 | { 108 | "latitude": 48.0017, 109 | "longitude": 13.6026, 110 | "radius": 40000 111 | }, 112 | { 113 | "latitude": 48.2761, 114 | "longitude": 14.25209, 115 | "radius": 40000 116 | }, 117 | { 118 | "latitude": 48.2854, 119 | "longitude": 14.35229, 120 | "radius": 40000 121 | }, 122 | { 123 | "latitude": 48.2092, 124 | "longitude": 13.4954, 125 | "radius": 40000 126 | }, 127 | { 128 | "latitude": 48.23239, 129 | "longitude": 14.25139, 130 | "radius": 40000 131 | }, 132 | { 133 | "latitude": 48.041, 134 | "longitude": 14.4258, 135 | "radius": 40000 136 | }, 137 | { 138 | "latitude": 48.25899, 139 | "longitude": 14.2107, 140 | "radius": 40000 141 | }, 142 | { 143 | "latitude": 48.3021, 144 | "longitude": 14.29969, 145 | "radius": 40000 146 | }, 147 | { 148 | "latitude": 48.20999, 149 | "longitude": 14.4777, 150 | "radius": 40000 151 | }, 152 | { 153 | "latitude": 48.4043, 154 | "longitude": 14.836, 155 | "radius": 40000 156 | }, 157 | { 158 | "latitude": 48.0293, 159 | "longitude": 14.22669, 160 | "radius": 40000 161 | }, 162 | { 163 | "latitude": 48.29949, 164 | "longitude": 14.29109, 165 | "radius": 40000 166 | }, 167 | { 168 | "latitude": 48.3506, 169 | "longitude": 14.41049, 170 | "radius": 40000 171 | }, 172 | { 173 | "latitude": 47.9649, 174 | "longitude": 14.2565, 175 | "radius": 40000 176 | }, 177 | { 178 | "latitude": 48.2089, 179 | "longitude": 13.48799, 180 | "radius": 40000 181 | }, 182 | { 183 | "latitude": 48.0502, 184 | "longitude": 14.13179, 185 | "radius": 40000 186 | }, 187 | { 188 | "latitude": 48.33629, 189 | "longitude": 13.7733, 190 | "radius": 40000 191 | }, 192 | { 193 | "latitude": 47.68949, 194 | "longitude": 13.6243, 195 | "radius": 40000 196 | }, 197 | { 198 | "latitude": 48.49919, 199 | "longitude": 14.5038, 200 | "radius": 40000 201 | }, 202 | { 203 | "latitude": 48.26969, 204 | "longitude": 14.28779, 205 | "radius": 40000 206 | }, 207 | { 208 | "latitude": 47.948, 209 | "longitude": 13.59609, 210 | "radius": 40000 211 | }, 212 | { 213 | "latitude": 47.92079, 214 | "longitude": 14.44009, 215 | "radius": 40000 216 | }, 217 | { 218 | "latitude": 48.1621, 219 | "longitude": 13.9995, 220 | "radius": 40000 221 | }, 222 | { 223 | "latitude": 47.86719, 224 | "longitude": 13.3261, 225 | "radius": 40000 226 | }, 227 | { 228 | "latitude": 48.07929, 229 | "longitude": 14.37469, 230 | "radius": 40000 231 | }, 232 | { 233 | "latitude": 48.28509, 234 | "longitude": 14.2958, 235 | "radius": 40000 236 | }, 237 | { 238 | "latitude": 47.86259, 239 | "longitude": 14.6709, 240 | "radius": 40000 241 | }, 242 | { 243 | "latitude": 47.92549, 244 | "longitude": 13.7896, 245 | "radius": 40000 246 | }, 247 | { 248 | "latitude": 48.2158, 249 | "longitude": 14.2804, 250 | "radius": 40000 251 | }, 252 | { 253 | "latitude": 48.4611, 254 | "longitude": 13.43369, 255 | "radius": 40000 256 | }, 257 | { 258 | "latitude": 48.0404, 259 | "longitude": 14.3142, 260 | "radius": 40000 261 | }, 262 | { 263 | "latitude": 47.87969, 264 | "longitude": 14.13269, 265 | "radius": 40000 266 | }, 267 | { 268 | "latitude": 48.25049, 269 | "longitude": 13.23499, 270 | "radius": 40000 271 | }, 272 | { 273 | "latitude": 48.2741, 274 | "longitude": 14.3107, 275 | "radius": 40000 276 | }, 277 | { 278 | "latitude": 48.2322, 279 | "longitude": 13.81079, 280 | "radius": 40000 281 | }, 282 | { 283 | "latitude": 48.3254, 284 | "longitude": 13.85109, 285 | "radius": 40000 286 | }, 287 | { 288 | "latitude": 48.23989, 289 | "longitude": 14.52639, 290 | "radius": 40000 291 | }, 292 | { 293 | "latitude": 48.0101, 294 | "longitude": 13.71949, 295 | "radius": 40000 296 | }, 297 | { 298 | "latitude": 47.98429, 299 | "longitude": 13.4095, 300 | "radius": 40000 301 | }, 302 | { 303 | "latitude": 48.256, 304 | "longitude": 13.0367, 305 | "radius": 40000 306 | }, 307 | { 308 | "latitude": 47.7108, 309 | "longitude": 13.62209, 310 | "radius": 40000 311 | }, 312 | { 313 | "latitude": 47.8931, 314 | "longitude": 14.1295, 315 | "radius": 40000 316 | }, 317 | { 318 | "latitude": 48.207, 319 | "longitude": 13.4832, 320 | "radius": 40000 321 | }, 322 | { 323 | "latitude": 48.22309, 324 | "longitude": 14.2382, 325 | "radius": 40000 326 | }, 327 | { 328 | "latitude": 47.92929, 329 | "longitude": 14.08559, 330 | "radius": 40000 331 | }, 332 | { 333 | "latitude": 48, 334 | "longitude": 13.2318, 335 | "radius": 40000 336 | }, 337 | { 338 | "latitude": 47.93159, 339 | "longitude": 13.493, 340 | "radius": 40000 341 | }, 342 | { 343 | "latitude": 48.2854, 344 | "longitude": 14.3111, 345 | "radius": 40000 346 | }, 347 | { 348 | "latitude": 48.0082, 349 | "longitude": 13.9251, 350 | "radius": 40000 351 | }, 352 | { 353 | "latitude": 48.0469, 354 | "longitude": 13.98729, 355 | "radius": 40000 356 | }, 357 | { 358 | "latitude": 48.26319, 359 | "longitude": 14.25879, 360 | "radius": 40000 361 | }, 362 | { 363 | "latitude": 48.1967, 364 | "longitude": 13.10299, 365 | "radius": 40000 366 | }, 367 | { 368 | "latitude": 48.51739, 369 | "longitude": 14.2951, 370 | "radius": 40000 371 | }, 372 | { 373 | "latitude": 48.32789, 374 | "longitude": 14.32549, 375 | "radius": 40000 376 | }, 377 | { 378 | "latitude": 48.24129, 379 | "longitude": 14.62569, 380 | "radius": 40000 381 | }, 382 | { 383 | "latitude": 48.2801, 384 | "longitude": 14.28339, 385 | "radius": 40000 386 | }, 387 | { 388 | "latitude": 48.41519, 389 | "longitude": 14.04249, 390 | "radius": 40000 391 | }, 392 | { 393 | "latitude": 48.05469, 394 | "longitude": 13.7733, 395 | "radius": 40000 396 | }, 397 | { 398 | "latitude": 48.09519, 399 | "longitude": 13.6618, 400 | "radius": 40000 401 | }, 402 | { 403 | "latitude": 47.90599, 404 | "longitude": 14.1208, 405 | "radius": 40000 406 | }, 407 | { 408 | "latitude": 48.3357, 409 | "longitude": 14.30029, 410 | "radius": 40000 411 | }, 412 | { 413 | "latitude": 48.145, 414 | "longitude": 14.4656, 415 | "radius": 40000 416 | }, 417 | { 418 | "latitude": 48.30139, 419 | "longitude": 14.16639, 420 | "radius": 40000 421 | }, 422 | { 423 | "latitude": 48.0666, 424 | "longitude": 14.4201, 425 | "radius": 40000 426 | }, 427 | { 428 | "latitude": 48.2629, 429 | "longitude": 14.3176, 430 | "radius": 40000 431 | }, 432 | { 433 | "latitude": 48.3021, 434 | "longitude": 14.29139, 435 | "radius": 40000 436 | }, 437 | { 438 | "latitude": 48.51129, 439 | "longitude": 14.5008, 440 | "radius": 40000 441 | }, 442 | { 443 | "latitude": 48.0191, 444 | "longitude": 13.7283, 445 | "radius": 40000 446 | }, 447 | { 448 | "latitude": 48.2671, 449 | "longitude": 14.2655, 450 | "radius": 40000 451 | }, 452 | { 453 | "latitude": 48.35739, 454 | "longitude": 14.5198, 455 | "radius": 40000 456 | }, 457 | { 458 | "latitude": 47.9555, 459 | "longitude": 13.5851, 460 | "radius": 40000 461 | }, 462 | { 463 | "latitude": 48.1533, 464 | "longitude": 14.0092, 465 | "radius": 40000 466 | }, 467 | { 468 | "latitude": 48.18539, 469 | "longitude": 14.0256, 470 | "radius": 40000 471 | }, 472 | { 473 | "latitude": 48.16849, 474 | "longitude": 14.0254, 475 | "radius": 40000 476 | }, 477 | { 478 | "latitude": 48.2909, 479 | "longitude": 14.27709, 480 | "radius": 40000 481 | }, 482 | { 483 | "latitude": 47.98799, 484 | "longitude": 13.6927, 485 | "radius": 40000 486 | }, 487 | { 488 | "latitude": 48.2498, 489 | "longitude": 14.3083, 490 | "radius": 40000 491 | }, 492 | { 493 | "latitude": 47.89239, 494 | "longitude": 13.76609, 495 | "radius": 40000 496 | }, 497 | { 498 | "latitude": 48.24729, 499 | "longitude": 14.25879, 500 | "radius": 40000 501 | }, 502 | { 503 | "latitude": 47.65489, 504 | "longitude": 13.60929, 505 | "radius": 40000 506 | }, 507 | { 508 | "latitude": 47.98099, 509 | "longitude": 13.8245, 510 | "radius": 40000 511 | }, 512 | { 513 | "latitude": 48.24129, 514 | "longitude": 14.26, 515 | "radius": 40000 516 | }, 517 | { 518 | "latitude": 48.24549, 519 | "longitude": 14.28489, 520 | "radius": 40000 521 | }, 522 | { 523 | "latitude": 48.24359, 524 | "longitude": 14.2319, 525 | "radius": 40000 526 | }, 527 | { 528 | "latitude": 48.0391, 529 | "longitude": 14.4188, 530 | "radius": 40000 531 | }, 532 | { 533 | "latitude": 48.06269, 534 | "longitude": 13.48239, 535 | "radius": 40000 536 | }, 537 | { 538 | "latitude": 48.1268, 539 | "longitude": 13.9425, 540 | "radius": 40000 541 | }, 542 | { 543 | "latitude": 47.85749, 544 | "longitude": 13.34189, 545 | "radius": 40000 546 | }, 547 | { 548 | "latitude": 48.2424, 549 | "longitude": 14.36219, 550 | "radius": 40000 551 | }, 552 | { 553 | "latitude": 48.3008, 554 | "longitude": 13.63799, 555 | "radius": 40000 556 | }, 557 | { 558 | "latitude": 48.2732, 559 | "longitude": 14.57699, 560 | "radius": 40000 561 | }, 562 | { 563 | "latitude": 48.5675, 564 | "longitude": 13.98229, 565 | "radius": 40000 566 | }, 567 | { 568 | "latitude": 48.28799, 569 | "longitude": 14.3, 570 | "radius": 40000 571 | }, 572 | { 573 | "latitude": 48.0032, 574 | "longitude": 13.4909, 575 | "radius": 40000 576 | }, 577 | { 578 | "latitude": 48.048, 579 | "longitude": 13.78129, 580 | "radius": 40000 581 | }, 582 | { 583 | "latitude": 48.30759, 584 | "longitude": 14.2958, 585 | "radius": 40000 586 | }, 587 | { 588 | "latitude": 48.678, 589 | "longitude": 13.90909, 590 | "radius": 40000 591 | }, 592 | { 593 | "latitude": 48.21509, 594 | "longitude": 14.46399, 595 | "radius": 40000 596 | }, 597 | { 598 | "latitude": 47.98059, 599 | "longitude": 13.6117, 600 | "radius": 40000 601 | }, 602 | { 603 | "latitude": 48.2316, 604 | "longitude": 13.9207, 605 | "radius": 40000 606 | }, 607 | { 608 | "latitude": 48.24499, 609 | "longitude": 13.03409, 610 | "radius": 40000 611 | }, 612 | { 613 | "latitude": 48.29209, 614 | "longitude": 14.305, 615 | "radius": 40000 616 | }, 617 | { 618 | "latitude": 48.35979, 619 | "longitude": 13.99639, 620 | "radius": 40000 621 | }, 622 | { 623 | "latitude": 48.05709, 624 | "longitude": 14.4375, 625 | "radius": 40000 626 | }, 627 | { 628 | "latitude": 48.1604, 629 | "longitude": 14.0246, 630 | "radius": 40000 631 | }, 632 | { 633 | "latitude": 48.25479, 634 | "longitude": 13.0437, 635 | "radius": 40000 636 | }, 637 | { 638 | "latitude": 48.0815, 639 | "longitude": 13.5647, 640 | "radius": 40000 641 | }, 642 | { 643 | "latitude": 48.3041, 644 | "longitude": 14.027, 645 | "radius": 40000 646 | }, 647 | { 648 | "latitude": 48.34469, 649 | "longitude": 14.1618, 650 | "radius": 40000 651 | }, 652 | { 653 | "latitude": 48.36579, 654 | "longitude": 13.56799, 655 | "radius": 40000 656 | }, 657 | { 658 | "latitude": 48.2965, 659 | "longitude": 14.2916, 660 | "radius": 40000 661 | }, 662 | { 663 | "latitude": 47.8983, 664 | "longitude": 13.94979, 665 | "radius": 40000 666 | }, 667 | { 668 | "latitude": 48.2285, 669 | "longitude": 13.02369, 670 | "radius": 40000 671 | }, 672 | { 673 | "latitude": 48.3301, 674 | "longitude": 14.28759, 675 | "radius": 40000 676 | }, 677 | { 678 | "latitude": 48.45669, 679 | "longitude": 13.4326, 680 | "radius": 40000 681 | }, 682 | { 683 | "latitude": 48.25849, 684 | "longitude": 13.03559, 685 | "radius": 40000 686 | }, 687 | { 688 | "latitude": 48.2537, 689 | "longitude": 14.2839, 690 | "radius": 40000 691 | }, 692 | { 693 | "latitude": 48.0037, 694 | "longitude": 13.6778, 695 | "radius": 40000 696 | }, 697 | { 698 | "latitude": 48.1631, 699 | "longitude": 14.0157, 700 | "radius": 40000 701 | }, 702 | { 703 | "latitude": 48.046, 704 | "longitude": 14.33259, 705 | "radius": 40000 706 | }, 707 | { 708 | "latitude": 48.22809, 709 | "longitude": 14.8465, 710 | "radius": 40000 711 | }, 712 | { 713 | "latitude": 48.22019, 714 | "longitude": 14.19089, 715 | "radius": 40000 716 | }, 717 | { 718 | "latitude": 47.9155, 719 | "longitude": 13.5339, 720 | "radius": 40000 721 | }, 722 | { 723 | "latitude": 48.0914, 724 | "longitude": 13.8675, 725 | "radius": 40000 726 | }, 727 | { 728 | "latitude": 48.31539, 729 | "longitude": 14.28079, 730 | "radius": 40000 731 | }, 732 | { 733 | "latitude": 48.31129, 734 | "longitude": 14.28339, 735 | "radius": 40000 736 | }, 737 | { 738 | "latitude": 48.3194, 739 | "longitude": 14.4693, 740 | "radius": 40000 741 | }, 742 | { 743 | "latitude": 48.1809, 744 | "longitude": 14.062, 745 | "radius": 40000 746 | }, 747 | { 748 | "latitude": 48.29389, 749 | "longitude": 14.31379, 750 | "radius": 40000 751 | }, 752 | { 753 | "latitude": 48.2537, 754 | "longitude": 14.2798, 755 | "radius": 40000 756 | }, 757 | { 758 | "latitude": 48.0297, 759 | "longitude": 14.42549, 760 | "radius": 40000 761 | }, 762 | { 763 | "latitude": 48.20429, 764 | "longitude": 14.2552, 765 | "radius": 40000 766 | }, 767 | { 768 | "latitude": 48.25329, 769 | "longitude": 14.31009, 770 | "radius": 40000 771 | }, 772 | { 773 | "latitude": 48.30059, 774 | "longitude": 14.29059, 775 | "radius": 40000 776 | }, 777 | { 778 | "latitude": 47.92649, 779 | "longitude": 13.78899, 780 | "radius": 40000 781 | }, 782 | { 783 | "latitude": 48.18009, 784 | "longitude": 14.0185, 785 | "radius": 40000 786 | }, 787 | { 788 | "latitude": 47.80659, 789 | "longitude": 13.7711, 790 | "radius": 40000 791 | }, 792 | { 793 | "latitude": 48.2284, 794 | "longitude": 13.935, 795 | "radius": 40000 796 | }, 797 | { 798 | "latitude": 48.3484, 799 | "longitude": 14.52939, 800 | "radius": 40000 801 | }, 802 | { 803 | "latitude": 48.0806, 804 | "longitude": 13.8583, 805 | "radius": 40000 806 | }, 807 | { 808 | "latitude": 48.3142, 809 | "longitude": 14.2832, 810 | "radius": 40000 811 | }, 812 | { 813 | "latitude": 48.21039, 814 | "longitude": 14.27299, 815 | "radius": 40000 816 | } 817 | ] -------------------------------------------------------------------------------- /tests/performance/cases/geofences-case-6.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "latitude": 51.18239, 4 | "longitude": 16.781489999999998, 5 | "radius": 40000 6 | }, 7 | { 8 | "latitude": 51.18239, 9 | "longitude": 16.781489999999998, 10 | "radius": 40000 11 | }, 12 | { 13 | "latitude": 51.21419, 14 | "longitude": 16.4861, 15 | "radius": 40000 16 | }, 17 | { 18 | "latitude": 51.2946, 19 | "longitude": 17.2868, 20 | "radius": 40000 21 | }, 22 | { 23 | "latitude": 51.13899, 24 | "longitude": 17.2299, 25 | "radius": 40000 26 | }, 27 | { 28 | "latitude": 51.2638, 29 | "longitude": 17.45419, 30 | "radius": 40000 31 | }, 32 | { 33 | "latitude": 51.24319, 34 | "longitude": 17.3371, 35 | "radius": 40000 36 | }, 37 | { 38 | "latitude": 51.02899, 39 | "longitude": 17.40999, 40 | "radius": 40000 41 | }, 42 | { 43 | "latitude": 51.34669, 44 | "longitude": 17.058999999999997, 45 | "radius": 40000 46 | }, 47 | { 48 | "latitude": 51.17509, 49 | "longitude": 17.05519, 50 | "radius": 40000 51 | }, 52 | { 53 | "latitude": 51.0483, 54 | "longitude": 17.415300000000002, 55 | "radius": 40000 56 | }, 57 | { 58 | "latitude": 51.2181, 59 | "longitude": 17.421889999999998, 60 | "radius": 40000 61 | }, 62 | { 63 | "latitude": 51.64989, 64 | "longitude": 16.96389, 65 | "radius": 40000 66 | }, 67 | { 68 | "latitude": 51.64989, 69 | "longitude": 16.96389, 70 | "radius": 10000 71 | }, 72 | { 73 | "latitude": 51.32279, 74 | "longitude": 17.288800000000002, 75 | "radius": 40000 76 | }, 77 | { 78 | "latitude": 51.04909, 79 | "longitude": 17.4236, 80 | "radius": 40000 81 | }, 82 | { 83 | "latitude": 51.27099, 84 | "longitude": 17.2985, 85 | "radius": 40000 86 | }, 87 | { 88 | "latitude": 50.7181, 89 | "longitude": 16.595190000000002, 90 | "radius": 40000 91 | }, 92 | { 93 | "latitude": 51.34059, 94 | "longitude": 17.1674, 95 | "radius": 40000 96 | }, 97 | { 98 | "latitude": 51.21709, 99 | "longitude": 17.477890000000002, 100 | "radius": 40000 101 | }, 102 | { 103 | "latitude": 51.19449, 104 | "longitude": 17.1152, 105 | "radius": 40000 106 | }, 107 | { 108 | "latitude": 51.0017, 109 | "longitude": 16.602600000000002, 110 | "radius": 40000 111 | }, 112 | { 113 | "latitude": 51.2761, 114 | "longitude": 17.252090000000003, 115 | "radius": 40000 116 | }, 117 | { 118 | "latitude": 51.2854, 119 | "longitude": 17.35229, 120 | "radius": 40000 121 | }, 122 | { 123 | "latitude": 51.2092, 124 | "longitude": 16.4954, 125 | "radius": 40000 126 | }, 127 | { 128 | "latitude": 51.23239, 129 | "longitude": 17.25139, 130 | "radius": 40000 131 | }, 132 | { 133 | "latitude": 51.041, 134 | "longitude": 17.425800000000002, 135 | "radius": 40000 136 | }, 137 | { 138 | "latitude": 51.25899, 139 | "longitude": 17.2107, 140 | "radius": 40000 141 | }, 142 | { 143 | "latitude": 51.3021, 144 | "longitude": 17.29969, 145 | "radius": 40000 146 | }, 147 | { 148 | "latitude": 51.20999, 149 | "longitude": 17.4777, 150 | "radius": 40000 151 | }, 152 | { 153 | "latitude": 51.4043, 154 | "longitude": 17.836, 155 | "radius": 40000 156 | }, 157 | { 158 | "latitude": 51.0293, 159 | "longitude": 17.226689999999998, 160 | "radius": 40000 161 | }, 162 | { 163 | "latitude": 51.29949, 164 | "longitude": 17.29109, 165 | "radius": 40000 166 | }, 167 | { 168 | "latitude": 51.3506, 169 | "longitude": 17.41049, 170 | "radius": 40000 171 | }, 172 | { 173 | "latitude": 50.9649, 174 | "longitude": 17.256500000000003, 175 | "radius": 40000 176 | }, 177 | { 178 | "latitude": 51.2089, 179 | "longitude": 16.48799, 180 | "radius": 40000 181 | }, 182 | { 183 | "latitude": 51.0502, 184 | "longitude": 17.131790000000002, 185 | "radius": 40000 186 | }, 187 | { 188 | "latitude": 51.33629, 189 | "longitude": 16.7733, 190 | "radius": 40000 191 | }, 192 | { 193 | "latitude": 50.68949, 194 | "longitude": 16.624299999999998, 195 | "radius": 40000 196 | }, 197 | { 198 | "latitude": 51.49919, 199 | "longitude": 17.5038, 200 | "radius": 40000 201 | }, 202 | { 203 | "latitude": 51.26969, 204 | "longitude": 17.28779, 205 | "radius": 40000 206 | }, 207 | { 208 | "latitude": 50.948, 209 | "longitude": 16.59609, 210 | "radius": 40000 211 | }, 212 | { 213 | "latitude": 50.92079, 214 | "longitude": 17.440089999999998, 215 | "radius": 40000 216 | }, 217 | { 218 | "latitude": 51.1621, 219 | "longitude": 16.999499999999998, 220 | "radius": 40000 221 | }, 222 | { 223 | "latitude": 50.86719, 224 | "longitude": 16.3261, 225 | "radius": 40000 226 | }, 227 | { 228 | "latitude": 51.07929, 229 | "longitude": 17.37469, 230 | "radius": 40000 231 | }, 232 | { 233 | "latitude": 51.28509, 234 | "longitude": 17.2958, 235 | "radius": 40000 236 | }, 237 | { 238 | "latitude": 50.86259, 239 | "longitude": 17.6709, 240 | "radius": 40000 241 | }, 242 | { 243 | "latitude": 50.92549, 244 | "longitude": 16.7896, 245 | "radius": 40000 246 | }, 247 | { 248 | "latitude": 51.2158, 249 | "longitude": 17.2804, 250 | "radius": 40000 251 | }, 252 | { 253 | "latitude": 51.4611, 254 | "longitude": 16.43369, 255 | "radius": 40000 256 | }, 257 | { 258 | "latitude": 51.0404, 259 | "longitude": 17.3142, 260 | "radius": 40000 261 | }, 262 | { 263 | "latitude": 50.87969, 264 | "longitude": 17.13269, 265 | "radius": 40000 266 | }, 267 | { 268 | "latitude": 51.25049, 269 | "longitude": 16.23499, 270 | "radius": 40000 271 | }, 272 | { 273 | "latitude": 51.2741, 274 | "longitude": 17.3107, 275 | "radius": 40000 276 | }, 277 | { 278 | "latitude": 51.2322, 279 | "longitude": 16.81079, 280 | "radius": 40000 281 | }, 282 | { 283 | "latitude": 51.3254, 284 | "longitude": 16.85109, 285 | "radius": 40000 286 | }, 287 | { 288 | "latitude": 51.23989, 289 | "longitude": 17.52639, 290 | "radius": 40000 291 | }, 292 | { 293 | "latitude": 51.0101, 294 | "longitude": 16.71949, 295 | "radius": 40000 296 | }, 297 | { 298 | "latitude": 50.98429, 299 | "longitude": 16.4095, 300 | "radius": 40000 301 | }, 302 | { 303 | "latitude": 51.256, 304 | "longitude": 16.0367, 305 | "radius": 40000 306 | }, 307 | { 308 | "latitude": 50.7108, 309 | "longitude": 16.62209, 310 | "radius": 40000 311 | }, 312 | { 313 | "latitude": 50.8931, 314 | "longitude": 17.1295, 315 | "radius": 40000 316 | }, 317 | { 318 | "latitude": 51.207, 319 | "longitude": 16.4832, 320 | "radius": 40000 321 | }, 322 | { 323 | "latitude": 51.22309, 324 | "longitude": 17.2382, 325 | "radius": 40000 326 | }, 327 | { 328 | "latitude": 50.92929, 329 | "longitude": 17.08559, 330 | "radius": 40000 331 | }, 332 | { 333 | "latitude": 51, 334 | "longitude": 16.2318, 335 | "radius": 40000 336 | }, 337 | { 338 | "latitude": 50.93159, 339 | "longitude": 16.493000000000002, 340 | "radius": 40000 341 | }, 342 | { 343 | "latitude": 51.2854, 344 | "longitude": 17.3111, 345 | "radius": 40000 346 | }, 347 | { 348 | "latitude": 51.0082, 349 | "longitude": 16.9251, 350 | "radius": 40000 351 | }, 352 | { 353 | "latitude": 51.0469, 354 | "longitude": 16.98729, 355 | "radius": 40000 356 | }, 357 | { 358 | "latitude": 51.26319, 359 | "longitude": 17.258789999999998, 360 | "radius": 40000 361 | }, 362 | { 363 | "latitude": 51.1967, 364 | "longitude": 16.10299, 365 | "radius": 40000 366 | }, 367 | { 368 | "latitude": 51.51739, 369 | "longitude": 17.295099999999998, 370 | "radius": 40000 371 | }, 372 | { 373 | "latitude": 51.32789, 374 | "longitude": 17.325490000000002, 375 | "radius": 40000 376 | }, 377 | { 378 | "latitude": 51.24129, 379 | "longitude": 17.62569, 380 | "radius": 40000 381 | }, 382 | { 383 | "latitude": 51.2801, 384 | "longitude": 17.28339, 385 | "radius": 40000 386 | }, 387 | { 388 | "latitude": 51.41519, 389 | "longitude": 17.04249, 390 | "radius": 40000 391 | }, 392 | { 393 | "latitude": 51.05469, 394 | "longitude": 16.7733, 395 | "radius": 40000 396 | }, 397 | { 398 | "latitude": 51.09519, 399 | "longitude": 16.6618, 400 | "radius": 40000 401 | }, 402 | { 403 | "latitude": 50.90599, 404 | "longitude": 17.1208, 405 | "radius": 40000 406 | }, 407 | { 408 | "latitude": 51.3357, 409 | "longitude": 17.30029, 410 | "radius": 40000 411 | }, 412 | { 413 | "latitude": 51.145, 414 | "longitude": 17.465600000000002, 415 | "radius": 40000 416 | }, 417 | { 418 | "latitude": 51.30139, 419 | "longitude": 17.16639, 420 | "radius": 40000 421 | }, 422 | { 423 | "latitude": 51.0666, 424 | "longitude": 17.420099999999998, 425 | "radius": 40000 426 | }, 427 | { 428 | "latitude": 51.2629, 429 | "longitude": 17.3176, 430 | "radius": 40000 431 | }, 432 | { 433 | "latitude": 51.3021, 434 | "longitude": 17.29139, 435 | "radius": 40000 436 | }, 437 | { 438 | "latitude": 51.51129, 439 | "longitude": 17.500799999999998, 440 | "radius": 40000 441 | }, 442 | { 443 | "latitude": 51.0191, 444 | "longitude": 16.7283, 445 | "radius": 40000 446 | }, 447 | { 448 | "latitude": 51.2671, 449 | "longitude": 17.2655, 450 | "radius": 40000 451 | }, 452 | { 453 | "latitude": 51.35739, 454 | "longitude": 17.5198, 455 | "radius": 40000 456 | }, 457 | { 458 | "latitude": 50.9555, 459 | "longitude": 16.5851, 460 | "radius": 40000 461 | }, 462 | { 463 | "latitude": 51.1533, 464 | "longitude": 17.0092, 465 | "radius": 40000 466 | }, 467 | { 468 | "latitude": 51.18539, 469 | "longitude": 17.0256, 470 | "radius": 40000 471 | }, 472 | { 473 | "latitude": 51.16849, 474 | "longitude": 17.025399999999998, 475 | "radius": 40000 476 | }, 477 | { 478 | "latitude": 51.2909, 479 | "longitude": 17.27709, 480 | "radius": 40000 481 | }, 482 | { 483 | "latitude": 50.98799, 484 | "longitude": 16.692700000000002, 485 | "radius": 40000 486 | }, 487 | { 488 | "latitude": 51.2498, 489 | "longitude": 17.3083, 490 | "radius": 40000 491 | }, 492 | { 493 | "latitude": 50.89239, 494 | "longitude": 16.76609, 495 | "radius": 40000 496 | }, 497 | { 498 | "latitude": 51.24729, 499 | "longitude": 17.258789999999998, 500 | "radius": 40000 501 | }, 502 | { 503 | "latitude": 50.65489, 504 | "longitude": 16.60929, 505 | "radius": 40000 506 | }, 507 | { 508 | "latitude": 50.98099, 509 | "longitude": 16.8245, 510 | "radius": 40000 511 | }, 512 | { 513 | "latitude": 51.24129, 514 | "longitude": 17.259999999999998, 515 | "radius": 40000 516 | }, 517 | { 518 | "latitude": 51.24549, 519 | "longitude": 17.28489, 520 | "radius": 40000 521 | }, 522 | { 523 | "latitude": 51.24359, 524 | "longitude": 17.2319, 525 | "radius": 40000 526 | }, 527 | { 528 | "latitude": 51.0391, 529 | "longitude": 17.418799999999997, 530 | "radius": 40000 531 | }, 532 | { 533 | "latitude": 51.06269, 534 | "longitude": 16.482390000000002, 535 | "radius": 40000 536 | }, 537 | { 538 | "latitude": 51.1268, 539 | "longitude": 16.942500000000003, 540 | "radius": 40000 541 | }, 542 | { 543 | "latitude": 50.85749, 544 | "longitude": 16.34189, 545 | "radius": 40000 546 | }, 547 | { 548 | "latitude": 51.2424, 549 | "longitude": 17.36219, 550 | "radius": 40000 551 | }, 552 | { 553 | "latitude": 51.3008, 554 | "longitude": 16.637990000000002, 555 | "radius": 40000 556 | }, 557 | { 558 | "latitude": 51.2732, 559 | "longitude": 17.576990000000002, 560 | "radius": 40000 561 | }, 562 | { 563 | "latitude": 51.5675, 564 | "longitude": 16.98229, 565 | "radius": 40000 566 | }, 567 | { 568 | "latitude": 51.28799, 569 | "longitude": 17.3, 570 | "radius": 40000 571 | }, 572 | { 573 | "latitude": 51.0032, 574 | "longitude": 16.4909, 575 | "radius": 40000 576 | }, 577 | { 578 | "latitude": 51.048, 579 | "longitude": 16.78129, 580 | "radius": 40000 581 | }, 582 | { 583 | "latitude": 51.30759, 584 | "longitude": 17.2958, 585 | "radius": 40000 586 | }, 587 | { 588 | "latitude": 51.678, 589 | "longitude": 16.90909, 590 | "radius": 40000 591 | }, 592 | { 593 | "latitude": 51.21509, 594 | "longitude": 17.463990000000003, 595 | "radius": 40000 596 | }, 597 | { 598 | "latitude": 50.98059, 599 | "longitude": 16.6117, 600 | "radius": 40000 601 | }, 602 | { 603 | "latitude": 51.2316, 604 | "longitude": 16.9207, 605 | "radius": 40000 606 | }, 607 | { 608 | "latitude": 51.24499, 609 | "longitude": 16.03409, 610 | "radius": 40000 611 | }, 612 | { 613 | "latitude": 51.29209, 614 | "longitude": 17.305, 615 | "radius": 40000 616 | }, 617 | { 618 | "latitude": 51.35979, 619 | "longitude": 16.996389999999998, 620 | "radius": 40000 621 | }, 622 | { 623 | "latitude": 51.05709, 624 | "longitude": 17.4375, 625 | "radius": 40000 626 | }, 627 | { 628 | "latitude": 51.1604, 629 | "longitude": 17.0246, 630 | "radius": 40000 631 | }, 632 | { 633 | "latitude": 51.25479, 634 | "longitude": 16.0437, 635 | "radius": 40000 636 | }, 637 | { 638 | "latitude": 51.0815, 639 | "longitude": 16.564700000000002, 640 | "radius": 40000 641 | }, 642 | { 643 | "latitude": 51.3041, 644 | "longitude": 17.027, 645 | "radius": 40000 646 | }, 647 | { 648 | "latitude": 51.34469, 649 | "longitude": 17.1618, 650 | "radius": 40000 651 | }, 652 | { 653 | "latitude": 51.36579, 654 | "longitude": 16.56799, 655 | "radius": 40000 656 | }, 657 | { 658 | "latitude": 51.2965, 659 | "longitude": 17.291600000000003, 660 | "radius": 40000 661 | }, 662 | { 663 | "latitude": 50.8983, 664 | "longitude": 16.94979, 665 | "radius": 40000 666 | }, 667 | { 668 | "latitude": 51.2285, 669 | "longitude": 16.023690000000002, 670 | "radius": 40000 671 | }, 672 | { 673 | "latitude": 51.3301, 674 | "longitude": 17.28759, 675 | "radius": 40000 676 | }, 677 | { 678 | "latitude": 51.45669, 679 | "longitude": 16.4326, 680 | "radius": 40000 681 | }, 682 | { 683 | "latitude": 51.25849, 684 | "longitude": 16.03559, 685 | "radius": 40000 686 | }, 687 | { 688 | "latitude": 51.2537, 689 | "longitude": 17.2839, 690 | "radius": 40000 691 | }, 692 | { 693 | "latitude": 51.0037, 694 | "longitude": 16.677799999999998, 695 | "radius": 40000 696 | }, 697 | { 698 | "latitude": 51.1631, 699 | "longitude": 17.015700000000002, 700 | "radius": 40000 701 | }, 702 | { 703 | "latitude": 51.046, 704 | "longitude": 17.33259, 705 | "radius": 40000 706 | }, 707 | { 708 | "latitude": 51.22809, 709 | "longitude": 17.8465, 710 | "radius": 40000 711 | }, 712 | { 713 | "latitude": 51.22019, 714 | "longitude": 17.19089, 715 | "radius": 40000 716 | }, 717 | { 718 | "latitude": 50.9155, 719 | "longitude": 16.5339, 720 | "radius": 40000 721 | }, 722 | { 723 | "latitude": 51.0914, 724 | "longitude": 16.8675, 725 | "radius": 40000 726 | }, 727 | { 728 | "latitude": 51.31539, 729 | "longitude": 17.28079, 730 | "radius": 40000 731 | }, 732 | { 733 | "latitude": 51.31129, 734 | "longitude": 17.28339, 735 | "radius": 40000 736 | }, 737 | { 738 | "latitude": 51.3194, 739 | "longitude": 17.4693, 740 | "radius": 40000 741 | }, 742 | { 743 | "latitude": 51.1809, 744 | "longitude": 17.061999999999998, 745 | "radius": 40000 746 | }, 747 | { 748 | "latitude": 51.29389, 749 | "longitude": 17.313789999999997, 750 | "radius": 40000 751 | }, 752 | { 753 | "latitude": 51.2537, 754 | "longitude": 17.2798, 755 | "radius": 40000 756 | }, 757 | { 758 | "latitude": 51.0297, 759 | "longitude": 17.42549, 760 | "radius": 40000 761 | }, 762 | { 763 | "latitude": 51.20429, 764 | "longitude": 17.255200000000002, 765 | "radius": 40000 766 | }, 767 | { 768 | "latitude": 51.25329, 769 | "longitude": 17.310090000000002, 770 | "radius": 40000 771 | }, 772 | { 773 | "latitude": 51.30059, 774 | "longitude": 17.29059, 775 | "radius": 40000 776 | }, 777 | { 778 | "latitude": 50.92649, 779 | "longitude": 16.78899, 780 | "radius": 40000 781 | }, 782 | { 783 | "latitude": 51.18009, 784 | "longitude": 17.0185, 785 | "radius": 40000 786 | }, 787 | { 788 | "latitude": 50.80659, 789 | "longitude": 16.7711, 790 | "radius": 40000 791 | }, 792 | { 793 | "latitude": 51.2284, 794 | "longitude": 16.935000000000002, 795 | "radius": 40000 796 | }, 797 | { 798 | "latitude": 51.3484, 799 | "longitude": 17.52939, 800 | "radius": 40000 801 | }, 802 | { 803 | "latitude": 51.0806, 804 | "longitude": 16.8583, 805 | "radius": 40000 806 | }, 807 | { 808 | "latitude": 51.3142, 809 | "longitude": 17.2832, 810 | "radius": 40000 811 | }, 812 | { 813 | "latitude": 51.21039, 814 | "longitude": 17.27299, 815 | "radius": 40000 816 | } 817 | ] --------------------------------------------------------------------------------