├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── HTML ├── .gitignore ├── gulpfile.js ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── robots.txt ├── readme.md ├── src │ ├── MapComponent.test.tsx │ ├── MapComponent.view.test.tsx │ ├── MapLayers.test.tsx │ ├── MapLayers.tsx │ ├── MapMarkers.tsx │ ├── MapShapes.tsx │ ├── __snapshots__ │ │ ├── MapComponent.test.tsx.snap │ │ ├── MapComponent.view.test.tsx.snap │ │ └── MapLayers.test.tsx.snap │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── mapComponent.tsx │ ├── mapComponent.view.tsx │ ├── models.ts │ ├── react-app-env.d.ts │ ├── serviceWorker.ts │ ├── setupTests.ts │ ├── styles │ │ ├── markerAnimations.css │ │ └── markers.css │ ├── testData │ │ ├── mockMapLayers.ts │ │ ├── mockMapMarkers.ts │ │ ├── mockMapShapes.ts │ │ └── svgIcons.ts │ ├── utilities.test.ts.old │ ├── utilities.ts │ └── webBase64Image.ts ├── tsconfig.json ├── yarn-error.log └── yarn.lock ├── WebViewLeaflet ├── ActivityOverlay.tsx ├── DebugMessageBox.tsx ├── WebViewLeaflet.tsx ├── WebViewLeaflet.view.tsx ├── assets │ └── index.html ├── index.d.ts ├── index.ts ├── models.ts ├── package.json ├── yarn-error.log └── yarn.lock ├── assets ├── icon.png └── splash.png ├── demoApp ├── .expo-shared │ └── assets.json ├── .gitignore ├── App.tsx ├── app.json ├── assets │ ├── icon.png │ └── splash.png ├── babel.config.js ├── gulpfile.js ├── package.json ├── tsconfig.json ├── yarn-error.log └── yarn.lock ├── package.json ├── readme.md ├── secrets.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | web-report/ 12 | HTML/dist/* 13 | HTML/cache/* 14 | HTML/build/* 15 | HTML/.cache/* 16 | HTML/node_modules 17 | WebViewLeaflet/dist/* 18 | WebViewLeaflet/cache/* 19 | WebViewLeaflet/build/* 20 | WebViewLeaflet/.cache/* 21 | WebViewLeaflet/node_modules 22 | secrets.js 23 | secrets.ts 24 | /HTML/precompile 25 | /HTML/readyForBuild 26 | /HTML/.cache 27 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ..\..\..\..\c:\Users\regin\Dropbox\react-native-webview-leaflet-5\html\build\index.html 2 | ..\..\..\..\c:\Users\regin\Dropbox\react-native-webview-leaflet-5\html\src\testData 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /HTML/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /HTML/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | const inlinesource = require("gulp-inline-source"); 3 | const replace = require("gulp-replace"); 4 | const clean = require("gulp-clean"); 5 | const rename = require("gulp-rename"); 6 | 7 | const REACT_BUILD_DIRECTORY = "build"; 8 | const REACT_BUILD_FILES = "./build/*.html"; 9 | const FILE_NAME_AFTER_ADDING_INLINE_TAGS = "indexWithTags.html"; 10 | const DIST_HTML_FILE_NAME = "index.html"; 11 | const DIST_DIRECTORY = "dist"; 12 | 13 | gulp.task("clean", function() { 14 | return gulp 15 | .src( 16 | [ 17 | `${REACT_BUILD_DIRECTORY}/${FILE_NAME_AFTER_ADDING_INLINE_TAGS}`, 18 | DIST_DIRECTORY 19 | ], 20 | { 21 | allowEmpty: true, 22 | read: false 23 | } 24 | ) 25 | .pipe(clean()); 26 | }); 27 | 28 | gulp.task("disableBrowserTestFlag", () => { 29 | return gulp 30 | .src(["./src/MapComponent.tsx"]) 31 | .pipe( 32 | replace( 33 | "const ENABLE_BROWSER_TESTING = true;", 34 | "const ENABLE_BROWSER_TESTING = false;" 35 | ) 36 | ) 37 | .pipe(gulp.dest("./src")); 38 | }); 39 | 40 | gulp.task("enableBrowserTestFlag", () => { 41 | return gulp 42 | .src(["./src/MapComponent.tsx"]) 43 | .pipe( 44 | replace( 45 | "const ENABLE_BROWSER_TESTING = false;", 46 | "const ENABLE_BROWSER_TESTING = true;" 47 | ) 48 | ) 49 | .pipe(gulp.dest("./src")); 50 | }); 51 | 52 | gulp.task("addInlineTags", function() { 53 | return gulp 54 | .src(REACT_BUILD_FILES) 55 | .pipe(replace('rel="stylesheet"', 'rel="stylesheet" inline')) 56 | .pipe(replace(">", " inline>")) 57 | .pipe(rename(FILE_NAME_AFTER_ADDING_INLINE_TAGS)) 58 | .pipe(gulp.dest(REACT_BUILD_DIRECTORY)); 59 | }); 60 | 61 | gulp.task("inlineSource", function() { 62 | return gulp 63 | .src(`./${REACT_BUILD_DIRECTORY}/${FILE_NAME_AFTER_ADDING_INLINE_TAGS}`) 64 | .pipe(inlinesource()) 65 | .pipe(rename(DIST_HTML_FILE_NAME)) 66 | .pipe(gulp.dest(DIST_DIRECTORY)); 67 | }); 68 | 69 | exports.build = gulp.series("clean", "addInlineTags", "inlineSource"); 70 | -------------------------------------------------------------------------------- /HTML/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^24.0.0", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.0", 12 | "@types/react-dom": "^16.9.0", 13 | "@types/react-leaflet": "^2.5.0", 14 | "@types/react-leaflet-markercluster": "^2.0.0", 15 | "@types/react-measure": "^2.0.5", 16 | "leaflet": "^1.6.0", 17 | "leaflet.markercluster": "^1.4.1", 18 | "react": "^16.12.0", 19 | "react-dom": "^16.12.0", 20 | "react-leaflet": "^2.6.1", 21 | "react-leaflet-markercluster": "^2.0.0-rc3", 22 | "react-measure": "^2.3.0", 23 | "react-scripts": "3.3.0", 24 | "typescript": "~3.7.2" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject", 31 | "cleanAssets": "del-cli --force ../WebViewLeaflet/assets/**/*", 32 | "copyDist": "npx copyfiles -u 1 ./dist/index.html ../WebViewLeaflet/assets", 33 | "dist": "gulp disableBrowserTestFlag && yarn build && gulp build && yarn cleanAssets && yarn copyDist && gulp enableBrowserTestFlag", 34 | "cpx": "cpx '../WebViewLeaflet/models.ts' ./src --watch" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "del-cli": "^3.0.0", 53 | "gulp": "^4.0.2", 54 | "gulp-clean": "^0.4.0", 55 | "gulp-inline-source": "^4.0.0", 56 | "gulp-rename": "^2.0.0", 57 | "gulp-replace": "^1.0.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /HTML/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reggie3/react-native-webview-leaflet/a27bc89559bc8ab470ae89b0d4faef1a2433ebb0/HTML/public/favicon.ico -------------------------------------------------------------------------------- /HTML/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /HTML/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /HTML/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /HTML/readme.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /HTML/src/MapComponent.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import MapComponent from "./MapComponent"; 4 | 5 | describe("MapComponent", () => { 6 | test("it renders", () => { 7 | const { asFragment } = render(); 8 | expect(asFragment()).toMatchSnapshot(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /HTML/src/MapComponent.view.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import MapComponentView from "./MapComponent.view"; 4 | 5 | describe("MapComponentView", () => { 6 | test("it renders", () => { 7 | const { asFragment } = render( 8 | {}} 10 | debugMessages={[]} 11 | mapCenterPosition={[36.56, -76.17]} 12 | mapLayers={[]} 13 | mapMarkers={[]} 14 | onMapEvent={() => {}} 15 | setMapRef={() => {}} 16 | zoom={13} 17 | /> 18 | ); 19 | expect(asFragment()).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /HTML/src/MapLayers.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { default as MapLayerComponent } from "./MapLayers"; 3 | import { render } from "@testing-library/react"; 4 | import mockMapLayers from "./testData/mockMapLayers"; 5 | import { Map } from "react-leaflet"; 6 | 7 | describe("MapLayers Component", () => { 8 | test("it renders", () => { 9 | console.log(MapLayerComponent); 10 | const { asFragment } = render( 11 | 12 | 13 | 14 | ); 15 | expect(asFragment()).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /HTML/src/MapLayers.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | TileLayer, 4 | LayersControl, 5 | WMSTileLayer, 6 | WMSTileLayerProps, 7 | TileLayerProps, 8 | ImageOverlay, 9 | ImageOverlayProps 10 | } from "react-leaflet"; 11 | import { MapLayer, MapLayerType } from "./models"; 12 | 13 | const { BaseLayer } = LayersControl; 14 | 15 | interface MapLayersProps { 16 | mapLayers: MapLayer[]; 17 | } 18 | 19 | class MapLayers extends React.Component { 20 | private Layer = (props: MapLayer): JSX.Element => { 21 | switch (props.layerType) { 22 | case MapLayerType.IMAGE_LAYER: 23 | return ; 24 | case MapLayerType.WMS_TILE_LAYER: 25 | return ; 26 | default: 27 | return ; 28 | } 29 | }; 30 | 31 | private Layers = (): JSX.Element[] => { 32 | const { mapLayers } = this.props; 33 | return mapLayers.map( 34 | (layer: MapLayer, index: number): JSX.Element => { 35 | if (layer.baseLayerName && mapLayers.length > 1) { 36 | return ( 37 | 42 | 43 | 44 | ); 45 | } 46 | return ; 47 | } 48 | ); 49 | }; 50 | 51 | render() { 52 | const { mapLayers } = this.props; 53 | if (mapLayers.length > 1) { 54 | return {this.Layers()}; 55 | } else { 56 | return <>{this.Layers()}; 57 | } 58 | } 59 | } 60 | 61 | export default MapLayers; 62 | -------------------------------------------------------------------------------- /HTML/src/MapMarkers.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { LayerGroup, Marker, Popup } from "react-leaflet"; 3 | import MarkerClusterGroup from "react-leaflet-markercluster"; 4 | import { createDivIcon } from "./utilities"; 5 | import { 6 | WebViewLeafletEvents, 7 | MapMarker, 8 | OWN_POSTION_MARKER_ID 9 | } from "./models"; 10 | import { LatLngExpression } from "leaflet"; 11 | require("react-leaflet-markercluster/dist/styles.min.css"); 12 | 13 | interface MapMarkersProps { 14 | mapMarkers: MapMarker[]; 15 | onMapEvent: (mapEvent: WebViewLeafletEvents, payload: any) => void; 16 | useMarkerClustering?: boolean; 17 | } 18 | 19 | export default class MapMarkers extends React.Component { 20 | private MapMarker = ({ mapMarker }: { mapMarker: MapMarker }) => { 21 | return ( 22 | { 27 | this.props.onMapEvent(WebViewLeafletEvents.ON_MAP_MARKER_CLICKED, { 28 | mapMarkerID: mapMarker.id 29 | }); 30 | }} 31 | > 32 | {mapMarker.title && {mapMarker.title}} 33 | 34 | ); 35 | }; 36 | 37 | render() { 38 | const { mapMarkers, useMarkerClustering = true } = this.props; 39 | if (useMarkerClustering) { 40 | return ( 41 | 42 | 43 | {mapMarkers.map((mapMarker: MapMarker) => { 44 | if (mapMarker.id !== OWN_POSTION_MARKER_ID) { 45 | return ( 46 | 50 | ); 51 | } else { 52 | return null; 53 | } 54 | })} 55 | 56 | {mapMarkers.map((mapMarker: MapMarker) => { 57 | if (mapMarker.id === OWN_POSTION_MARKER_ID) { 58 | return ; 59 | } else { 60 | return null; 61 | } 62 | })} 63 | 64 | ); 65 | } else { 66 | return ( 67 | 68 | {mapMarkers.map((mapMarker: MapMarker) => { 69 | return ; 70 | })} 71 | 72 | ); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /HTML/src/MapShapes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Circle, 4 | Polygon, 5 | CircleMarker, 6 | Polyline, 7 | Rectangle, 8 | CircleMarkerProps, 9 | PolylineProps, 10 | PolygonProps, 11 | RectangleProps, 12 | CircleProps 13 | } from "react-leaflet"; 14 | import { MapShapeType, MapShape, WebViewLeafletEvents } from "./models"; 15 | 16 | export interface MapMapShapesProps { 17 | mapShapes: MapShape[]; 18 | onMapEvent: (mapEvent: WebViewLeafletEvents, payload: any) => void; 19 | } 20 | 21 | class MapShapes extends React.Component { 22 | private Shape = (props: any) => { 23 | switch (props.shapeType) { 24 | case MapShapeType.CIRCLE: 25 | return ; 26 | case MapShapeType.CIRCLE_MARKER: { 27 | return ; 28 | } 29 | case MapShapeType.POLYGON: { 30 | return ; 31 | } 32 | case MapShapeType.POLYLINE: { 33 | return ; 34 | } 35 | case MapShapeType.RECTANGLE: { 36 | return ; 37 | } 38 | default: 39 | console.warn("Unknown map shape type", props.shapeType); 40 | return null; 41 | } 42 | }; 43 | 44 | render() { 45 | return ( 46 | <> 47 | {this.props.mapShapes.map(mapShape => { 48 | const props = { ...mapShape, color: mapShape.color ?? "white" }; 49 | return ; 50 | })} 51 | 52 | ); 53 | } 54 | } 55 | 56 | export default MapShapes; 57 | -------------------------------------------------------------------------------- /HTML/src/__snapshots__/MapComponent.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`MapComponent it renders 1`] = ` 4 | 5 |
9 | 10 | `; 11 | -------------------------------------------------------------------------------- /HTML/src/__snapshots__/MapComponent.view.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`MapComponentView it renders 1`] = ` 4 | 5 |
9 | 10 | `; 11 | -------------------------------------------------------------------------------- /HTML/src/__snapshots__/MapLayers.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`MapLayers Component it renders 1`] = ` 4 | 5 |
10 |
14 |
17 |
21 |
25 | 32 | 39 | 46 | 53 |
54 |
55 |
56 |
59 |
62 |
65 |
68 |
71 |
72 | 181 | 182 | `; 183 | -------------------------------------------------------------------------------- /HTML/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /HTML/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./MapComponent"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /HTML/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /HTML/src/mapComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "leaflet/dist/leaflet.css"; 3 | import "leaflet/dist/images/layers-2x.png"; 4 | import "leaflet/dist/images/layers.png"; 5 | import "leaflet/dist/images/marker-icon-2x.png"; 6 | import icon from "leaflet/dist/images/marker-icon.png"; 7 | import iconShadow from "leaflet/dist/images/marker-shadow.png"; 8 | import MapComponentView from "./MapComponent.view"; 9 | import L from "leaflet"; 10 | import mockMapLayers from "./testData/mockMapLayers"; 11 | import mockMapShapes from "./testData/mockMapShapes"; 12 | import mockMapMarkers from "./testData/mockMapMarkers"; 13 | import { 14 | WebViewLeafletEvents, 15 | MapEventMessage, 16 | MapLayer, 17 | MapMarker, 18 | MapShape, 19 | INFINITE_ANIMATION_ITERATIONS, 20 | AnimationType, 21 | WebviewLeafletMessagePayload 22 | } from "./models"; 23 | import "./styles/markers.css"; 24 | import "./styles/markerAnimations.css"; 25 | import { LatLng } from "react-leaflet"; 26 | 27 | export const SHOW_DEBUG_INFORMATION = false; 28 | const ENABLE_BROWSER_TESTING = true; 29 | 30 | interface State { 31 | debugMessages: string[]; 32 | isFromNative: boolean; 33 | isMobile: boolean; 34 | mapCenterPosition: LatLng; 35 | mapLayers: MapLayer[]; 36 | mapMarkers: MapMarker[]; 37 | mapShapes: MapShape[]; 38 | ownPositionMarker: MapMarker; 39 | mapRef: any; 40 | zoom: number; 41 | } 42 | 43 | export default class MapComponent extends Component<{}, State> { 44 | constructor(props: {}) { 45 | super(props); 46 | this.state = { 47 | debugMessages: ["test"], 48 | isFromNative: false, 49 | isMobile: null, 50 | mapCenterPosition: { lat: 36.56, lng: -76.17 }, 51 | mapLayers: [], 52 | mapMarkers: [], 53 | mapShapes: [], 54 | mapRef: null, 55 | ownPositionMarker: null, 56 | zoom: 6 57 | }; 58 | } 59 | 60 | componentDidMount = () => { 61 | let DefaultIcon = L.icon({ 62 | iconUrl: icon, 63 | shadowUrl: iconShadow 64 | }); 65 | L.Marker.prototype.options.icon = DefaultIcon; 66 | 67 | this.addEventListeners(); 68 | this.sendMessage({ 69 | msg: WebViewLeafletEvents.MAP_COMPONENT_MOUNTED 70 | }); 71 | if (ENABLE_BROWSER_TESTING) { 72 | this.loadMockData(); 73 | } 74 | }; 75 | 76 | componentDidUpdate = (prevProps: any, prevState: State) => { 77 | const { mapRef } = this.state; 78 | if (mapRef && !prevState.mapRef) { 79 | mapRef.current?.leafletElement.invalidateSize(); 80 | this.sendMessage({ 81 | msg: WebViewLeafletEvents.MAP_READY 82 | }); 83 | } 84 | }; 85 | 86 | private addDebugMessage = (msg: any) => { 87 | if (typeof msg === "object") { 88 | this.addDebugMessage("STRINGIFIED"); 89 | this.setState({ 90 | debugMessages: [ 91 | ...this.state.debugMessages, 92 | JSON.stringify(msg, null, 4) 93 | ] 94 | }); 95 | } else { 96 | this.setState({ debugMessages: [...this.state.debugMessages, msg] }); 97 | } 98 | }; 99 | 100 | private addEventListeners = () => { 101 | if (document) { 102 | document.addEventListener("message", this.handleMessage); 103 | this.addDebugMessage("set document listeners"); 104 | this.sendMessage({ 105 | msg: WebViewLeafletEvents.DOCUMENT_EVENT_LISTENER_ADDED 106 | }); 107 | } 108 | if (window) { 109 | window.addEventListener("message", this.handleMessage); 110 | this.addDebugMessage("setting Window"); 111 | this.sendMessage({ 112 | msg: WebViewLeafletEvents.WINDOW_EVENT_LISTENER_ADDED 113 | }); 114 | } 115 | if (!document && !window) { 116 | this.sendMessage({ 117 | error: WebViewLeafletEvents.UNABLE_TO_ADD_EVENT_LISTENER 118 | }); 119 | return; 120 | } 121 | }; 122 | 123 | private handleMessage = (event: any & { data: State }) => { 124 | this.addDebugMessage(event.data); 125 | try { 126 | if (event.data.mapCenterPosition) { 127 | this.state.mapRef.leafletElement.flyTo([ 128 | event.data.mapCenterPosition.lat, 129 | event.data.mapCenterPosition.lng 130 | ]); 131 | } 132 | this.setState({ ...this.state, ...event.data }); 133 | } catch (error) { 134 | this.addDebugMessage({ error: JSON.stringify(error) }); 135 | } 136 | }; 137 | 138 | protected sendMessage = (message: MapEventMessage) => { 139 | // @ts-ignore 140 | if (window.ReactNativeWebView) { 141 | // @ts-ignore 142 | window.ReactNativeWebView.postMessage(JSON.stringify(message)); 143 | console.log("sendMessage ", JSON.stringify(message)); 144 | } 145 | }; 146 | 147 | private loadMockData = () => { 148 | this.addDebugMessage("loading mock data"); 149 | this.setState({ 150 | mapLayers: mockMapLayers, 151 | mapMarkers: mockMapMarkers, 152 | mapShapes: mockMapShapes, 153 | ownPositionMarker: { 154 | id: "Own Position", 155 | position: { lat: 36.56, lng: -76.17 }, 156 | icon: "❤️", 157 | size: [32, 32], 158 | animation: { 159 | duration: 1, 160 | delay: 0, 161 | iterationCount: INFINITE_ANIMATION_ITERATIONS, 162 | type: AnimationType.BOUNCE 163 | } 164 | } 165 | }); 166 | }; 167 | 168 | private onMapEvent = ( 169 | webViewLeafletEvent: WebViewLeafletEvents, 170 | payload?: WebviewLeafletMessagePayload 171 | ) => { 172 | if (!payload && this.state.mapRef?.leafletElement) { 173 | debugger; 174 | const mapCenterPosition: LatLng = { 175 | lat: this.state.mapRef.leafletElement?.getCenter().lat, 176 | lng: this.state.mapRef.leafletElement?.getCenter().lng 177 | }; 178 | 179 | payload = { 180 | mapCenterPosition: mapCenterPosition, 181 | bounds: this.state.mapRef.leafletElement?.getBounds(), 182 | zoom: this.state.mapRef.leafletElement?.getZoom() 183 | }; 184 | } 185 | this.sendMessage({ event: webViewLeafletEvent, payload }); 186 | }; 187 | 188 | private setMapRef = (mapRef: any) => { 189 | if (!this.state.mapRef) { 190 | this.setState({ mapRef }); 191 | } 192 | }; 193 | 194 | render() { 195 | const { 196 | debugMessages, 197 | mapCenterPosition, 198 | mapLayers, 199 | mapMarkers, 200 | mapShapes, 201 | ownPositionMarker, 202 | zoom 203 | } = this.state; 204 | return ( 205 | 217 | ); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /HTML/src/mapComponent.view.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useState, useEffect } from "react"; 3 | import Measure from "react-measure"; 4 | import { Map, LatLng } from "react-leaflet"; 5 | import MapLayers from "./MapLayers"; 6 | import MapMarkers from "./MapMarkers"; 7 | import { SHOW_DEBUG_INFORMATION } from "./MapComponent"; 8 | import { WebViewLeafletEvents, MapLayer, MapMarker, MapShape } from "./models"; 9 | import MapShapes from "./MapShapes"; 10 | import { LatLngExpression } from "leaflet"; 11 | 12 | interface MapComponentViewProps { 13 | addDebugMessage: (msg: any) => void; 14 | debugMessages: string[]; 15 | mapCenterPosition: LatLng; 16 | mapLayers: MapLayer[]; 17 | mapMarkers: MapMarker[]; 18 | mapShapes: MapShape[]; 19 | onMapEvent: (mapEvent: WebViewLeafletEvents, payload?: any) => void; 20 | ownPositionMarker: MapMarker; 21 | setMapRef: (mapRef: any) => void; 22 | zoom: number; 23 | } 24 | 25 | const MapComponentView: React.FC = ({ 26 | addDebugMessage, 27 | debugMessages, 28 | mapCenterPosition, 29 | mapLayers = [], 30 | mapMarkers = [], 31 | mapShapes = [], 32 | onMapEvent, 33 | ownPositionMarker, 34 | setMapRef, 35 | zoom = 13 36 | }: MapComponentViewProps) => { 37 | const [dimensions, setDimensions] = useState({ height: 0, width: 0 }); 38 | const [combinedMapMarkers, setCombinedMapMarkers] = useState([]); 39 | 40 | useEffect(() => { 41 | const combinedMapMarkers = mapMarkers; 42 | if (ownPositionMarker) { 43 | combinedMapMarkers.push(ownPositionMarker); 44 | } 45 | 46 | setCombinedMapMarkers(combinedMapMarkers); 47 | }, [mapMarkers, ownPositionMarker]); 48 | 49 | return ( 50 | <> 51 | { 54 | const { height, width } = contentRect.bounds; 55 | setDimensions({ height, width }); 56 | }} 57 | > 58 | {({ measureRef }) => ( 59 |
71 | {dimensions.height > 0 && ( 72 | { 74 | setMapRef(ref); 75 | }} 76 | center={mapCenterPosition as LatLngExpression} 77 | onClick={(event: any) => { 78 | const { containerPoint, layerPoint, latlng } = event; 79 | onMapEvent(WebViewLeafletEvents.ON_MAP_TOUCHED, { 80 | containerPoint, 81 | layerPoint, 82 | touchLatLng: latlng 83 | }); 84 | }} 85 | onZoomLevelsChange={() => { 86 | onMapEvent(WebViewLeafletEvents.ON_ZOOM_LEVELS_CHANGE); 87 | }} 88 | onResize={() => { 89 | onMapEvent(WebViewLeafletEvents.ON_RESIZE); 90 | }} 91 | onZoomStart={() => { 92 | onMapEvent(WebViewLeafletEvents.ON_ZOOM_START); 93 | }} 94 | onMoveStart={() => { 95 | onMapEvent(WebViewLeafletEvents.ON_MOVE_START); 96 | }} 97 | onZoom={() => { 98 | onMapEvent(WebViewLeafletEvents.ON_ZOOM); 99 | }} 100 | onMove={() => { 101 | onMapEvent(WebViewLeafletEvents.ON_MOVE); 102 | }} 103 | onZoomEnd={() => { 104 | onMapEvent(WebViewLeafletEvents.ON_ZOOM_END); 105 | }} 106 | onMoveEnd={() => { 107 | onMapEvent(WebViewLeafletEvents.ON_MOVE_END); 108 | }} 109 | onUnload={() => { 110 | onMapEvent(WebViewLeafletEvents.ON_UNLOAD); 111 | }} 112 | onViewReset={() => { 113 | onMapEvent(WebViewLeafletEvents.ON_VIEW_RESET); 114 | }} 115 | maxZoom={17} 116 | zoom={zoom} 117 | style={{ width: "100%", height: dimensions.height }} 118 | > 119 | 120 | 124 | 125 | 126 | )} 127 |
128 | )} 129 |
130 | {SHOW_DEBUG_INFORMATION ? ( 131 |
145 |
    146 | {debugMessages.map((message, index) => { 147 | return
  • {message}
  • ; 148 | })} 149 |
150 |
151 | ) : null} 152 | 153 | ); 154 | }; 155 | 156 | export default MapComponentView; 157 | -------------------------------------------------------------------------------- /HTML/src/models.ts: -------------------------------------------------------------------------------- 1 | import * as ReactLeaflet from "react-leaflet"; 2 | export type LatLng = ReactLeaflet.LatLng; 3 | export type Point = ReactLeaflet.Point; 4 | export type LatLngBounds = ReactLeaflet.LatLngBounds; 5 | 6 | export const OWN_POSTION_MARKER_ID = "OWN_POSTION_MARKER_ID"; 7 | 8 | export enum WebViewLeafletEvents { 9 | MAP_COMPONENT_MOUNTED = "MAP_COMPONENT_MOUNTED", 10 | MAP_READY = "MAP_READY", 11 | DOCUMENT_EVENT_LISTENER_ADDED = "DOCUMENT_EVENT_LISTENER_ADDED", 12 | WINDOW_EVENT_LISTENER_ADDED = "WINDOW_EVENT_LISTENER_ADDED", 13 | UNABLE_TO_ADD_EVENT_LISTENER = "UNABLE_TO_ADD_EVENT_LISTENER", 14 | DOCUMENT_EVENT_LISTENER_REMOVED = "DOCUMENT_EVENT_LISTENER_REMOVED", 15 | WINDOW_EVENT_LISTENER_REMOVED = "WINDOW_EVENT_LISTENER_REMOVED", 16 | ON_MOVE_END = "onMoveEnd", 17 | ON_MOVE_START = "onMoveStart", 18 | ON_MOVE = "onMove", 19 | ON_RESIZE = "onResize", 20 | ON_UNLOAD = "onUnload", 21 | ON_VIEW_RESET = "onViewReset", 22 | ON_ZOOM_END = "onZoomEnd", 23 | ON_ZOOM_LEVELS_CHANGE = "onZoomLevelsChange", 24 | ON_ZOOM_START = "onZoomStart", 25 | ON_ZOOM = "onZoom", 26 | ON_MAP_TOUCHED = "onMapClicked", 27 | ON_MAP_MARKER_CLICKED = "onMapMarkerClicked" 28 | // ON_MAP_SHAPE_CLICKED = "onMapShapeClicked" cannot click on shapes yet 29 | } 30 | 31 | export enum AnimationType { 32 | BOUNCE = "bounce", 33 | FADE = "fade", 34 | PULSE = "pulse", 35 | JUMP = "jump", 36 | SPIN = "spin", 37 | WAGGLE = "waggle" 38 | } 39 | 40 | export enum MapLayerType { 41 | IMAGE_LAYER = "ImageOverlay", 42 | TILE_LAYER = "TileLayer", 43 | VECTOR_LAYER = "VectorLayer", 44 | VIDEO_LAYER = "VideoOverlay", 45 | WMS_TILE_LAYER = "WMSTileLayer" 46 | } 47 | 48 | export enum MapShapeType { 49 | CIRCLE = "Circle", 50 | CIRCLE_MARKER = "CircleMarker", 51 | POLYLINE = "Polyline", 52 | POLYGON = "Polygon", 53 | RECTANGLE = "Rectangle" 54 | } 55 | 56 | export const INFINITE_ANIMATION_ITERATIONS: string = "infinite"; 57 | 58 | export enum AnimationDirection { 59 | NORMAL = "nomal", 60 | REVERSE = "reverse", 61 | ALTERNATE = "alternate", 62 | ALTERNATE_REVERSE = "alternate-reverse" 63 | } 64 | export interface MapMarkerAnimation { 65 | type: AnimationType; 66 | duration?: number; 67 | delay?: number; 68 | direction?: AnimationDirection; 69 | iterationCount?: number | typeof INFINITE_ANIMATION_ITERATIONS; 70 | } 71 | 72 | export interface MapMarker { 73 | animation?: MapMarkerAnimation; 74 | position: LatLng; 75 | divIcon?: L.DivIcon; 76 | icon: any; 77 | iconAnchor?: Point; 78 | id?: string; 79 | size?: Point; 80 | title?: string; 81 | } 82 | 83 | export interface MapEventMessage { 84 | event?: any; 85 | msg?: string; 86 | error?: string; 87 | payload?: any; 88 | } 89 | 90 | export interface MapLayer { 91 | attribution?: string; 92 | baseLayer?: boolean; 93 | baseLayerIsChecked?: boolean; 94 | baseLayerName?: string; 95 | bounds?: LatLngBounds; 96 | id?: string; 97 | layerType?: MapLayerType; 98 | opacity?: number; 99 | pane?: string; 100 | subLayer?: string; 101 | url?: string; 102 | zIndex?: number; 103 | } 104 | 105 | export interface MapShape { 106 | bounds?: LatLng[]; 107 | center?: LatLng; 108 | color?: string; 109 | id?: string; 110 | positions?: LatLng[] | LatLng[][]; 111 | radius?: number; 112 | shapeType: MapShapeType; 113 | } 114 | 115 | export interface MapStartupMessage { 116 | mapLayers?: MapLayer[]; 117 | mapMarkers?: MapMarker[]; 118 | mapShapes?: MapShape[]; 119 | mapCenterPosition?: LatLng; 120 | ownPositionMarker?: OwnPositionMarker; 121 | zoom?: number; 122 | } 123 | 124 | export type WebviewLeafletMessagePayload = { 125 | bounds?: LatLngBounds; 126 | mapCenterPosition: LatLng; 127 | mapMarkerID?: string; 128 | touchLatLng?: LatLng; 129 | zoom?: number; 130 | }; 131 | 132 | export interface WebviewLeafletMessage { 133 | event?: any; 134 | msg?: string; 135 | error?: string; 136 | payload?: WebviewLeafletMessagePayload; 137 | } 138 | 139 | export type OwnPositionMarker = { 140 | animation: MapMarkerAnimation; 141 | id?: string; 142 | icon: string; 143 | position: LatLng; 144 | size: Point; 145 | }; 146 | -------------------------------------------------------------------------------- /HTML/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /HTML/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready.then(registration => { 142 | registration.unregister(); 143 | }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /HTML/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /HTML/src/styles/markerAnimations.css: -------------------------------------------------------------------------------- 1 | .marker { 2 | background-color: rgba(255, 255, 255, 0); 3 | width: 100%; 4 | height: 100%; 5 | position: relative; 6 | } 7 | 8 | /* div containing the marker parent */ 9 | .clearMarkerContainer { 10 | background-color: rgba(57, 57, 216, 0); 11 | display: flex; 12 | justify-content: center; 13 | } 14 | 15 | /* div containing all the animated portions of the marker */ 16 | .animationContainer { 17 | display: flex; 18 | justify-content: center; 19 | align-items: flex-end; 20 | } 21 | 22 | @keyframes bounce { 23 | 0% { 24 | transform: scale(1, 0.8) translateY(10px); 25 | } 26 | 45% { 27 | transform: scale(0.8, 1) translateY(-27px); 28 | } 29 | 50% { 30 | transform: scale(0.8, 1) translateY(-30px); 31 | } 32 | 55% { 33 | transform: scale(0.8, 1) translateY(-27px); 34 | } 35 | 100% { 36 | transform: scale(1, 0.8) translateY(10px); 37 | } 38 | } 39 | 40 | @keyframes fade { 41 | 0% { 42 | opacity: 0.1; 43 | } 44 | 50% { 45 | opacity: 1; 46 | } 47 | 100% { 48 | opacity: 0.1; 49 | } 50 | } 51 | 52 | @keyframes pulse { 53 | 0% { 54 | transform: scale(1); 55 | } 56 | 50% { 57 | transform: scale(1.25); 58 | } 59 | 100% { 60 | transform: scale(1); 61 | } 62 | } 63 | 64 | @keyframes jump { 65 | 0% { 66 | transform: none; 67 | } 68 | 50% { 69 | transform: translateY(-2em); 70 | } 71 | } 72 | 73 | @keyframes waggle { 74 | 0% { 75 | transform: none; 76 | } 77 | 50% { 78 | transform: rotateZ(-20deg) scale(1.2); 79 | } 80 | 60% { 81 | transform: rotateZ(25deg) scale(1.2); 82 | } 83 | 67.5% { 84 | transform: rotateZ(-15deg) scale(1.2); 85 | } 86 | 75% { 87 | transform: rotateZ(15deg) scale(1.2); 88 | } 89 | 82.5% { 90 | transform: rotateZ(-12deg) scale(1.2); 91 | } 92 | 85% { 93 | transform: rotateZ(0) scale(1.2); 94 | } 95 | 100% { 96 | transform: rotateZ(0) scale(1); 97 | } 98 | } 99 | 100 | @keyframes spin { 101 | 50% { 102 | transform: rotateZ(-20deg); 103 | animation-timing-function: ease; 104 | } 105 | 100% { 106 | transform: rotateZ(360deg); 107 | } 108 | } 109 | 110 | @keyframes beat { 111 | to { 112 | transform: scale(0.7); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /HTML/src/styles/markers.css: -------------------------------------------------------------------------------- 1 | /*General marker style*/ 2 | .marker { 3 | background-color: rgba(255, 255, 255, 0); 4 | width: 100%; 5 | height: 100%; 6 | position: relative; 7 | } 8 | 9 | /* div containing the marker parent */ 10 | .clearMarkerContainer { 11 | background-color: rgba(57, 57, 216, 0); 12 | display: flex; 13 | justify-content: center; 14 | } 15 | 16 | /* div containing all the animated portions of the marker */ 17 | .animationContainer { 18 | display: flex; 19 | justify-content: center; 20 | align-items: flex-end; 21 | } 22 | 23 | @keyframes bounce { 24 | 0% { 25 | transform: scale(1, 0.8) translateY(10px); 26 | } 27 | 45% { 28 | transform: scale(0.8, 1) translateY(-27px); 29 | } 30 | 50% { 31 | transform: scale(0.8, 1) translateY(-30px); 32 | } 33 | 55% { 34 | transform: scale(0.8, 1) translateY(-27px); 35 | } 36 | 100% { 37 | transform: scale(1, 0.8) translateY(10px); 38 | } 39 | } 40 | 41 | @keyframes fade { 42 | 0% { 43 | opacity: 0.1; 44 | } 45 | 50% { 46 | opacity: 1; 47 | } 48 | 100% { 49 | opacity: 0.1; 50 | } 51 | } 52 | 53 | @keyframes pulse { 54 | 0% { 55 | transform: scale(1); 56 | } 57 | 50% { 58 | transform: scale(1.25); 59 | } 60 | 100% { 61 | transform: scale(1); 62 | } 63 | } 64 | 65 | @keyframes jump { 66 | 0% { 67 | transform: none; 68 | } 69 | 50% { 70 | transform: translateY(-2em); 71 | } 72 | } 73 | 74 | @keyframes waggle { 75 | 0% { 76 | transform: none; 77 | } 78 | 50% { 79 | transform: rotateZ(-20deg) scale(1.2); 80 | } 81 | 60% { 82 | transform: rotateZ(25deg) scale(1.2); 83 | } 84 | 67.5% { 85 | transform: rotateZ(-15deg) scale(1.2); 86 | } 87 | 75% { 88 | transform: rotateZ(15deg) scale(1.2); 89 | } 90 | 82.5% { 91 | transform: rotateZ(-12deg) scale(1.2); 92 | } 93 | 85% { 94 | transform: rotateZ(0) scale(1.2); 95 | } 96 | 100% { 97 | transform: rotateZ(0) scale(1); 98 | } 99 | } 100 | 101 | @keyframes spin { 102 | 50% { 103 | transform: rotateZ(-20deg); 104 | animation-timing-function: ease; 105 | } 106 | 100% { 107 | transform: rotateZ(360deg); 108 | } 109 | } 110 | 111 | @keyframes beat { 112 | to { 113 | transform: scale(0.7); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /HTML/src/testData/mockMapLayers.ts: -------------------------------------------------------------------------------- 1 | import { MapLayer, MapLayerType } from "../models"; 2 | 3 | const mockMapLayers: MapLayer[] = [ 4 | { 5 | attribution: 6 | '&copy OpenStreetMap contributors', 7 | baseLayerIsChecked: true, 8 | baseLayerName: "OpenStreetMap.Mapnik", 9 | url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" 10 | }, 11 | { 12 | attribution: 13 | '&copy OpenStreetMap contributors', 14 | baseLayerIsChecked: false, 15 | baseLayerName: "OpenStreetMap.BlackAndWhite", 16 | url: "https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png" 17 | }, 18 | { 19 | baseLayerName: "WMS Tile Layer", 20 | subLayer: "nasa:bluemarble", 21 | layerType: MapLayerType.WMS_TILE_LAYER, 22 | url: "https://demo.boundlessgeo.com/geoserver/ows" 23 | }, 24 | { 25 | baseLayerName: "Image", 26 | layerType: MapLayerType.IMAGE_LAYER, 27 | url: "http://www.lib.utexas.edu/maps/historical/newark_nj_1922.jpg", 28 | bounds: [ 29 | [40.712216, -74.22655], 30 | [40.773941, -74.12544] 31 | ] 32 | } 33 | ]; 34 | 35 | export default mockMapLayers; 36 | -------------------------------------------------------------------------------- /HTML/src/testData/mockMapMarkers.ts: -------------------------------------------------------------------------------- 1 | import * as svgIcons from "./svgIcons"; 2 | import { LatLng } from "leaflet"; 3 | import { MapMarker, AnimationType } from "../models"; 4 | 5 | const emoji = ["😴", "😄", "😃", "⛔", "🎠", "🚓", "🚇"]; 6 | const duration = Math.floor(Math.random() * 3) + 1; 7 | const delay = Math.floor(Math.random()) * 0.5; 8 | const iterationCount = "infinite"; 9 | 10 | const mapMarkers: MapMarker[] = [ 11 | { 12 | id: "2", 13 | position: { lat: 37.06452161, lng: -75.67364786 }, 14 | icon: "😴", 15 | size: [64, 64], 16 | animation: { 17 | duration, 18 | delay, 19 | iterationCount, 20 | type: AnimationType.PULSE 21 | } 22 | }, 23 | { 24 | id: "19", 25 | position: { lat: 36.46410354, lng: -75.6432701 }, 26 | icon: 27 | "https://www.catster.com/wp-content/uploads/2018/07/Savannah-cat-long-body-shot.jpg", 28 | size: [32, 32], 29 | animation: { 30 | duration, 31 | delay, 32 | iterationCount, 33 | type: AnimationType.BOUNCE 34 | } 35 | }, 36 | { 37 | id: "100", 38 | position: new LatLng(37.23310632, -76.23518332), 39 | icon: emoji[Math.floor(Math.random() * emoji.length)], 40 | animation: { 41 | duration, 42 | delay, 43 | iterationCount, 44 | type: AnimationType.WAGGLE 45 | } 46 | }, 47 | { 48 | id: "1", 49 | position: { lat: 36.46410354, lng: -75.6432701 }, 50 | icon: "😴", 51 | size: [32, 32], 52 | animation: { 53 | type: AnimationType.SPIN, 54 | duration, 55 | delay, 56 | iterationCount 57 | } 58 | }, 59 | { 60 | id: "1000", 61 | position: new LatLng(36.60061515, -76.48888338), 62 | icon: svgIcons.greenCircle, 63 | animation: { 64 | duration, 65 | delay, 66 | iterationCount, 67 | type: AnimationType.PULSE 68 | } 69 | }, 70 | { 71 | id: Math.floor(Math.random() * 1000).toString(), 72 | position: { lat: 37.0580835, lng: -75.82318747 }, 73 | icon: "Fish", 74 | animation: { 75 | type: AnimationType.WAGGLE, 76 | duration, 77 | delay, 78 | iterationCount 79 | } 80 | }, 81 | { 82 | id: Math.floor(Math.random() * 1000).toString(), 83 | position: { lat: 37.23310632, lng: -76.23518332 }, 84 | icon: emoji[Math.floor(Math.random() * emoji.length)], 85 | size: [4, 4], 86 | animation: { 87 | type: AnimationType.PULSE, 88 | duration, 89 | delay, 90 | iterationCount 91 | } 92 | } 93 | /* 94 | { 95 | id: Math.floor(Math.random() * 1000), 96 | coords: [36.94994253, -76.64318409], 97 | icon: emoji[Math.floor(Math.random() * emoji.length)], 98 | animation: { 99 | name: animations[Math.floor(Math.random() * animations.length)], 100 | duration: Math.floor(Math.random() * 3) + 1, 101 | delay: Math.floor(Math.random()) * 0.5, 102 | iterationCount 103 | } 104 | }, 105 | { 106 | id: Math.floor(Math.random() * 1000), 107 | coords: [37.19810239, -76.28058546], 108 | icon: emoji[Math.floor(Math.random() * emoji.length)], 109 | animation: { 110 | name: animations[Math.floor(Math.random() * animations.length)], 111 | duration: Math.floor(Math.random() * 3) + 1, 112 | delay: Math.floor(Math.random()) * 0.5, 113 | iterationCount 114 | } 115 | }, 116 | { 117 | id: Math.floor(Math.random() * 1000), 118 | coords: [37.02416165, -76.56052521], 119 | icon: emoji[Math.floor(Math.random() * emoji.length)], 120 | animation: { 121 | name: animations[Math.floor(Math.random() * animations.length)], 122 | duration: Math.floor(Math.random() * 3) + 1, 123 | delay: Math.floor(Math.random()) * 0.5, 124 | iterationCount 125 | } 126 | }, 127 | { 128 | id: Math.floor(Math.random() * 1000), 129 | coords: [36.91541467, -75.49279245], 130 | icon: emoji[Math.floor(Math.random() * emoji.length)], 131 | animation: { 132 | name: animations[Math.floor(Math.random() * animations.length)], 133 | duration: Math.floor(Math.random() * 3) + 1, 134 | delay: Math.floor(Math.random()) * 0.5, 135 | iterationCount 136 | } 137 | }, 138 | { 139 | id: Math.floor(Math.random() * 1000), 140 | coords: [36.70503123, -76.32755185], 141 | icon: emoji[Math.floor(Math.random() * emoji.length)], 142 | animation: { 143 | name: animations[Math.floor(Math.random() * animations.length)], 144 | duration: Math.floor(Math.random() * 3) + 1, 145 | delay: Math.floor(Math.random()) * 0.5, 146 | iterationCount 147 | } 148 | }, 149 | { 150 | id: Math.floor(Math.random() * 1000), 151 | coords: [36.31605891, -76.45141618], 152 | icon: emoji[Math.floor(Math.random() * emoji.length)], 153 | animation: { 154 | name: animations[Math.floor(Math.random() * animations.length)], 155 | duration: Math.floor(Math.random() * 3) + 1, 156 | delay: Math.floor(Math.random()) * 0.5, 157 | iterationCount 158 | } 159 | }, 160 | { 161 | id: Math.floor(Math.random() * 1000), 162 | coords: [36.59436803, -76.89486842], 163 | icon: emoji[Math.floor(Math.random() * emoji.length)], 164 | animation: { 165 | name: animations[Math.floor(Math.random() * animations.length)], 166 | duration: Math.floor(Math.random() * 3) + 1, 167 | delay: Math.floor(Math.random()) * 0.5, 168 | iterationCount 169 | } 170 | }, 171 | { 172 | id: Math.floor(Math.random() * 1000), 173 | coords: [37.35740877, -75.77910112], 174 | icon: emoji[Math.floor(Math.random() * emoji.length)], 175 | animation: { 176 | name: animations[Math.floor(Math.random() * animations.length)], 177 | duration: Math.floor(Math.random() * 3) + 1, 178 | delay: Math.floor(Math.random()) * 0.5, 179 | iterationCount 180 | } 181 | }, 182 | { 183 | id: Math.floor(Math.random() * 1000), 184 | coords: [37.31509182, -76.76693784], 185 | icon: emoji[Math.floor(Math.random() * emoji.length)], 186 | animation: { 187 | name: animations[Math.floor(Math.random() * animations.length)], 188 | duration: Math.floor(Math.random() * 3) + 1, 189 | delay: Math.floor(Math.random()) * 0.5, 190 | iterationCount 191 | } 192 | }, 193 | { 194 | id: Math.floor(Math.random() * 1000), 195 | coords: [36.91815909, -76.06707072], 196 | icon: emoji[Math.floor(Math.random() * emoji.length)], 197 | animation: { 198 | name: animations[Math.floor(Math.random() * animations.length)], 199 | duration: Math.floor(Math.random() * 3) + 1, 200 | delay: Math.floor(Math.random()) * 0.5, 201 | iterationCount 202 | } 203 | }, 204 | { 205 | id: Math.floor(Math.random() * 1000), 206 | coords: [36.611917, -75.76758822], 207 | icon: emoji[Math.floor(Math.random() * emoji.length)], 208 | animation: { 209 | name: animations[Math.floor(Math.random() * animations.length)], 210 | duration: Math.floor(Math.random() * 3) + 1, 211 | delay: Math.floor(Math.random()) * 0.5, 212 | iterationCount 213 | } 214 | }, 215 | { 216 | id: Math.floor(Math.random() * 1000), 217 | coords: [36.79520769, -76.3959497], 218 | icon: emoji[Math.floor(Math.random() * emoji.length)], 219 | animation: { 220 | name: animations[Math.floor(Math.random() * animations.length)], 221 | duration: Math.floor(Math.random() * 3) + 1, 222 | delay: Math.floor(Math.random()) * 0.5, 223 | iterationCount 224 | } 225 | }, 226 | { 227 | id: Math.floor(Math.random() * 1000), 228 | coords: [37.42854666, -75.95883052], 229 | icon: emoji[Math.floor(Math.random() * emoji.length)], 230 | animation: { 231 | name: animations[Math.floor(Math.random() * animations.length)], 232 | duration: Math.floor(Math.random() * 3) + 1, 233 | delay: Math.floor(Math.random()) * 0.5, 234 | iterationCount 235 | } 236 | }, 237 | { 238 | id: Math.floor(Math.random() * 1000), 239 | coords: [36.78673099, -76.90459724], 240 | icon: emoji[Math.floor(Math.random() * emoji.length)], 241 | animation: { 242 | name: animations[Math.floor(Math.random() * animations.length)], 243 | duration: Math.floor(Math.random() * 3) + 1, 244 | delay: Math.floor(Math.random()) * 0.5, 245 | iterationCount 246 | } 247 | }, 248 | { 249 | id: Math.floor(Math.random() * 1000), 250 | coords: [37.20966767, -75.58799685], 251 | icon: emoji[Math.floor(Math.random() * emoji.length)], 252 | animation: { 253 | name: animations[Math.floor(Math.random() * animations.length)], 254 | duration: Math.floor(Math.random() * 3) + 1, 255 | delay: Math.floor(Math.random()) * 0.5, 256 | iterationCount 257 | } 258 | } */ 259 | ]; 260 | export default mapMarkers; 261 | -------------------------------------------------------------------------------- /HTML/src/testData/mockMapShapes.ts: -------------------------------------------------------------------------------- 1 | import { MapShapeType, MapShape } from "../models"; 2 | 3 | export const circle: MapShape = { 4 | shapeType: MapShapeType.CIRCLE, 5 | color: "#123123", 6 | id: "1", 7 | center: { lat: 34.225727, lng: -77.94471 }, 8 | radius: 2000 9 | }; 10 | 11 | export const circleMarker: MapShape = { 12 | shapeType: MapShapeType.CIRCLE_MARKER, 13 | color: "red", 14 | id: "2", 15 | center: { lat: 38.437424, lng: -78.867912 }, 16 | radius: 15 17 | }; 18 | 19 | export const polygon: MapShape = { 20 | shapeType: MapShapeType.POLYGON, 21 | color: "blue", 22 | id: "3", 23 | positions: [ 24 | { lat: 38.80118939192329, lng: -74.69604492187501 }, 25 | { lat: 38.19502155795575, lng: -74.65209960937501 }, 26 | { lat: 39.07890809706475, lng: -71.46606445312501 } 27 | ] 28 | }; 29 | 30 | export const multiPolygon: MapShape = { 31 | shapeType: MapShapeType.POLYGON, 32 | color: "violet", 33 | id: "4", 34 | positions: [ 35 | [ 36 | { lat: 37.13842453422676, lng: -74.28955078125001 }, 37 | { lat: 36.4433803110554, lng: -74.26208496093751 }, 38 | { lat: 36.43896124085948, lng: -73.00964355468751 }, 39 | { lat: 36.43896124085948, lng: -73.00964355468751 } 40 | ], 41 | [ 42 | { lat: 37.505368263398104, lng: -72.38891601562501 }, 43 | { lat: 37.309014074275915, lng: -71.96594238281251 }, 44 | { lat: 36.69044623523481, lng: -71.87805175781251 }, 45 | { lat: 36.58024660149866, lng: -72.75146484375001 }, 46 | { lat: 37.36579146999664, lng: -72.88330078125001 } 47 | ] 48 | ] 49 | }; 50 | 51 | export const polyline: MapShape = { 52 | shapeType: MapShapeType.POLYLINE, 53 | color: "orange", 54 | id: "5", 55 | positions: [ 56 | { lat: 35.411438052435486, lng: -78.67858886718751 }, 57 | { lat: 35.9602229692967, lng: -79.18945312500001 }, 58 | { lat: 35.97356075349624, lng: -78.30505371093751 } 59 | ] 60 | }; 61 | 62 | export const multiPolyline: MapShape = { 63 | shapeType: MapShapeType.POLYLINE, 64 | color: "purple", 65 | id: "5a", 66 | positions: [ 67 | [ 68 | { lat: 36.36822190085111, lng: -79.26086425781251 }, 69 | { lat: 36.659606226479696, lng: -79.28833007812501 }, 70 | { lat: 36.721273880045004, lng: -79.81018066406251 } 71 | ], 72 | [ 73 | { lat: 35.43381992014202, lng: -79.79370117187501 }, 74 | { lat: 35.44277092585766, lng: -81.23840332031251 }, 75 | { lat: 35.007502842952896, lng: -80.837402343750017 } 76 | ] 77 | ] 78 | }; 79 | 80 | export const rectangle: MapShape = { 81 | shapeType: MapShapeType.RECTANGLE, 82 | color: "yellow", 83 | id: "6", 84 | bounds: [ 85 | { lat: 36.5, lng: -75.7 }, 86 | { lat: 38.01, lng: -73.13 } 87 | ] 88 | }; 89 | 90 | export default [circle, circleMarker, polygon, polyline, rectangle]; 91 | -------------------------------------------------------------------------------- /HTML/src/testData/svgIcons.ts: -------------------------------------------------------------------------------- 1 | export const greenCircle = ` 2 | 3 | `; 4 | -------------------------------------------------------------------------------- /HTML/src/utilities.test.ts.old: -------------------------------------------------------------------------------- 1 | import { 2 | convertWebViewLeafletLatLngToNumberArray, 3 | convertWebViewLeafletLatLngBoundsToLeaftletBounds, 4 | getAnimatedHTMLString 5 | } from "./utilities"; 6 | import { 7 | WebViewLeafletLatLng, 8 | WebViewLeafletLatLngBounds 9 | } from "../../WebViewLeaflet/models"; 10 | import { AnimationType } from "./models"; 11 | 12 | const singleLatLng: WebViewLeafletLatLng = { lat: 34.225727, lng: -77.94471 }; 13 | const latLngArray: WebViewLeafletLatLng[] = [ 14 | { lat: 38.80118939192329, lng: -74.69604492187501 }, 15 | { lat: 38.19502155795575, lng: -74.65209960937501 }, 16 | { lat: 39.07890809706475, lng: -71.46606445312501 } 17 | ]; 18 | const latLng2DArray: WebViewLeafletLatLng[][] = [ 19 | [ 20 | { lat: 37.13842453422676, lng: -74.28955078125001 }, 21 | { lat: 36.4433803110554, lng: -74.26208496093751 }, 22 | { lat: 36.43896124085948, lng: -73.00964355468751 }, 23 | { lat: 36.43896124085948, lng: -73.00964355468751 } 24 | ], 25 | [ 26 | { lat: 37.505368263398104, lng: -72.38891601562501 }, 27 | { lat: 37.309014074275915, lng: -71.96594238281251 }, 28 | { lat: 36.69044623523481, lng: -71.87805175781251 }, 29 | { lat: 36.58024660149866, lng: -72.75146484375001 }, 30 | { lat: 37.36579146999664, lng: -72.88330078125001 } 31 | ] 32 | ]; 33 | 34 | /* describe('convertWebViewLeafletLatLngToNumberArray', () => { 35 | it('can covert a single lat lng to a number array ', () => { 36 | const singleConverted = convertWebViewLeafletLatLngToNumberArray( 37 | singleLatLng 38 | ); 39 | expect(singleConverted).toEqual([34.225727, -77.94471]); 40 | }); 41 | 42 | it('can covert a an array of latLng to an array of number arrays', () => { 43 | const convertedLatLongArray = convertWebViewLeafletLatLngToNumberArray( 44 | latLngArray 45 | ); 46 | expect(convertedLatLongArray).toEqual([ 47 | [38.80118939192329, -74.69604492187501], 48 | [38.19502155795575, -74.65209960937501], 49 | [39.07890809706475, -71.46606445312501] 50 | ]); 51 | }); 52 | 53 | it('can covert a 2D Array of latLngs to a 2D number array ', () => { 54 | const latLng2DArrayConverted = convertWebViewLeafletLatLngToNumberArray( 55 | latLng2DArray 56 | ); 57 | expect(latLng2DArrayConverted).toEqual([ 58 | [ 59 | [37.13842453422676, -74.28955078125001], 60 | [36.4433803110554, -74.26208496093751], 61 | [36.43896124085948, -73.00964355468751], 62 | [36.43896124085948, -73.00964355468751] 63 | ], 64 | [ 65 | [37.505368263398104, -72.38891601562501], 66 | [37.309014074275915, -71.96594238281251], 67 | [36.69044623523481, -71.87805175781251], 68 | [36.58024660149866, -72.75146484375001], 69 | [37.36579146999664, -72.88330078125001] 70 | ] 71 | ]); 72 | }); 73 | }); */ 74 | 75 | const cornerBounds: WebViewLeafletLatLngBounds = { 76 | southWest: { lat: 36.665099, lng: -76.842042 }, 77 | northEast: { lat: 37.365855, lng: -76.158245 } 78 | }; 79 | const arrayBounds: WebViewLeafletLatLngBounds = [ 80 | { lat: 38.89688, lng: -77.302505 }, 81 | { lat: 37.829395, lng: -76.756299 } 82 | ]; 83 | 84 | /* describe('convertWebViewLeafletLatLngBoundsToLeaftletBounds', () => { 85 | it('can covert WebViewLeafletLatLngBoundsCorner objects', () => { 86 | const convertedBounds = convertWebViewLeafletLatLngBoundsToLeaftletBounds( 87 | cornerBounds 88 | ); 89 | 90 | expect(convertedBounds).toEqual({ 91 | southWest: [36.665099, -76.842042], 92 | northEast: [37.365855, -76.158245] 93 | }); 94 | }); 95 | it('can covert WebViewLeafletLatLngBounds[] objects', () => { 96 | const convertedBounds = convertWebViewLeafletLatLngBoundsToLeaftletBounds( 97 | arrayBounds 98 | ); 99 | 100 | expect(convertedBounds).toEqual([ 101 | [38.89688, -77.302505], 102 | [37.829395, -76.756299] 103 | ]); 104 | }); 105 | }); */ 106 | 107 | const mapMarker = { 108 | id: 2, 109 | coords: { lat: 37.06452161, lng: -75.67364786 }, 110 | icon: "😴", 111 | size: [64, 64], 112 | animation: { 113 | duration: 1, 114 | delay: 0.5, 115 | type: AnimationType.BOUNCE 116 | } 117 | }; 118 | 119 | /* describe('getAnimatedHTMLString', () => { 120 | it('returns a div with an animated emoji', () => { 121 | const div = getAnimatedHTMLString(mapMarker.icon, mapMarker.animation); 122 | expect(div).toBe(`
128 |
😴
129 |
`); 130 | }); 131 | }); */ 132 | -------------------------------------------------------------------------------- /HTML/src/utilities.ts: -------------------------------------------------------------------------------- 1 | import L, { DivIcon } from "leaflet"; 2 | import base64Image from "./webBase64Image"; 3 | import { MapMarker, MapMarkerAnimation } from "./models"; 4 | import { Point } from "react-leaflet"; 5 | 6 | export const createDivIcon = (mapMarker: MapMarker): DivIcon => { 7 | let divIcon: DivIcon = L.divIcon({ 8 | className: "clearMarkerContainer", 9 | html: mapMarker.animation 10 | ? getAnimatedHTMLString( 11 | mapMarker.icon || "📍", 12 | mapMarker.animation || null, 13 | mapMarker.size || [24, 24] 14 | ) 15 | : getUnanimatedHTMLString(mapMarker.icon, mapMarker.size), 16 | iconAnchor: mapMarker.iconAnchor || null 17 | }); 18 | return divIcon; 19 | }; 20 | 21 | /* 22 | Get the HTML string containing the icon div, and animation parameters 23 | */ 24 | export const getAnimatedHTMLString = ( 25 | icon: any, 26 | animation: MapMarkerAnimation, 27 | size: Point = [24, 24] 28 | ) => { 29 | return `
37 | ${getIconFromEmojiOrImageOrSVG(icon, size)} 38 |
`; 39 | }; 40 | 41 | const getUnanimatedHTMLString = (icon: any, size: Point = [24, 24]): string => { 42 | return `
${getIconFromEmojiOrImageOrSVG( 43 | icon, 44 | size 45 | )}
`; 46 | }; 47 | 48 | const getIconFromEmojiOrImageOrSVG = (icon: any, size: Point) => { 49 | if (icon.includes("svg") || icon.includes("SVG")) { 50 | //@ts-ignore 51 | return `
52 | ${icon} 53 |
`; 54 | } else if (icon.includes("//") && icon.includes("http")) { 55 | //@ts-ignore 56 | 57 | return ``; 58 | } else if (icon.includes("base64")) { 59 | //@ts-ignore 60 | 61 | return ``; 62 | } else { 63 | return `
${icon}
`; 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /HTML/src/webBase64Image.ts: -------------------------------------------------------------------------------- 1 | const base64Image = 2 | 'data:image/gif;base64,R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzWlwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2ccMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjAJ8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8AAF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMuQeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSHpzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGRs/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78AAAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMiwocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7GnwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euTeJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dtGCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWlMc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPeiUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYIm4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZcNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3ADTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kVMyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDGqCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMWZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bDGdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB776aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJHgxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiAFB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPAgCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHgrhGSQJxCS+0pCZbEhAAOw=='; 3 | 4 | export default base64Image; 5 | -------------------------------------------------------------------------------- /HTML/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "noEmit": true, 18 | "jsx": "react", 19 | "strict": true, 20 | "strictNullChecks": false, 21 | "isolatedModules": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /WebViewLeaflet/ActivityOverlay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, ActivityIndicator, StyleSheet } from 'react-native'; 3 | 4 | interface Props {} 5 | 6 | export const ActivityOverlay: React.FC = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | const styles = StyleSheet.create({ 17 | activityOverlayStyle: { 18 | ...StyleSheet.absoluteFillObject, 19 | backgroundColor: 'rgba(255, 255, 255, .5)', 20 | display: 'flex', 21 | justifyContent: 'center', 22 | alignContent: 'center', 23 | borderRadius: 5 24 | }, 25 | activityIndicatorContainer: { 26 | backgroundColor: 'lightgray', 27 | padding: 10, 28 | borderRadius: 50, 29 | alignSelf: 'center', 30 | shadowColor: '#000000', 31 | shadowOffset: { 32 | width: 0, 33 | height: 3 34 | }, 35 | shadowRadius: 5, 36 | shadowOpacity: 1.0 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /WebViewLeaflet/DebugMessageBox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ScrollView, Text, View } from "react-native"; 3 | 4 | export interface Props { 5 | debugMessages: string[]; 6 | doShowDebugMessages: boolean; 7 | } 8 | 9 | const DebugMessageBox = ({ 10 | debugMessages = [], 11 | doShowDebugMessages = false 12 | }: Props) => { 13 | if (doShowDebugMessages) { 14 | return ( 15 | 27 | 28 | {debugMessages.map((msg, idx) => { 29 | if (typeof msg === "object") { 30 | return ( 31 | {`- ${JSON.stringify( 32 | msg 33 | )}`} 34 | ); 35 | } 36 | return {`- ${msg}`}; 37 | })} 38 | 39 | 40 | ); 41 | } 42 | return null; 43 | }; 44 | 45 | export default DebugMessageBox; 46 | -------------------------------------------------------------------------------- /WebViewLeaflet/WebViewLeaflet.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { WebView } from "react-native-webview"; 3 | import AssetUtils from "expo-asset-utils"; 4 | import { Asset } from "expo-asset"; 5 | import WebViewLeafletView from "./WebViewLeaflet.view"; 6 | import { 7 | MapMarker, 8 | WebviewLeafletMessage, 9 | MapStartupMessage, 10 | WebViewLeafletEvents, 11 | MapLayer, 12 | MapShape, 13 | OwnPositionMarker, 14 | OWN_POSTION_MARKER_ID 15 | } from "./models"; 16 | import { ActivityOverlay } from "./ActivityOverlay"; 17 | import * as FileSystem from "expo-file-system"; 18 | import { LatLng } from "react-leaflet"; 19 | import isEqual from "lodash.isequal"; 20 | // @ts-ignore node types 21 | const INDEX_FILE_PATH = require(`./assets/index.html`); 22 | 23 | export interface WebViewLeafletProps { 24 | backgroundColor?: string; 25 | doShowDebugMessages?: boolean; 26 | loadingIndicator?: () => React.ReactElement; 27 | onError?: (syntheticEvent: any) => void; 28 | onLoadEnd?: () => void; 29 | onLoadStart?: () => void; 30 | onMessageReceived: (message: WebviewLeafletMessage) => void; 31 | mapLayers?: MapLayer[]; 32 | mapMarkers?: MapMarker[]; 33 | mapShapes?: MapShape[]; 34 | mapCenterPosition?: LatLng; 35 | ownPositionMarker?: OwnPositionMarker; 36 | zoom?: number; 37 | } 38 | 39 | interface State { 40 | debugMessages: string[]; 41 | mapCurrentCenterPosition: LatLng; 42 | webviewContent: string; 43 | isLoading: boolean; 44 | } 45 | 46 | class WebViewLeaflet extends React.Component { 47 | private webViewRef: any; 48 | static defaultProps = { 49 | backgroundColor: "#FAEBD7", 50 | doDisplayCenteringButton: true, 51 | doShowDebugMessages: false, 52 | loadingIndicator: () => { 53 | return ; 54 | }, 55 | onError: (syntheticEvent: any) => {}, 56 | onLoadEnd: () => {}, 57 | onLoadStart: () => {} 58 | }; 59 | 60 | constructor(props) { 61 | super(props); 62 | this.state = { 63 | debugMessages: [], 64 | isLoading: null, 65 | mapCurrentCenterPosition: null, 66 | webviewContent: null 67 | }; 68 | this.webViewRef = null; 69 | } 70 | 71 | componentDidMount = () => { 72 | this.loadHTMLFile(); 73 | }; 74 | 75 | private loadHTMLFile = async () => { 76 | try { 77 | let asset: Asset = await AssetUtils.resolveAsync(INDEX_FILE_PATH); 78 | let fileString: string = await FileSystem.readAsStringAsync( 79 | asset.localUri 80 | ); 81 | 82 | this.setState({ webviewContent: fileString }); 83 | } catch (error) { 84 | console.warn(error); 85 | console.warn("Unable to resolve index file"); 86 | } 87 | }; 88 | 89 | componentDidUpdate = (prevProps: WebViewLeafletProps, prevState: State) => { 90 | const { webviewContent } = this.state; 91 | const { 92 | mapCenterPosition, 93 | mapMarkers, 94 | mapLayers, 95 | mapShapes, 96 | ownPositionMarker, 97 | zoom 98 | } = this.props; 99 | 100 | if (!prevState.webviewContent && webviewContent) { 101 | this.updateDebugMessages("file loaded"); 102 | } 103 | if (!isEqual(mapCenterPosition, prevProps.mapCenterPosition)) { 104 | this.sendMessage({ mapCenterPosition }); 105 | } 106 | if (!isEqual(mapMarkers, prevProps.mapMarkers)) { 107 | this.sendMessage({ mapMarkers }); 108 | } 109 | if (!isEqual(mapLayers, prevProps.mapLayers)) { 110 | this.sendMessage({ mapLayers }); 111 | } 112 | if (!isEqual(mapShapes, prevProps.mapShapes)) { 113 | this.sendMessage({ mapShapes }); 114 | } 115 | if (!isEqual(ownPositionMarker, prevProps.ownPositionMarker)) { 116 | this.sendMessage({ ownPositionMarker }); 117 | } 118 | if (zoom !== prevProps.zoom) { 119 | this.sendMessage({ zoom }); 120 | } 121 | }; 122 | 123 | private setMapCenterPosition = () => { 124 | const { mapCurrentCenterPosition } = this.state; 125 | const { mapCenterPosition } = this.props; 126 | 127 | if (!isEqual(mapCenterPosition, mapCurrentCenterPosition)) { 128 | this.setState({ 129 | mapCurrentCenterPosition: mapCenterPosition 130 | }); 131 | this.sendMessage({ 132 | mapCenterPosition 133 | }); 134 | } 135 | }; 136 | 137 | // Handle messages received from webview contents 138 | private handleMessage = (data: string) => { 139 | const { onMessageReceived } = this.props; 140 | 141 | let message: WebviewLeafletMessage = JSON.parse(data); 142 | this.updateDebugMessages(`received: ${JSON.stringify(message)}`); 143 | if (message.msg === WebViewLeafletEvents.MAP_READY) { 144 | this.sendStartupMessage(); 145 | } 146 | if (message.event === WebViewLeafletEvents.ON_MOVE_END) { 147 | this.setState({ 148 | mapCurrentCenterPosition: message.payload.mapCenterPosition 149 | }); 150 | } 151 | onMessageReceived(message); 152 | }; 153 | 154 | // Send message to webview 155 | private sendMessage = (payload: object) => { 156 | this.updateDebugMessages(`sending: ${payload}`); 157 | 158 | this.webViewRef?.injectJavaScript( 159 | `window.postMessage(${JSON.stringify(payload)}, '*');` 160 | ); 161 | }; 162 | 163 | // Send a startup message with initalizing values to the map 164 | private sendStartupMessage = () => { 165 | let startupMessage: MapStartupMessage = {}; 166 | const { 167 | mapLayers, 168 | mapMarkers, 169 | mapShapes, 170 | mapCenterPosition, 171 | ownPositionMarker, 172 | zoom = 7 173 | } = this.props; 174 | if (mapLayers) { 175 | startupMessage.mapLayers = mapLayers; 176 | } 177 | if (mapMarkers) { 178 | startupMessage.mapMarkers = mapMarkers; 179 | } 180 | if (mapCenterPosition) { 181 | startupMessage.mapCenterPosition = mapCenterPosition; 182 | } 183 | if (mapShapes) { 184 | startupMessage.mapShapes = mapShapes; 185 | } 186 | if (ownPositionMarker) { 187 | startupMessage.ownPositionMarker = { 188 | ...ownPositionMarker, 189 | id: OWN_POSTION_MARKER_ID 190 | }; 191 | } 192 | 193 | startupMessage.zoom = zoom; 194 | 195 | this.setState({ isLoading: false }); 196 | this.updateDebugMessages("sending startup message"); 197 | 198 | this.webViewRef.injectJavaScript( 199 | `window.postMessage(${JSON.stringify(startupMessage)}, '*');` 200 | ); 201 | }; 202 | 203 | // Add a new debug message to the debug message array 204 | private updateDebugMessages = (debugMessage: string) => { 205 | this.setState({ 206 | debugMessages: [...this.state.debugMessages, debugMessage] 207 | }); 208 | }; 209 | 210 | private onError = (syntheticEvent: any) => { 211 | this.props.onError(syntheticEvent); 212 | }; 213 | private onLoadEnd = () => { 214 | this.setState({ isLoading: false }); 215 | this.props.onLoadEnd(); 216 | }; 217 | private onLoadStart = () => { 218 | this.setState({ isLoading: true }); 219 | this.props.onLoadStart(); 220 | }; 221 | 222 | // Output rendered item to screen 223 | render() { 224 | const { debugMessages, webviewContent } = this.state; 225 | const { 226 | backgroundColor, 227 | doShowDebugMessages, 228 | loadingIndicator 229 | } = this.props; 230 | 231 | if (webviewContent) { 232 | return ( 233 | { 244 | this.webViewRef = ref; 245 | }} 246 | /> 247 | ); 248 | } else { 249 | return null; 250 | } 251 | } 252 | } 253 | 254 | export default WebViewLeaflet; 255 | -------------------------------------------------------------------------------- /WebViewLeaflet/WebViewLeaflet.view.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { StyleSheet, View, NativeSyntheticEvent } from 'react-native'; 3 | import { WebView } from 'react-native-webview'; 4 | import { Asset } from 'expo-asset'; 5 | import DebugMessageBox from './DebugMessageBox'; 6 | import { WebViewError } from 'react-native-webview/lib/WebViewTypes'; 7 | 8 | export interface Props { 9 | backgroundColor: string; 10 | debugMessages: string[]; 11 | doShowDebugMessages: boolean; 12 | handleMessage: (data: string) => void; 13 | webviewContent: string; 14 | loadingIndicator: () => ReactElement; 15 | onError: (syntheticEvent: NativeSyntheticEvent) => void; 16 | onLoadEnd: () => void; 17 | onLoadStart: () => void; 18 | setWebViewRef: (ref: WebView) => void; 19 | } 20 | 21 | const WebViewLeafletView = ({ 22 | backgroundColor, 23 | debugMessages, 24 | doShowDebugMessages, 25 | handleMessage, 26 | webviewContent, 27 | loadingIndicator, 28 | onError, 29 | onLoadEnd, 30 | onLoadStart, 31 | setWebViewRef 32 | }: Props) => { 33 | return ( 34 | 41 | {webviewContent && ( 42 | { 50 | setWebViewRef(component); 51 | }} 52 | javaScriptEnabled={true} 53 | onLoadEnd={onLoadEnd} 54 | onLoadStart={onLoadStart} 55 | onMessage={(event) => { 56 | if (event && event.nativeEvent && event.nativeEvent.data) { 57 | handleMessage(event.nativeEvent.data); 58 | } 59 | }} 60 | domStorageEnabled={true} 61 | useWebKit={true} 62 | startInLoadingState={true} 63 | onError={onError} 64 | originWhitelist={['*']} 65 | /* renderLoading={loadingIndicator || null} */ 66 | source={{ 67 | html: webviewContent 68 | }} 69 | allowFileAccess={true} 70 | allowUniversalAccessFromFileURLs={true} 71 | allowFileAccessFromFileURLs={true} 72 | /> 73 | )} 74 | 78 | 79 | ); 80 | }; 81 | 82 | export default WebViewLeafletView; 83 | -------------------------------------------------------------------------------- /WebViewLeaflet/index.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { WebViewError } from "react-native-webview/lib/WebViewTypes"; 3 | 4 | // Type definitions for react-native-webivew-leaflet 5 | // Project: react-native-webview-leaflet 6 | // Definitions by: Reginald Johnson https:// 7 | 8 | /*~ This is the module template file. You should rename it to index.d.ts 9 | *~ and place it in a folder with the same name as the module. 10 | *~ For example, if you were writing a file for "super-greeter", this 11 | *~ file should be 'super-greeter/index.d.ts' 12 | */ 13 | 14 | /*~ If this module is a UMD module that exposes a global variable 'myLib' when 15 | *~ loaded outside a module loader environment, declare that global here. 16 | *~ Otherwise, delete this declaration. 17 | */ 18 | export as namespace ReactNativeWebViewLeaflet; 19 | 20 | export { 21 | default as WebViewLeaflet, 22 | WebViewLeafletProps 23 | } from "./WebViewLeaflet"; 24 | export { 25 | LatLng, 26 | Point, 27 | LatLngBounds, 28 | AnimationDirection, 29 | AnimationType, 30 | INFINITE_ANIMATION_ITERATIONS, 31 | WebViewLeafletEvents, 32 | MapMarkerAnimation, 33 | MapMarker, 34 | MapLayer, 35 | MapLayerType, 36 | MapShapeType, 37 | WebviewLeafletMessage 38 | } from "./models"; 39 | -------------------------------------------------------------------------------- /WebViewLeaflet/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as WebViewLeaflet, 3 | WebViewLeafletProps 4 | } from "./WebViewLeaflet"; 5 | export { 6 | LatLng, 7 | Point, 8 | LatLngBounds, 9 | AnimationDirection, 10 | AnimationType, 11 | INFINITE_ANIMATION_ITERATIONS, 12 | WebViewLeafletEvents, 13 | MapMarkerAnimation, 14 | MapMarker, 15 | MapLayer, 16 | MapLayerType, 17 | MapShapeType, 18 | WebviewLeafletMessage 19 | } from "./models"; 20 | -------------------------------------------------------------------------------- /WebViewLeaflet/models.ts: -------------------------------------------------------------------------------- 1 | import * as ReactLeaflet from "react-leaflet"; 2 | export type LatLng = ReactLeaflet.LatLng; 3 | export type Point = ReactLeaflet.Point; 4 | export type LatLngBounds = ReactLeaflet.LatLngBounds; 5 | 6 | export const OWN_POSTION_MARKER_ID = "OWN_POSTION_MARKER_ID"; 7 | 8 | export enum WebViewLeafletEvents { 9 | MAP_COMPONENT_MOUNTED = "MAP_COMPONENT_MOUNTED", 10 | MAP_READY = "MAP_READY", 11 | DOCUMENT_EVENT_LISTENER_ADDED = "DOCUMENT_EVENT_LISTENER_ADDED", 12 | WINDOW_EVENT_LISTENER_ADDED = "WINDOW_EVENT_LISTENER_ADDED", 13 | UNABLE_TO_ADD_EVENT_LISTENER = "UNABLE_TO_ADD_EVENT_LISTENER", 14 | DOCUMENT_EVENT_LISTENER_REMOVED = "DOCUMENT_EVENT_LISTENER_REMOVED", 15 | WINDOW_EVENT_LISTENER_REMOVED = "WINDOW_EVENT_LISTENER_REMOVED", 16 | ON_MOVE_END = "onMoveEnd", 17 | ON_MOVE_START = "onMoveStart", 18 | ON_MOVE = "onMove", 19 | ON_RESIZE = "onResize", 20 | ON_UNLOAD = "onUnload", 21 | ON_VIEW_RESET = "onViewReset", 22 | ON_ZOOM_END = "onZoomEnd", 23 | ON_ZOOM_LEVELS_CHANGE = "onZoomLevelsChange", 24 | ON_ZOOM_START = "onZoomStart", 25 | ON_ZOOM = "onZoom", 26 | ON_MAP_TOUCHED = "onMapClicked", 27 | ON_MAP_MARKER_CLICKED = "onMapMarkerClicked" 28 | // ON_MAP_SHAPE_CLICKED = "onMapShapeClicked" cannot click on shapes yet 29 | } 30 | 31 | export enum AnimationType { 32 | BOUNCE = "bounce", 33 | FADE = "fade", 34 | PULSE = "pulse", 35 | JUMP = "jump", 36 | SPIN = "spin", 37 | WAGGLE = "waggle" 38 | } 39 | 40 | export enum MapLayerType { 41 | IMAGE_LAYER = "ImageOverlay", 42 | TILE_LAYER = "TileLayer", 43 | VECTOR_LAYER = "VectorLayer", 44 | VIDEO_LAYER = "VideoOverlay", 45 | WMS_TILE_LAYER = "WMSTileLayer" 46 | } 47 | 48 | export enum MapShapeType { 49 | CIRCLE = "Circle", 50 | CIRCLE_MARKER = "CircleMarker", 51 | POLYLINE = "Polyline", 52 | POLYGON = "Polygon", 53 | RECTANGLE = "Rectangle" 54 | } 55 | 56 | export const INFINITE_ANIMATION_ITERATIONS: string = "infinite"; 57 | 58 | export enum AnimationDirection { 59 | NORMAL = "nomal", 60 | REVERSE = "reverse", 61 | ALTERNATE = "alternate", 62 | ALTERNATE_REVERSE = "alternate-reverse" 63 | } 64 | export interface MapMarkerAnimation { 65 | type: AnimationType; 66 | duration?: number; 67 | delay?: number; 68 | direction?: AnimationDirection; 69 | iterationCount?: number | typeof INFINITE_ANIMATION_ITERATIONS; 70 | } 71 | 72 | export interface MapMarker { 73 | animation?: MapMarkerAnimation; 74 | position: LatLng; 75 | divIcon?: L.DivIcon; 76 | icon: any; 77 | iconAnchor?: Point; 78 | id?: string; 79 | size?: Point; 80 | title?: string; 81 | } 82 | 83 | export interface MapEventMessage { 84 | event?: any; 85 | msg?: string; 86 | error?: string; 87 | payload?: any; 88 | } 89 | 90 | export interface MapLayer { 91 | attribution?: string; 92 | baseLayer?: boolean; 93 | baseLayerIsChecked?: boolean; 94 | baseLayerName?: string; 95 | bounds?: LatLngBounds; 96 | id?: string; 97 | layerType?: MapLayerType; 98 | opacity?: number; 99 | pane?: string; 100 | subLayer?: string; 101 | url?: string; 102 | zIndex?: number; 103 | } 104 | 105 | export interface MapShape { 106 | bounds?: LatLng[]; 107 | center?: LatLng; 108 | color?: string; 109 | id?: string; 110 | positions?: LatLng[] | LatLng[][]; 111 | radius?: number; 112 | shapeType: MapShapeType; 113 | } 114 | 115 | export interface MapStartupMessage { 116 | mapLayers?: MapLayer[]; 117 | mapMarkers?: MapMarker[]; 118 | mapShapes?: MapShape[]; 119 | mapCenterPosition?: LatLng; 120 | ownPositionMarker?: OwnPositionMarker; 121 | zoom?: number; 122 | } 123 | 124 | export type WebviewLeafletMessagePayload = { 125 | bounds?: LatLngBounds; 126 | mapCenterPosition: LatLng; 127 | mapMarkerID?: string; 128 | touchLatLng?: LatLng; 129 | zoom?: number; 130 | }; 131 | 132 | export interface WebviewLeafletMessage { 133 | event?: any; 134 | msg?: string; 135 | error?: string; 136 | payload?: WebviewLeafletMessagePayload; 137 | } 138 | 139 | export type OwnPositionMarker = { 140 | animation: MapMarkerAnimation; 141 | id?: string; 142 | icon: string; 143 | position: LatLng; 144 | size: Point; 145 | }; 146 | -------------------------------------------------------------------------------- /WebViewLeaflet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-webview-leaflet", 3 | "version": "5.0.2", 4 | "description": "A React Native component that uses a React Native WebView to provide a Leaflet map.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/reggie3/react-native-webview-leaflet.git" 8 | }, 9 | "keywords": [ 10 | "react-native", 11 | "leaflet", 12 | "expo", 13 | "webview" 14 | ], 15 | "main": "index.ts", 16 | "typings": "index.d.ts", 17 | "scripts": { 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "author": "", 21 | "license": "ISC", 22 | "dependencies": { 23 | "expo-asset": "^7.0.0", 24 | "expo-asset-utils": "^1.2.0", 25 | "expo-file-system": "^8.0.0", 26 | "leaflet": "^1.5.1", 27 | "lodash.isequal": "^4.5.0", 28 | "react-dom": "^16.10.2", 29 | "react-leaflet": "^2.6.1", 30 | "react-native-webview": "^8.0.3" 31 | }, 32 | "peerDependencies": { 33 | "react": "^16.9.0", 34 | "react-leaflet": "^2.6.1", 35 | "react-native": "^0.61.2" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.0.0-0", 39 | "@babel/plugin-proposal-class-properties": "^7.5.5", 40 | "@babel/preset-react": "^7.6.3", 41 | "@types/node": "^13.1.6", 42 | "@types/react-leaflet": "^2.4.0", 43 | "babel-plugin-transform-class-properties": "^6.24.1", 44 | "gulp": "4", 45 | "gulp-run": "^1.7.1", 46 | "parcel-bundler": "^1.12.4", 47 | "parcel-plugin-inliner": "^1.0.10", 48 | "typescript": "^3.6.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reggie3/react-native-webview-leaflet/a27bc89559bc8ab470ae89b0d4faef1a2433ebb0/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reggie3/react-native-webview-leaflet/a27bc89559bc8ab470ae89b0d4faef1a2433ebb0/assets/splash.png -------------------------------------------------------------------------------- /demoApp/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, 3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true 4 | } -------------------------------------------------------------------------------- /demoApp/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | web-report/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /demoApp/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Alert, StyleSheet, Text, View } from "react-native"; 3 | import { 4 | INFINITE_ANIMATION_ITERATIONS, 5 | LatLng, 6 | WebViewLeaflet, 7 | WebViewLeafletEvents, 8 | WebviewLeafletMessage, 9 | AnimationType, 10 | MapShapeType 11 | } from "react-native-webview-leaflet"; 12 | import { mapboxToken } from "./secrets"; 13 | import { Button } from "native-base"; 14 | import lodashSample from "lodash.sample"; 15 | import * as Location from "expo-location"; 16 | import * as Permissions from "expo-permissions"; 17 | 18 | type LatLngObject = { lat: number; lng: number }; 19 | const locations: { icon: string; position: LatLng; name: string }[] = [ 20 | { 21 | icon: "⭐", 22 | position: { lat: 38.895, lng: -77.0366 }, 23 | name: "Washington DC" 24 | }, 25 | { 26 | icon: "🎢", 27 | position: { lat: 37.8399, lng: -77.4442 }, 28 | name: "Kings Dominion" 29 | }, 30 | { 31 | icon: "🎢", 32 | position: { lat: 37.23652, lng: -76.646 }, 33 | name: "Busch Gardens Williamsburg" 34 | }, 35 | { 36 | icon: "⚓", 37 | position: { lat: 36.8477, lng: -76.2951 }, 38 | name: "USS Wisconsin (BB-64)" 39 | }, 40 | { 41 | icon: "🏰", 42 | position: { lat: 28.3852, lng: -81.5639 }, 43 | name: "Walt Disney World" 44 | } 45 | ]; 46 | 47 | const getDuration = (): number => Math.floor(Math.random() * 3) + 1; 48 | const getDelay = (): number => Math.floor(Math.random()) * 0.5; 49 | const iterationCount = "infinite"; 50 | 51 | export default function App() { 52 | const [mapCenterPosition, setMapCenterPosition] = useState({ 53 | lat: 36.850769, 54 | lng: -76.285873 55 | }); 56 | const [ownPosition, setOwnPosition] = useState(null); 57 | const [webViewLeafletRef, setWebViewLeafletRef] = useState(null); 58 | 59 | const onMessageReceived = (message: WebviewLeafletMessage) => { 60 | switch (message.event) { 61 | case WebViewLeafletEvents.ON_MAP_MARKER_CLICKED: 62 | Alert.alert( 63 | `Map Marker Touched, ID: ${message.payload.mapMarkerID || "unknown"}` 64 | ); 65 | 66 | break; 67 | case WebViewLeafletEvents.ON_MAP_TOUCHED: 68 | const position: LatLngObject = message.payload 69 | .touchLatLng as LatLngObject; 70 | Alert.alert(`Map Touched at:`, `${position.lat}, ${position.lng}`); 71 | break; 72 | default: 73 | console.log("App received", message); 74 | } 75 | }; 76 | 77 | useEffect(() => { 78 | getLocationAsync(); 79 | }); 80 | 81 | const getLocationAsync = async () => { 82 | let { status } = await Permissions.askAsync(Permissions.LOCATION); 83 | if (status !== "granted") { 84 | console.warn("Permission to access location was denied"); 85 | } 86 | 87 | let location = await Location.getCurrentPositionAsync({}); 88 | if (!ownPosition) { 89 | setOwnPosition({ 90 | lat: location.coords.latitude, 91 | lng: location.coords.longitude 92 | }); 93 | } 94 | }; 95 | 96 | return ( 97 | 98 | 99 | React Native Webview Leaflet Demo 100 | 101 | 102 | { 103 | { 105 | setWebViewLeafletRef(ref); 106 | }} 107 | backgroundColor={"green"} 108 | onMessageReceived={onMessageReceived} 109 | mapLayers={[ 110 | { 111 | attribution: 112 | '&copy OpenStreetMap contributors', 113 | baseLayerIsChecked: true, 114 | baseLayerName: "OpenStreetMap.Mapnik", 115 | url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" 116 | }, 117 | { 118 | baseLayerName: "Mapbox", 119 | //url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 120 | url: `https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}.png?access_token=${mapboxToken}`, 121 | attribution: 122 | "&copy OpenStreetMap contributors" 123 | } 124 | ]} 125 | mapMarkers={[ 126 | ...locations.map(location => { 127 | return { 128 | id: location.name.replace(" ", "-"), 129 | position: location.position, 130 | icon: location.icon, 131 | animation: { 132 | duration: getDuration(), 133 | delay: getDelay(), 134 | iterationCount: INFINITE_ANIMATION_ITERATIONS, 135 | type: lodashSample( 136 | Object.values(AnimationType) 137 | ) as AnimationType 138 | } 139 | }; 140 | }), 141 | { 142 | id: "1", 143 | position: { lat: 36.46410354, lng: -75.6432701 }, 144 | icon: 145 | "https://www.catster.com/wp-content/uploads/2018/07/Savannah-cat-long-body-shot.jpg", 146 | size: [32, 32], 147 | animation: { 148 | duration: getDuration(), 149 | delay: getDelay(), 150 | iterationCount, 151 | type: AnimationType.BOUNCE 152 | } 153 | } 154 | ]} 155 | mapShapes={[ 156 | { 157 | shapeType: MapShapeType.CIRCLE, 158 | color: "#123123", 159 | id: "1", 160 | center: { lat: 34.225727, lng: -77.94471 }, 161 | radius: 2000 162 | }, 163 | { 164 | shapeType: MapShapeType.CIRCLE_MARKER, 165 | color: "red", 166 | id: "2", 167 | center: { lat: 38.437424, lng: -78.867912 }, 168 | radius: 15 169 | }, 170 | { 171 | shapeType: MapShapeType.POLYGON, 172 | color: "blue", 173 | id: "3", 174 | positions: [ 175 | { lat: 38.80118939192329, lng: -74.69604492187501 }, 176 | { lat: 38.19502155795575, lng: -74.65209960937501 }, 177 | { lat: 39.07890809706475, lng: -71.46606445312501 } 178 | ] 179 | }, 180 | { 181 | shapeType: MapShapeType.POLYGON, 182 | color: "violet", 183 | id: "4", 184 | positions: [ 185 | [ 186 | { lat: 37.13842453422676, lng: -74.28955078125001 }, 187 | { lat: 36.4433803110554, lng: -74.26208496093751 }, 188 | { lat: 36.43896124085948, lng: -73.00964355468751 }, 189 | { lat: 36.43896124085948, lng: -73.00964355468751 } 190 | ], 191 | [ 192 | { lat: 37.505368263398104, lng: -72.38891601562501 }, 193 | { lat: 37.309014074275915, lng: -71.96594238281251 }, 194 | { lat: 36.69044623523481, lng: -71.87805175781251 }, 195 | { lat: 36.58024660149866, lng: -72.75146484375001 }, 196 | { lat: 37.36579146999664, lng: -72.88330078125001 } 197 | ] 198 | ] 199 | }, 200 | { 201 | shapeType: MapShapeType.POLYLINE, 202 | color: "orange", 203 | id: "5", 204 | positions: [ 205 | { lat: 35.411438052435486, lng: -78.67858886718751 }, 206 | { lat: 35.9602229692967, lng: -79.18945312500001 }, 207 | { lat: 35.97356075349624, lng: -78.30505371093751 } 208 | ] 209 | }, 210 | { 211 | shapeType: MapShapeType.POLYLINE, 212 | color: "purple", 213 | id: "5a", 214 | positions: [ 215 | [ 216 | { lat: 36.36822190085111, lng: -79.26086425781251 }, 217 | { lat: 36.659606226479696, lng: -79.28833007812501 }, 218 | { lat: 36.721273880045004, lng: -79.81018066406251 } 219 | ], 220 | [ 221 | { lat: 35.43381992014202, lng: -79.79370117187501 }, 222 | { lat: 35.44277092585766, lng: -81.23840332031251 }, 223 | { lat: 35.007502842952896, lng: -80.837402343750017 } 224 | ] 225 | ] 226 | }, 227 | { 228 | shapeType: MapShapeType.RECTANGLE, 229 | color: "yellow", 230 | id: "6", 231 | bounds: [ 232 | { lat: 36.5, lng: -75.7 }, 233 | { lat: 38.01, lng: -73.13 } 234 | ] 235 | } 236 | ]} 237 | mapCenterPosition={mapCenterPosition} 238 | ownPositionMarker={ 239 | ownPosition && { 240 | position: ownPosition, 241 | icon: "❤️", 242 | size: [32, 32], 243 | animation: { 244 | duration: getDuration(), 245 | delay: getDelay(), 246 | iterationCount, 247 | type: AnimationType.BOUNCE 248 | } 249 | } 250 | } 251 | zoom={7} 252 | /> 253 | } 254 | 255 | 256 | {locations.map(location => { 257 | return ( 258 | 268 | ); 269 | })} 270 | 282 | 283 | 284 | ); 285 | } 286 | 287 | const styles = StyleSheet.create({ 288 | container: { 289 | flex: 1, 290 | backgroundColor: "#fff" 291 | }, 292 | header: { 293 | height: 60, 294 | backgroundColor: "dodgerblue", 295 | paddingHorizontal: 10, 296 | paddingTop: 30, 297 | width: "100%" 298 | }, 299 | headerText: { 300 | color: "white", 301 | fontSize: 18, 302 | fontWeight: "600" 303 | }, 304 | mapControls: { 305 | backgroundColor: "rgba(255,255,255,.5)", 306 | borderRadius: 5, 307 | bottom: 25, 308 | flexDirection: "row", 309 | justifyContent: "space-between", 310 | left: 0, 311 | marginHorizontal: 10, 312 | padding: 7, 313 | position: "absolute", 314 | right: 0 315 | }, 316 | mapButton: { 317 | alignItems: "center", 318 | height: 42, 319 | justifyContent: "center", 320 | width: 42 321 | }, 322 | mapButtonEmoji: { 323 | fontSize: 28 324 | } 325 | }); 326 | -------------------------------------------------------------------------------- /demoApp/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-webview-leaflet-demo", 4 | "slug": "demoApp", 5 | "privacy": "public", 6 | "sdkVersion": "36.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /demoApp/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reggie3/react-native-webview-leaflet/a27bc89559bc8ab470ae89b0d4faef1a2433ebb0/demoApp/assets/icon.png -------------------------------------------------------------------------------- /demoApp/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reggie3/react-native-webview-leaflet/a27bc89559bc8ab470ae89b0d4faef1a2433ebb0/demoApp/assets/splash.png -------------------------------------------------------------------------------- /demoApp/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /demoApp/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | const clean = require("gulp-clean"); 3 | const run = require("gulp-run"); 4 | 5 | gulp.task("clean", function() { 6 | return gulp 7 | .src("node_modules", { allowEmpty: true, read: false }) 8 | .pipe(clean()); 9 | }); 10 | -------------------------------------------------------------------------------- /demoApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject", 9 | "cleanNodeModules": "del-cli --force ./node_modules", 10 | "clean": "yarn upgrade react-native-webview-leaflet" 11 | }, 12 | "dependencies": { 13 | "expo": "~36.0.0", 14 | "expo-location": "^8.0.0", 15 | "expo-permissions": "^8.0.0", 16 | "lodash.sample": "^4.2.1", 17 | "native-base": "^2.13.8", 18 | "react": "~16.9.0", 19 | "react-dom": "~16.9.0", 20 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz", 21 | "react-native-screens": "2.0.0-alpha.12", 22 | "react-native-web": "~0.11.7", 23 | "react-native-webview-leaflet": "file:../WebViewLeaflet" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.0.0", 27 | "@types/react": "~16.9.0", 28 | "@types/react-native": "~0.60.23", 29 | "babel-preset-expo": "~8.0.0", 30 | "del-cli": "^3.0.0", 31 | "gulp-clean": "^0.4.0", 32 | "gulp-run": "^1.7.1", 33 | "typescript": "~3.6.3" 34 | }, 35 | "private": true 36 | } 37 | -------------------------------------------------------------------------------- /demoApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "jsx": "react-native", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "gulp": "^4.0.2", 4 | "gulp-run": "^1.7.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # React Native Webview Leaflet V5 2 | 3 | ## A Leaflet map component with no native code for React Native applications 4 | 5 | ### Why Use This Library 6 | 7 | This component is useful if you want to display HTML elements on an interactive map. Since the elements are standard HTML items, they can be SVG's, emojis, text, images, etc. 8 | 9 | Additionally, the elements can even be animated, updated, and changed as required. 10 | 11 | ### Why Not Use This Library 12 | 13 | You may not want to use this library if you'd rather use Google map tiles and data vice the tiles and map data from Open Street Maps. 14 | 15 | [![npm](https://img.shields.io/npm/v/react-native-webview-leaflet.svg)](https://www.npmjs.com/package/react-native-webview-leaflet) 16 | [![npm](https://img.shields.io/npm/dm/react-native-webview-leaflet.svg)](https://www.npmjs.com/package/react-native-webview-leaflet) 17 | [![npm](https://img.shields.io/npm/dt/react-native-webview-leaflet.svg)](https://www.npmjs.com/package/react-native-webview-leaflet) 18 | [![npm](https://img.shields.io/npm/l/react-native-webview-leaflet.svg)](https://github.com/react-native-component/react-native-webview-leaflet/blob/master/LICENSE) 19 | 20 | [![Alt text](https://img.youtube.com/vi/Jpo-Mg3BSVk/0.jpg)](https://www.youtube.com/watch?v=Jpo-Mg3BSVk) 21 | 22 | ## Installation 23 | 24 | Install using npm or yarn like this: 25 | 26 | ```javascript 27 | npm install --save react-native-webview-leaflet 28 | ``` 29 | 30 | or 31 | 32 | ```javascript 33 | yarn add react-native-webview-leaflet 34 | ``` 35 | 36 | ## Usage 37 | 38 | and import like so 39 | 40 | ```javascript 41 | import WebViewLeaflet from "react-native-webview-leaflet"; 42 | ``` 43 | 44 | A typical example is shown below: 45 | 46 | ```javascript 47 | (this.webViewLeaflet = component)} 49 | // The rest of your props, see the list below 50 | /> 51 | ``` 52 | 53 | ## Props 54 | 55 | | property | required | type | purpose | 56 | | ------------------- | -------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 57 | | backgroundColor | optional | string | Color seen prior to the map loading | 58 | | doShowDebugMessages | optional | boolean | show debug information from the component containing the Leaflet map | 59 | | loadingIndicator | optional | React.ReactElement | custom component displayed while the map is loading | 60 | | onError | optional | function | Will receive an error event | 61 | | onLoadEnd | optional | function | Called when map stops loading | 62 | | onLoadStart | optional | function | Called when the map starts to load | 63 | | onMessageReceived | required | function | This function receives messages in the form of a WebviewLeafletMessage object from the map | 64 | | mapLayers | optional | MapLayer array | An array of map layers | 65 | | mapMarkers | optional | MapMarker array | An array of map markers | 66 | | mapShapes | optional | MapShape[] | An array of map shapes | 67 | | mapCenterPosition | optional | {lat: [Lat], lng: [Lng]} object | The center position of the map. This coordinate will not be accurate if the map has been moved manually. However, calling the map's setMapCenterPosition function will cause the map to revert to this location | 68 | | ownPositionMarker | optional | Marker | A special marker that has an ID of OWN_POSTION_MARKER_ID | | 69 | | zoom | optional | number | Desired zoom value of the map | 70 | 71 | ### Example Marker 72 | 73 | ```javascript 74 | ownPositionMarker={{ 75 | id: '1', 76 | coords: {lat: 36.00, lng, -76.00}, 77 | icon: "❤️", 78 | size: [24, 24], 79 | animation: { 80 | name: AnimationType.BOUNCE, 81 | duration: ".5", 82 | delay: 0, 83 | interationCount: INFINITE_ANIMATION_ITERATIONS 84 | } 85 | }} 86 | ``` 87 | 88 | After loading, the map expects to receive an array of map layer information objects. A sample object showing a [MapBox](https://www.mapbox.com/) tile layer is shown below. 89 | 90 | ```javascript 91 | { 92 | baseLayerName: 'OpenStreetMap', // the name of the layer, this will be seen in the layer selection control 93 | baseLayerIsChecked: 'true', // if the layer is selected in the layer selection control 94 | layerType: 'TileLayer', // Optional: a MapLayerType enum specifying the type of layer see "Types of Layers" below. Defaults to TILE_LAYER 95 | baseLayer: true, 96 | // url of tiles 97 | url: `https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}.png?access_token=${mapboxToken}`, 98 | // attribution string to be shown for this layer 99 | attribution: 100 | '&copy OpenStreetMap contributors' 101 | } 102 | ``` 103 | 104 | ### Types of Layers 105 | 106 | ```javascript 107 | export enum MapLayerType { 108 | IMAGE_LAYER = "ImageOverlay", 109 | TILE_LAYER = "TileLayer", 110 | VECTOR_LAYER = "VectorLayer", 111 | VIDEO_LAYER = "VideoOverlay", 112 | WMS_TILE_LAYER = "WMSTileLayer" 113 | } 114 | ``` 115 | 116 | ### Communicating with the map 117 | 118 | #### Listening for Events 119 | 120 | This library supports map clicked, map marker clicked, and the map events that are exposed by Leaflet. 121 | 122 | ##### Leaflet Map Events 123 | 124 | The following Map Events are passed to the function designated by the onMessageReceived prop. 125 | 126 | ```javascript 127 | export enum WebViewLeafletEvents { 128 | MAP_COMPONENT_MOUNTED = "MAP_COMPONENT_MOUNTED", 129 | MAP_READY = "MAP_READY", 130 | DOCUMENT_EVENT_LISTENER_ADDED = "DOCUMENT_EVENT_LISTENER_ADDED", 131 | WINDOW_EVENT_LISTENER_ADDED = "WINDOW_EVENT_LISTENER_ADDED", 132 | UNABLE_TO_ADD_EVENT_LISTENER = "UNABLE_TO_ADD_EVENT_LISTENER", 133 | DOCUMENT_EVENT_LISTENER_REMOVED = "DOCUMENT_EVENT_LISTENER_REMOVED", 134 | WINDOW_EVENT_LISTENER_REMOVED = "WINDOW_EVENT_LISTENER_REMOVED", 135 | ON_MOVE_END = "onMoveEnd", 136 | ON_MOVE_START = "onMoveStart", 137 | ON_MOVE = "onMove", 138 | ON_RESIZE = "onResize", 139 | ON_UNLOAD = "onUnload", 140 | ON_VIEW_RESET = "onViewReset", 141 | ON_ZOOM_END = "onZoomEnd", 142 | ON_ZOOM_LEVELS_CHANGE = "onZoomLevelsChange", 143 | ON_ZOOM_START = "onZoomStart", 144 | ON_ZOOM = "onZoom", 145 | ON_MAP_TOUCHED = "onMapClicked", 146 | ON_MAP_MARKER_CLICKED = "onMapMarkerClicked" 147 | } 148 | ``` 149 | 150 | Events prefixed with "ON" will receive the below object containing information about the map 151 | 152 | ```javascript 153 | { 154 | center, // center of the map 155 | bounds, // the bounds of the map 156 | zoom; // the zoom level of the map 157 | } 158 | ``` 159 | 160 | ### Creating Map Markers 161 | 162 | ```javascript 163 | { 164 | id: uuidV1(), // The ID attached to the marker. It will be returned when onMarkerClicked is called 165 | position: {lat: [LATITTUDE], lng: [LONGITUDE]}, // Latitude and Longitude of the marker 166 | icon: '🍇', // HTML element that will be displayed as the marker. It can also be text or an SVG string. 167 | size: [32, 32], 168 | animation: { 169 | duration: getDuration(), 170 | delay: getDelay(), 171 | iterationCount, 172 | type: AnimationType.BOUNCE 173 | } 174 | } 175 | ``` 176 | 177 | ### Adding Leaflet Geometry Layers to the Map 178 | 179 | Thanks to @gotoglup for the PR adding leaflet geometry layers. A geometry layer can be added to the may by following the example below: 180 | 181 | ```javascript 182 | mapShapes={[ 183 | { 184 | shapeType: MapShapeType.CIRCLE, 185 | color: "#123123", 186 | id: "1", 187 | center: { lat: 34.225727, lng: -77.94471 }, 188 | radius: 2000 189 | } 190 | ]} 191 | ``` 192 | 193 | ### Available Animations 194 | 195 | Marker animations can be specified by setting the animation.type of the marker object to an AnimationType enum. 196 | Values for AnimationType can be found in the models.ts file in the WebViewLeaflet directory of this project. 197 | 198 | ### Animation Information 199 | 200 | Animations are kept in the file [markers.css](https://github.com/reggie3/react-native-webview-leaflet/blob/master/web/markers.css) They are just keyframe animations like this: 201 | 202 | ```javascript 203 | @keyframes spin { 204 | 50% { 205 | transform: rotateZ(-20deg); 206 | animation-timing-function: ease; 207 | } 208 | 100% { 209 | transform: rotateZ(360deg); 210 | } 211 | } 212 | ``` 213 | 214 | ``` 215 | ## Changelog 216 | 217 | ### 4.5.0 218 | 219 | * Removed Expo dependencies from the library and added polygons vectors (Thanks @gutoglup - https://github.com/gutoglup) 220 | 221 | ### 4.3.1 222 | 223 | * Fixed issue with using expo-asset-utils that prevented this package from working with iOS apps in simulator 224 | 225 | ### 4.2.0 226 | 227 | * Replace Expo dependency with expo-asset-utils 228 | * Fixed bug that caused map to not display when no ownPositionMarker was provided 229 | 230 | ### 4.1.15 231 | 232 | * Keep own position marker from being clustered 233 | 234 | ### 4.1.0 235 | 236 | * Added optional marker clustering using react-leaflet-markercluster 237 | * Update preview video in readme 238 | 239 | ### 4.0.0 240 | 241 | * Fixed map centering, and map centering button (see issue #36(https://github.com/reggie3/react-native-webview-leaflet/issues/36) ) 242 | #### BREAKING CHANGES: 243 | * Center map on own current location button defaults to not being shown 244 | * Center map on own current location button requires that a "ownPositionMarker" object be passed to the WebViewLeaflet component 245 | * WebViewLeaflet component requires a "centerPosition" prop for initial centering 246 | * Map tile Layers are now passed as props to the WebViewLeaflet component 247 | 248 | 249 | ### 3.1.45 250 | 251 | * Works in production APK files. 252 | * Renders http images as map icons. 253 | 254 | ### 3.0.0 255 | 256 | * Introduced user specified tile layers. 257 | 258 | ### 2.0.0 259 | 260 | * Initial release of version 2 built on React-Leaflet 261 | 262 | ### 5.0.0 263 | 264 | * Add TypeScript support 265 | * Switch to react-native-community/react-native-webview implementation 266 | * Add ability to draw shapes on the map (Leaflet vector layers) 267 | * Display map layer vector icons 268 | * Simplify event communication 269 | 270 | ## LICENSE 271 | 272 | MIT 273 | ``` 274 | -------------------------------------------------------------------------------- /secrets.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.mapboxToken = 'pk.eyJ1Ijoid2hlcmVzbXl3YXZlcyIsImEiOiJjanJ6cGZtd24xYmU0M3lxcmVhMDR2dWlqIn0.QQSWbd-riqn1U5ppmyQjRw'; 4 | --------------------------------------------------------------------------------