diff --git a/__mocks__/azure-maps-control.js b/__mocks__/azure-maps-control.js index 2263333..5e57d71 100644 --- a/__mocks__/azure-maps-control.js +++ b/__mocks__/azure-maps-control.js @@ -1,3 +1,11 @@ +class DataSource { + add = jest.fn() + clear = jest.fn() + remove = jest.fn() + importDataFromUrl = jest.fn() + setOptions = jest.fn() +} + module.exports = { Map: jest.fn(() => ({ controls: { @@ -109,13 +117,11 @@ module.exports = { })) }, source: { - DataSource: jest.fn(() => ({ - add: jest.fn(), - clear: jest.fn(), - remove: jest.fn(), - importDataFromUrl: jest.fn(), - setOptions: jest.fn() - })) + DataSource, + VectorTileSource: jest.fn((id, options) => ({ + getId: jest.fn(() => id), + getOptions: jest.fn(() => options) + })) }, Shape: jest.fn(() => ({ setCoordinates: jest.fn(), diff --git a/assets/coverage.png b/assets/coverage.png index a0d79aa..f2f9bb6 100644 Binary files a/assets/coverage.png and b/assets/coverage.png differ diff --git a/src/components/AzureMapFeature/useFeature.ts b/src/components/AzureMapFeature/useFeature.ts index d5895f4..8cab57f 100644 --- a/src/components/AzureMapFeature/useFeature.ts +++ b/src/components/AzureMapFeature/useFeature.ts @@ -1,6 +1,7 @@ import { useEffect } from 'react' import { DataSourceType, IAzureMapFeature, ShapeType, FeatureType } from '../../types' import { useCheckRef } from '../../hooks/useCheckRef' +import atlas from 'azure-maps-control' export const useFeature = ( { setCoords, setProperties }: IAzureMapFeature, @@ -10,17 +11,25 @@ export const useFeature = ( ) => { // Simple feature's usecases and methods useCheckRef(dataSourceRef, featureRef, (dref, fref) => { - dref.add(fref) - return () => { - dref.remove(fref) + if(dref instanceof atlas.source.DataSource){ + dref.add(fref) + return () => { + dref.remove(fref) + } + } else if (dataSourceRef instanceof atlas.source.VectorTileSource) { + console.error(`Unable to add Feature(${fref.id}) to VectorTileSource(${dataSourceRef.getId()}): AzureMapFeature has to be a child of AzureMapDataSourceProvider`) } }) // Shape's usecases and methods useCheckRef(dataSourceRef, shapeRef, (dref, sref) => { - dref.add(sref) - return () => { - dref.remove(sref) + if(dref instanceof atlas.source.DataSource){ + dref.add(sref) + return () => { + dref.remove(sref) + } + } else if (dataSourceRef instanceof atlas.source.VectorTileSource) { + console.error(`Unable to add Shape(${sref.getId()}) to VectorTileSource(${dataSourceRef.getId()}): AzureMapFeature has to be a child of AzureMapDataSourceProvider`) } }) diff --git a/src/contexts/AzureMapDataSourceContext.test.tsx b/src/contexts/AzureMapDataSourceContext.test.tsx index e08f7cd..624b592 100644 --- a/src/contexts/AzureMapDataSourceContext.test.tsx +++ b/src/contexts/AzureMapDataSourceContext.test.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react' import { renderHook } from '@testing-library/react-hooks' -import { Map } from 'azure-maps-control' +import atlas, { Map } from 'azure-maps-control' import React from 'react' import { AzureMapsContext } from '../contexts/AzureMapContext' import { @@ -65,15 +65,18 @@ describe('AzureMapDataSourceProvider tests', () => { const { result } = renderHook(() => useContextConsumer(), { wrapper: wrapWithDataSourceContext({ id: 'id', dataFromUrl: 'dataFromUrl' }) }) - expect(result.current.dataSourceRef?.importDataFromUrl).toHaveBeenCalledWith('dataFromUrl') + expect(result.current.dataSourceRef).toBeInstanceOf(atlas.source.DataSource) + expect((result.current.dataSourceRef as atlas.source.DataSource).importDataFromUrl).toHaveBeenCalledWith('dataFromUrl') }) it('should call add collection if collection was not falsy', () => { const { result } = renderHook(() => useContextConsumer(), { wrapper: wrapWithDataSourceContext({ id: 'id', collection: [] }) }) - expect(result.current.dataSourceRef?.add).toHaveBeenCalledWith([]) - expect(result.current.dataSourceRef?.clear).toHaveBeenCalledWith() + expect(result.current.dataSourceRef).toBeInstanceOf(atlas.source.DataSource) + const dataSourceRef = result.current.dataSourceRef as atlas.source.DataSource + expect(dataSourceRef.add).toHaveBeenCalledWith([]) + expect(dataSourceRef.clear).toHaveBeenCalledWith() }) it('should call add collection and clear method if collection was changed', () => { @@ -81,14 +84,17 @@ describe('AzureMapDataSourceProvider tests', () => { wrapper: wrapWithDataSourceContext({ id: 'id', collection: [] }) }) rerender({}) - expect(result.current.dataSourceRef?.add).toHaveBeenCalledTimes(2) - expect(result.current.dataSourceRef?.clear).toHaveBeenCalledTimes(1) + expect(result.current.dataSourceRef).toBeInstanceOf(atlas.source.DataSource) + const dataSourceRef = result.current.dataSourceRef as atlas.source.DataSource + expect(dataSourceRef.add).toHaveBeenCalledTimes(2) + expect(dataSourceRef.clear).toHaveBeenCalledTimes(1) }) it('should call setOptions and clear method if options was changed', () => { const { result, rerender } = renderHook(() => useContextConsumer(), { wrapper: wrapWithDataSourceContext({ id: 'id', options: { option: 'option' } }) }) - expect(result.current.dataSourceRef?.setOptions).toHaveBeenLastCalledWith({ option: 'option' }) + expect(result.current.dataSourceRef).toBeInstanceOf(atlas.source.DataSource) + expect((result.current.dataSourceRef as atlas.source.DataSource).setOptions).toHaveBeenLastCalledWith({ option: 'option' }) }) }) diff --git a/src/contexts/AzureMapDataSourceContext.tsx b/src/contexts/AzureMapDataSourceContext.tsx index 2b71d4f..356ecfd 100644 --- a/src/contexts/AzureMapDataSourceContext.tsx +++ b/src/contexts/AzureMapDataSourceContext.tsx @@ -38,11 +38,13 @@ const AzureMapDataSourceStatefulProvider = ({ mref.events.add(eventType as any, dref, events[eventType]) } mref.sources.add(dref) - if (dataFromUrl) { - dref.importDataFromUrl(dataFromUrl) - } - if (collection) { - dref.add(collection) + if (dref instanceof atlas.source.DataSource) { + if (dataFromUrl) { + dref.importDataFromUrl(dataFromUrl) + } + if (collection) { + dref.add(collection) + } } }) @@ -74,5 +76,6 @@ const AzureMapDataSourceStatefulProvider = ({ export { AzureMapDataSourceContext, AzureMapDataSourceConsumer, - AzureMapDataSourceStatefulProvider as AzureMapDataSourceProvider + AzureMapDataSourceStatefulProvider as AzureMapDataSourceProvider, + Provider as AzureMapDataSourceRawProvider } diff --git a/src/contexts/AzureMapVectorTileSourceProvider.test.tsx b/src/contexts/AzureMapVectorTileSourceProvider.test.tsx new file mode 100644 index 0000000..25e4db8 --- /dev/null +++ b/src/contexts/AzureMapVectorTileSourceProvider.test.tsx @@ -0,0 +1,69 @@ +import { renderHook } from '@testing-library/react-hooks' +import React, { useContext } from 'react' +import { Map } from 'azure-maps-control' +import { IAzureVectorTileSourceStatefulProviderProps } from "../types" +import { AzureMapsContext } from './AzureMapContext' +import { AzureMapVectorTileSourceProvider } from './AzureMapVectorTileSourceProvider' +import { AzureMapDataSourceContext } from '../contexts/AzureMapDataSourceContext' + +const mapContextProps = { + mapRef: null, + isMapReady: false, + setMapReady: jest.fn(), + removeMapRef: jest.fn(), + setMapRef: jest.fn() +} +const mapRef = new Map('fake', {}) + +const useContextConsumer = () => { + const dataSourceContext = useContext(AzureMapDataSourceContext) + return dataSourceContext +} + +const wrapWithVectorTileSourceContext = (props: IAzureVectorTileSourceStatefulProviderProps) => ({ + children +}: { + children?: any +}) => { + return ( + + {children} + + ) +} + +describe('AzureMapVectorTileSourceProvider tests', () => { + it('should create data source with passed id and options', () => { + const { result } = renderHook(() => useContextConsumer(), { + wrapper: wrapWithVectorTileSourceContext({ id: 'id', options: { minZoom: 0, maxZoom: 12 } }) + }) + + expect(result.current.dataSourceRef?.getId()).toEqual('id') + expect(result.current.dataSourceRef?.getOptions()).toEqual({ minZoom: 0, maxZoom: 12 }) + }) + + it('should add data source to the map ref on mount', () => { + mapRef.sources.add = jest.fn() + const { result } = renderHook(() => useContextConsumer(), { + wrapper: wrapWithVectorTileSourceContext({ id: 'id' }) + }) + expect(mapRef.sources.add).toHaveBeenCalledWith(result.current.dataSourceRef) + }) + + it('should add event to data source', () => { + mapRef.events.add = jest.fn() + renderHook(() => useContextConsumer(), { + wrapper: wrapWithVectorTileSourceContext({ id: 'id', events: { sourceadded: (source) => {} } }) + }) + expect(mapRef.events.add).toHaveBeenCalledWith( + 'sourceadded', + expect.any(Object), + expect.any(Function) + ) + }) +}) \ No newline at end of file diff --git a/src/contexts/AzureMapVectorTileSourceProvider.tsx b/src/contexts/AzureMapVectorTileSourceProvider.tsx new file mode 100644 index 0000000..486cb1b --- /dev/null +++ b/src/contexts/AzureMapVectorTileSourceProvider.tsx @@ -0,0 +1,43 @@ +import atlas from 'azure-maps-control' +import React, { useContext, useState } from 'react' +import { useCheckRef } from '../hooks/useCheckRef' +import { DataSourceType, IAzureMapsContextProps, IAzureMapSourceEventType, IAzureVectorTileSourceStatefulProviderProps, MapType } from '../types' +import { AzureMapDataSourceRawProvider as Provider } from './AzureMapDataSourceContext' +import { AzureMapsContext } from './AzureMapContext' + +/** + * @param id datasource identifier + * @param children layer providers representing datasource layers + * @param options vector tile datasource options: see atlas.VectorTileSourceOptions + * @param events a object containing sourceadded, sourceremoved event handlers + */ +const AzureMapVectorTileSourceStatefulProvider = ({ + id, + children, + options, + events = {}, +}: IAzureVectorTileSourceStatefulProviderProps) => { + const [dataSourceRef] = useState(new atlas.source.VectorTileSource(id, options)) + const { mapRef } = useContext(AzureMapsContext) + useCheckRef(mapRef, dataSourceRef, (mref, dref) => { + for (const eventType in events) { + const handler = events[eventType as IAzureMapSourceEventType] as (e: atlas.source.Source) => void | undefined + if(handler) { + mref.events.add(eventType as IAzureMapSourceEventType, dref, handler) + } + } + mref.sources.add(dref) + }) + + return ( + + {mapRef && children} + + ) +} + +export { AzureMapVectorTileSourceStatefulProvider as AzureMapVectorTileSourceProvider } \ No newline at end of file diff --git a/src/hooks/useAzureMapLayer.tsx b/src/hooks/useAzureMapLayer.tsx index ecf45e4..f7b0c82 100644 --- a/src/hooks/useAzureMapLayer.tsx +++ b/src/hooks/useAzureMapLayer.tsx @@ -14,7 +14,7 @@ import { MapType } from '../types' export const constructLayer = ( { id, options = {}, type }: Omit, - dataSourceRef: atlas.source.DataSource + dataSourceRef: DataSourceType ) => { switch (type) { case 'SymbolLayer': diff --git a/src/react-azure-maps.ts b/src/react-azure-maps.ts index 4e90c51..f7f8c2a 100644 --- a/src/react-azure-maps.ts +++ b/src/react-azure-maps.ts @@ -10,8 +10,9 @@ export { export { AzureMapDataSourceContext, AzureMapDataSourceConsumer, - AzureMapDataSourceProvider + AzureMapDataSourceProvider, } from './contexts/AzureMapDataSourceContext' +export { AzureMapVectorTileSourceProvider } from './contexts/AzureMapVectorTileSourceProvider' export { AzureMapLayerContext, AzureMapLayerConsumer, diff --git a/src/types.ts b/src/types.ts index 9a07595..dd83f6e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,7 +32,8 @@ import atlas, { UserInteractionOptions, Control, BubbleLayerOptions, - LayerOptions + LayerOptions, + VectorTileSourceOptions } from 'azure-maps-control' export type IAzureMapOptions = ServiceOptions & @@ -120,7 +121,7 @@ export type IAzureMapPopup = { } export type IAzureMapDataSourceContextState = { - dataSourceRef: atlas.source.DataSource | null + dataSourceRef: atlas.source.DataSource | atlas.source.VectorTileSource | null } export type IAzureMapLayerContextState = { @@ -132,10 +133,16 @@ export type IAzureDataSourceChildren = | ReactElement | ReactElement +export type IAzureVectorTileSourceChildren = ReactElement + export type IAzureMapDataSourceEvent = { [property in IAzureMapDataSourceEventType]: (e: Shape[]) => void } +export type IAzureMapVectorTileSourceEvent = { + [property in IAzureMapSourceEventType]?: (e: atlas.source.VectorTileSource) => void +} + export type IAzureMapEvent = { [property in IAzureMapEventsType]: ( e: @@ -169,6 +176,17 @@ export type IAzureDataSourceStatefulProviderProps = { index?: number } +export type IAzureVectorTileSourceStatefulProviderProps = { + id: string, + children?: | Array + | IAzureVectorTileSourceChildren + | null + options?: VectorTileSourceOptions, + events?: IAzureMapVectorTileSourceEvent + // NOTE: not sure yet why this is needed, haven't seen this used in AzureMapsDataSource, though IAzureGeoJSONDataSourceStatefulProviderProps has it + index?: number +} + export type IAzureMapLayerEvent = { [property in IAzureMapLayerEventType]: ( e: MapMouseEvent | MapTouchEvent | MapMouseWheelEvent @@ -307,7 +325,7 @@ export type IAzureMapLayerProps = IAzureMapLayerContextState export type IAzureMapMouseEventRef = HtmlMarker // && other possible iterfaces export type IAzureMapsContextProps = IAzureMapContextState export type IAzureMapDataSourceProps = IAzureMapDataSourceContextState -export type DataSourceType = atlas.source.DataSource +export type DataSourceType = atlas.source.DataSource | atlas.source.VectorTileSource export type LayerType = atlas.layer.SymbolLayer | atlas.layer.ImageLayer | atlas.layer.TileLayer export type MapType = atlas.Map export type GeometryType = atlas.data.Geometry