├── .editorconfig ├── .gitattributes ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── .gitignore ├── .watchmanconfig ├── .yarnrc ├── LICENSE ├── README.md ├── babel.config.js ├── drawes.gif ├── example ├── App.js ├── app.json ├── assets │ ├── location.png │ └── palette.png ├── babel.config.js ├── metro.config.js ├── package-lock.json ├── package.json ├── src │ ├── App.tsx │ └── components │ │ ├── index.ts │ │ └── menu-card │ │ ├── index.ts │ │ └── menu-card.tsx ├── tsconfig.json └── webpack.config.js ├── lefthook.yml ├── package-lock.json ├── package.json ├── scripts └── bootstrap.js ├── src ├── components │ ├── canvas │ │ ├── canvas.tsx │ │ └── index.ts │ ├── draw-control │ │ ├── draw-control.tsx │ │ ├── gesture-responder.style.tsx │ │ ├── gesture-responder.tsx │ │ └── index.ts │ ├── index.ts │ └── overlay-polygon │ │ ├── index.ts │ │ └── polygon.tsx ├── constants │ ├── index.ts │ └── map.contstant.ts ├── hooks │ └── use-validator │ │ └── index.ts ├── index.ts ├── maps │ ├── index.ts │ └── maps.tsx └── types │ └── index.d.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v3 9 | with: 10 | node-version: 16.x 11 | 12 | - name: Cache dependencies 13 | id: yarn-cache 14 | uses: actions/cache@v3 15 | with: 16 | path: | 17 | **/node_modules 18 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 19 | restore-keys: | 20 | ${{ runner.os }}-yarn- 21 | 22 | - name: Install dependencies 23 | if: steps.yarn-cache.outputs.cache-hit != 'true' 24 | run: | 25 | yarn install --cwd example --frozen-lockfile 26 | yarn install --frozen-lockfile 27 | shell: bash 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | - name: Setup 13 | uses: ./.github/actions/setup 14 | 15 | - name: Lint files 16 | run: yarn lint 17 | 18 | - name: Typecheck files 19 | run: yarn typescript 20 | 21 | test: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | - name: Setup 28 | uses: ./.github/actions/setup 29 | 30 | - name: Run unit tests 31 | run: yarn test --maxWorkers=2 --coverage 32 | 33 | build: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v3 38 | 39 | - name: Setup 40 | uses: ./.github/actions/setup 41 | 42 | - name: Build package 43 | run: yarn prepare 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .idea 35 | .gradle 36 | local.properties 37 | android.iml 38 | 39 | # Cocoapods 40 | # 41 | example/ios/Pods 42 | 43 | # node.js 44 | # 45 | node_modules/ 46 | npm-debug.log 47 | yarn-debug.log 48 | yarn-error.log 49 | 50 | # BUCK 51 | buck-out/ 52 | \.buckd/ 53 | android/app/libs 54 | android/keystores/debug.keystore 55 | 56 | # Expo 57 | .expo/* 58 | 59 | # generated by bob 60 | lib/ 61 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # Override Yarn command so we can automatically setup the repo on running `yarn` 2 | 3 | yarn-path "scripts/bootstrap.js" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Алексей Петрусевич 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Accordion Animated 3 |
4 | 5 |
6 |
7 | 8 |

React Native Maps Draw (Polygon)

9 |

Interactive drawing of polygons on the map. Beta version

10 |
Made with ❤️ by developer for developers
11 | 12 |
13 |

14 | build 15 | build 16 | build 17 | build 18 |

19 | 20 | 21 | 22 | ## Thanks 23 |

Please, click on ⭐ button.

24 | 25 | 26 | ## Installation 27 | 28 | ```bash 29 | yarn add @dev-event/react-native-maps-draw 30 | # or 31 | npm install @dev-event/react-native-maps-draw 32 | ``` 33 | > Also, you need to install [react-native-gesture-handler](https://github.com/software-mansion/react-native-gesture-handler) & [react-native-svg](https://github.com/react-native-community/react-native-svg), and follow theirs installation instructions. 34 | 35 | ## 🦥 Motivation 36 | - 💚 I love [React Native](https://reactnative.dev/) 37 | 38 | 39 | Big love and gratitude to all people who are working on React Native, Expo and React Native Navigation! 40 | 41 | ## Usage 42 | 43 | For more complete example open [App.tsx](https://github.com/dev-event/react-native-maps-draw/blob/main/example/src/App.tsx) 44 | 45 | ```tsx 46 | import MapViewGestures from 'react-native-maps-draw'; 47 | import type { TTouchPoint } from 'react-native-maps-draw'; 48 | import MapView, { Polygon, Marker } from 'react-native-maps'; 49 | 50 | const AnimatedPolygon = Animated.createAnimatedComponent(Polygon); 51 | 52 | export default function App() { 53 | const mapRef = useRef(null); 54 | 55 | const convertByPoint = async (item: any) => 56 | await mapRef.current?.coordinateForPoint(item); 57 | 58 | const handlePolygon = useCallback( 59 | (_: any, index: number) => ( 60 | 67 | ), 68 | [polygon.polygons] 69 | ); 70 | 71 | 72 | return ( 73 | 74 | 75 | {...rest} 76 | 77 | 78 | {... && ( 79 | 88 | )} 89 | 90 | ); 91 | } 92 | ``` 93 | 94 | 95 | ## 🎉 Example 96 | 97 | Checkout the example [here](https://github.com/dev-event/react-native-accordion). 98 | 99 | 100 | 101 | ## ✌️ Contributing 102 | 103 | Pull requests are always welcome! Feel free to open a new GitHub issue for any changes that can be made. 104 | 105 | ## Author 106 | 107 | Reach out to me at one of the following places! 108 | 109 | - E-mail effectwaater@gmail.com 110 | 111 | 112 | ## License 113 | 114 | [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](http://badges.mit-license.org) 115 | 116 | - **[MIT license](http://opensource.org/licenses/mit-license.php)** 117 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /drawes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-chief/react-native-maps-draw/4525f1aa28d22c33bc62409dd4cd0d1072d38b75/drawes.gif -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | export { default } from './src/App'; 2 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/adaptive-icon.png", 26 | "backgroundColor": "#FFFFFF" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/assets/location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-chief/react-native-maps-draw/4525f1aa28d22c33bc62409dd4cd0d1072d38b75/example/assets/location.png -------------------------------------------------------------------------------- /example/assets/palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-chief/react-native-maps-draw/4525f1aa28d22c33bc62409dd4cd0d1072d38b75/example/assets/palette.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = function (api) { 5 | api.cache(true); 6 | 7 | return { 8 | presets: ['babel-preset-expo'], 9 | plugins: [ 10 | [ 11 | 'module-resolver', 12 | { 13 | extensions: ['.tsx', '.ts', '.js', '.json'], 14 | alias: { 15 | // For development, we want to alias the library to the source 16 | [pak.name]: path.join(__dirname, '..', pak.source), 17 | }, 18 | }, 19 | ], 20 | ], 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const escape = require('escape-string-regexp'); 3 | const { getDefaultConfig } = require('@expo/metro-config'); 4 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 5 | const pak = require('../package.json'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | 9 | const modules = Object.keys({ 10 | ...pak.peerDependencies, 11 | }); 12 | 13 | const defaultConfig = getDefaultConfig(__dirname); 14 | 15 | module.exports = { 16 | ...defaultConfig, 17 | 18 | projectRoot: __dirname, 19 | watchFolders: [root], 20 | 21 | // We need to make sure that only one version is loaded for peerDependencies 22 | // So we block them at the root, and alias them to the versions in example's node_modules 23 | resolver: { 24 | ...defaultConfig.resolver, 25 | 26 | blacklistRE: exclusionList( 27 | modules.map( 28 | (m) => 29 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 30 | ) 31 | ), 32 | 33 | extraNodeModules: modules.reduce((acc, name) => { 34 | acc[name] = path.join(__dirname, 'node_modules', name); 35 | return acc; 36 | }, {}), 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@types/react": "~18.0.24", 13 | "@types/react-native": "~0.70.6", 14 | "expo": "~47.0.8", 15 | "expo-status-bar": "~1.4.2", 16 | "react": "18.1.0", 17 | "react-native": "0.70.5", 18 | "react-native-maps": "^1.3.2", 19 | "typescript": "^4.6.3" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.12.9", 23 | "babel-plugin-module-resolver": "^4.1.0" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useRef, useMemo } from 'react'; 2 | import { 3 | StyleSheet, 4 | Animated, 5 | View, 6 | Image, 7 | SafeAreaView, 8 | Text, 9 | } from 'react-native'; 10 | import MapViewGestures from 'react-native-maps-draw'; 11 | import type { TTouchPoint } from 'react-native-maps-draw'; 12 | import MapView, { Polygon, Marker } from 'react-native-maps'; 13 | import { MenuCard } from './components'; 14 | 15 | const AnimatedPolygon = Animated.createAnimatedComponent(Polygon); 16 | 17 | export default function App() { 18 | const mapRef = useRef(null); 19 | 20 | const initialPolygon = useRef({ 21 | polygons: [], 22 | distance: 0, 23 | lastLatLng: undefined, 24 | initialLatLng: undefined, 25 | centerLatLng: undefined, 26 | }); 27 | 28 | const [isActiveDraw, setDrawMode] = useState(false); 29 | const [polygon, setPolygon] = useState(initialPolygon.current); 30 | const [isReady, setIsReady] = useState(false); 31 | const [points, setPoints] = useState([]); 32 | 33 | const handleMapReady = useCallback( 34 | () => mapRef.current && setIsReady(true), 35 | [] 36 | ); 37 | 38 | const convertByPoint = async (item: any) => 39 | await mapRef.current?.coordinateForPoint(item); 40 | 41 | const handleRemovePolygon = (): void => setPolygon(initialPolygon.current); 42 | 43 | const handleCanvasEndDraw = useCallback((locations: any) => { 44 | setPolygon(locations); 45 | setDrawMode(false); 46 | }, []); 47 | 48 | const handlePolygon = useCallback( 49 | (_: any, index: number) => ( 50 | 57 | ), 58 | [polygon.polygons] 59 | ); 60 | 61 | const isVisiblePolygons = useMemo( 62 | () => isReady && polygon.polygons && polygon.polygons.length > 0, 63 | [isReady, polygon.polygons] 64 | ); 65 | 66 | return ( 67 | 68 | 69 | {isVisiblePolygons && ( 70 | <> 71 | {polygon.centerLatLng && ( 72 | 76 | 77 | 82 | 83 | 84 | )} 85 | {polygon.polygons.map(handlePolygon)} 86 | 87 | )} 88 | 89 | 90 | {isActiveDraw && ( 91 | 100 | )} 101 | 102 | 103 | Menu 104 | 110 | { 114 | setPolygon(initialPolygon.current); 115 | setPoints([]); 116 | setDrawMode(true); 117 | }} 118 | /> 119 | 120 | 121 | 122 | ); 123 | } 124 | 125 | const styles = StyleSheet.create({ 126 | container: { 127 | flex: 1, 128 | backgroundColor: 'white', 129 | }, 130 | panel: { 131 | flexDirection: 'column', 132 | bottom: '0%', 133 | width: '100%', 134 | height: '20%', 135 | backgroundColor: 'white', 136 | position: 'absolute', 137 | borderTopLeftRadius: 24, 138 | borderTopRightRadius: 24, 139 | paddingTop: 12, 140 | paddingBottom: 12, 141 | paddingHorizontal: 24, 142 | shadowColor: '#000', 143 | shadowOffset: { 144 | width: 0, 145 | height: 2, 146 | }, 147 | shadowOpacity: 0.25, 148 | shadowRadius: 3.84, 149 | 150 | elevation: 5, 151 | }, 152 | 153 | title: { 154 | color: '#241f1f', 155 | fontSize: 24, 156 | fontWeight: 'bold', 157 | marginBottom: 24, 158 | alignSelf: 'center', 159 | }, 160 | img: { 161 | height: 18, 162 | width: 18, 163 | tintColor: 'white', 164 | }, 165 | map: { 166 | flex: 1, 167 | }, 168 | 169 | card: { 170 | height: 40, 171 | width: 40, 172 | alignItems: 'center', 173 | justifyContent: 'center', 174 | borderRadius: 24, 175 | backgroundColor: 'orange', 176 | shadowColor: '#000', 177 | shadowOffset: { 178 | width: 0, 179 | height: 2, 180 | }, 181 | shadowOpacity: 0.25, 182 | shadowRadius: 3.84, 183 | 184 | elevation: 5, 185 | }, 186 | }); 187 | -------------------------------------------------------------------------------- /example/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MenuCard } from './menu-card'; 2 | -------------------------------------------------------------------------------- /example/src/components/menu-card/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './menu-card'; 2 | -------------------------------------------------------------------------------- /example/src/components/menu-card/menu-card.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; 3 | 4 | export type TMenuCard = { 5 | enabled: boolean; 6 | title: string; 7 | onTap: () => void; 8 | }; 9 | const MenuCard: FC = ({ title, onTap, enabled }) => { 10 | return ( 11 | 12 | 22 | 27 | 28 | {title} 29 | 30 | ); 31 | }; 32 | 33 | export default MenuCard; 34 | 35 | const styles = StyleSheet.create({ 36 | title: { 37 | color: '#241f1f', 38 | fontSize: 14, 39 | marginTop: 8, 40 | fontWeight: '500', 41 | }, 42 | button: { 43 | flexDirection: 'column', 44 | alignItems: 'center', 45 | }, 46 | img: { 47 | height: 24, 48 | width: 24, 49 | tintColor: 'white', 50 | }, 51 | }); 52 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | // Avoid expo-cli auto-generating a tsconfig 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 3 | const { resolver } = require('./metro.config'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const node_modules = path.join(__dirname, 'node_modules'); 7 | 8 | module.exports = async function (env, argv) { 9 | const config = await createExpoWebpackConfigAsync(env, argv); 10 | 11 | config.module.rules.push({ 12 | test: /\.(js|jsx|ts|tsx)$/, 13 | include: path.resolve(root, 'src'), 14 | use: 'babel-loader', 15 | }); 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we alias them to the versions in example's node_modules 19 | Object.assign(config.resolve.alias, { 20 | ...resolver.extraNodeModules, 21 | 'react-native-web': path.join(node_modules, 'react-native-web'), 22 | }); 23 | 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | files: git diff --name-only @{push} 6 | glob: "*.{js,ts,jsx,tsx}" 7 | run: npx eslint {files} 8 | types: 9 | files: git diff --name-only @{push} 10 | glob: "*.{js,ts, jsx, tsx}" 11 | run: npx tsc --noEmit 12 | commit-msg: 13 | parallel: true 14 | commands: 15 | commitlint: 16 | run: npx commitlint --edit 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-maps-draw", 3 | "version": "0.1.0", 4 | "description": "test", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "android", 14 | "ios", 15 | "cpp", 16 | "*.podspec", 17 | "!lib/typescript/example", 18 | "!ios/build", 19 | "!android/build", 20 | "!android/gradle", 21 | "!android/gradlew", 22 | "!android/gradlew.bat", 23 | "!android/local.properties", 24 | "!**/__tests__", 25 | "!**/__fixtures__", 26 | "!**/__mocks__", 27 | "!**/.*" 28 | ], 29 | "scripts": { 30 | "test": "jest", 31 | "typescript": "tsc --noEmit", 32 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 33 | "prepare": "bob build", 34 | "release": "release-it", 35 | "example": "yarn --cwd example", 36 | "bootstrap": "yarn example && yarn install" 37 | }, 38 | "keywords": [ 39 | "react-native", 40 | "ios", 41 | "android" 42 | ], 43 | "repository": "https://github.com/dev-event/react-native-maps-draw", 44 | "author": "Алексей Петрусевич (https://github.com/dev-event)", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/dev-event/react-native-maps-draw/issues" 48 | }, 49 | "homepage": "https://github.com/dev-event/react-native-maps-draw#readme", 50 | "publishConfig": { 51 | "registry": "https://registry.npmjs.org/" 52 | }, 53 | "devDependencies": { 54 | "@arkweid/lefthook": "^0.7.7", 55 | "@commitlint/config-conventional": "^17.0.2", 56 | "@react-native-community/eslint-config": "^3.0.2", 57 | "@release-it/conventional-changelog": "^5.0.0", 58 | "@types/invariant": "^2.2.35", 59 | "@types/jest": "^28.1.2", 60 | "@types/react": "~17.0.21", 61 | "@types/react-dom": "^18.0.9", 62 | "@types/react-native": "^0.70.0", 63 | "commitlint": "^17.0.2", 64 | "del-cli": "^5.0.0", 65 | "eslint": "8.22.0", 66 | "eslint-config-prettier": "^8.5.0", 67 | "eslint-plugin-prettier": "^4.0.0", 68 | "jest": "^28.1.1", 69 | "pod-install": "^0.1.0", 70 | "prettier": "^2.0.5", 71 | "react": "18.1.0", 72 | "react-native": "0.70.5", 73 | "react-native-builder-bob": "^0.20.0", 74 | "release-it": "^15.0.0", 75 | "typescript": "^4.5.2" 76 | }, 77 | "resolutions": { 78 | "@types/react": "17.0.21" 79 | }, 80 | "peerDependencies": { 81 | "react": "*", 82 | "react-native": "*" 83 | }, 84 | "engines": { 85 | "node": ">= 16.0.0" 86 | }, 87 | "packageManager": "^yarn@1.22.15", 88 | "jest": { 89 | "preset": "react-native", 90 | "modulePathIgnorePatterns": [ 91 | "/example/node_modules", 92 | "/lib/" 93 | ] 94 | }, 95 | "commitlint": { 96 | "extends": [ 97 | "@commitlint/config-conventional" 98 | ] 99 | }, 100 | "release-it": { 101 | "git": { 102 | "commitMessage": "chore: release ${version}", 103 | "tagName": "v${version}" 104 | }, 105 | "npm": { 106 | "publish": true 107 | }, 108 | "github": { 109 | "release": true 110 | }, 111 | "plugins": { 112 | "@release-it/conventional-changelog": { 113 | "preset": "angular" 114 | } 115 | } 116 | }, 117 | "eslintConfig": { 118 | "root": true, 119 | "extends": [ 120 | "@react-native-community", 121 | "prettier" 122 | ], 123 | "rules": { 124 | "prettier/prettier": [ 125 | "error", 126 | { 127 | "quoteProps": "consistent", 128 | "singleQuote": true, 129 | "tabWidth": 2, 130 | "trailingComma": "es5", 131 | "useTabs": false 132 | } 133 | ] 134 | } 135 | }, 136 | "eslintIgnore": [ 137 | "node_modules/", 138 | "lib/" 139 | ], 140 | "prettier": { 141 | "quoteProps": "consistent", 142 | "singleQuote": true, 143 | "tabWidth": 2, 144 | "trailingComma": "es5", 145 | "useTabs": false 146 | }, 147 | "react-native-builder-bob": { 148 | "source": "src", 149 | "output": "lib", 150 | "targets": [ 151 | "commonjs", 152 | "module", 153 | [ 154 | "typescript", 155 | { 156 | "project": "tsconfig.build.json" 157 | } 158 | ] 159 | ] 160 | }, 161 | "dependencies": { 162 | "geolib": "3.3.3", 163 | "invariant": "^2.2.4", 164 | "react-native-svg": "13.6.0" 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const child_process = require('child_process'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const args = process.argv.slice(2); 7 | const options = { 8 | cwd: process.cwd(), 9 | env: process.env, 10 | stdio: 'inherit', 11 | encoding: 'utf-8', 12 | }; 13 | 14 | if (os.type() === 'Windows_NT') { 15 | options.shell = true; 16 | } 17 | 18 | let result; 19 | 20 | if (process.cwd() !== root || args.length) { 21 | // We're not in the root of the project, or additional arguments were passed 22 | // In this case, forward the command to `yarn` 23 | result = child_process.spawnSync('yarn', args, options); 24 | } else { 25 | // If `yarn` is run without arguments, perform bootstrap 26 | result = child_process.spawnSync('yarn', ['bootstrap'], options); 27 | } 28 | 29 | process.exitCode = result.status; 30 | -------------------------------------------------------------------------------- /src/components/canvas/canvas.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Polyline } from 'react-native-svg'; 2 | import type { TCanvas } from '../../types'; 3 | import React, { FC } from 'react'; 4 | 5 | const Canvas: FC = ({ 6 | path, 7 | colorLine, 8 | widthLine, 9 | fillColorCanvas, 10 | containerSize, 11 | }) => { 12 | return ( 13 | 18 | 24 | 25 | ); 26 | }; 27 | 28 | export default Canvas; 29 | -------------------------------------------------------------------------------- /src/components/canvas/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './canvas'; 2 | -------------------------------------------------------------------------------- /src/components/draw-control/draw-control.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import type { TCanvas, TGestureControl } from '../../types'; 3 | import GestureHandler from './gesture-responder'; 4 | import Canvas from '../canvas'; 5 | 6 | export type IDrawControl = TGestureControl & TCanvas; 7 | 8 | const DrawControl: FC = ({ 9 | path, 10 | widthLine, 11 | colorLine, 12 | containerSize, 13 | fillColorCanvas, 14 | onEndTouchEvents, 15 | onStartTouchEvents, 16 | onChangeTouchEvents, 17 | ...rest 18 | }) => { 19 | return ( 20 | <> 21 | 26 | 27 | 35 | 36 | ); 37 | }; 38 | 39 | export default DrawControl; 40 | -------------------------------------------------------------------------------- /src/components/draw-control/gesture-responder.style.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | gestureResponder: { 5 | backgroundColor: 'transparent', 6 | position: 'absolute', 7 | left: 0, 8 | right: 0, 9 | top: 0, 10 | bottom: 0, 11 | zIndex: 1, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/draw-control/gesture-responder.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useRef } from 'react'; 2 | import { PanResponder, View } from 'react-native'; 3 | import type { TGestureControl, TTouchPoint } from '../../types'; 4 | import { styles } from './gesture-responder.style'; 5 | 6 | const GestureHandler: FC = ({ 7 | onEndTouchEvents, 8 | onStartTouchEvents, 9 | onChangeTouchEvents, 10 | }) => { 11 | const pathRef = useRef([]); 12 | 13 | const panResponder = PanResponder.create({ 14 | onStartShouldSetPanResponder: () => true, 15 | onStartShouldSetPanResponderCapture: () => true, 16 | onMoveShouldSetPanResponder: () => true, 17 | onMoveShouldSetPanResponderCapture: () => true, 18 | 19 | onPanResponderGrant: (e, gestureState) => { 20 | pathRef.current = []; 21 | onStartTouchEvents?.(e, gestureState); 22 | }, 23 | onPanResponderMove: (event) => { 24 | pathRef.current.push({ 25 | x: event.nativeEvent.locationX, 26 | y: event.nativeEvent.locationY, 27 | }); 28 | onChangeTouchEvents?.([...pathRef.current]); 29 | }, 30 | onPanResponderRelease: () => { 31 | const points = [...pathRef.current]; 32 | onChangeTouchEvents(points); 33 | 34 | onEndTouchEvents?.(points); 35 | }, 36 | }); 37 | 38 | return ; 39 | }; 40 | 41 | export default GestureHandler; 42 | -------------------------------------------------------------------------------- /src/components/draw-control/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './draw-control'; 2 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Canvas } from './canvas'; 2 | export { default as OverlayPolygon } from './overlay-polygon'; 3 | export { default as DrawControl } from './draw-control'; 4 | -------------------------------------------------------------------------------- /src/components/overlay-polygon/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './polygon'; 2 | -------------------------------------------------------------------------------- /src/components/overlay-polygon/polygon.tsx: -------------------------------------------------------------------------------- 1 | import { useWindowDimensions, StyleSheet } from 'react-native'; 2 | import { Svg, Defs, Rect, Mask, Polygon } from 'react-native-svg'; 3 | import React, { FC } from 'react'; 4 | import type { TPolygon } from '../../types'; 5 | 6 | const MapPolygon: FC = ({ 7 | path, 8 | fillOverlay, 9 | widthOverlayLine, 10 | colorWidthOverlayLine, 11 | backgroundOverlayPolygon, 12 | }) => { 13 | const { width, height } = useWindowDimensions(); 14 | 15 | return ( 16 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | 45 | 46 | ); 47 | }; 48 | 49 | export default MapPolygon; 50 | 51 | const styles = StyleSheet.create({ 52 | container: { 53 | position: 'absolute', 54 | zIndex: 4, 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './map.contstant'; 2 | -------------------------------------------------------------------------------- /src/constants/map.contstant.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_ACTIVE_COLOR_LINE_WIDTH = 3; 2 | const DEFAULT_INDEX_INITIAL_LAT_LNG = 0; 3 | const DEFAULT_CREATED_NEW_POLYGON = false; 4 | const DEFAULT_BACKGROUND_VIEW_CANVAS = 'rgba(0,0,0,0.10)'; 5 | const DEFAULT_FILL_BACKGROUND_CANVAS = 'rgba(0,0,0,0.0)'; 6 | const DEFAULT_ACTIVE_COLOR_LINE = 'rgba(28, 170, 255, 0.88)'; 7 | const DEFAULT_UNIT_DISTANCE = 'm'; 8 | 9 | export { 10 | DEFAULT_ACTIVE_COLOR_LINE_WIDTH, 11 | DEFAULT_FILL_BACKGROUND_CANVAS, 12 | DEFAULT_BACKGROUND_VIEW_CANVAS, 13 | DEFAULT_INDEX_INITIAL_LAT_LNG, 14 | DEFAULT_CREATED_NEW_POLYGON, 15 | DEFAULT_ACTIVE_COLOR_LINE, 16 | DEFAULT_UNIT_DISTANCE, 17 | }; 18 | -------------------------------------------------------------------------------- /src/hooks/use-validator/index.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import invariant from 'invariant'; 3 | import type { TMap } from '../../types'; 4 | 5 | const useValidator = ({ points, onChangePoints }: TMap) => { 6 | useMemo(() => { 7 | invariant( 8 | points instanceof Array, 9 | `'points' was provided but with wrong type ! expected type is a points[{ x: number; y: number},...].` 10 | ); 11 | 12 | invariant( 13 | typeof onChangePoints === 'function', 14 | `'onChangePoints' was provided but with wrong type ! expected type is a function.` 15 | ); 16 | // eslint-disable-next-line react-hooks/exhaustive-deps 17 | }, []); 18 | }; 19 | 20 | export default useValidator; 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './maps'; 2 | export * from './components'; 3 | export * from './types/index.d'; 4 | -------------------------------------------------------------------------------- /src/maps/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './maps'; 2 | -------------------------------------------------------------------------------- /src/maps/maps.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useState } from 'react'; 2 | import { StyleSheet, useWindowDimensions, View } from 'react-native'; 3 | import { DrawControl } from '../components'; 4 | import type { TMap, TLocation } from '../types'; 5 | import { 6 | DEFAULT_ACTIVE_COLOR_LINE_WIDTH, 7 | DEFAULT_FILL_BACKGROUND_CANVAS, 8 | DEFAULT_BACKGROUND_VIEW_CANVAS, 9 | DEFAULT_INDEX_INITIAL_LAT_LNG, 10 | DEFAULT_ACTIVE_COLOR_LINE, 11 | DEFAULT_UNIT_DISTANCE, 12 | } from '../constants/map.contstant'; 13 | import * as _GEO from 'geolib'; 14 | import useValidator from '../hooks/use-validator'; 15 | 16 | export default (props: TMap) => { 17 | useValidator(props); 18 | const { 19 | points, 20 | convertByPoint, 21 | colorLine = DEFAULT_ACTIVE_COLOR_LINE, 22 | widthLine = DEFAULT_ACTIVE_COLOR_LINE_WIDTH, 23 | onEndDraw, 24 | onStartDraw, 25 | unitDistance = DEFAULT_UNIT_DISTANCE, 26 | onChangePoints, 27 | fillColorCanvas = DEFAULT_FILL_BACKGROUND_CANVAS, 28 | styleViewGesture, 29 | backgroundCanvas = DEFAULT_BACKGROUND_VIEW_CANVAS, 30 | } = props; 31 | const { width, height } = useWindowDimensions(); 32 | const [containerSize, setContainerSize] = useState({ width, height }); 33 | 34 | const containerStyle = useMemo( 35 | () => [ 36 | { zIndex: 1, backgroundColor: backgroundCanvas }, 37 | StyleSheet.absoluteFill, 38 | styleViewGesture, 39 | ], 40 | [backgroundCanvas, styleViewGesture] 41 | ); 42 | 43 | const path = useMemo( 44 | () => points.map((item: any) => `${item.x},${item.y}`).join(' '), 45 | [points] 46 | ); 47 | 48 | const calculatedCenterPolygon = (coordinates: TLocation[]) => 49 | Promise.resolve(_GEO.getCenter(coordinates)); 50 | 51 | const convertPointToCoordinates = useCallback( 52 | (polygons) => { 53 | const lengthPolygon = polygons.length; 54 | if (polygons && lengthPolygon > 0) { 55 | calculatedCenterPolygon(polygons).then((centerLatLng) => { 56 | if (onEndDraw && centerLatLng) { 57 | const distance = _GEO.convertDistance( 58 | _GEO.getPathLength(polygons), 59 | unitDistance 60 | ); 61 | const initialLatLng = polygons[DEFAULT_INDEX_INITIAL_LAT_LNG]; 62 | const lastLatLng = polygons[lengthPolygon - 1]; 63 | onEndDraw({ 64 | polygons, 65 | distance, 66 | centerLatLng, 67 | initialLatLng, 68 | lastLatLng, 69 | }); 70 | } 71 | }); 72 | } 73 | }, 74 | [onEndDraw, unitDistance] 75 | ); 76 | 77 | const handleEndDraw = useCallback( 78 | async (data) => { 79 | await Promise.all(data.map(convertByPoint)).then( 80 | convertPointToCoordinates 81 | ); 82 | }, 83 | [convertByPoint, convertPointToCoordinates] 84 | ); 85 | 86 | const handleSetContainerSize = useCallback((event) => { 87 | setContainerSize({ 88 | width: event.nativeEvent.layout.width, 89 | height: event.nativeEvent.layout.height, 90 | }); 91 | }, []); 92 | 93 | return ( 94 | 95 | 105 | 106 | ); 107 | }; 108 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GestureResponderEvent, 3 | PanResponderGestureState, 4 | StyleProp, 5 | ViewStyle, 6 | } from 'react-native'; 7 | 8 | export type TTouchPoint = { 9 | x: number; 10 | y: number; 11 | }; 12 | 13 | export type TPath = { 14 | path: string; 15 | }; 16 | 17 | export type TContainerSize = { 18 | width: number; 19 | height: number; 20 | }; 21 | 22 | export type TCanvas = { 23 | colorLine?: string; 24 | widthLine?: number; 25 | containerSize: TContainerSize; 26 | fillColorCanvas?: string; 27 | } & TPath; 28 | 29 | export type TGestureControlShape = { 30 | onEndTouchEvents?: (points: TouchPoint[]) => void; 31 | onStartTouchEvents?: ( 32 | event: GestureResponderEvent, 33 | state: PanResponderGestureState 34 | ) => void; 35 | }; 36 | export type TGestureControl = { 37 | onChangeTouchEvents: (points: TouchPoint[]) => void; 38 | } & TGestureControlShape; 39 | 40 | export type TPolygon = { 41 | fillOverlay?: string; 42 | widthOverlayLine?: number; 43 | colorWidthOverlayLine?: string; 44 | backgroundOverlayPolygon?: string; 45 | } & TPath; 46 | 47 | export type TLocation = { 48 | latitude: number; 49 | longitude: number; 50 | }; 51 | 52 | export type TGestureCoordinates = { 53 | event: GestureResponderEvent; 54 | state: PanResponderGestureState; 55 | points: TTouchPoint[]; 56 | }; 57 | export type TDrawResult = { 58 | distance: number; 59 | polygons: TLocation[]; 60 | lastLatLng: TLocation; 61 | initialLatLng: TLocation; 62 | centerLatLng: false | TLocation; 63 | }; 64 | 65 | export type TMap = { 66 | points: TTouchPoint[]; 67 | colorLine?: string; 68 | convertByPoint: any; 69 | widthLine?: number; 70 | onEndDraw?: (item: TDrawResult) => void; 71 | // renderPath?: (path: string) => FC | null; 72 | onStartDraw?: () => void; 73 | unitDistance?: 'm' | 'km' | 'cm' | 'mm' | 'mi' | 'sm' | 'ft' | 'in' | 'yd'; 74 | onChangePoints: (points: TouchPoint[]) => void; 75 | fillColorCanvas?: string; 76 | backgroundCanvas?: string; 77 | styleViewGesture?: StyleProp; 78 | }; 79 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": ["example"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "react-native-maps-draw": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "importsNotUsedAsValues": "error", 11 | "forceConsistentCasingInFileNames": true, 12 | "jsx": "react", 13 | "lib": ["esnext"], 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitReturns": true, 18 | "noImplicitUseStrict": false, 19 | "noStrictGenericChecks": false, 20 | "noUncheckedIndexedAccess": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "skipLibCheck": true, 25 | "strict": true, 26 | "target": "esnext" 27 | } 28 | } 29 | --------------------------------------------------------------------------------