├── .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 |

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 |
15 |
16 |
17 |
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 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------