├── .nvmrc ├── src ├── index.tsx ├── components │ ├── index.ts │ ├── libs │ │ ├── here-map-js.d.ts │ │ ├── validateMapType.ts │ │ ├── initInteractionStyles.ts │ │ ├── initPlatform.ts │ │ ├── initInteraction.ts │ │ ├── initMapObjectEvents.ts │ │ ├── loadHMap.ts │ │ ├── defaults.ts │ │ └── buildMap.ts │ ├── Map │ │ ├── index.tsx │ │ ├── objects │ │ │ ├── index.ts │ │ │ ├── Marker │ │ │ │ └── index.tsx │ │ │ ├── Rectangle │ │ │ │ └── index.tsx │ │ │ ├── Circle │ │ │ │ └── index.tsx │ │ │ ├── Layer │ │ │ │ └── index.tsx │ │ │ ├── Polyline │ │ │ │ └── index.tsx │ │ │ ├── BaseMapObject.tsx │ │ │ └── Polygon │ │ │ │ └── index.tsx │ │ └── Map.tsx │ └── Platform │ │ └── index.tsx └── contexts │ ├── platform.tsx │ └── map.tsx ├── .gitignore ├── .storybook ├── preview.js └── main.js ├── .babelrc.json ├── CONTRIBUTING.MD ├── tsdx.config.js ├── .github └── workflows │ ├── lint.yml │ └── main.yml ├── LICENSE ├── stories ├── Platform.stories.tsx ├── useHPlatform.stories.tsx ├── Map.stories.tsx ├── Layer.stories.tsx ├── Polygon.stories.tsx ├── Rectangle.stories.tsx ├── Polyline.stories.tsx ├── Circle.stories.tsx └── Marker.stories.tsx ├── tsconfig.json ├── package.json └── README.MD /.nvmrc: -------------------------------------------------------------------------------- 1 | node -v 2 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Map'; 2 | export * from './Platform'; 3 | -------------------------------------------------------------------------------- /src/components/libs/here-map-js.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@limistah/here-map-js'; 2 | -------------------------------------------------------------------------------- /src/components/Map/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Map'; 2 | export * from './objects'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | storybook-static 7 | .rough/ -------------------------------------------------------------------------------- /src/components/Map/objects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Polyline'; 2 | export * from './Rectangle'; 3 | export * from './Polygon'; 4 | export * from './Marker'; 5 | export * from './Circle'; 6 | export * from './Layer'; 7 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters 2 | export const parameters = { 3 | // https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args 4 | actions: { argTypesRegex: '^on.*' }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/contexts/platform.tsx: -------------------------------------------------------------------------------- 1 | // provider a provider 2 | // provider a consumer 3 | 4 | import { createContext } from 'react'; 5 | import { IHPlatformState } from '../components/Platform'; 6 | 7 | // create a hook for the platform context 8 | export const PlatformContext = createContext({ 9 | platform: null, 10 | }); 11 | -------------------------------------------------------------------------------- /src/contexts/map.tsx: -------------------------------------------------------------------------------- 1 | // provider a provider 2 | // provider a consumer 3 | 4 | import { createContext } from 'react'; 5 | import { IHMapState } from '../components/Map'; 6 | 7 | // create a hook for the platform context 8 | export const MapContext = createContext({ 9 | map: null, 10 | ui: null, 11 | interaction: null, 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/libs/validateMapType.ts: -------------------------------------------------------------------------------- 1 | import { MAP_TYPES, mapTypes } from './defaults'; 2 | 3 | export const validateMapType = (mapType: MAP_TYPES) => { 4 | if (!mapTypes.includes(mapType)) { 5 | throw new Error( 6 | 'mapType Should be one from https://developer.here.com/documentation/maps/topics/map-types.html in dot notation' 7 | ); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | ["@babel/preset-typescript", {}], 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "chrome": 100, 10 | "safari": 15, 11 | "firefox": 91 12 | } 13 | } 14 | ] 15 | ], 16 | "plugins": ["@babel/plugin-proposal-optional-chaining"] 17 | } 18 | -------------------------------------------------------------------------------- /src/components/libs/initInteractionStyles.ts: -------------------------------------------------------------------------------- 1 | export const initInteractionStyles = () => { 2 | const style = document.createElement('style'); 3 | const css = `.grab = {cursor: move;cursor: grab;cursor: -moz-grab;cursor: -webkit-grab;}.grabbing{cursor:grabbing;cursor:-moz-grabbing;cursor:-webkit-grabbing}`; 4 | style.type = 'text/css'; 5 | style.appendChild(document.createTextNode(css)); 6 | const head = document.head || document.getElementsByTagName('head')[0]; 7 | head.appendChild(style); 8 | }; 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | 2 | This repository uses TSDX by Jared Palmer, it follows the same convention. 3 | 4 | Contribution guidelines: 5 | - Fork the repository 6 | - Add make a change 7 | - Add example to illustrate the result of your change 8 | - Please include test 9 | - Submit a PR to this repo, and ask for a review. 10 | - Wait for your changes to be merged. 11 | 12 | 13 | Possible things to do: 14 | - Add more examples 15 | - Include more storybook examples 16 | - HereMaps has a new feature? Add it in! 17 | - Rewrite for performance optimizations. 18 | 19 | -------------------------------------------------------------------------------- /tsdx.config.js: -------------------------------------------------------------------------------- 1 | // Not transpiled with TypeScript or Babel, so use plain Es6/Node.js! 2 | const replace = require('@rollup/plugin-replace'); 3 | 4 | module.exports = { 5 | // This function will run for each entry/format/env combination 6 | rollup(config, opts) { 7 | config.plugins = config.plugins.map(p => 8 | p.name === 'replace' 9 | ? replace({ 10 | 'process.env.NODE_ENV': JSON.stringify(opts.env), 11 | preventAssignment: true, 12 | }) 13 | : p 14 | ); 15 | return config; // always return a config. 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/libs/initPlatform.ts: -------------------------------------------------------------------------------- 1 | import { DefaultOptionsType } from './defaults'; 2 | 3 | export const initHPlatform = (options?: DefaultOptionsType) => { 4 | const { app_id, app_code, apikey } = options || {}; 5 | if ((!app_id || !app_code) && !apikey) { 6 | throw new Error('Options must include appId and appCode OR an apiKey'); 7 | } 8 | // @ts-ignore 9 | const h = window.H; 10 | if (typeof h === 'undefined' || !h.service) { 11 | throw new Error('Here Map JavaScript files are not loaded.'); 12 | } 13 | return new h.service.Platform({ apikey: String(apikey) }); 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Test and Lint the changes 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build-and-deploy: 8 | name: Build, lint, and test on Node 14.x 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v2 15 | 16 | - name: Install Node 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 18.x 20 | 21 | - name: Install deps and build (with cache) 22 | uses: bahmutov/npm-install@v1 23 | 24 | - name: Lint 25 | run: yarn lint 26 | 27 | - name: Test 28 | run: yarn test --ci --coverage --maxWorkers=2 29 | -------------------------------------------------------------------------------- /src/components/libs/initInteraction.ts: -------------------------------------------------------------------------------- 1 | import { mapEventTypes, mapEvents } from './defaults'; 2 | 3 | export const initInteraction = ( 4 | map: H.Map, 5 | interactive: boolean, 6 | useEvents: boolean, 7 | events: typeof mapEvents 8 | ) => { 9 | let behavior = interactive 10 | ? new H.mapevents.Behavior(new H.mapevents.MapEvents(map)) 11 | : null; 12 | if (useEvents && interactive) { 13 | for (const type in events) { 14 | if (events.hasOwnProperty(type)) { 15 | const callback = events[type as mapEventTypes]; 16 | if (callback && typeof callback === 'function') { 17 | map.addEventListener(type, callback); 18 | } 19 | } 20 | } 21 | } 22 | return behavior; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/libs/initMapObjectEvents.ts: -------------------------------------------------------------------------------- 1 | import { DefaultOptionsType, mapEventTypes, mapEvents } from './defaults'; 2 | 3 | export const initMapObjectEvents = ( 4 | mapObject: H.map.Object, 5 | objectEvents: typeof mapEvents, 6 | platformOptions: Pick 7 | ) => { 8 | const { useEvents, interactive } = platformOptions; 9 | if (useEvents && interactive && objectEvents) { 10 | for (const type in objectEvents) { 11 | if (objectEvents.hasOwnProperty(type)) { 12 | const callback = objectEvents[type as mapEventTypes]; 13 | if (callback && typeof callback === 'function') { 14 | mapObject.addEventListener(type, callback); 15 | } 16 | } 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'], 3 | addons: [ 4 | '@storybook/addon-links', 5 | '@storybook/addon-essentials', 6 | '@storybook/addon-docs', 7 | ], 8 | 9 | // https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration 10 | typescript: { 11 | // check: true, // type-check stories during Storybook build 12 | reactDocgen: 'react-docgen-typescript', 13 | reactDocgenTypescriptOptions: { 14 | compilerOptions: { 15 | allowSyntheticDefaultImports: false, 16 | esModuleInterop: false, 17 | }, 18 | propFilter: () => true, 19 | }, 20 | }, 21 | 22 | framework: { 23 | name: '@storybook/react-webpack5', 24 | options: {}, 25 | }, 26 | 27 | docs: { 28 | autodocs: true, 29 | defaultName: 'Documentation', 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deploy the changes 2 | on: 3 | push: 4 | branches: 5 | - 2_0 6 | 7 | jobs: 8 | build-and-deploy: 9 | name: Build, lint, and test on Node 14.x 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Install Node 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 18.x 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Build 26 | run: yarn build && yarn build-storybook 27 | - uses: JS-DevTools/npm-publish@v2 28 | with: 29 | token: ${{ secrets.NPM_TOKEN }} 30 | 31 | - name: Deploy storybook 🚀 32 | uses: JamesIves/github-pages-deploy-action@v4 33 | with: 34 | folder: storybook-static # The folder the action should deploy. 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aleem Isiaka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /stories/Platform.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HPlatform, IHPlatform } from '../src/'; 4 | const appId = 'EF8K24SYpkpXUO9rkbfA'; 5 | const apiKey = 'TIAGlD6jic7l9Aa8Of8IFxo3EUemmcZlHm_agfAm6Ew'; 6 | const meta: Meta = { 7 | title: 'HPlatform JSX', 8 | component: HPlatform, 9 | args: { 10 | options: { 11 | appId, 12 | apiKey, 13 | includeUI: true, 14 | includePlaces: false, 15 | version: 'v3/3.1', 16 | interactive: true, 17 | }, 18 | children: <>Overriden Children If all went well, yaay! 🙂 , 19 | }, 20 | }; 21 | 22 | export default meta; 23 | 24 | const Template: Story = args => { 25 | return ( 26 | 27 | {args.children || 'Render Anything'} 28 | 29 | ); 30 | }; 31 | 32 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 33 | // https://storybook.js.org/docs/react/workflows/unit-testing 34 | export const Default = Template.bind({}); 35 | 36 | Default.args = {}; 37 | -------------------------------------------------------------------------------- /src/components/Map/objects/Marker/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { mapEvents } from '../../../libs/defaults'; 3 | import { BaseMapObject } from '../BaseMapObject'; 4 | import React from 'react'; 5 | 6 | export interface IHMapMarkerProps { 7 | coords: H.geo.IPoint; 8 | options?: H.map.Marker.Options; 9 | setViewBounds: boolean; 10 | events: typeof mapEvents; 11 | icon?: string | HTMLImageElement | HTMLCanvasElement; 12 | } 13 | 14 | export const HMapMarker = (props: IHMapMarkerProps) => { 15 | if (!props.coords.lng || !props.coords.lat) { 16 | throw new Error( 17 | "coords should be an object having 'lat' and 'lng' as props" 18 | ); 19 | } 20 | const initFn = useCallback(() => { 21 | const { coords, options, icon } = props; 22 | const _options: H.map.Marker.Options | undefined = { ...options }; 23 | if (icon) _options.icon = new H.map.Icon(icon); 24 | return new H.map.Marker(coords, _options); 25 | }, [props.coords]); 26 | 27 | return ( 28 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/Map/objects/Rectangle/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { mapEvents } from '../../../libs/defaults'; 3 | import { BaseMapObject } from '../BaseMapObject'; 4 | 5 | export interface IHMapRectangleProps { 6 | points: number[]; 7 | options?: H.map.Spatial.Options; 8 | setViewBounds: boolean; 9 | events: typeof mapEvents; 10 | } 11 | 12 | export const HMapRectangle = (props: IHMapRectangleProps) => { 13 | if (!Array.isArray(props.points)) { 14 | throw new Error( 15 | 'points should be an array of objects containing lat and lng properties' 16 | ); 17 | } 18 | const initFn = useCallback(() => { 19 | const { points, options } = props; 20 | // Get a bounding box 21 | const boundingBox = new H.geo.Rect( 22 | points[0], 23 | points[1], 24 | points[2], 25 | points[3] 26 | ); 27 | // Initialize a LineString and add all the points to it: 28 | return new H.map.Rect(boundingBox, options); 29 | }, [props.points]); 30 | 31 | return ( 32 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/Map/objects/Circle/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { mapEvents } from '../../../libs/defaults'; 3 | import { BaseMapObject } from '../BaseMapObject'; 4 | import React from 'react'; 5 | 6 | export interface IHMapCircleProps { 7 | coords: H.geo.IPoint; 8 | options?: H.map.Circle.Options; 9 | setViewBounds: boolean; 10 | events: typeof mapEvents; 11 | radius: number; 12 | zoom?: number; 13 | } 14 | 15 | export const HMapCircle = (props: IHMapCircleProps) => { 16 | const { coords, options, radius, events, zoom } = props; 17 | 18 | if (!coords.lng || !coords.lat) { 19 | throw new Error( 20 | "coords should be an object having 'lat' and 'lng' as props" 21 | ); 22 | } 23 | 24 | if (!props.radius) { 25 | console.info('radius is not set, default radius of 1000 is used'); 26 | } 27 | const initFn = useCallback(() => { 28 | const circle = new H.map.Circle(coords, radius || 1000, options); 29 | return circle; 30 | }, [coords]); 31 | 32 | return ( 33 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/Map/objects/Layer/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from 'react'; 2 | import { MAP_TYPES } from '../../../libs/defaults'; 3 | import { MapContext } from '../../../../contexts/map'; 4 | import { validateMapType } from '../../../libs/validateMapType'; 5 | import * as dotProp from 'dot-prop'; 6 | import { PlatformContext } from '../../../../contexts/platform'; 7 | 8 | export interface IHMapLayerProps { 9 | type: MAP_TYPES; 10 | } 11 | 12 | export const HMapLayer = (props: IHMapLayerProps) => { 13 | const { type } = props; 14 | const mapContext = useContext(MapContext); 15 | const platformContext = useContext(PlatformContext); 16 | if (!mapContext.map) { 17 | throw new Error('A map Object must be a child of '); 18 | } 19 | validateMapType(type); 20 | 21 | useEffect(() => { 22 | const defaultLayers = platformContext.platform?.createDefaultLayers(); 23 | const mapLayer = dotProp.getProperty(defaultLayers, type); 24 | if (mapLayer) { 25 | mapContext.map?.addLayer((mapLayer as unknown) as H.map.layer.Layer); 26 | } else { 27 | console.error(type, ' is not supported as a layer.'); 28 | } 29 | }, [type]); 30 | 31 | return null; 32 | }; 33 | -------------------------------------------------------------------------------- /stories/useHPlatform.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HPlatform, IHPlatform, useHPlatform } from '../src'; 4 | import { ILoadHMapOptions } from '../src/components/libs/loadHMap'; 5 | const appId = 'EF8K24SYpkpXUO9rkbfA'; 6 | const apiKey = 'TIAGlD6jic7l9Aa8Of8IFxo3EUemmcZlHm_agfAm6Ew'; 7 | const meta: Meta = { 8 | title: 'useHPlatform', 9 | component: HPlatform, 10 | args: { 11 | options: { 12 | appId, 13 | apiKey, 14 | includeUI: true, 15 | includePlaces: false, 16 | version: 'v3/3.1', 17 | interactive: true, 18 | }, 19 | children: <>Overriden Children If all went well, yaay! 🙂 , 20 | }, 21 | }; 22 | 23 | export default meta; 24 | 25 | const Template: Story = args => { 26 | return useHPlatform( 27 | { ...args }, 28 | <>Render Some Children Here, if Platform did load successfully 29 | ); 30 | }; 31 | 32 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 33 | // https://storybook.js.org/docs/react/workflows/unit-testing 34 | export const Default = Template.bind({}); 35 | 36 | Default.args = {}; 37 | -------------------------------------------------------------------------------- /stories/Map.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HMap, IHMapProps, useHPlatform } from '../src'; 4 | const appId = 'EF8K24SYpkpXUO9rkbfA'; 5 | const apiKey = 'TIAGlD6jic7l9Aa8Of8IFxo3EUemmcZlHm_agfAm6Ew'; 6 | const meta: Meta = { 7 | title: 'HMap', 8 | component: HMap, 9 | argTypes: {}, 10 | args: { 11 | options: { 12 | center: { lat: 52.5321472, lng: 13.3935785 }, 13 | }, 14 | style: { 15 | height: '480px', 16 | width: '100%', 17 | }, 18 | useEvents: true, 19 | }, 20 | }; 21 | 22 | export default meta; 23 | 24 | const Template: Story = args => { 25 | const renderHMapComponents = useHPlatform( 26 | { 27 | appId, 28 | apiKey, 29 | includeUI: true, 30 | includePlaces: false, 31 | version: 'v3/3.1', 32 | interactive: true, 33 | }, 34 | 35 | ); 36 | return renderHMapComponents; 37 | }; 38 | 39 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 40 | // https://storybook.js.org/docs/react/workflows/unit-testing 41 | export const Default = Template.bind({}); 42 | 43 | Default.args = {}; 44 | -------------------------------------------------------------------------------- /src/components/Map/objects/Polyline/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { mapEvents } from '../../../libs/defaults'; 3 | import { BaseMapObject } from '../BaseMapObject'; 4 | import React from 'react'; 5 | 6 | export interface IHMapPolylineProps { 7 | points: H.geo.IPoint[]; 8 | options?: H.map.Polyline.Options; 9 | setViewBounds: boolean; 10 | events: typeof mapEvents; 11 | } 12 | 13 | export const HMapPolyline = (props: IHMapPolylineProps) => { 14 | if (!Array.isArray(props.points)) { 15 | throw new Error( 16 | 'points should be an array of objects containing lat and lng properties' 17 | ); 18 | } 19 | const initFn = useCallback(() => { 20 | const { points, options } = props; 21 | // Initialize a LineString and add all the points to it: 22 | const lineString = new H.geo.LineString(); 23 | points.forEach(function(point) { 24 | lineString.pushPoint(point); 25 | }); 26 | 27 | // Initialize a polyLine with the lineString: 28 | return new H.map.Polyline(lineString, options); 29 | }, [props.points]); 30 | 31 | return ( 32 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /stories/Layer.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HMap, HMapLayer, IHMapLayerProps, useHPlatform } from '../src'; 4 | 5 | const appId = 'EF8K24SYpkpXUO9rkbfA'; 6 | const apiKey = 'TIAGlD6jic7l9Aa8Of8IFxo3EUemmcZlHm_agfAm6Ew'; 7 | 8 | const centerCoords = { lat: 52.5309825, lng: 13.3845921 }; 9 | 10 | const meta: Meta = { 11 | title: 'HMapLayer', 12 | component: HMapLayer, 13 | argTypes: {}, 14 | args: { 15 | type: 'vector.normal.traffic', 16 | }, 17 | }; 18 | 19 | export default meta; 20 | 21 | const Template: Story = args => { 22 | const renderHMapComponents = useHPlatform( 23 | { 24 | appId, 25 | apiKey, 26 | includeUI: true, 27 | includePlaces: false, 28 | version: 'v3/3.1', 29 | interactive: true, 30 | }, 31 | 41 | 42 | 43 | ); 44 | return renderHMapComponents; 45 | }; 46 | 47 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 48 | // https://storybook.js.org/docs/react/workflows/unit-testing 49 | export const Default = Template.bind({}); 50 | 51 | Default.args = {}; 52 | -------------------------------------------------------------------------------- /src/components/Map/objects/BaseMapObject.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from 'react'; 2 | import { MapContext } from '../../../contexts/map'; 3 | import { IHMapState } from '../Map'; 4 | import { initMapObjectEvents } from '../../libs/initMapObjectEvents'; 5 | import { mapEvents } from '../../libs/defaults'; 6 | 7 | type Object = H.map.Circle | H.map.Marker | H.map.Polyline | H.map.Polygon; 8 | 9 | interface Props { 10 | initializeFn: (ctx: IHMapState) => Object; 11 | initializeDeps: any; 12 | events: typeof mapEvents; 13 | zoom?: number; 14 | } 15 | 16 | export const BaseMapObject = ({ 17 | initializeFn, 18 | initializeDeps, 19 | events, 20 | zoom, 21 | }: Props) => { 22 | const mapContext = useContext(MapContext); 23 | if (!mapContext.map) { 24 | throw new Error('A map Object must be a child of HMap'); 25 | } 26 | 27 | useEffect(() => { 28 | const object = initializeFn(mapContext); 29 | mapContext?.map?.getViewModel().setLookAtData({ 30 | bounds: object.getGeometry(), 31 | }); 32 | mapContext.map?.setZoom(zoom || 4); 33 | // Add event listener to the object if intention of using the object is defined 34 | const { useEvents, interactive } = mapContext.options || {}; 35 | initMapObjectEvents(object, events, { 36 | interactive: Boolean(interactive), 37 | useEvents: Boolean(useEvents), 38 | }); 39 | mapContext.map?.addObject(object); 40 | }, [initializeDeps]); 41 | 42 | return null; 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/Map/objects/Polygon/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { mapEvents } from '../../../libs/defaults'; 3 | import { BaseMapObject } from '../BaseMapObject'; 4 | import React from 'react'; 5 | 6 | export interface IHMapPolygonProps { 7 | points: number[] | [string]; 8 | options?: H.map.Polyline.Options; 9 | setViewBounds: boolean; 10 | events: typeof mapEvents; 11 | } 12 | 13 | export const HMapPolygon = (props: IHMapPolygonProps) => { 14 | if (!Array.isArray(props.points)) { 15 | throw new Error( 16 | 'points should be an array of objects containing lat and lng properties' 17 | ); 18 | } 19 | const initFn = useCallback(() => { 20 | const { points, options } = props; 21 | 22 | let lineString: H.geo.LineString; 23 | const firstEl = points[0]; 24 | if (typeof firstEl === 'string' && firstEl.split(',').length === 2) { 25 | lineString = new H.geo.LineString(); 26 | const p = points as string[]; 27 | p.forEach(function(coords: string) { 28 | const c = coords.split(',').map(c => Number(c)); 29 | // c has to be lat, lng, alt 30 | lineString.pushLatLngAlt.apply(lineString, [c[0], c[1], c[2]]); 31 | }); 32 | } else { 33 | lineString = new H.geo.LineString(points as number[]); 34 | } 35 | 36 | // Initialize a LineString and add all the points to it: 37 | return new H.map.Polygon(lineString, options); 38 | }, [props.points]); 39 | 40 | return ( 41 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | "typeRoots": [ 35 | "node_modules/@types" 36 | ], 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/libs/loadHMap.ts: -------------------------------------------------------------------------------- 1 | import hereMapJS from '@limistah/here-map-js'; 2 | import { DefaultOptionsType, defaultOptions } from './defaults'; 3 | import merge from 'lodash.merge'; 4 | 5 | // Merges the option with the defaults to create a unison and make required values available 6 | const optionMerger = (options: ILoadHMapOptions) => { 7 | const { appId, appCode, apiKey, ...opts } = options; 8 | return merge(defaultOptions, { 9 | ...opts, 10 | app_id: appId, 11 | app_code: appCode, 12 | apikey: apiKey, 13 | }); 14 | }; 15 | 16 | export class ILoadHMapOptions { 17 | version?: string; // Version of the api to load. Defaults to v3 18 | interactive?: boolean; // Adds interactive scripts 19 | includeUI?: boolean; // Should add the UI scripts 20 | includePlaces?: boolean; // Include the places script 21 | useHTTPS?: boolean; 22 | useCIT?: boolean; 23 | appId?: string; 24 | appCode?: string; 25 | apiKey?: string; 26 | } 27 | 28 | export const loadHMap = async ( 29 | options: ILoadHMapOptions = { 30 | includePlaces: false, 31 | includeUI: false, 32 | interactive: false, 33 | version: 'v3/3.1', 34 | } 35 | ): Promise => { 36 | const mergedOptions = optionMerger(options); 37 | const { 38 | VERSION, 39 | version, 40 | interactive, 41 | includeUI, 42 | // includePlaces, 43 | } = mergedOptions; 44 | // Returns async loading of the component 45 | // First load the core, to save us reference error if all of the libraries are loaded asynchronously due to race conditions 46 | return hereMapJS({ 47 | includeUI, 48 | includePlaces: false, 49 | interactive, 50 | version: version || VERSION, 51 | }).then(() => mergedOptions); 52 | }; 53 | -------------------------------------------------------------------------------- /stories/Polygon.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HMap, HMapPolygon, IHMapPolygonProps, useHPlatform } from '../src'; 4 | const appId = 'EF8K24SYpkpXUO9rkbfA'; 5 | const apiKey = 'TIAGlD6jic7l9Aa8Of8IFxo3EUemmcZlHm_agfAm6Ew'; 6 | 7 | const points = [52, 13, 100, 48, 2, 100, 48, 16, 100, 52, 13, 100]; 8 | 9 | function logEvent(evt) { 10 | const evtLog = ['event "', evt.type, '" @ ' + evt.target.getData()].join(''); 11 | console.log(evtLog); 12 | } 13 | 14 | const meta: Meta = { 15 | title: 'HMapPolygon', 16 | component: HMap, 17 | argTypes: {}, 18 | args: { 19 | points, 20 | setVisibility: true, 21 | options: { style: { lineWidth: 1 } }, 22 | events: { 23 | pointerdown: logEvent, 24 | pointerenter: logEvent, 25 | pointerleave: logEvent, 26 | pointermove: logEvent, 27 | }, 28 | }, 29 | }; 30 | 31 | export default meta; 32 | 33 | const Template: Story = args => { 34 | const renderHMapComponents = useHPlatform( 35 | { 36 | appId, 37 | apiKey, 38 | includeUI: true, 39 | includePlaces: false, 40 | version: 'v3/3.1', 41 | interactive: true, 42 | }, 43 | 53 | 54 | 55 | ); 56 | return renderHMapComponents; 57 | }; 58 | 59 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 60 | // https://storybook.js.org/docs/react/workflows/unit-testing 61 | export const Default = Template.bind({}); 62 | 63 | Default.args = {}; 64 | -------------------------------------------------------------------------------- /stories/Rectangle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { 4 | HMap, 5 | IHMapPolylineProps, 6 | HMapRectangle, 7 | useHPlatform, 8 | IHMapRectangleProps, 9 | } from '../src'; 10 | const appId = 'EF8K24SYpkpXUO9rkbfA'; 11 | const apiKey = 'TIAGlD6jic7l9Aa8Of8IFxo3EUemmcZlHm_agfAm6Ew'; 12 | 13 | const points = [53.1, 13.1, 43.1, 40.1]; 14 | 15 | function logEvent(evt) { 16 | const evtLog = ['event "', evt.type, '" @ ' + evt.target.getData()].join(''); 17 | console.log(evtLog); 18 | } 19 | 20 | const meta: Meta = { 21 | title: 'HMapRectangle', 22 | component: HMap, 23 | argTypes: {}, 24 | args: { 25 | points, 26 | setVisibility: true, 27 | options: { style: { lineWidth: 2 } }, 28 | events: { 29 | pointerdown: logEvent, 30 | pointerenter: logEvent, 31 | pointerleave: logEvent, 32 | pointermove: logEvent, 33 | }, 34 | }, 35 | }; 36 | 37 | export default meta; 38 | 39 | const Template: Story = args => { 40 | const renderHMapComponents = useHPlatform( 41 | { 42 | appId, 43 | apiKey, 44 | includeUI: true, 45 | includePlaces: false, 46 | version: 'v3/3.1', 47 | interactive: true, 48 | }, 49 | 59 | 60 | 61 | ); 62 | return renderHMapComponents; 63 | }; 64 | 65 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 66 | // https://storybook.js.org/docs/react/workflows/unit-testing 67 | export const Default = Template.bind({}); 68 | 69 | Default.args = {}; 70 | -------------------------------------------------------------------------------- /stories/Polyline.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HMap, HMapPolyline, IHMapPolylineProps, useHPlatform } from '../src'; 4 | const appId = 'EF8K24SYpkpXUO9rkbfA'; 5 | const apiKey = 'TIAGlD6jic7l9Aa8Of8IFxo3EUemmcZlHm_agfAm6Ew'; 6 | 7 | const points = [ 8 | { lat: 53.3477, lng: -6.2597 }, 9 | { lat: 51.5008, lng: -0.1224 }, 10 | { lat: 48.8567, lng: 2.3508 }, 11 | { lat: 52.5166, lng: 13.3833 }, 12 | ]; 13 | 14 | function logEvent(evt) { 15 | const evtLog = ['event "', evt.type, '" @ ' + evt.target.getData()].join(''); 16 | console.log(evtLog); 17 | } 18 | 19 | const meta: Meta = { 20 | title: 'HMapPolyline', 21 | component: HMap, 22 | argTypes: {}, 23 | args: { 24 | points, 25 | setVisibility: true, 26 | options: { style: { lineWidth: 4 } }, 27 | events: { 28 | pointerdown: logEvent, 29 | pointerenter: logEvent, 30 | pointerleave: logEvent, 31 | pointermove: logEvent, 32 | }, 33 | }, 34 | }; 35 | 36 | export default meta; 37 | 38 | const Template: Story = args => { 39 | console.log(args); 40 | const renderHMapComponents = useHPlatform( 41 | { 42 | appId, 43 | apiKey, 44 | includeUI: true, 45 | includePlaces: false, 46 | version: 'v3/3.1', 47 | interactive: true, 48 | }, 49 | 59 | 60 | 61 | ); 62 | return renderHMapComponents; 63 | }; 64 | 65 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 66 | // https://storybook.js.org/docs/react/workflows/unit-testing 67 | export const Default = Template.bind({}); 68 | 69 | Default.args = {}; 70 | -------------------------------------------------------------------------------- /stories/Circle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { HMap, HMapCircle, IHMapCircleProps, useHPlatform } from '../src'; 4 | 5 | const appId = 'EF8K24SYpkpXUO9rkbfA'; 6 | const apiKey = 'TIAGlD6jic7l9Aa8Of8IFxo3EUemmcZlHm_agfAm6Ew'; 7 | 8 | const centerCoords = { lat: 52.5309825, lng: 13.3845921 }; 9 | 10 | const circleOptions = { 11 | style: { 12 | strokeColor: 'rgba(55, 85, 170, 0.6)', // Color of the perimeter 13 | lineWidth: 4, 14 | fillColor: 'rgba(0, 128, 0, 0.7)', // Color of the circle 15 | }, 16 | }; 17 | 18 | function logEvent(evt) { 19 | const evtLog = ['event "', evt.type, '" @ ' + evt.target.getData()].join(''); 20 | console.log(evtLog); 21 | } 22 | 23 | const meta: Meta = { 24 | title: 'HMapCircle', 25 | component: HMapCircle, 26 | argTypes: {}, 27 | args: { 28 | coords: centerCoords, 29 | options: circleOptions, 30 | radius: 10000, 31 | setVisibility: true, 32 | zoom: 8, 33 | events: { 34 | pointerdown: logEvent, 35 | pointerenter: logEvent, 36 | pointerleave: logEvent, 37 | pointermove: logEvent, 38 | }, 39 | }, 40 | }; 41 | 42 | export default meta; 43 | 44 | const Template: Story = args => { 45 | const renderHMapComponents = useHPlatform( 46 | { 47 | appId, 48 | apiKey, 49 | includeUI: true, 50 | includePlaces: false, 51 | version: 'v3/3.1', 52 | interactive: true, 53 | }, 54 | 64 | 65 | 66 | ); 67 | return renderHMapComponents; 68 | }; 69 | 70 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 71 | // https://storybook.js.org/docs/react/workflows/unit-testing 72 | export const Default = Template.bind({}); 73 | 74 | Default.args = {}; 75 | -------------------------------------------------------------------------------- /src/components/Platform/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useEffect, useState } from 'react'; 2 | import { ILoadHMapOptions, loadHMap } from '../libs/loadHMap'; 3 | import { initHPlatform } from '../libs/initPlatform'; 4 | import { DefaultOptionsType } from '../libs/defaults'; 5 | import { PlatformContext } from '../../contexts/platform'; 6 | 7 | export interface IHPlatform { 8 | children?: React.ReactNode | React.ReactNode[]; 9 | options: ILoadHMapOptions; 10 | } 11 | 12 | export interface IHPlatformState { 13 | options?: DefaultOptionsType; 14 | platform: H.service.Platform | null; 15 | reInitMap?: () => void; 16 | } 17 | 18 | export const HPlatform = (props: IHPlatform) => { 19 | // Reload the map resources if the options changes 20 | useEffect(() => { 21 | loadHMap(props.options).then((options: DefaultOptionsType) => { 22 | setPlatformState({ 23 | ...platformState, 24 | options, 25 | }); 26 | }); 27 | }, [props.options]); 28 | 29 | const initilizePlatform = () => { 30 | const platform = initHPlatform(platformState.options); 31 | setPlatformState((prevState: IHPlatformState) => ({ 32 | ...prevState, 33 | platform, 34 | })); 35 | }; 36 | 37 | const [platformState, setPlatformState] = useState({ 38 | reInitMap: initilizePlatform, 39 | platform: null, 40 | }); 41 | 42 | useEffect(() => { 43 | // initialize the platform when the js files are loaded the options are updated 44 | platformState.options && initilizePlatform(); 45 | }, [platformState.options]); 46 | 47 | const { platform, options } = platformState; 48 | 49 | return ( 50 | 51 | {typeof platform?.setBaseUrl == 'function' && 52 | (options?.app_code || options?.apikey) && 53 | props.children} 54 | 55 | ); 56 | }; 57 | 58 | // Use this to create A Here Map Platform 59 | export const useHPlatform = ( 60 | platformOptions: ILoadHMapOptions, 61 | children?: React.ReactNode | ReactNode[] 62 | ) => {children}; 63 | 64 | -------------------------------------------------------------------------------- /stories/Marker.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { 4 | HMap, 5 | IHMapPolylineProps, 6 | HMapRectangle, 7 | useHPlatform, 8 | IHMapRectangleProps, 9 | HMapMarker, 10 | IHMapMarkerProps, 11 | } from '../src'; 12 | const appId = 'EF8K24SYpkpXUO9rkbfA'; 13 | const apiKey = 'TIAGlD6jic7l9Aa8Of8IFxo3EUemmcZlHm_agfAm6Ew'; 14 | 15 | const coords = { lat: 52.5309825, lng: 13.3845921 }; 16 | 17 | function logEvent(evt) { 18 | const evtLog = ['event "', evt.type, '" @ ' + evt.target.getData()].join(''); 19 | console.log(evtLog); 20 | } 21 | 22 | 23 | const icon = 24 | '' + 26 | 'H'; 30 | 31 | const meta: Meta = { 32 | title: 'HMapMarker', 33 | component: HMap, 34 | argTypes: {}, 35 | args: { 36 | coords, 37 | setVisibility: true, 38 | options: { style: { lineWidth: 2 } }, 39 | icon, 40 | events: { 41 | pointerdown: logEvent, 42 | pointerenter: logEvent, 43 | pointerleave: logEvent, 44 | pointermove: logEvent, 45 | }, 46 | }, 47 | }; 48 | 49 | export default meta; 50 | 51 | const Template: Story = args => { 52 | const renderHMapComponents = useHPlatform( 53 | { 54 | appId, 55 | apiKey, 56 | includeUI: true, 57 | includePlaces: false, 58 | version: 'v3/3.1', 59 | interactive: true, 60 | }, 61 | 71 | 72 | 73 | ); 74 | return renderHMapComponents; 75 | }; 76 | 77 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 78 | // https://storybook.js.org/docs/react/workflows/unit-testing 79 | export const Default = Template.bind({}); 80 | 81 | export const MarkerIcon = Template.bind({ icon }); 82 | 83 | Default.args = {}; 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-here-map", 3 | "version": "4.0.0", 4 | "description": "React components for working with Here Maps API", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "src" 10 | ], 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "author": "Aleem Isiaka", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/limistah/react-here-map/issues" 18 | }, 19 | "homepage": "https://github.com/limistah/react-here-map#readme", 20 | "scripts": { 21 | "start": "tsdx watch", 22 | "build": "tsdx build", 23 | "test": "tsdx test --passWithNoTests", 24 | "lint": "tsdx lint src", 25 | "prepare": "tsdx build", 26 | "size": "size-limit", 27 | "analyze": "size-limit --why", 28 | "storybook": "storybook dev -p 6006", 29 | "build-storybook": "storybook build" 30 | }, 31 | "peerDependencies": { 32 | "react": ">=16" 33 | }, 34 | "husky": { 35 | "hooks": { 36 | "pre-commit": "tsdx lint" 37 | } 38 | }, 39 | "prettier": { 40 | "printWidth": 80, 41 | "semi": true, 42 | "singleQuote": true, 43 | "trailingComma": "es5" 44 | }, 45 | "module": "dist/react-here-map.esm.js", 46 | "size-limit": [ 47 | { 48 | "path": "dist/react-here-map.cjs.production.min.js", 49 | "limit": "10 KB" 50 | }, 51 | { 52 | "path": "dist/react-here-map.esm.js", 53 | "limit": "10 KB" 54 | } 55 | ], 56 | "resolutions": { 57 | "jackspeak": "2.1.1" 58 | }, 59 | "devDependencies": { 60 | "@babel/core": "^7.23.6", 61 | "@babel/plugin-proposal-optional-chaining": "^7.21.0", 62 | "@babel/preset-env": "^7.23.6", 63 | "@size-limit/preset-small-lib": "^11.0.1", 64 | "@storybook/addon-essentials": "^7.6.5", 65 | "@storybook/addon-info": "^5.3.21", 66 | "@storybook/addon-links": "^7.6.5", 67 | "@storybook/addons": "^7.6.5", 68 | "@storybook/react": "^7.6.5", 69 | "@storybook/react-webpack5": "^7.6.5", 70 | "@types/heremaps": "^3.1.14", 71 | "@types/lodash.merge": "^4.6.9", 72 | "@types/react": "^18.2.45", 73 | "@types/react-dom": "^18.2.17", 74 | "babel-loader": "^9.1.3", 75 | "husky": "^8.0.3", 76 | "react": "^18.2.0", 77 | "react-dom": "^18.2.0", 78 | "react-is": "^18.2.0", 79 | "size-limit": "^11.0.1", 80 | "storybook": "^7.6.5", 81 | "storybook-addon-react-docgen": "^1.2.44", 82 | "tsdx": "^0.14.1", 83 | "tslib": "^2.6.2", 84 | "typescript": "^5.3.3" 85 | }, 86 | "dependencies": { 87 | "@limistah/here-map-js": "^4.1.2", 88 | "@storybook/addon-docs": "^7.6.6", 89 | "dot-prop": "^8.0.2", 90 | "lodash.merge": "^4.6.2" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 2 | # react-here-map 3 | 4 | React components for rendering and working with 5 | [Here Maps](https://www.here.com/). 6 | 7 | It simplifies the use of the Here Map JavaScript API with the help React 8 | components. 9 | 10 | The components can be imported and easily rendered. It also comes with seamless 11 | configuration and modifications. 12 | 13 | ## Demo 14 | 15 | [See it here](https://limistah.github.io/react-here-map/) 16 | 17 | ## Installation 18 | 19 | Using NPM: 20 | 21 | --- 22 | ```bash 23 | npm i --save react-here-map 24 | ``` 25 | --- 26 | Using Yarn: 27 | 28 | --- 29 | ```bash 30 | yarn add react-here-map 31 | ``` 32 | --- 33 | ## General Usage 34 | 35 | --- 36 | ```js 37 | import React from "react"; 38 | import ReactDOM from "react-dom"; 39 | import HPlatform, { HMap, HMapCircle } from "react-here-map"; 40 | 41 | const points = [ 42 | { lat: 52.5309825, lng: 13.3845921 }, 43 | { lat: 52.5311923, lng: 13.3853495 }, 44 | { lat: 52.5313532, lng: 13.3861756 }, 45 | { lat: 52.5315142, lng: 13.3872163 }, 46 | { lat: 52.5316215, lng: 13.3885574 }, 47 | { lat: 52.5320399, lng: 13.3925807 }, 48 | { lat: 52.5321472, lng: 13.3935785 }, 49 | ]; 50 | 51 | ReactDOM.render( 52 | 62 | 75 | 97 | 98 | , 99 | document.getElementById("app") 100 | ); 101 | ``` 102 | --- 103 | ## CHANGES 104 | 105 | **06/05/2020** 106 | 107 | - Includes support for V3.1 API_KEY 108 | 109 | ## Contributions 110 | 111 | See the [./CONTRIBUTING.MD](CONTRIBUTING.MD) 112 | 113 | ## Licence 114 | 115 | MIT 116 | 117 | -------------------------------------------------------------------------------- /src/components/Map/Map.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useRef, useState } from 'react'; 2 | import { MAP_TYPES, defaultOptions } from '../libs/defaults'; 3 | import { PlatformContext } from '../../contexts/platform'; 4 | import { MapContext } from '../../contexts/map'; 5 | import merge from 'lodash.merge'; 6 | import { IBuildMapResult, buildMap } from '../libs/buildMap'; 7 | 8 | export interface IHMapProps { 9 | loadingEl?: React.ReactNode; 10 | style?: React.CSSProperties; 11 | children?: React.ReactNode | React.ReactNode[]; 12 | options?: IHMapOptions; 13 | uiLang?: string; 14 | ref?: React.RefObject | null; 15 | build?: boolean; 16 | interactive?: boolean; 17 | useEvents?: boolean; 18 | } 19 | 20 | export type IHMapPropsRequired = Omit< 21 | IHMapProps, 22 | 'loadingEl' | 'style' | 'children' | 'options' | 'ref' 23 | >; 24 | 25 | export type IHMapOptions = H.Map.Options & { 26 | mapType?: MAP_TYPES; 27 | }; 28 | 29 | export type IHMapOptionsMerged = IHMapPropsRequired & { 30 | container: React.RefObject | null; 31 | }; 32 | 33 | export interface IHMapState extends IBuildMapResult {} 34 | 35 | export const HMap = (props: IHMapProps) => { 36 | // props.options?.center 37 | // const Platform = useHPlatform() 38 | const platformState = useContext(PlatformContext); 39 | 40 | const [mapState, setMapState] = useState({ 41 | map: null, 42 | ui: null, 43 | options: undefined, 44 | interaction: null, 45 | }); 46 | 47 | const containerRef = props.ref || useRef(null); 48 | useEffect(() => { 49 | const mergedOptions = merge( 50 | { 51 | container: containerRef, 52 | build: true, 53 | }, 54 | platformState.options, 55 | { 56 | mapOptions: { 57 | ...platformState.options?.mapOptions, 58 | ...props.options, 59 | }, 60 | }, 61 | { 62 | interactive: props.interactive, 63 | useEvents: props.useEvents, 64 | } 65 | ); 66 | 67 | const buildResult = buildMap(platformState.platform, mergedOptions); 68 | 69 | setMapState(buildResult); 70 | }, []); 71 | 72 | const { style, loadingEl, children } = props; 73 | // const { options } = this.state.builder; 74 | 75 | const options = {}; 76 | 77 | const LoadingComponent = () => { 78 | return
Loading
; 79 | }; 80 | 81 | const loading = loadingEl || ; 82 | 83 | return ( 84 | // only render map provider when there is a platform state 85 | platformState.platform && ( 86 | 87 |
93 | {typeof H === 'undefined' && !options && loading} 94 | {typeof H === 'object' && mapState.map && options && children} 95 |
96 |
97 | ) 98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /src/components/libs/defaults.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {string} Default version for the API 3 | */ 4 | const VERSION = 'v3/3.1'; 5 | 6 | export const mapTypes = [ 7 | 'raster.normal.map', 8 | 'raster.normal.xbase', 9 | 'raster.normal.xbasenight', 10 | 'raster.normal.basen', 11 | 'raster.normal.basenight', 12 | 'raster.normal.mapnight', 13 | 'raster.normal.traffic', 14 | 'raster.normal.trafficnight', 15 | 'raster.normal.transit', 16 | 'raster.normal.panoram', 17 | 'raster.normal.panoramnight', 18 | 'raster.normal.labels', 19 | 'raster.normal.metaInfo', 20 | 'raster.satellite.xbase', 21 | 'raster.satellite.base', 22 | 'raster.satellite.map', 23 | 'raster.satellite.traffic', 24 | 'raster.satellite.panorama', 25 | 'raster.satellite.labels', 26 | 'raster.terrain.xbase', 27 | 'raster.terrain.base', 28 | 'raster.terrain.map', 29 | 'raster.terrain.traffic', 30 | 'raster.terrain.panorama', 31 | 'raster.terrain.labels', 32 | 'raster.incidents', 33 | 'raster.venues', 34 | 'vector.normal.map', 35 | 'vector.normal.xbase', 36 | 'vector.normal.xbasenight', 37 | 'vector.normal.basen', 38 | 'vector.normal.basenight', 39 | 'vector.normal.mapnight', 40 | 'vector.normal.traffic', 41 | 'vector.normal.trafficnight', 42 | 'vector.normal.transit', 43 | 'vector.normal.panoram', 44 | 'vector.normal.panoramnight', 45 | 'vector.normal.labels', 46 | 'vector.normal.metaInfo', 47 | 'vector.satellite.xbase', 48 | 'vector.satellite.base', 49 | 'vector.satellite.map', 50 | 'vector.satellite.traffic', 51 | 'vector.satellite.panorama', 52 | 'vector.satellite.labels', 53 | 'vector.terrain.xbase', 54 | 'vector.terrain.base', 55 | 'vector.terrain.map', 56 | 'vector.terrain.traffic', 57 | 'vector.terrain.panorama', 58 | 'vector.terrain.labels', 59 | 'vector.incidents', 60 | 'vector.venues', 61 | ] as const; 62 | export type MAP_TYPES = typeof mapTypes[number]; 63 | 64 | const MAP_TYPE: MAP_TYPES = 'vector.normal.map'; 65 | 66 | const mapOptions = { 67 | zoom: 8, 68 | center: { 69 | lat: 6.5243793, 70 | lng: 3.3792057, 71 | }, 72 | mapType: MAP_TYPE, 73 | }; 74 | const useEvents = false; 75 | const interactive = false; 76 | const includeUI = false; 77 | const containerId = 'HERE_MAP_CONTAINER'; 78 | 79 | export type mapEventTypes = 80 | | 'pointerdown' 81 | | 'pointerup' 82 | | 'pointermove' 83 | | 'pointerenter' 84 | | 'pointerleave' 85 | | 'pointercancel' 86 | | 'dragstart' 87 | | 'drag' 88 | | 'dragend' 89 | | 'tab' 90 | | 'dbltap'; 91 | 92 | const defaultClassName = 'here-map-container'; 93 | 94 | const includePlaces = false; 95 | 96 | // Function that does really nothing, still it is a function, and has its right! 97 | const noop = () => {}; 98 | 99 | export const mapEvents: Record = { 100 | pointercancel: noop, 101 | drag: noop, 102 | dragend: noop, 103 | tab: noop, 104 | dbltap: noop, 105 | pointerdown: noop, 106 | pointerenter: noop, 107 | pointerleave: noop, 108 | pointermove: noop, 109 | pointerup: noop, 110 | dragstart: noop, 111 | }; 112 | 113 | export type DefaultOptionsType = typeof defaultOptions; 114 | 115 | export const defaultOptions = { 116 | VERSION, 117 | mapEvents, 118 | MAP_TYPE, 119 | mapTypes, 120 | mapOptions, 121 | interactive, 122 | includeUI, 123 | includePlaces, 124 | useEvents, 125 | containerId, 126 | defaultClassName, 127 | app_id: '', 128 | app_code: '', 129 | apikey: '', 130 | }; 131 | -------------------------------------------------------------------------------- /src/components/libs/buildMap.ts: -------------------------------------------------------------------------------- 1 | // import initInteraction from './initInteraction'; 2 | // import initDefaultUI from './initDefaultUI'; 3 | // import initInteractionStyles from './initInteractionStyles'; 4 | 5 | import * as dotProp from 'dot-prop'; 6 | import { 7 | DefaultOptionsType, 8 | MAP_TYPES, 9 | mapEventTypes, 10 | mapEvents, 11 | } from './defaults'; 12 | import { IHMapOptions, IHMapOptionsMerged } from '../Map'; 13 | import { validateMapType } from './validateMapType'; 14 | import { initInteractionStyles } from './initInteractionStyles'; 15 | 16 | const initMap = ( 17 | container: React.RefObject, 18 | mapLayer: any, 19 | mapOptions: IHMapOptions 20 | ): H.Map | null => { 21 | // Instantiate (and display) a map object: 22 | const map = container.current 23 | ? new H.Map(container.current, mapLayer, mapOptions) 24 | : null; 25 | map?.setCenter(mapOptions.center as H.geo.Point); 26 | return map; 27 | }; 28 | 29 | export const initInteraction = ( 30 | map: H.Map | null, 31 | interactive: boolean, 32 | useEvents: boolean, 33 | events: typeof mapEvents 34 | ): H.mapevents.Behavior | null => { 35 | const behavior = 36 | interactive && map 37 | ? new H.mapevents.Behavior(new H.mapevents.MapEvents(map)) 38 | : null; 39 | if (useEvents && interactive && map) { 40 | for (const type in events) { 41 | if (events.hasOwnProperty(type)) { 42 | const callback = events[type as mapEventTypes]; 43 | if (callback && typeof callback === 'function') { 44 | map.addEventListener(type, callback); 45 | } 46 | } 47 | } 48 | } 49 | return behavior; 50 | }; 51 | 52 | export const initDefaultUI = ( 53 | platform: any, 54 | map: any, 55 | includeUI: boolean, 56 | uiLang?: string 57 | ) => { 58 | if (!includeUI) { 59 | throw new Error('includeUI must be set to true to initialize default UI'); 60 | } 61 | 62 | // Create the default UI components 63 | return H.ui.UI.createDefault(map, platform.createDefaultLayers(), uiLang); 64 | }; 65 | 66 | export interface IBuildMapResult { 67 | map: H.Map | null; 68 | interaction: H.mapevents.Behavior | null; 69 | ui: H.ui.UI | null; 70 | options?: IHMapOptionsMerged & DefaultOptionsType & { mapType: MAP_TYPES }; 71 | } 72 | 73 | export const buildMap = ( 74 | platform: any, 75 | options: IHMapOptionsMerged & DefaultOptionsType 76 | ): IBuildMapResult => { 77 | // Get values from the options 78 | const { 79 | useEvents, 80 | mapEvents, 81 | interactive, 82 | includeUI, 83 | mapOptions, 84 | uiLang, 85 | container, 86 | build, 87 | } = options; 88 | 89 | const retObject: IBuildMapResult = { 90 | map: null, 91 | interaction: null, 92 | ui: null, 93 | options: { ...options, mapType: mapOptions.mapType || 'vector.normal.map' }, 94 | }; 95 | 96 | if (container && build && retObject.options) { 97 | validateMapType(retObject.options.mapType); 98 | // Get all the default layers so we can set which to use based on the map type 99 | const defaultLayers = platform.createDefaultLayers(); 100 | 101 | const mapLayer = dotProp.getProperty( 102 | defaultLayers, 103 | retObject.options.mapType 104 | ); 105 | // Create a Map 106 | retObject.map = mapLayer ? initMap(container, mapLayer, mapOptions) : null; 107 | while (interactive && !retObject.interaction) { 108 | retObject.interaction = initInteraction( 109 | retObject.map, 110 | interactive, 111 | useEvents, 112 | mapEvents 113 | ); 114 | if (includeUI) { 115 | retObject.ui = initDefaultUI( 116 | platform, 117 | retObject.map, 118 | includeUI, 119 | uiLang 120 | ); 121 | } 122 | // Adds the grabbing to the document 123 | initInteractionStyles(); 124 | } 125 | } 126 | return retObject; 127 | }; 128 | --------------------------------------------------------------------------------