diff --git a/docs/api-reference/map.md b/docs/api-reference/map.md index 4c2556a4..acb709c8 100644 --- a/docs/api-reference/map.md +++ b/docs/api-reference/map.md @@ -26,12 +26,13 @@ function App() { Imperative methods are accessible via a [React ref](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) or the [useMap](./use-map.md) hook. -```js +```tsx import * as React from 'react'; import Map from 'react-map-gl'; +import mapboxgl from 'mapbox-gl'; function App() { - const mapRef = React.useRef(); + const mapRef = React.useRef>(); const onMapLoad = React.useCallback(() => { mapRef.current.on('move', () => { @@ -39,7 +40,7 @@ function App() { }); }, []); - return ; + return ; } ``` @@ -47,26 +48,28 @@ The [MapRef](./types.md#mapref) object exposes [Map methods](https://docs.mapbox You can still access the hidden members via `getMap()`: -#### `getMap()`: MapboxMap {#getmap} +#### `getMap()`: MapboxMap {#getmap} Returns the native [Map](https://docs.mapbox.com/mapbox-gl-js/api/map/) instance associated with this component. ## Properties +Aside from the props listed below, the `Map` component supports all parameters of the [Map](https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters) constructor. Beware that this is not an exhaustive list of all props. Different base map libraries may offer different options and default values. When in doubt, refer to your base map library's documentation. + ### Layout options -#### `id`: string {#id} +#### `id`: string {#id} Map container id. -#### `style`: CSSProperties {#style} +#### `style`: CSSProperties {#style} Default: `{position: 'relative', width: '100%', height: '100%'}` Map container CSS. -#### `cursor`: string {#cursor} +#### `cursor`: string {#cursor} Default: `'auto'` @@ -74,40 +77,40 @@ The current cursor [type](https://developer.mozilla.org/en-US/docs/Web/CSS/curso ### Styling options -#### `fog`: [Fog](./types.md#fog) {#fog} +#### `fog`: [Fog](./types.md#fog) {#fog} The fog property of the style. Must conform to the [Fog Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/fog/). If `undefined` is provided, removes the fog from the map. -#### `light`: [Light](./types.md#light) {#light} +#### `light`: [Light](./types.md#light) {#light} Light properties of the style. Must conform to the [Light Style Specification](https://www.mapbox.com/mapbox-gl-style-spec/#light). -#### `mapStyle`: [MapboxStyle](./types.md#mapboxstyle) | string | Immutable {#mapstyle} +#### `mapStyle`: [MapboxStyle](./types.md#mapboxstyle) | string | Immutable {#mapstyle} Default: (empty style) The map's Mapbox style. This must be an a JSON object conforming to the schema described in the [Mapbox Style Specification](https://mapbox.com/mapbox-gl-style-spec/), or a URL to such JSON. -#### `projection`: string | [ProjectionSpecification](./types.md#projectionspecification) {#projection} +#### `projection`: string | [ProjectionSpecification](./types.md#projectionspecification) {#projection} Default: `'mercator'` The projection the map should be rendered in. Available projections are Albers (`'albers'`), Equal Earth (`'equalEarth'`), Equirectangular/Plate Carrée/WGS84 (`'equirectangular'`), Lambert (`'lambertConformalConic'`), Mercator (`'mercator'`), Natural Earth (`'naturalEarth'`), and Winkel Tripel (`'winkelTripel'`). Conic projections such as Albers and Lambert have configurable `center` and `parallels` properties that allow developers to define the region in which the projection has minimal distortion; see [example](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#setprojection). -#### `renderWorldCopies`: boolean {#renderworldcopies} +#### `renderWorldCopies`: boolean {#renderworldcopies} Default: `true` If `true`, multiple copies of the world will be rendered, when zoomed out. -#### `styleDiffing`: boolean {#stylediffing} +#### `styleDiffing`: boolean {#stylediffing} Default: `true` Enable diffing when `mapStyle` changes. If `false`, force a 'full' update, removing the current style and building the given one instead of attempting a diff-based update. -#### `terrain`: [TerrainSpecification](./types.md#terrainspecification) {#terrain} +#### `terrain`: [TerrainSpecification](./types.md#terrainspecification) {#terrain} Terrain property of the style. Must conform to the [Terrain Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/terrain/). If `undefined` is provided, removes terrain from the map. @@ -115,7 +118,7 @@ If `undefined` is provided, removes terrain from the map. ### Camera options -#### `initialViewState`: object {#initialviewstate} +#### `initialViewState`: object {#initialviewstate} The initial view state of the map. If specified, `longitude`, `latitude`, `zoom` etc. in props are ignored when constructing the map. Only specify `initialViewState` if `Map` is being used as an **uncontrolled component**. See [state management](../get-started/state-management.md) for examples. @@ -127,57 +130,57 @@ The initial view state of the map. If specified, `longitude`, `latitude`, `zoom` - `pitch`: number - The initial pitch (tilt) of the map. Default `0`. - `bearing`: number - The initial bearing (rotation) of the map. Default `0`. -#### `longitude`: number {#longitude} +#### `longitude`: number {#longitude} The longitude of the map center. -#### `latitude`: number {#latitude} +#### `latitude`: number {#latitude} The latitude of the map center. -#### `zoom`: number {#zoom} +#### `zoom`: number {#zoom} The [zoom level](https://docs.mapbox.com/help/glossary/camera/#zoom-level) of the map. -#### `pitch`: number {#pitch} +#### `pitch`: number {#pitch} The initial [pitch](https://docs.mapbox.com/help/glossary/camera/#pitch) (tilt) of the map, measured in degrees away from the plane of the screen (0-85). -#### `bearing`: number {#bearing} +#### `bearing`: number {#bearing} The initial [bearing](https://docs.mapbox.com/help/glossary/camera/#bearing) (rotation) of the map, measured in degrees counter-clockwise from north. -#### `padding`: [PaddingOptions](./types.md#paddingoptions) {#padding} +#### `padding`: [PaddingOptions](./types.md#paddingoptions) {#padding} Default: `null` The padding in pixels around the viewport. -#### `minZoom`: number {#minzoom} +#### `minZoom`: number {#minzoom} Default: `0` The minimum zoom level of the map (0-24). -#### `maxZoom`: number {#maxzoom} +#### `maxZoom`: number {#maxzoom} Default: `22` The maximum zoom level of the map (0-24). -#### `minPitch`: number {#minpitch} +#### `minPitch`: number {#minpitch} Default: `0` The minimum pitch of the map (0-85). -#### `maxPitch`: number {#maxpitch} +#### `maxPitch`: number {#maxpitch} Default: `60` The maximum pitch of the map (0-85). -#### `maxBounds`: [LngLatBoundsLike](./types.md#lnglatboundslike) {#maxbounds} +#### `maxBounds`: [LngLatBoundsLike](./types.md#lnglatboundslike) {#maxbounds} Default: `null` @@ -185,55 +188,55 @@ If set, the map is constrained to the given bounds. ### Input handler options -#### `boxZoom`: boolean {#boxzoom} +#### `boxZoom`: boolean {#boxzoom} Default: `true` If `true`, the "box zoom" interaction is enabled (see [BoxZoomHandler](https://docs.mapbox.com/mapbox-gl-js/api/handlers/#boxzoomhandler)). -#### `doubleClickZoom`: boolean {#doubleclickzoom} +#### `doubleClickZoom`: boolean {#doubleclickzoom} Default: `true` If `true`, the "double click to zoom" interaction is enabled (see [DoubleClickZoomHandler](https://docs.mapbox.com/mapbox-gl-js/api/handlers/#doubleclickzoomhandler)). -#### `dragRotate`: boolean {#dragrotate} +#### `dragRotate`: boolean {#dragrotate} Default: `true` If `true`, the "drag to rotate" interaction is enabled (see [DragRotateHandler](https://docs.mapbox.com/mapbox-gl-js/api/handlers/#dragrotatehandler)). -#### `dragPan`: boolean | [DragPanOptions](./types.md#dragpanoptions) {#dragpan} +#### `dragPan`: boolean | [DragPanOptions](./types.md#dragpanoptions) {#dragpan} Default: `true` If `true`, the "drag to pan" interaction is enabled. Optionally accpt an object value that is the options to [DragPanHandler#enable](https://docs.mapbox.com/mapbox-gl-js/api/handlers/#dragpanhandler). -#### `keyboard`: boolean {#keyboard} +#### `keyboard`: boolean {#keyboard} Default: `true` If `true`, keyboard shortcuts are enabled (see [KeyboardHandler](https://docs.mapbox.com/mapbox-gl-js/api/handlers/#keyboardhandler)). -#### `scrollZoom`: boolean | [ZoomRotateOptions](./types.md#zoomrotateoptions) {#scrollzoom} +#### `scrollZoom`: boolean | [ZoomRotateOptions](./types.md#zoomrotateoptions) {#scrollzoom} Default: `true` If `true`, the "scroll to zoom" interaction is enabled. Optionally accpt an object value that is the options to [ScrollZoomHandler#enable](https://docs.mapbox.com/mapbox-gl-js/api/handlers/#scrollzoomhandler). -#### `touchPitch`: boolean {#touchpitch} +#### `touchPitch`: boolean {#touchpitch} Default: `true` If `true`, the "drag to pitch" interaction is enabled. Optionally accpt an object value that is the options to [TouchPitchHandler#enable](https://docs.mapbox.com/mapbox-gl-js/api/handlers/#touchpitchhandler). -#### `touchZoomRotate`: boolean | [ZoomRotateOptions](./types.md#zoomrotateoptions) {#touchzoomrotate} +#### `touchZoomRotate`: boolean | [ZoomRotateOptions](./types.md#zoomrotateoptions) {#touchzoomrotate} Default: `true` If `true`, the "pinch to rotate and zoom" interaction is enabled. Optionally accpt an object value that is the options to [TouchZoomRotateHandler#enable](https://docs.mapbox.com/mapbox-gl-js/api/handlers/#touchzoomrotatehandler). -#### `interactiveLayerIds`: string[] {#interactivelayerids} +#### `interactiveLayerIds`: string[] {#interactivelayerids} Default: `null` @@ -247,19 +250,19 @@ See the [Callbacks](#callbacks) section for affected events. ### Callbacks -#### `onResize`: (event: [MapboxEvent](./types.md#mapboxevent)) => void {#onresize} +#### `onResize`: (event: [MapboxEvent](./types.md#mapboxevent)) => void {#onresize} Called when the map has been resized. -#### `onLoad`: (event: [MapboxEvent](./types.md#mapboxevent)) => void {#onload} +#### `onLoad`: (event: [MapboxEvent](./types.md#mapboxevent)) => void {#onload} Called after all necessary resources have been downloaded and the first visually complete rendering of the map has occurred. -#### `onRender`: (event: [MapboxEvent](./types.md#mapboxevent)) => void {#onrender} +#### `onRender`: (event: [MapboxEvent](./types.md#mapboxevent)) => void {#onrender} Called whenever the map is drawn to the screen. -#### `onIdle`: (event: [MapboxEvent](./types.md#mapboxevent)) => void {#onidle} +#### `onIdle`: (event: [MapboxEvent](./types.md#mapboxevent)) => void {#onidle} Called after the last frame rendered before the map enters an "idle" state: @@ -267,194 +270,196 @@ Called after the last frame rendered before the map enters an "idle" state: - All currently requested tiles have loaded - All fade/transition animations have completed -#### `onRemove`: (event: [MapboxEvent](./types.md#mapboxevent)) => void {#onremove} +#### `onRemove`: (event: [MapboxEvent](./types.md#mapboxevent)) => void {#onremove} Called when the map has been removed. -#### `onError`: (event: [ErrorEvent](./types.md#errorevent)) => void {#onerror} +#### `onError`: (event: [ErrorEvent](./types.md#errorevent)) => void {#onerror} Default: `evt => console.error(evt.error)` Called when an error occurs. -#### `onMouseDown`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmousedown} +#### `onMouseDown`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmousedown} Called when a pointing device (usually a mouse) is pressed within the map. If `interactiveLayerIds` is specified, the event will contain an additional `features` field that contains features under the cursor from the specified layer. -#### `onMouseUp`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmouseup} +#### `onMouseUp`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmouseup} Called when a pointing device (usually a mouse) is released within the map. If `interactiveLayerIds` is specified, the event will contain an additional `features` field that contains features under the cursor from the specified layer. -#### `onMouseOver`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmouseover} +#### `onMouseOver`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmouseover} Called when a pointing device (usually a mouse) is moved within the map. As you move the cursor across a web page containing a map, the event will fire each time it enters the map or any child elements. -#### `onMouseEnter`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmouseenter} +#### `onMouseEnter`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmouseenter} Called when a pointing device (usually a mouse) enters a visible portion of the layer(s) specified by `interactiveLayerIds` from outside that layer or outside the map canvas. -#### `onMouseMove`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmousemove} +#### `onMouseMove`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmousemove} Called when a pointing device (usually a mouse) is moved while the cursor is inside the map. As you move the cursor across the map, the event will fire every time the cursor changes position within the map. If `interactiveLayerIds` is specified, the event will contain an additional `features` field that contains features under the cursor from the specified layer. -#### `onMouseLeave`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmouseleave} +#### `onMouseLeave`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmouseleave} Called when a pointing device (usually a mouse) leaves a visible portion of the layer(s) specified by `interactiveLayerIds` or moves from the layer to outside the map canvas. -#### `onMouseOut`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmouseout} +#### `onMouseOut`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onmouseout} Called when a point device (usually a mouse) leaves the map's canvas. -#### `onClick`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onclick} +#### `onClick`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#onclick} Called when a pointing device (usually a mouse) is pressed and released at the same point on the map. If `interactiveLayerIds` is specified, the event will contain an additional `features` field that contains features under the cursor from the specified layer. -#### `onDblClick`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#ondblclick} +#### `onDblClick`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#ondblclick} Called when a pointing device (usually a mouse) is pressed and released twice at the same point on the map in rapid succession. If `interactiveLayerIds` is specified, the event will contain an additional `features` field that contains features under the cursor from the specified layer. -#### `onContextMenu`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#oncontextmenu} +#### `onContextMenu`: (event: [MapLayerMouseEvent](./types.md#maplayermouseevent)) => void {#oncontextmenu} Called when the right button of the mouse is clicked or the context menu key is pressed within the map. If `interactiveLayerIds` is specified, the event will contain an additional `features` field that contains features under the cursor from the specified layer. -#### `onWheel`: (event: [MapWheelEvent](./types.md#mapwheelevent)) => void {#onwheel} +#### `onWheel`: (event: [MapWheelEvent](./types.md#mapwheelevent)) => void {#onwheel} Called when a wheel event occurs within the map. -#### `onTouchStart`: (event: [MapLayerTouchEvent](./types.md#maplayertouchevent)) => void {#ontouchstart} +#### `onTouchStart`: (event: [MapLayerTouchEvent](./types.md#maplayertouchevent)) => void {#ontouchstart} Called when a `touchstart` event occurs within the map. If `interactiveLayerIds` is specified, the event will contain an additional `features` field that contains features under the cursor from the specified layer. -#### `onTouchEnd`: (event: [MapLayerTouchEvent](./types.md#maplayertouchevent)) => void {#ontouchend} +#### `onTouchEnd`: (event: [MapLayerTouchEvent](./types.md#maplayertouchevent)) => void {#ontouchend} Called when a `touchend` event occurs within the map. If `interactiveLayerIds` is specified, the event will contain an additional `features` field that contains features under the cursor from the specified layer. -#### `onTouchMove`: (event: [MapLayerTouchEvent](./types.md#maplayertouchevent)) => void {#ontouchmove} +#### `onTouchMove`: (event: [MapLayerTouchEvent](./types.md#maplayertouchevent)) => void {#ontouchmove} Called when a `touchmove` event occurs within the map. If `interactiveLayerIds` is specified, the event will contain an additional `features` field that contains features under the cursor from the specified layer. -#### `onTouchCancel`: (event: [MapLayerTouchEvent](./types.md#maplayertouchevent)) => void {#ontouchcancel} +#### `onTouchCancel`: (event: [MapLayerTouchEvent](./types.md#maplayertouchevent)) => void {#ontouchcancel} Called when a `touchcancel` event occurs within the map. If `interactiveLayerIds` is specified, the event will contain an additional `features` field that contains features under the cursor from the specified layer. -#### `onMoveStart`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onmovestart} +#### `onMoveStart`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onmovestart} Called just before the map begins a transition from one view to another. -#### `onMove`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onmove} +#### `onMove`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onmove} Called repeatedly during an animated transition from one view to another. When `Map` is used as a controlled component, `event.viewState` reflects the view state that the camera "proposes" to move to, as a result of either user interaction or methods such as [flyTo](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#flyto). The camera does not actually change until the application updates the view state props (`longitude`, `latitude`, `zoom` etc.). See [state management](../get-started/state-management.md) for examples. -#### `onMoveEnd`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onmoveend} +#### `onMoveEnd`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onmoveend} Called just after the map completes a transition from one view to another. -#### `onDragStart`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#ondragstart} +#### `onDragStart`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#ondragstart} Called when a "drag to pan" interaction starts. -#### `onDrag`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#ondrag} +#### `onDrag`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#ondrag} Called repeatedly during a "drag to pan" interaction. -#### `onDragEnd`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#ondragend} +#### `onDragEnd`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#ondragend} Called when a "drag to pan" interaction ends. -#### `onZoomStart`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onzoomstart} +#### `onZoomStart`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onzoomstart} Called just before the map begins a transition from one zoom level to another. -#### `onZoom`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onzoom} +#### `onZoom`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onzoom} Called repeatedly during an animated transition from one zoom level to another. When `Map` is used as a controlled component, `event.viewState.zoom` reflects the zoom that the camera "proposes" to change to, as a result of either user interaction or methods such as [flyTo](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#flyto). The camera does not actually change until the application updates the `zoom` prop. -#### `onZoomEnd`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onzoomend} +#### `onZoomEnd`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onzoomend} Called just after the map completes a transition from one zoom level to another. -#### `onRotateStart`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onrotatestart} +#### `onRotateStart`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onrotatestart} Called just before the map begins a transition from one bearing (rotation) to another. -#### `onRotate`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onrotate} +#### `onRotate`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onrotate} Called repeatedly during an animated transition from one bearing (rotation) to another. When `Map` is used as a controlled component, `event.viewState.bearing` reflects the zoom that the camera "proposes" to change to, as a result of either user interaction or methods such as [flyTo](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#flyto). The camera does not actually change until the application updates the `bearing` prop. -#### `onRotateEnd`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onrotateend} +#### `onRotateEnd`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onrotateend} Called just after the map completes a transition from one bearing (rotation) to another. -#### `onPitchStart`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onpitchstart} +#### `onPitchStart`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onpitchstart} Called just before the map begins a transition from one pitch (tilt) to another. -#### `onPitch`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onpitch} +#### `onPitch`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onpitch} Called repeatedly during an animated transition from one pitch (tilt) to another. When `Map` is used as a controlled component, `event.viewState.pitch` reflects the zoom that the camera "proposes" to change to, as a result of either user interaction or methods such as [flyTo](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#flyto). The camera does not actually change until the application updates the `pitch` prop. -#### `onPitchEnd`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onpitchend} +#### `onPitchEnd`: (event: [ViewStateChangeEvent](./types.md#viewstatechangeevent)) => void {#onpitchend} Called just after the map completes a transition from one pitch (tilt) to another. -#### `onBoxZoomStart`: (event: [MapBoxZoomEvent](./types.md#mapboxzoomevent)) => void {#onboxzoomstart} +#### `onBoxZoomStart`: (event: [MapBoxZoomEvent](./types.md#mapboxzoomevent)) => void {#onboxzoomstart} Called when a "box zoom" interaction starts. -#### `onBoxZoomEnd`: (event: [MapBoxZoomEvent](./types.md#mapboxzoomevent)) => void {#onboxzoomend} +#### `onBoxZoomEnd`: (event: [MapBoxZoomEvent](./types.md#mapboxzoomevent)) => void {#onboxzoomend} Called when a "box zoom" interaction ends. -#### `onBoxZoomCancel`: (event:[MapBoxZoomEvent](./types.md#mapboxzoomevent)) => void {#onboxzoomcancel} +#### `onBoxZoomCancel`: (event:[MapBoxZoomEvent](./types.md#mapboxzoomevent)) => void {#onboxzoomcancel} Called when the user cancels a "box zoom" interaction, or when the bounding box does not meet the minimum size threshold. -#### `onData`: (event: [MapStyleDataEvent](./types.md#mapstyledataevent) | [MapSourceDataEvent](./types.md#mapsourcedataevent)) => void {#ondata} +#### `onData`: (event: [MapStyleDataEvent](./types.md#mapstyledataevent) | [MapSourceDataEvent](./types.md#mapsourcedataevent)) => void {#ondata} Called when any map data loads or changes. See [MapDataEvent](https://docs.mapbox.com/mapbox-gl-js/api/events/#mapdataevent) for more information. -#### `onStyleData`: (event: [MapStyleDataEvent](./types.md#mapstyledataevent)) => void {#onstyledata} +#### `onStyleData`: (event: [MapStyleDataEvent](./types.md#mapstyledataevent)) => void {#onstyledata} Called when the map's style loads or changes. See [MapDataEvent](https://docs.mapbox.com/mapbox-gl-js/api/events/#mapdataevent) for more information. -#### `onSourceData`: (event: [MapSourceDataEvent](./types.md#mapsourcedataevent)) => void {#onsourcedata} +#### `onSourceData`: (event: [MapSourceDataEvent](./types.md#mapsourcedataevent)) => void {#onsourcedata} Called when one of the map's sources loads or changes, including if a tile belonging to a source loads or changes. See [MapDataEvent](https://docs.mapbox.com/mapbox-gl-js/api/events/#mapdataevent) for more information. ### Other options -Props in this section are not reactive. They are only used once when the Map instance is constructed. +The following props, along with any parameter of the [Map](https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters) not listed above, can be specified to construct the underlying `Map` instance. -#### `mapLib`: any {#maplib} +Note: props in this section are not reactive. They are only used once when the Map instance is constructed. + +#### `mapLib`: any {#maplib} Specify the underlying base map library for the Map component. The value can be provided with several options: @@ -497,156 +502,21 @@ function App() { ``` -#### `mapboxAccessToken`: string {#mapboxaccesstoken} +#### `mapboxAccessToken`: string {#mapboxaccesstoken} Token used to access the Mapbox data service. See [about map tokens](../get-started/mapbox-tokens.md). -#### `antialias`: boolean {#antialias} - -Default: `false` - -If `true` , the gl context will be created with [MSAA antialiasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing), which can be useful for antialiasing custom layers. -This is `false` by default as a performance optimization. - -#### `attributionControl`: boolean {#attributioncontrol} - -Default: `true` - -If `true`, an attribution control will be added to the map. - -#### `baseApiUrl`: string {#baseapiurl} +#### `baseApiUrl`: string The map's default API URL for requesting tiles, styles, sprites, and glyphs. -#### `bearingSnap`: number {#bearingsnap} - -Default: `7` - -Snap to north threshold in degrees. - -#### `clickTolerance`: number {#clicktolerance} - -Default: `3` - -The max number of pixels a user can shift the mouse pointer during a click for it to be considered a valid click (as opposed to a mouse drag). - -#### `collectResourceTiming`: boolean {#collectresourcetiming} - -Default: `false` - -If `true`, Resource Timing API information will be collected for requests made by GeoJSON and Vector Tile web workers (this information is normally inaccessible from the main Javascript thread). Information will be returned in a `resourceTiming` property of relevant `data` events. - -#### `cooperativeGestures`: boolean {#cooperativegestures} - -Default: `false` - -If `true` , scroll zoom will require pressing the ctrl or ⌘ key while scrolling to zoom map, and touch pan will require using two fingers while panning to move the map. Touch pitch will require three fingers to activate if enabled. - -#### `crossSourceCollisions`: boolean {#crosssourcecollisions} - -Default: `true` - -If `true`, symbols from multiple sources can collide with each other during collision detection. If `false`, collision detection is run separately for the symbols in each source. - -#### `customAttribution`: string | string[] {#customattribution} - -Default: `null` - -String or strings to show in an AttributionControl. -Only applicable if `attributionControl` is `true`. - -#### `fadeDuration`: number {#fadeduration} - -Default: `300` - -Controls the duration of the fade-in/fade-out animation for label collisions, in milliseconds. This setting affects all symbol layers. This setting does not affect the duration of runtime styling transitions or raster tile cross-fading. - -#### `failIfMajorPerformanceCaveat`: boolean {#failifmajorperformancecaveat} - -Default: `false` - -If true, map creation will fail if the implementation determines that the performance of the created WebGL context would be dramatically lower than expected. - -#### `hash`: boolean | string {#hash} - -Default: `false` - -If `true`, the map's position (zoom, center latitude, center longitude, bearing, and pitch) will be synced with the hash fragment of the page's URL. -For example, `http://path/to/my/page.html#2.59/39.26/53.07/-24.1/60`. - -An additional string may optionally be provided to indicate a parameter-styled hash, -e.g. `http://path/to/my/page.html#map=2.59/39.26/53.07/-24.1/60&foo=bar`, where `foo` is a custom parameter and bar is an arbitrary hash distinct from the map hash. - -#### `interactive`: boolean {#interactive} - -Default: `true` - -If `false`, no mouse, touch, or keyboard listeners are attached to the map, so it will not respond to input. - -#### `locale`: Record\ {#locale} - -Default: `null` - -A patch to apply to the default localization table for UI strings, e.g. control tooltips. -The `locale` object maps namespaced UI string IDs to translated strings in the target language; see `src/ui/default_locale.js` for an example with all supported string IDs. -The object may specify all UI strings (thereby adding support for a new translation) or only a subset of strings (thereby patching the default translation table). - -#### `localFontFamily`: string {#localfontfamily} - -Default: `null` - -Defines a CSS font-family for locally overriding generation of all glyphs. Font settings from the map's style will be ignored, except for font-weight keywords (light/regular/medium/bold). If set, this option overrides the setting in localIdeographFontFamily. - -#### `localIdeographFontFamily`: string {#localideographfontfamily} - -Default: `'sans-serif'` - -Defines a CSS font-family for locally overriding generation of glyphs in the 'CJK Unified Ideographs', 'Hiragana', 'Katakana', 'Hangul Syllables' and 'CJK Symbols and Punctuation' ranges. Overrides font settings from the map's style. See [example](https://www.mapbox.com/mapbox-gl-js/example/local-ideographs). - -#### `logoPosition`: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' {#logoposition} - -Default: `'bottom-left'` - -A string representing the position of the Mapbox wordmark on the map. - -#### `maxParallelImageRequests`: number {#maxparallelimagerequests} +#### `maxParallelImageRequests`: number Default: `16` The maximum number of images (raster tiles, sprites, icons) to load in parallel. -#### `maxTileCacheSize`: number {#maxtilecachesize} - -Default: `null` - -The maximum number of tiles stored in the tile cache for a given source. If omitted, the cache will be dynamically sized based on the current viewport. - -#### `optimizeForTerrain`: boolean {#optimizeforterrain} - -Default: `true` - -If true, map will prioritize rendering for performance by reordering layers. -If false, layers will always be drawn in the specified order. - -#### `pitchWithRotate`: boolean {#pitchwithrotate} - -Default: `true` - -If `false`, the map's pitch (tilt) control with "drag to rotate" interaction will be disabled. - -#### `preserveDrawingBuffer`: boolean {#preservedrawingbuffer} - -Default: `false` - -If `true`, The maps canvas can be exported to a PNG using `map.getCanvas().toDataURL()`;. This is `false` by default as a performance optimization. - -#### `refreshExpiredTiles`: boolean {#refreshexpiredtiles} - -Default: `true` - -If `false`, the map won't attempt to re-request tiles once they expire per their HTTP `cacheControl`/`expires` headers. - -#### `reuseMaps`: boolean {#reusemaps} +#### `reuseMaps`: boolean {#reusemaps} Default: `false` @@ -656,7 +526,7 @@ If `reuseMaps` is set to `true`, when a map component is unmounted, the underlyi Note that since some map options cannot be modified after initialization, when reusing maps, only the reactive props and `initialViewState` of the new component are respected. -#### `RTLTextPlugin`: string {#rtltextplugin} +#### `RTLTextPlugin`: string {#rtltextplugin} Default: `'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.3/mapbox-gl-rtl-text.js'` @@ -664,38 +534,20 @@ Sets the map's [RTL text plugin](https://www.mapbox.com/mapbox-gl-js/plugins/#ma Setting this prop is the equivelant of calling [mapboxgl.setRTLTextPlugin](https://docs.mapbox.com/mapbox-gl-js/api/properties/#setrtltextplugin) with `lazy: true`. -#### `testMode`: boolean {#testmode} - -Default: `false` - -Silences errors and warnings generated due to an invalid accessToken, useful when using the library to write unit tests. - -#### `trackResize`: boolean {#trackresize} - -Default: `true` - -If `true`, the map will automatically resize when the browser window resizes. - -#### `transformRequest`: [TransformRequestFunction](./types.md#transformrequestfunction) {#transformrequest} - -Default: `null` - -A callback run before the Map makes a request for an external URL. The callback can be used to modify the url, set headers, or set the credentials property for cross-origin requests. - -#### `workerClass`: object {#workerclass} +#### `workerClass`: object {#workerclass} Default: `null` Provides an interface for external module bundlers such as Webpack or Rollup to package mapbox-gl's WebWorker into a separate class and integrate it with the library. Takes precedence over `workerUrl`. -#### `workerCount`: number {#workercount} +#### `workerCount`: number {#workercount} Default: `2` The number of web workers instantiated on a page with mapbox-gl maps. -#### `workerUrl`: string {#workerurl} +#### `workerUrl`: string {#workerurl} Provides an interface for loading mapbox-gl's WebWorker bundle from a self-hosted URL. This is useful if your site needs to operate in a strict CSP (Content Security Policy) environment wherein you are not allowed to load JavaScript code from a Blob URL, which is default behavior. diff --git a/docs/get-started/get-started.md b/docs/get-started/get-started.md index f101df3a..9d0f6622 100644 --- a/docs/get-started/get-started.md +++ b/docs/get-started/get-started.md @@ -1,23 +1,24 @@ # Get Started -## Installation +## Using with Mapbox GL JS -Using `react-map-gl` requires `node >= v8` and `react >= 16.3`. +### Installation + +Using `react-map-gl` requires `node >= 12` and `react >= 16.3`. ```sh -npm install --save react-map-gl mapbox-gl +npm install --save react-map-gl mapbox-gl @types/mapbox-gl ``` -## Example +### Example -```js +```tsx title="app.tsx" import * as React from 'react'; import Map from 'react-map-gl'; function App() { return ( +```html title="index.html" ``` @@ -48,42 +48,75 @@ Find out your mapbox version by running `yarn list mapbox-gl` or `npm ls mapbox- Or embed it in your app by using [css-loader](https://webpack.github.io/docs/stylesheets.html) with Webpack or [postcss](https://www.npmjs.com/package/rollup-plugin-postcss) with rollup: -```js +```ts title="app.tsx" /// app.js import 'mapbox-gl/dist/mapbox-gl.css'; ``` +## Using with Maplibre GL JS -## Using with a mapbox-gl Fork - -Install your choice of fork along with react-map-gl, for example: +### Installation ```bash npm install --save react-map-gl maplibre-gl ``` -Then override the `mapLib` prop of `Map`: -```js +### Example + +```tsx title="app.tsx" import * as React from 'react'; -import Map from 'react-map-gl'; -import maplibregl from 'maplibre-gl'; +import Map from 'react-map-gl/maplibre'; function App() { - return ; + return ( + + ); } ``` -To use the stylesheet from the fork: +### Styling -```html - +maplibre-gl requires its stylesheet be included at all times. The marker, popup and navigation components in react-map-gl also need the stylesheet to work properly. + +You may add the stylesheet to the head of your page: + +```html title="index.html" ``` -Or +Find out your maplibre version by running `yarn list maplibre-gl` or `npm ls maplibre-gl`. -```js -/// app.js +Or embed it in your app by using [css-loader](https://webpack.github.io/docs/stylesheets.html) with Webpack or [postcss](https://www.npmjs.com/package/rollup-plugin-postcss) with rollup: + +```ts title="app.tsx" import 'maplibre-gl/dist/maplibre-gl.css'; ``` + +## Using with Other Compatible Base Map Libraries + +```bash +npm install --save react-map-gl my-mapbox-fork +``` + +Then override the `mapLib` prop of `Map`: + +```tsx title="app.tsx" +import * as React from 'react'; +import Map from 'react-map-gl'; + +// Include style sheet +import 'my-mapbox-fork/path/to/style-sheet.css'; + +function App() { + return ; +} +``` diff --git a/docs/upgrade-guide.md b/docs/upgrade-guide.md index ebdbe9b1..4cb02832 100644 --- a/docs/upgrade-guide.md +++ b/docs/upgrade-guide.md @@ -1,14 +1,35 @@ # Upgrade Guide -## Upgrading to v8.0 +## Upgrading to v7.1 -In v8.0, `react-map-gl` no long require any vender-specific base map library as dependency. As a result, the `Map` component's `mapLib` prop must be provided. Users who use the package with `mapbox-gl` now need to specify `mapLib` as follows: +- `maplibre-gl` users no longer need to install `mapbox-gl` or a placeholder package as dependency. Change your imports to the new endpoint `react-map-gl/maplibre`. Components imported from here do not require setting the `mapLib` prop, and use the types defined by `maplibre-gl`. -```jsx - +```tsx title="map-v7.0.tsx" +import Map from 'react-map-gl'; +import maplibregl from 'maplibre-gl'; + +function App() { + return ; +} ``` -For additional options and examples, see [mapLib](./api-reference/map.md#maplib) documentation. +```tsx title="map-v7.1.tsx" +import Map from 'react-map-gl/maplibre'; + +function App() { + return +} +``` + +- The `@types/mapbox-gl` dependency has relaxed its version constraint. If you use `mapbox-gl` as the base map library, it's recommended to explicitly list `@types/mapbox-gl` in your package.json with a version matching that of `mapbox-gl` (v1 or v2). This package is no longer required by the non-mapbox code path, and may be further demoted to an optional peer dependency in a future release. ## Upgrading to v7.0 diff --git a/maplibre/package.json b/maplibre/package.json new file mode 100644 index 00000000..b795f167 --- /dev/null +++ b/maplibre/package.json @@ -0,0 +1,6 @@ +{ + "internal": true, + "main": "../dist/es5/exports-maplibre.js", + "module": "../dist/esm/exports-maplibre.js", + "types": "../dist/esm/exports-maplibre.d.ts" +} diff --git a/package.json b/package.json index cd588e39..d6065161 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "files": [ "src", "dist", + "maplibre", "README.md" ], "scripts": { @@ -36,7 +37,8 @@ "update-release-branch": "scripts/update-release-branch.sh" }, "dependencies": { - "@types/mapbox-gl": "^2.7.11" + "@maplibre/maplibre-gl-style-spec": "^19.2.1", + "@types/mapbox-gl": ">=1.0.0" }, "peerDependencies": { "mapbox-gl": ">=1.13.0", diff --git a/src/components/attribution-control.ts b/src/components/attribution-control.ts index 17f1fc26..9ba419bb 100644 --- a/src/components/attribution-control.ts +++ b/src/components/attribution-control.ts @@ -1,38 +1,32 @@ import * as React from 'react'; -import {useEffect} from 'react'; +import {useEffect, memo} from 'react'; import {applyReactStyle} from '../utils/apply-react-style'; import useControl from './use-control'; -import type {ControlPosition, MapboxAttributionControl} from '../types'; +import type {ControlPosition, AttributionControlInstance} from '../types'; -export type AttributionControlProps = { - /** - * If true , force a compact attribution that shows the full attribution on mouse hover. - * If false , force the full attribution control. The default is a responsive attribution - * that collapses when the map is less than 640 pixels wide. */ - compact?: boolean; - /** String or strings to show in addition to any other attributions. */ - customAttribution?: string | string[]; +export type AttributionControlProps = OptionsT & { /** Placement of the control relative to the map. */ position?: ControlPosition; /** CSS style override, applied to the control's container */ style?: React.CSSProperties; }; -function AttributionControl(props: AttributionControlProps): null { - const ctrl = useControl( - ({mapLib}) => new mapLib.AttributionControl(props), +function AttributionControl( + props: AttributionControlProps +): null { + const ctrl = useControl( + ({mapLib}) => new mapLib.AttributionControl(props) as ControlT, { position: props.position } ); useEffect(() => { - // @ts-ignore applyReactStyle(ctrl._container, props.style); }, [props.style]); return null; } -export default React.memo(AttributionControl); +export default memo(AttributionControl); diff --git a/src/components/fullscreen-control.ts b/src/components/fullscreen-control.tsx similarity index 63% rename from src/components/fullscreen-control.ts rename to src/components/fullscreen-control.tsx index 755c3b9b..e79fb64e 100644 --- a/src/components/fullscreen-control.ts +++ b/src/components/fullscreen-control.tsx @@ -1,12 +1,12 @@ /* global document */ import * as React from 'react'; -import {useEffect} from 'react'; +import {useEffect, memo} from 'react'; import {applyReactStyle} from '../utils/apply-react-style'; import useControl from './use-control'; -import type {ControlPosition, MapboxFullscreenControl} from '../types'; +import type {ControlPosition, FullscreenControlInstance} from '../types'; -export type FullscreenControlProps = { +export type FullscreenControlProps = Omit & { /** Id of the DOM element which should be made full screen. By default, the map container * element will be made full screen. */ containerId?: string; @@ -16,21 +16,22 @@ export type FullscreenControlProps = { style?: React.CSSProperties; }; -function FullscreenControl(props: FullscreenControlProps): null { - const ctrl = useControl( +function FullscreenControl( + props: FullscreenControlProps +): null { + const ctrl = useControl( ({mapLib}) => new mapLib.FullscreenControl({ container: props.containerId && document.getElementById(props.containerId) - }), + }) as ControlT, {position: props.position} ); useEffect(() => { - // @ts-ignore applyReactStyle(ctrl._controlContainer, props.style); }, [props.style]); return null; } -export default React.memo(FullscreenControl); +export default memo(FullscreenControl); diff --git a/src/components/geolocate-control.ts b/src/components/geolocate-control.ts index e8f5f5ba..4b74d1a0 100644 --- a/src/components/geolocate-control.ts +++ b/src/components/geolocate-control.ts @@ -1,83 +1,54 @@ import * as React from 'react'; -import {forwardRef, useImperativeHandle, useRef, useEffect} from 'react'; +import {useImperativeHandle, useRef, useEffect, forwardRef, memo} from 'react'; import {applyReactStyle} from '../utils/apply-react-style'; import useControl from './use-control'; import type { ControlPosition, - PositionOptions, - FitBoundsOptions, - MapboxGeolocateControl, + GeolocateControlInstance, GeolocateEvent, GeolocateResultEvent, GeolocateErrorEvent } from '../types'; -export type GeolocateControlRef = { - /** Triggers a geolocate event */ - trigger: () => boolean; -}; - -export type GeolocateControlProps = { - /** - * A Geolocation API PositionOptions object. - * @default {enableHighAccuracy:false,timeout:6000} - */ - positionOptions?: PositionOptions; - /** A Map#fitBounds options object to use when the map is panned and zoomed to the user's location. - * @default {maxZoom:15} - */ - fitBoundsOptions?: FitBoundsOptions; - /** If true the GeolocateControl becomes a toggle button and when active the map will receive - * updates to the user's location as it changes. Default false. - * @default false - */ - trackUserLocation?: boolean; - /** Draw a transparent circle will be drawn around the user location indicating the accuracy - * (95% confidence level) of the user's location. Set to false to disable. - * This only has effect if `showUserLocation` is true. - * @default true - */ - showAccuracyCircle?: boolean; - /** - * Show a dot on the map at the user's location. Set to false to disable. - * @default true - */ - showUserLocation?: boolean; - /** If true an arrow will be drawn next to the user location dot indicating the device's heading. - * This only has affect when `trackUserLocation` is true. Default false. - * @default false - */ - showUserHeading?: boolean; +export type GeolocateControlProps< + OptionsT, + ControlT extends GeolocateControlInstance +> = OptionsT & { /** Placement of the control relative to the map. */ position?: ControlPosition; /** CSS style override, applied to the control's container */ style?: React.CSSProperties; /** Called on each Geolocation API position update that returned as success. */ - onGeolocate?: (e: GeolocateResultEvent) => void; + onGeolocate?: (e: GeolocateResultEvent) => void; /** Called on each Geolocation API position update that returned as an error. */ - onError?: (e: GeolocateErrorEvent) => void; + onError?: (e: GeolocateErrorEvent) => void; /** Called on each Geolocation API position update that returned as success but user position * is out of map `maxBounds`. */ - onOutOfMaxBounds?: (e: GeolocateResultEvent) => void; + onOutOfMaxBounds?: (e: GeolocateResultEvent) => void; /** Called when the GeolocateControl changes to the active lock state. */ - onTrackUserLocationStart?: (e: GeolocateEvent) => void; + onTrackUserLocationStart?: (e: GeolocateEvent) => void; /** Called when the GeolocateControl changes to the background state. */ - onTrackUserLocationEnd?: (e: GeolocateEvent) => void; + onTrackUserLocationEnd?: (e: GeolocateEvent) => void; }; -function GeolocateControl(props: GeolocateControlProps, ref: React.Ref) { +function GeolocateControl( + props: GeolocateControlProps, + ref: React.Ref +) { const thisRef = useRef({props}); - const ctrl = useControl( + const ctrl = useControl( ({mapLib}) => { - const gc = new mapLib.GeolocateControl(props); + const gc = new mapLib.GeolocateControl(props) as ControlT; // Hack: fix GeolocateControl reuse // When using React strict mode, the component is mounted twice. // GeolocateControl's UI creation is asynchronous. Removing and adding it back causes the UI to be initialized twice. + // @ts-expect-error private method const setupUI = gc._setupUI; + // @ts-expect-error private method gc._setupUI = args => { if (!gc._container.hasChildNodes()) { setupUI(args); @@ -85,19 +56,19 @@ function GeolocateControl(props: GeolocateControlProps, ref: React.Ref { - thisRef.current.props.onGeolocate?.(e as GeolocateResultEvent); + thisRef.current.props.onGeolocate?.(e as GeolocateResultEvent); }); gc.on('error', e => { - thisRef.current.props.onError?.(e as GeolocateErrorEvent); + thisRef.current.props.onError?.(e as GeolocateErrorEvent); }); gc.on('outofmaxbounds', e => { - thisRef.current.props.onOutOfMaxBounds?.(e as GeolocateResultEvent); + thisRef.current.props.onOutOfMaxBounds?.(e as GeolocateResultEvent); }); gc.on('trackuserlocationstart', e => { - thisRef.current.props.onTrackUserLocationStart?.(e as GeolocateEvent); + thisRef.current.props.onTrackUserLocationStart?.(e as GeolocateEvent); }); gc.on('trackuserlocationend', e => { - thisRef.current.props.onTrackUserLocationEnd?.(e as GeolocateEvent); + thisRef.current.props.onTrackUserLocationEnd?.(e as GeolocateEvent); }); return gc; @@ -107,22 +78,13 @@ function GeolocateControl(props: GeolocateControlProps, ref: React.Ref ({ - trigger: () => ctrl.trigger() - }), - [] - ); + useImperativeHandle(ref, () => ctrl, []); useEffect(() => { - // @ts-ignore applyReactStyle(ctrl._container, props.style); }, [props.style]); return null; } -const GeolocateControlWithRef = forwardRef(GeolocateControl); - -export default React.memo(GeolocateControlWithRef); +export default memo(forwardRef(GeolocateControl)); diff --git a/src/components/layer.ts b/src/components/layer.ts index 699d0509..115b21d9 100644 --- a/src/components/layer.ts +++ b/src/components/layer.ts @@ -3,19 +3,19 @@ import {MapContext} from './map'; import assert from '../utils/assert'; import {deepEqual} from '../utils/deep-equal'; -import type {MapboxMap, AnyLayer} from '../types'; +import type {MapInstance, AnyLayer, CustomLayerInterface} from '../types'; // Omiting property from a union type, see // https://github.com/microsoft/TypeScript/issues/39556#issuecomment-656925230 type OptionalId = T extends {id: string} ? Omit & {id?: string} : T; -export type LayerProps = OptionalId & { +export type LayerProps = OptionalId & { /** If set, the layer will be inserted before the specified layer */ beforeId?: string; }; /* eslint-disable complexity, max-statements */ -function updateLayer(map: MapboxMap, id: string, props: LayerProps, prevProps: LayerProps) { +function updateLayer(map: MapInstance, id: string, props: LayerProps, prevProps: LayerProps) { assert(props.id === prevProps.id, 'layer id changed'); assert(props.type === prevProps.type, 'layer type changed'); @@ -23,7 +23,15 @@ function updateLayer(map: MapboxMap, id: string, props: LayerProps, prevProps: L return; } - const {layout = {}, paint = {}, filter, minzoom, maxzoom, beforeId} = props; + const { + layout = {}, + paint = {}, + // @ts-expect-error filter is not defined on some layer types + filter, + minzoom, + maxzoom, + beforeId + } = props; if (beforeId !== prevProps.beforeId) { map.moveLayer(id, beforeId); @@ -54,6 +62,7 @@ function updateLayer(map: MapboxMap, id: string, props: LayerProps, prevProps: L } } } + // @ts-expect-error filter is not defined on some layer types if (!deepEqual(filter, prevProps.filter)) { map.setFilter(id, filter); } @@ -62,7 +71,7 @@ function updateLayer(map: MapboxMap, id: string, props: LayerProps, prevProps: L } } -function createLayer(map: MapboxMap, id: string, props: LayerProps) { +function createLayer(map: MapInstance, id: string, props: LayerProps) { // @ts-ignore if (map.style && map.style._loaded && (!('source' in props) || map.getSource(props.source))) { const options: LayerProps = {...props, id}; @@ -78,7 +87,7 @@ function createLayer(map: MapboxMap, id: string, props: LayerProps) { let layerCounter = 0; function Layer(props: LayerProps) { - const map: MapboxMap = useContext(MapContext).map.getMap(); + const map = useContext(MapContext).map.getMap(); const propsRef = useRef(props); const [, setStyleLoaded] = useState(0); diff --git a/src/components/map.tsx b/src/components/map.tsx index b64bd42c..4cddcd7d 100644 --- a/src/components/map.tsx +++ b/src/components/map.tsx @@ -1,13 +1,5 @@ import * as React from 'react'; -import { - useState, - useRef, - useEffect, - useContext, - useMemo, - forwardRef, - useImperativeHandle -} from 'react'; +import {useState, useRef, useEffect, useContext, useMemo, useImperativeHandle} from 'react'; import {MountedMapsContext} from './use-map'; import Mapbox, {MapboxProps} from '../mapbox/mapbox'; @@ -16,17 +8,32 @@ import createRef, {MapRef} from '../mapbox/create-ref'; import type {CSSProperties} from 'react'; import useIsomorphicLayoutEffect from '../utils/use-isomorphic-layout-effect'; import setGlobals, {GlobalSettings} from '../utils/set-globals'; +import type {MapLib, MapInstance} from '../types'; -export type MapContextValue = { - mapLib: any; - map: MapRef; +export type MapContextValue = { + mapLib: MapLib; + map: MapRef; }; export const MapContext = React.createContext(null); -export type MapProps = MapboxProps & +// Redecalare forwardRef to support generics +// https://fettblog.eu/typescript-react-generic-forward-refs/ +declare module 'react' { + function forwardRef( + render: (props: P, ref: React.Ref) => React.ReactElement | null + ): (props: P & React.RefAttributes) => React.ReactElement | null; +} + +type MapInitOptions = Omit< + MapOptions, + 'style' | 'container' | 'bounds' | 'fitBoundsOptions' | 'center' +>; + +export type MapProps = MapInitOptions & + MapboxProps & GlobalSettings & { - mapLib?: any; + mapLib?: MapLib | Promise>; reuseMaps?: boolean; /** Map container id */ id?: string; @@ -35,20 +42,24 @@ export type MapProps = MapboxProps & children?: any; }; -function Map(props: MapProps, ref: React.Ref) { +export default function Map( + props: MapProps, + ref: React.Ref>, + defaultLib: MapLib | Promise> +) { const mountedMapsContext = useContext(MountedMapsContext); - const [mapInstance, setMapInstance] = useState(null); + const [mapInstance, setMapInstance] = useState>(null); const containerRef = useRef(); - const {current: contextValue} = useRef({mapLib: null, map: null}); + const {current: contextValue} = useRef>({mapLib: null, map: null}); useEffect(() => { const mapLib = props.mapLib; let isMounted = true; - let mapbox; + let mapbox: Mapbox; - Promise.resolve(mapLib) - .then(module => { + Promise.resolve(mapLib || defaultLib) + .then((module: MapLib | {default: MapLib}) => { if (!isMounted) { return; } @@ -70,7 +81,7 @@ function Map(props: MapProps, ref: React.Ref) { if (!mapbox) { mapbox = new Mapbox(mapboxgl.Map, props, containerRef.current); } - contextValue.map = createRef(mapbox, mapboxgl); + contextValue.map = createRef(mapbox); contextValue.mapLib = mapboxgl; setMapInstance(mapbox); @@ -140,5 +151,3 @@ function Map(props: MapProps, ref: React.Ref) { ); } - -export default forwardRef(Map); diff --git a/src/components/marker.ts b/src/components/marker.ts index 3419beff..34c896ba 100644 --- a/src/components/marker.ts +++ b/src/components/marker.ts @@ -1,86 +1,46 @@ /* global document */ import * as React from 'react'; import {createPortal} from 'react-dom'; -import {useEffect, useMemo, useRef, useContext} from 'react'; +import {useEffect, useMemo, useRef, useContext, memo} from 'react'; import {applyReactStyle} from '../utils/apply-react-style'; -import type { - MarkerDragEvent, - MapboxPopup, - PointLike, - Anchor, - Alignment, - MapboxEvent, - MapboxMarker -} from '../types'; +import type {MarkerEvent, MarkerDragEvent, PointLike, MarkerInstance} from '../types'; import {MapContext} from './map'; import {arePointsEqual} from '../utils/deep-equal'; -export type MarkerProps = { +export type MarkerProps = OptionsT & { /** Longitude of the anchor location */ longitude: number; /** Latitude of the anchor location */ latitude: number; - /** A string indicating the part of the Marker that should be positioned closest to the coordinate set via Marker.setLngLat. - * Options are `'center'`, `'top'`, `'bottom'`, `'left'`, `'right'`, `'top-left'`, `'top-right'`, `'bottom-left'`, and `'bottom-right'`. - * @default "center" - */ - anchor?: Anchor; - /** - * The max number of pixels a user can shift the mouse pointer during a click on the marker for it to be considered a valid click - * (as opposed to a marker drag). The default (0) is to inherit map's clickTolerance. - */ - clickTolerance?: number; - /** The color to use for the default marker if options.element is not provided. - * @default "#3FB1CE" - */ - color?: string; - /** A boolean indicating whether or not a marker is able to be dragged to a new position on the map. - * @default false - */ + + // These types will be further constraint by OptionsT draggable?: boolean; - /** The offset in pixels as a PointLike object to apply relative to the element's center. Negatives indicate left and up. */ offset?: PointLike; - /** `map` aligns the `Marker` to the plane of the map. - * `viewport` aligns the `Marker` to the plane of the viewport. - * `auto` automatically matches the value of `rotationAlignment`. - * @default "auto" - */ - pitchAlignment?: Alignment; - /** The rotation angle of the marker in degrees, relative to its `rotationAlignment` setting. A positive value will rotate the marker clockwise. - * @default 0 - */ + pitchAlignment?: string; rotation?: number; - /** `map` aligns the `Marker`'s rotation relative to the map, maintaining a bearing as the map rotates. - * `viewport` aligns the `Marker`'s rotation relative to the viewport, agnostic to map rotations. - * `auto` is equivalent to `viewport`. - * @default "auto" - */ - rotationAlignment?: Alignment; - /** The scale to use for the default marker if options.element is not provided. - * The default scale (1) corresponds to a height of `41px` and a width of `27px`. - * @default 1 - */ - scale?: number; - /** A Popup instance that is bound to the marker */ - popup?: MapboxPopup; + rotationAlignment?: string; + popup?: any; + /** CSS style override, applied to the control's container */ style?: React.CSSProperties; - onClick?: (e: MapboxEvent) => void; - onDragStart?: (e: MarkerDragEvent) => void; - onDrag?: (e: MarkerDragEvent) => void; - onDragEnd?: (e: MarkerDragEvent) => void; + onClick?: (e: MarkerEvent) => void; + onDragStart?: (e: MarkerDragEvent) => void; + onDrag?: (e: MarkerDragEvent) => void; + onDragEnd?: (e: MarkerDragEvent) => void; children?: React.ReactNode; }; /* eslint-disable complexity,max-statements */ -function Marker(props: MarkerProps) { +function Marker( + props: MarkerProps +) { const {map, mapLib} = useContext(MapContext); const thisRef = useRef({props}); thisRef.current.props = props; - const marker: MapboxMarker = useMemo(() => { + const marker: MarkerT = useMemo(() => { let hasChildren = false; React.Children.forEach(props.children, el => { if (el) { @@ -92,7 +52,8 @@ function Marker(props: MarkerProps) { element: hasChildren ? document.createElement('div') : null }; - const mk = new mapLib.Marker(options).setLngLat([props.longitude, props.latitude]); + const mk = new mapLib.Marker(options) as MarkerT; + mk.setLngLat([props.longitude, props.latitude]); mk.getElement().addEventListener('click', (e: MouseEvent) => { thisRef.current.props.onClick?.({ @@ -103,17 +64,17 @@ function Marker(props: MarkerProps) { }); mk.on('dragstart', e => { - const evt = e as MarkerDragEvent; + const evt = e as MarkerDragEvent; evt.lngLat = marker.getLngLat(); thisRef.current.props.onDragStart?.(evt); }); mk.on('drag', e => { - const evt = e as MarkerDragEvent; + const evt = e as MarkerDragEvent; evt.lngLat = marker.getLngLat(); thisRef.current.props.onDrag?.(evt); }); mk.on('dragend', e => { - const evt = e as MarkerDragEvent; + const evt = e as MarkerDragEvent; evt.lngLat = marker.getLngLat(); thisRef.current.props.onDragEnd?.(evt); }); @@ -170,4 +131,4 @@ function Marker(props: MarkerProps) { return createPortal(props.children, marker.getElement()); } -export default React.memo(Marker); +export default memo(Marker); diff --git a/src/components/navigation-control.ts b/src/components/navigation-control.ts index c51e4b8e..3f25f7f2 100644 --- a/src/components/navigation-control.ts +++ b/src/components/navigation-control.ts @@ -1,43 +1,29 @@ import * as React from 'react'; -import {useEffect} from 'react'; +import {useEffect, memo} from 'react'; import {applyReactStyle} from '../utils/apply-react-style'; import useControl from './use-control'; -import type {ControlPosition, MapboxNavigationControl} from '../types'; +import type {ControlPosition, NavigationControlInstance} from '../types'; -export type NavigationControlProps = { - /** If true the compass button is included. - * @default true - */ - showCompass?: boolean; - /** If true the zoom-in and zoom-out buttons are included. - * @default true - */ - showZoom?: boolean; - /** If true the pitch is visualized by rotating X-axis of compass. - * @default false - */ - visualizePitch?: boolean; +export type NavigationControlProps = OptionsT & { /** Placement of the control relative to the map. */ position?: ControlPosition; /** CSS style override, applied to the control's container */ style?: React.CSSProperties; }; -function NavigationControl(props: NavigationControlProps): null { - const ctrl = useControl( - ({mapLib}) => new mapLib.NavigationControl(props), - { - position: props.position - } - ); +function NavigationControl( + props: NavigationControlProps +): null { + const ctrl = useControl(({mapLib}) => new mapLib.NavigationControl(props) as ControlT, { + position: props.position + }); useEffect(() => { - // @ts-ignore applyReactStyle(ctrl._container, props.style); }, [props.style]); return null; } -export default React.memo(NavigationControl); +export default memo(NavigationControl); diff --git a/src/components/popup.ts b/src/components/popup.ts index ea233ebe..cf50d5c3 100644 --- a/src/components/popup.ts +++ b/src/components/popup.ts @@ -1,66 +1,31 @@ /* global document */ import * as React from 'react'; import {createPortal} from 'react-dom'; -import {useEffect, useMemo, useRef, useContext} from 'react'; +import {useEffect, useMemo, useRef, useContext, memo} from 'react'; import {applyReactStyle} from '../utils/apply-react-style'; -import type {PopupEvent, Anchor, PointLike, MapboxPopup} from '../types'; +import type {PopupEvent, PopupInstance} from '../types'; import {MapContext} from './map'; import {deepEqual} from '../utils/deep-equal'; -export type PopupProps = { +export type PopupProps = OptionsT & { /** Longitude of the anchor location */ longitude: number; /** Latitude of the anchor location */ latitude: number; - /** - * A string indicating the part of the popup that should be positioned closest to the coordinate. - * Options are `'center'`, `'top'`, `'bottom'`, `'left'`, `'right'`, `'top-left'`, `'top-right'`, `'bottom-left'`, - * and `'bottom-right'`. If unset, the anchor will be dynamically set to ensure the popup falls within the map - * container with a preference for `'bottom'`. - */ - anchor?: Anchor; - /** - * If `true`, a close button will appear in the top right corner of the popup. - * @default true - */ - closeButton?: boolean; - /** - * If `true`, the popup will close when the map is clicked. - * @default true - */ - closeOnClick?: boolean; - /** - * If `true`, the popup will closed when the map moves. - * @default false - */ - closeOnMove?: boolean; - /** - * If `true`, the popup will try to focus the first focusable element inside the popup. - * @default true - */ - focusAfterOpen?: boolean; - /** - * A pixel offset applied to the popup's location specified as: - * - a single number specifying a distance from the popup's location - * - a PointLike specifying a constant offset - * - an object of Points specifing an offset for each anchor position. - */ - offset?: number | PointLike | Partial<{[anchor in Anchor]: PointLike}>; - /** Space-separated CSS class names to add to popup container. */ + + // These types will be further constraint by OptionsT + anchor?: string; + offset?: any; className?: string; - /** - * A string that sets the CSS property of the popup's maximum width (for example, `'300px'`). - * To ensure the popup resizes to fit its content, set this property to `'none'` - * @default "240px" - */ maxWidth?: string; + /** CSS style override, applied to the control's container */ style?: React.CSSProperties; - onOpen?: (e: PopupEvent) => void; - onClose?: (e: PopupEvent) => void; + onOpen?: (e: PopupEvent) => void; + onClose?: (e: PopupEvent) => void; children?: React.ReactNode; }; @@ -70,7 +35,9 @@ function getClassList(className: string) { } /* eslint-disable complexity,max-statements */ -function Popup(props: PopupProps) { +function Popup( + props: PopupProps +) { const {map, mapLib} = useContext(MapContext); const container = useMemo(() => { return document.createElement('div'); @@ -78,18 +45,19 @@ function Popup(props: PopupProps) { const thisRef = useRef({props}); thisRef.current.props = props; - const popup: MapboxPopup = useMemo(() => { + const popup: PopupT = useMemo(() => { const options = {...props}; - const pp = new mapLib.Popup(options).setLngLat([props.longitude, props.latitude]); + const pp = new mapLib.Popup(options) as PopupT; + pp.setLngLat([props.longitude, props.latitude]); pp.once('open', e => { - thisRef.current.props.onOpen?.(e as PopupEvent); + thisRef.current.props.onOpen?.(e as PopupEvent); }); return pp; }, []); useEffect(() => { const onClose = e => { - thisRef.current.props.onClose?.(e as PopupEvent); + thisRef.current.props.onClose?.(e as PopupEvent); }; popup.on('close', onClose); popup.setDOMContent(container).addTo(map.getMap()); @@ -114,19 +82,14 @@ function Popup(props: PopupProps) { if (popup.getLngLat().lng !== props.longitude || popup.getLngLat().lat !== props.latitude) { popup.setLngLat([props.longitude, props.latitude]); } - // @ts-ignore if (props.offset && !deepEqual(popup.options.offset, props.offset)) { popup.setOffset(props.offset); } - // @ts-ignore if (popup.options.anchor !== props.anchor || popup.options.maxWidth !== props.maxWidth) { - // @ts-ignore popup.options.anchor = props.anchor; popup.setMaxWidth(props.maxWidth); } - // @ts-ignore if (popup.options.className !== props.className) { - // @ts-ignore const prevClassList = getClassList(popup.options.className); const nextClassList = getClassList(props.className); @@ -140,7 +103,6 @@ function Popup(props: PopupProps) { popup.addClassName(c); } } - // @ts-ignore popup.options.className = props.className; } } @@ -148,5 +110,4 @@ function Popup(props: PopupProps) { return createPortal(props.children, container); } -// @ts-ignore -export default React.memo(Popup); +export default memo(Popup); diff --git a/src/components/scale-control.ts b/src/components/scale-control.ts index 5d0a55ba..b5039cce 100644 --- a/src/components/scale-control.ts +++ b/src/components/scale-control.ts @@ -1,46 +1,46 @@ import * as React from 'react'; -import {useEffect} from 'react'; +import {useEffect, useRef, memo} from 'react'; import {applyReactStyle} from '../utils/apply-react-style'; import useControl from './use-control'; -import type {ControlPosition, MapboxScaleControl} from '../types'; +import type {ControlPosition, ScaleControlInstance} from '../types'; -export type ScaleControlProps = { - /** Unit of the distance. - * @default "metric" - */ - unit?: 'imperial' | 'metric' | 'nautical'; - /** The maximum length of the scale control in pixels. - * @default 100 - */ +export type ScaleControlProps = OptionsT & { + // These props will be further constraint by OptionsT + unit?: string; maxWidth?: number; + /** Placement of the control relative to the map. */ position?: ControlPosition; /** CSS style override, applied to the control's container */ style?: React.CSSProperties; }; -function ScaleControl(props: ScaleControlProps): null { - const ctrl = useControl(({mapLib}) => new mapLib.ScaleControl(props), { +function ScaleControl( + props: ScaleControlProps +): null { + const ctrl = useControl(({mapLib}) => new mapLib.ScaleControl(props) as ControlT, { position: props.position }); + const propsRef = useRef>(props); - const {style, unit = 'metric', maxWidth = 100} = props; + const prevProps = propsRef.current; + propsRef.current = props; - // @ts-ignore - if (ctrl.options.unit !== unit || ctrl.options.maxWidth !== maxWidth) { - // @ts-ignore - ctrl.options.maxWidth = maxWidth; - // This method will trigger an update - ctrl.setUnit(unit); + const {style} = props; + + if (props.maxWidth !== undefined && props.maxWidth !== prevProps.maxWidth) { + ctrl.options.maxWidth = props.maxWidth; + } + if (props.unit !== undefined && props.unit !== prevProps.unit) { + ctrl.setUnit(props.unit); } useEffect(() => { - // @ts-ignore applyReactStyle(ctrl._container, style); }, [style]); return null; } -export default React.memo(ScaleControl); +export default memo(ScaleControl); diff --git a/src/components/source.ts b/src/components/source.ts index 2be5c36c..fe6c1601 100644 --- a/src/components/source.ts +++ b/src/components/source.ts @@ -6,22 +6,25 @@ import assert from '../utils/assert'; import {deepEqual} from '../utils/deep-equal'; import type { - MapboxMap, - AnySourceData, + MapInstance, + AnySource, + CustomSource, GeoJSONSource, + GeoJSONSourceImplementation, ImageSource, - VideoSource, - AnySourceImpl + ImageSourceImplemtation, + VectorSource, + AnySourceImplementation } from '../types'; -export type SourceProps = AnySourceData & { +export type SourceProps = (AnySource | CustomSource) & { id?: string; children?: any; }; let sourceCounter = 0; -function createSource(map: MapboxMap, id: string, props: SourceProps) { +function createSource(map: MapInstance, id: string, props: SourceProps) { // @ts-ignore if (map.style && map.style._loaded) { const options = {...props}; @@ -35,7 +38,7 @@ function createSource(map: MapboxMap, id: string, props: SourceProps) { } /* eslint-disable complexity */ -function updateSource(source: AnySourceImpl, props: SourceProps, prevProps: SourceProps) { +function updateSource(source: AnySourceImplementation, props: SourceProps, prevProps: SourceProps) { assert(props.id === prevProps.id, 'source id changed'); assert(props.type === prevProps.type, 'source type changed'); @@ -56,26 +59,24 @@ function updateSource(source: AnySourceImpl, props: SourceProps, prevProps: Sour const type = props.type; if (type === 'geojson') { - // @ts-expect-error there seems to be mismatch between GeoJSONSource.setData and GeoJSONSourceRaw.data - (source as GeoJSONSource).setData(props.data); + (source as GeoJSONSourceImplementation).setData((props as GeoJSONSource).data as any); } else if (type === 'image') { - (source as ImageSource).updateImage({url: props.url, coordinates: props.coordinates}); - } else if ( - (type === 'canvas' || type === 'video') && - changedKeyCount === 1 && - changedKey === 'coordinates' - ) { - (source as VideoSource).setCoordinates(props.coordinates); - } else if (type === 'vector' && 'setUrl' in source) { + (source as ImageSourceImplemtation).updateImage({ + url: props.url, + coordinates: props.coordinates + }); + } else if ('setCoordinates' in source && changedKeyCount === 1 && changedKey === 'coordinates') { + source.setCoordinates((props as unknown as ImageSource).coordinates); + } else if ('setUrl' in source) { // Added in 1.12.0: // vectorTileSource.setTiles // vectorTileSource.setUrl switch (changedKey) { case 'url': - source.setUrl(props.url); + source.setUrl((props as unknown as VectorSource).url); break; case 'tiles': - source.setTiles(props.tiles); + source.setTiles((props as unknown as VectorSource).tiles); break; default: } @@ -87,7 +88,7 @@ function updateSource(source: AnySourceImpl, props: SourceProps, prevProps: Sour /* eslint-enable complexity */ function Source(props: SourceProps) { - const map: MapboxMap = useContext(MapContext).map.getMap(); + const map = useContext(MapContext).map.getMap(); const propsRef = useRef(props); const [, setStyleLoaded] = useState(0); @@ -95,6 +96,7 @@ function Source(props: SourceProps) { useEffect(() => { if (map) { + /* global setTimeout */ const forceUpdate = () => setTimeout(() => setStyleLoaded(version => version + 1), 0); map.on('styledata', forceUpdate); forceUpdate(); diff --git a/src/components/use-map.tsx b/src/components/use-map.tsx index fb13d4ff..c8e02064 100644 --- a/src/components/use-map.tsx +++ b/src/components/use-map.tsx @@ -3,19 +3,20 @@ import {useState, useCallback, useMemo, useContext} from 'react'; import {MapRef} from '../mapbox/create-ref'; import {MapContext} from './map'; +import {MapInstance} from '../types'; type MountedMapsContextValue = { - maps: {[id: string]: MapRef}; - onMapMount: (map: MapRef, id: string) => void; + maps: {[id: string]: MapRef}; + onMapMount: (map: MapRef, id: string) => void; onMapUnmount: (id: string) => void; }; export const MountedMapsContext = React.createContext(null); export const MapProvider: React.FC<{children?: React.ReactNode}> = props => { - const [maps, setMaps] = useState<{[id: string]: MapRef}>({}); + const [maps, setMaps] = useState<{[id: string]: MapRef}>({}); - const onMapMount = useCallback((map: MapRef, id: string = 'default') => { + const onMapMount = useCallback((map: MapRef, id: string = 'default') => { setMaps(currMaps => { if (id === 'current') { throw new Error("'current' cannot be used as map id"); @@ -51,7 +52,7 @@ export const MapProvider: React.FC<{children?: React.ReactNode}> = props => { ); }; -export function useMap(): {current?: MapRef; [id: string]: MapRef | undefined} { +export function useMap() { const maps = useContext(MountedMapsContext)?.maps; const currentMap = useContext(MapContext); @@ -59,5 +60,5 @@ export function useMap(): {current?: MapRef; [id: string]: MapRef | undefined} { return {...maps, current: currentMap?.map}; }, [maps, currentMap]); - return mapsWithCurrent; + return mapsWithCurrent as {current?: MapRef; [id: string]: MapRef | undefined}; } diff --git a/src/exports-mapbox.ts b/src/exports-mapbox.ts new file mode 100644 index 00000000..2d8509f9 --- /dev/null +++ b/src/exports-mapbox.ts @@ -0,0 +1,104 @@ +import * as React from 'react'; +import type { + Map as MapboxMap, + MapboxOptions, + Marker as MapboxMarker, + MarkerOptions, + Popup as MapboxPopup, + PopupOptions, + AttributionControl as MapboxAttributionControl, + FullscreenControl as MapboxFullscreenControl, + GeolocateControl as MapboxGeolocateControl, + NavigationControl as MapboxNavigationControl, + ScaleControl as MapboxScaleControl +} from 'mapbox-gl'; + +import {default as _Map, MapProps as _MapProps} from './components/map'; +import {default as _Marker, MarkerProps as _MarkerProps} from './components/marker'; +import {default as _Popup, PopupProps as _PopupProps} from './components/popup'; +import { + default as _AttributionControl, + AttributionControlProps as _AttributionControlProps +} from './components/attribution-control'; +import { + default as _FullscreenControl, + FullscreenControlProps as _FullscreenControlProps +} from './components/fullscreen-control'; +import { + default as _GeolocateControl, + GeolocateControlProps as _GeolocateControlProps +} from './components/geolocate-control'; +import { + default as _NavigationControl, + NavigationControlProps as _NavigationControlProps +} from './components/navigation-control'; +import { + default as _ScaleControl, + ScaleControlProps as _ScaleControlProps +} from './components/scale-control'; +import {useMap as _useMap} from './components/use-map'; +import type {MapRef as _MapRef} from './mapbox/create-ref'; + +export function useMap() { + return _useMap(); +} + +export type MapProps = _MapProps; +export type MapRef = _MapRef; +const mapLib = import('mapbox-gl'); +export const Map = (() => { + return React.forwardRef(function Map(props: MapProps, ref: React.Ref) { + return _Map(props, ref, mapLib); + }); +})(); + +export type MarkerProps = _MarkerProps; +export const Marker = _Marker as (props: MarkerProps) => React.ReactElement | null; + +export type PopupProps = _PopupProps; +export const Popup = _Popup as (props: PopupProps) => React.ReactElement | null; + +type AttributionControlOptions = ConstructorParameters[0]; +export type AttributionControlProps = _AttributionControlProps; +export const AttributionControl = _AttributionControl as ( + props: AttributionControlProps +) => React.ReactElement | null; + +type FullscreenControlOptions = ConstructorParameters[0]; +export type FullscreenControlProps = _FullscreenControlProps; +export const FullscreenControl = _FullscreenControl as ( + props: FullscreenControlProps +) => React.ReactElement | null; + +type NavigationControlOptions = ConstructorParameters[0]; +export type NavigationControlProps = _NavigationControlProps; +export const NavigationControl = _NavigationControl as ( + props: NavigationControlProps +) => React.ReactElement | null; + +type GeolocateControlOptions = ConstructorParameters[0]; +export type GeolocateControlProps = _GeolocateControlProps< + GeolocateControlOptions, + MapboxGeolocateControl +>; +export const GeolocateControl = _GeolocateControl as ( + props: GeolocateControlProps & React.RefAttributes +) => React.ReactElement | null; + +type ScaleControlOptions = ConstructorParameters[0]; +export type ScaleControlProps = _ScaleControlProps; +export const ScaleControl = _ScaleControl as ( + props: ScaleControlProps +) => React.ReactElement | null; + +export {default as Source} from './components/source'; +export {default as Layer} from './components/layer'; +export {default as useControl} from './components/use-control'; +export {MapProvider} from './components/use-map'; + +export default Map; + +// Types +export * from './types/public'; +export type {SourceProps} from './components/source'; +export type {LayerProps} from './components/layer'; diff --git a/src/exports-maplibre.ts b/src/exports-maplibre.ts new file mode 100644 index 00000000..194871b4 --- /dev/null +++ b/src/exports-maplibre.ts @@ -0,0 +1,104 @@ +import * as React from 'react'; +import type { + Map as MaplibreMap, + MapOptions, + Marker as MaplibreMarker, + MarkerOptions, + Popup as MaplibrePopup, + PopupOptions, + AttributionControl as MaplibreAttributionControl, + FullscreenControl as MaplibreFullscreenControl, + GeolocateControl as MaplibreGeolocateControl, + NavigationControl as MaplibreNavigationControl, + ScaleControl as MaplibreScaleControl +} from 'maplibre-gl'; + +import {default as _Map, MapProps as _MapProps} from './components/map'; +import {default as _Marker, MarkerProps as _MarkerProps} from './components/marker'; +import {default as _Popup, PopupProps as _PopupProps} from './components/popup'; +import { + default as _AttributionControl, + AttributionControlProps as _AttributionControlProps +} from './components/attribution-control'; +import { + default as _FullscreenControl, + FullscreenControlProps as _FullscreenControlProps +} from './components/fullscreen-control'; +import { + default as _GeolocateControl, + GeolocateControlProps as _GeolocateControlProps +} from './components/geolocate-control'; +import { + default as _NavigationControl, + NavigationControlProps as _NavigationControlProps +} from './components/navigation-control'; +import { + default as _ScaleControl, + ScaleControlProps as _ScaleControlProps +} from './components/scale-control'; +import {useMap as _useMap} from './components/use-map'; +import type {MapRef as _MapRef} from './mapbox/create-ref'; + +export function useMap() { + return _useMap(); +} + +export type MapProps = _MapProps; +export type MapRef = _MapRef; +const mapLib = import('maplibre-gl'); +export const Map = (() => { + return React.forwardRef(function Map(props: MapProps, ref: React.Ref) { + return _Map(props, ref, mapLib); + }); +})(); + +export type MarkerProps = _MarkerProps; +export const Marker = _Marker as (props: MarkerProps) => React.ReactElement | null; + +export type PopupProps = _PopupProps; +export const Popup = _Popup as (props: PopupProps) => React.ReactElement | null; + +type AttributionControlOptions = ConstructorParameters[0]; +export type AttributionControlProps = _AttributionControlProps; +export const AttributionControl = _AttributionControl as ( + props: AttributionControlProps +) => React.ReactElement | null; + +type FullscreenControlOptions = ConstructorParameters[0]; +export type FullscreenControlProps = _FullscreenControlProps; +export const FullscreenControl = _FullscreenControl as ( + props: FullscreenControlProps +) => React.ReactElement | null; + +type NavigationControlOptions = ConstructorParameters[0]; +export type NavigationControlProps = _NavigationControlProps; +export const NavigationControl = _NavigationControl as ( + props: NavigationControlProps +) => React.ReactElement | null; + +type GeolocateControlOptions = ConstructorParameters[0]; +export type GeolocateControlProps = _GeolocateControlProps< + GeolocateControlOptions, + MaplibreGeolocateControl +>; +export const GeolocateControl = _GeolocateControl as ( + props: GeolocateControlProps & React.RefAttributes +) => React.ReactElement | null; + +type ScaleControlOptions = ConstructorParameters[0]; +export type ScaleControlProps = _ScaleControlProps; +export const ScaleControl = _ScaleControl as ( + props: ScaleControlProps +) => React.ReactElement | null; + +export {default as Source} from './components/source'; +export {default as Layer} from './components/layer'; +export {default as useControl} from './components/use-control'; +export {MapProvider} from './components/use-map'; + +export default Map; + +// Types +export * from './types/public'; +export type {SourceProps} from './components/source'; +export type {LayerProps} from './components/layer'; diff --git a/src/index.ts b/src/index.ts index 5b4c31f2..0a9a29f5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,2 @@ -export {default} from './components/map'; - -export {default as Map} from './components/map'; -export {default as Marker} from './components/marker'; -export {default as Popup} from './components/popup'; -export {default as AttributionControl} from './components/attribution-control'; -export {default as FullscreenControl} from './components/fullscreen-control'; -export {default as GeolocateControl} from './components/geolocate-control'; -export {default as NavigationControl} from './components/navigation-control'; -export {default as ScaleControl} from './components/scale-control'; -export {default as Source} from './components/source'; -export {default as Layer} from './components/layer'; -export {default as useControl} from './components/use-control'; -export {MapProvider, useMap} from './components/use-map'; - -// Types -export * from './types/external'; -export type {MapProps} from './components/map'; -export type {MapRef} from './mapbox/create-ref'; -export type {MarkerProps} from './components/marker'; -export type {PopupProps} from './components/popup'; -export type {AttributionControlProps} from './components/attribution-control'; -export type {FullscreenControlProps} from './components/fullscreen-control'; -export type {GeolocateControlProps, GeolocateControlRef} from './components/geolocate-control'; -export type {NavigationControlProps} from './components/navigation-control'; -export type {ScaleControlProps} from './components/scale-control'; -export type {SourceProps} from './components/source'; -export type {LayerProps} from './components/layer'; +export * from './exports-mapbox'; +export {default as default} from './exports-mapbox'; diff --git a/src/mapbox/create-ref.ts b/src/mapbox/create-ref.ts index 17b1002d..0418390a 100644 --- a/src/mapbox/create-ref.ts +++ b/src/mapbox/create-ref.ts @@ -1,4 +1,4 @@ -import type {MapboxMap, LngLatLike, PointLike, ElevationQueryOptions} from '../types'; +import type {MapInstance, MapInstanceInternal, LngLatLike, PointLike} from '../types'; import type Mapbox from './mapbox'; /** These methods may break the react binding if called directly */ @@ -25,16 +25,18 @@ const skipMethods = [ 'remove' ] as const; -export type MapRef = { - getMap(): MapboxMap; -} & Omit; +export type MapRef = { + getMap(): MapT; +} & Omit; -export default function createRef(mapInstance: Mapbox, mapLib: any): MapRef { +export default function createRef( + mapInstance: Mapbox +): MapRef { if (!mapInstance) { return null; } - const map: MapboxMap = mapInstance.map; + const map = mapInstance.map as MapInstanceInternal; const result: any = { getMap: () => map, @@ -46,18 +48,24 @@ export default function createRef(mapInstance: Mapbox, mapLib: any): MapRef { getPadding: () => mapInstance.transform.padding, getBounds: () => mapInstance.transform.getBounds(), project: (lnglat: LngLatLike) => { - return mapInstance.transform.locationPoint(mapLib.LngLat.convert(lnglat)); + const tr = map.transform; + map.transform = mapInstance.transform; + const result = map.project(lnglat); + map.transform = tr; + return result; }, unproject: (point: PointLike) => { - return mapInstance.transform.pointLocation(mapLib.Point.convert(point)); - }, - queryTerrainElevation: (lnglat: LngLatLike, options: ElevationQueryOptions) => { - // @ts-ignore transform not defined const tr = map.transform; - // @ts-ignore transform not defined + map.transform = mapInstance.transform; + const result = map.unproject(point); + map.transform = tr; + return result; + }, + // options diverge between mapbox and maplibre + queryTerrainElevation: (lnglat: LngLatLike, options?: any) => { + const tr = map.transform; map.transform = mapInstance.transform; const result = map.queryTerrainElevation(lnglat, options); - // @ts-ignore transform not defined map.transform = tr; return result; } diff --git a/src/mapbox/mapbox.ts b/src/mapbox/mapbox.ts index 7214daf4..758eb772 100644 --- a/src/mapbox/mapbox.ts +++ b/src/mapbox/mapbox.ts @@ -4,20 +4,17 @@ import {deepEqual} from '../utils/deep-equal'; import type { Transform, - Projection, ViewState, ViewStateChangeEvent, - DragPanOptions, - ZoomRotateOptions, - TransformRequestFunction, + Point, + PointLike, + PaddingOptions, Light, Fog, - Point, - TerrainSpecification, + Terrain, MapboxStyle, ImmutableLike, LngLatBoundsLike, - FitBoundsOptions, MapMouseEvent, MapLayerMouseEvent, MapLayerTouchEvent, @@ -25,13 +22,14 @@ import type { MapBoxZoomEvent, MapStyleDataEvent, MapSourceDataEvent, - MapboxEvent, + MapEvent, ErrorEvent, - MapboxGeoJSONFeature, - MapboxMap + MapGeoJSONFeature, + MapInstance, + MapInstanceInternal } from '../types'; -export type MapboxProps = Partial & { +export type MapboxProps = Partial & { // Init options mapboxAccessToken?: string; @@ -40,209 +38,17 @@ export type MapboxProps = Partial & { /** The initial bounds of the map. If bounds is specified, it overrides longitude, latitude and zoom options. */ bounds?: LngLatBoundsLike; /** A fitBounds options object to use only when setting the bounds option. */ - fitBoundsOptions?: FitBoundsOptions; + fitBoundsOptions?: { + offset?: PointLike; + minZoom?: number; + maxZoom?: number; + padding?: number | PaddingOptions; + }; }; /** If provided, render into an external WebGL context */ gl?: WebGLRenderingContext; - /** - * If true, the gl context will be created with MSA antialiasing, which can be useful for antialiasing custom layers. - * This is false by default as a performance optimization. - * @default false - */ - antialias?: boolean; - /** - * If true, an attribution control will be added to the map. - * @default true - */ - attributionControl?: boolean; - /** - * Snap to north threshold in degrees. - * @default 7 - */ - bearingSnap?: number; - /** - * The max number of pixels a user can shift the mouse pointer during a click for it to be - * considered a valid click (as opposed to a mouse drag). - * @default 3 - */ - clickTolerance?: number; - /** - * If `true`, Resource Timing API information will be collected for requests made by GeoJSON - * and Vector Tile web workers (this information is normally inaccessible from the main - * Javascript thread). Information will be returned in a `resourceTiming` property of - * relevant `data` events. - * @default false - */ - collectResourceTiming?: boolean; - /** - * If `true` , scroll zoom will require pressing the ctrl or ⌘ key while scrolling to zoom map, - * and touch pan will require using two fingers while panning to move the map. - * Touch pitch will require three fingers to activate if enabled. - */ - cooperativeGestures?: boolean; - /** - * If `true`, symbols from multiple sources can collide with each other during collision - * detection. If `false`, collision detection is run separately for the symbols in each source. - * @default true - */ - crossSourceCollisions?: boolean; - /** String or strings to show in an AttributionControl. - * Only applicable if options.attributionControl is `true`. */ - customAttribution?: string | string[]; - /** - * Controls the duration of the fade-in/fade-out animation for label collisions, in milliseconds. - * This setting affects all symbol layers. This setting does not affect the duration of runtime - * styling transitions or raster tile cross-fading. - * @default 300 - */ - fadeDuration?: number; - /** If true, map creation will fail if the implementation determines that the performance of the created WebGL context would be dramatically lower than expected. - * @default false - */ - failIfMajorPerformanceCaveat?: boolean; - /** If `true`, the map's position (zoom, center latitude, center longitude, bearing, and pitch) will be synced with the hash fragment of the page's URL. - * For example, `http://path/to/my/page.html#2.59/39.26/53.07/-24.1/60`. - * An additional string may optionally be provided to indicate a parameter-styled hash, - * e.g. http://path/to/my/page.html#map=2.59/39.26/53.07/-24.1/60&foo=bar, where foo - * is a custom parameter and bar is an arbitrary hash distinct from the map hash. - */ - hash?: boolean | string; - /** If false, no mouse, touch, or keyboard listeners are attached to the map, so it will not respond to input - * @default true - */ - interactive?: boolean; - /** A patch to apply to the default localization table for UI strings, e.g. control tooltips. - * The `locale` object maps namespaced UI string IDs to translated strings in the target language; - * see `src/ui/default_locale.js` for an example with all supported string IDs. - * The object may specify all UI strings (thereby adding support for a new translation) or - * only a subset of strings (thereby patching the default translation table). - */ - locale?: {[key: string]: string}; - /** - * Overrides the generation of all glyphs and font settings except font-weight keywords - * Also overrides localIdeographFontFamily - * @default null - */ - localFontFamily?: string; - /** - * If specified, defines a CSS font-family for locally overriding generation of glyphs in the - * 'CJK Unified Ideographs' and 'Hangul Syllables' ranges. In these ranges, font settings from - * the map's style will be ignored, except for font-weight keywords (light/regular/medium/bold). - * The purpose of this option is to avoid bandwidth-intensive glyph server requests. - * @default "sans-serif" - */ - localIdeographFontFamily?: string; - /** - * A string representing the position of the Mapbox wordmark on the map. - * @default "bottom-left" - */ - logoPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; - /** - * The maximum number of tiles stored in the tile cache for a given source. If omitted, the - * cache will be dynamically sized based on the current viewport. - * @default null - */ - maxTileCacheSize?: number; - /** - * If true, map will prioritize rendering for performance by reordering layers - * If false, layers will always be drawn in the specified order - * @default true - */ - optimizeForTerrain?: boolean; - /** - * If `false`, the map's pitch (tilt) control with "drag to rotate" interaction will be disabled. - * @default true - */ - pitchWithRotate?: boolean; - /** If true, The maps canvas can be exported to a PNG using map.getCanvas().toDataURL();. This is false by default as a performance optimization. - * @default false - */ - preserveDrawingBuffer?: boolean; - /** - * If `false`, the map won't attempt to re-request tiles once they expire per their HTTP - * `cacheControl`/`expires` headers. - * @default true - */ - refreshExpiredTiles?: boolean; - /** - * Allows for the usage of the map in automated tests without an accessToken with custom self-hosted test fixtures. - * @default null - */ - testMode?: boolean; - /** - * If true, the map will automatically resize when the browser window resizes - * @default true - */ - trackResize?: boolean; - /** - * A callback run before the Map makes a request for an external URL. The callback can be - * used to modify the url, set headers, or set the credentials property for cross-origin requests. - * @default null - */ - transformRequest?: TransformRequestFunction; - - // Handlers - - /** - * If true, enable the "box zoom" interaction (see BoxZoomHandler) - * @default true - */ - boxZoom?: boolean; - /** - * If true, enable the "double click to zoom" interaction (see DoubleClickZoomHandler). - * @default true - */ - doubleClickZoom?: boolean; - /** - * If `true`, the "drag to pan" interaction is enabled. - * An `Object` value is passed as options to {@link DragPanHandler#enable}. - * @default true - */ - dragPan?: boolean | DragPanOptions; - /** - * If true, enable the "drag to rotate" interaction (see DragRotateHandler). - * @default true - */ - dragRotate?: boolean; - /** - * If true, enable keyboard shortcuts (see KeyboardHandler). - * @default true - */ - keyboard?: boolean; - /** - * If `true`, the "scroll to zoom" interaction is enabled. - * An `Object` value is passed as options to {@link ScrollZoomHandler#enable}. - * @default true - */ - scrollZoom?: boolean | ZoomRotateOptions; - /** - * If `true`, the "drag to pitch" interaction is enabled. - * An `Object` value is passed as options to {@link TouchPitchHandler#enable}. - * @default true - */ - touchPitch?: boolean; - /** - * If `true`, the "pinch to rotate and zoom" interaction is enabled. - * An `Object` value is passed as options to {@link TouchZoomRotateHandler#enable}. - * @default true - */ - touchZoomRotate?: boolean | ZoomRotateOptions; - - // Constraints - - /** If set, the map is constrained to the given bounds. */ - maxBounds?: LngLatBoundsLike; - /** Maximum pitch of the map. */ - maxPitch?: number; - /** Maximum zoom of the map. */ - maxZoom?: number; - /** Minimum pitch of the map. */ - minPitch?: number; - /** Minimum zoom of the map. */ - minZoom?: number; - /** For external controller to override the camera state */ viewState?: ViewState & { width: number; @@ -252,82 +58,66 @@ export type MapboxProps = Partial & { // Styling /** Mapbox style */ - mapStyle?: string | MapboxStyle | ImmutableLike; + mapStyle?: string | MapboxStyle | ImmutableLike; /** Enable diffing when the map style changes * @default true */ styleDiffing?: boolean; - /** The fog property of the style. Must conform to the Fog Style Specification . - * If `undefined` is provided, removes the fog from the map. */ - fog?: Fog; - /** Light properties of the map. */ - light?: Light; - /** Terrain property of the style. Must conform to the Terrain Style Specification . - * If `undefined` is provided, removes terrain from the map. */ - terrain?: TerrainSpecification; + /** Default layers to query on pointer events */ interactiveLayerIds?: string[]; - /** The projection the map should be rendered in - * @default "mercator" - */ - projection?: Projection; - /** - * If `true`, multiple copies of the world will be rendered, when zoomed out. - * @default true - */ - renderWorldCopies?: boolean; /** CSS cursor */ cursor?: string; // Callbacks - onMouseDown?: (e: MapLayerMouseEvent) => void; - onMouseUp?: (e: MapLayerMouseEvent) => void; - onMouseOver?: (e: MapLayerMouseEvent) => void; - onMouseMove?: (e: MapLayerMouseEvent) => void; - onClick?: (e: MapLayerMouseEvent) => void; - onDblClick?: (e: MapLayerMouseEvent) => void; - onMouseEnter?: (e: MapLayerMouseEvent) => void; - onMouseLeave?: (e: MapLayerMouseEvent) => void; - onMouseOut?: (e: MapLayerMouseEvent) => void; - onContextMenu?: (e: MapLayerMouseEvent) => void; - onTouchStart?: (e: MapLayerTouchEvent) => void; - onTouchEnd?: (e: MapLayerTouchEvent) => void; - onTouchMove?: (e: MapLayerTouchEvent) => void; - onTouchCancel?: (e: MapLayerTouchEvent) => void; + onMouseDown?: (e: MapLayerMouseEvent) => void; + onMouseUp?: (e: MapLayerMouseEvent) => void; + onMouseOver?: (e: MapLayerMouseEvent) => void; + onMouseMove?: (e: MapLayerMouseEvent) => void; + onClick?: (e: MapLayerMouseEvent) => void; + onDblClick?: (e: MapLayerMouseEvent) => void; + onMouseEnter?: (e: MapLayerMouseEvent) => void; + onMouseLeave?: (e: MapLayerMouseEvent) => void; + onMouseOut?: (e: MapLayerMouseEvent) => void; + onContextMenu?: (e: MapLayerMouseEvent) => void; + onTouchStart?: (e: MapLayerTouchEvent) => void; + onTouchEnd?: (e: MapLayerTouchEvent) => void; + onTouchMove?: (e: MapLayerTouchEvent) => void; + onTouchCancel?: (e: MapLayerTouchEvent) => void; - onMoveStart?: (e: ViewStateChangeEvent) => void; - onMove?: (e: ViewStateChangeEvent) => void; - onMoveEnd?: (e: ViewStateChangeEvent) => void; - onDragStart?: (e: ViewStateChangeEvent) => void; - onDrag?: (e: ViewStateChangeEvent) => void; - onDragEnd?: (e: ViewStateChangeEvent) => void; - onZoomStart?: (e: ViewStateChangeEvent) => void; - onZoom?: (e: ViewStateChangeEvent) => void; - onZoomEnd?: (e: ViewStateChangeEvent) => void; - onRotateStart?: (e: ViewStateChangeEvent) => void; - onRotate?: (e: ViewStateChangeEvent) => void; - onRotateEnd?: (e: ViewStateChangeEvent) => void; - onPitchStart?: (e: ViewStateChangeEvent) => void; - onPitch?: (e: ViewStateChangeEvent) => void; - onPitchEnd?: (e: ViewStateChangeEvent) => void; + onMoveStart?: (e: ViewStateChangeEvent) => void; + onMove?: (e: ViewStateChangeEvent) => void; + onMoveEnd?: (e: ViewStateChangeEvent) => void; + onDragStart?: (e: ViewStateChangeEvent) => void; + onDrag?: (e: ViewStateChangeEvent) => void; + onDragEnd?: (e: ViewStateChangeEvent) => void; + onZoomStart?: (e: ViewStateChangeEvent) => void; + onZoom?: (e: ViewStateChangeEvent) => void; + onZoomEnd?: (e: ViewStateChangeEvent) => void; + onRotateStart?: (e: ViewStateChangeEvent) => void; + onRotate?: (e: ViewStateChangeEvent) => void; + onRotateEnd?: (e: ViewStateChangeEvent) => void; + onPitchStart?: (e: ViewStateChangeEvent) => void; + onPitch?: (e: ViewStateChangeEvent) => void; + onPitchEnd?: (e: ViewStateChangeEvent) => void; - onWheel?: (e: MapWheelEvent) => void; - onBoxZoomStart?: (e: MapBoxZoomEvent) => void; - onBoxZoomEnd?: (e: MapBoxZoomEvent) => void; - onBoxZoomCancel?: (e: MapBoxZoomEvent) => void; + onWheel?: (e: MapWheelEvent) => void; + onBoxZoomStart?: (e: MapBoxZoomEvent) => void; + onBoxZoomEnd?: (e: MapBoxZoomEvent) => void; + onBoxZoomCancel?: (e: MapBoxZoomEvent) => void; - onResize?: (e: MapboxEvent) => void; - onLoad?: (e: MapboxEvent) => void; - onRender?: (e: MapboxEvent) => void; - onIdle?: (e: MapboxEvent) => void; - onError?: (e: ErrorEvent) => void; - onRemove?: (e: MapboxEvent) => void; - onData?: (e: MapStyleDataEvent | MapSourceDataEvent) => void; - onStyleData?: (e: MapStyleDataEvent) => void; - onSourceData?: (e: MapSourceDataEvent) => void; + onResize?: (e: MapEvent) => void; + onLoad?: (e: MapEvent) => void; + onRender?: (e: MapEvent) => void; + onIdle?: (e: MapEvent) => void; + onError?: (e: ErrorEvent) => void; + onRemove?: (e: MapEvent) => void; + onData?: (e: MapStyleDataEvent | MapSourceDataEvent) => void; + onStyleData?: (e: MapStyleDataEvent) => void; + onSourceData?: (e: MapSourceDataEvent) => void; }; -const DEFAULT_STYLE = {version: 8, sources: {}, layers: []}; +const DEFAULT_STYLE = {version: 8, sources: {}, layers: []} as MapboxStyle; const pointerEvents = { mousedown: 'onMouseDown', @@ -377,7 +167,7 @@ const otherEvents = { sourcedata: 'onSourceData', error: 'onError' }; -const settingNames: (keyof MapboxProps)[] = [ +const settingNames = [ 'minZoom', 'maxZoom', 'minPitch', @@ -386,7 +176,7 @@ const settingNames: (keyof MapboxProps)[] = [ 'projection', 'renderWorldCopies' ]; -const handlerNames: (keyof MapboxProps)[] = [ +const handlerNames = [ 'scrollZoom', 'boxZoom', 'dragRotate', @@ -400,13 +190,12 @@ const handlerNames: (keyof MapboxProps)[] = [ /** * A wrapper for mapbox-gl's Map class */ -export default class Mapbox { - private _MapClass: typeof MapboxMap; - // mapboxgl.Map instance. Not using type here because we are accessing - // private members and methods - private _map: any = null; +export default class Mapbox { + private _MapClass: {new (options: any): MapInstance}; + // mapboxgl.Map instance + private _map: MapInstanceInternal = null; // User-supplied props - props: MapboxProps; + props: MapboxProps; // Mapbox map is stateful. // During method calls/user interactions, map.transform is mutated and @@ -419,7 +208,7 @@ export default class Mapbox { // Internal states private _internalUpdate: boolean = false; private _inRender: boolean = false; - private _hoveredFeatures: MapboxGeoJSONFeature[] = null; + private _hoveredFeatures: MapGeoJSONFeature[] = null; private _deferredEvents: { move: boolean; zoom: boolean; @@ -434,21 +223,25 @@ export default class Mapbox { static savedMaps: Mapbox[] = []; - constructor(MapClass: typeof MapboxMap, props: MapboxProps, container: HTMLDivElement) { + constructor( + MapClass: {new (options: any): MapInstance}, + props: MapboxProps, + container: HTMLDivElement + ) { this._MapClass = MapClass; this.props = props; this._initialize(container); } - get map(): MapboxMap { - return this._map as MapboxMap; + get map(): MapT { + return this._map; } get transform(): Transform { return this._renderTransform; } - setProps(props: MapboxProps) { + setProps(props: MapboxProps) { const oldProps = this.props; this.props = props; @@ -470,8 +263,11 @@ export default class Mapbox { } } - static reuse(props: MapboxProps, container: HTMLDivElement) { - const that = Mapbox.savedMaps.pop(); + static reuse( + props: MapboxProps, + container: HTMLDivElement + ): Mapbox { + const that = Mapbox.savedMaps.pop() as Mapbox; if (!that) { return null; } @@ -557,7 +353,7 @@ export default class Mapbox { }; } - const map: any = new this._MapClass(mapOptions); + const map = new this._MapClass(mapOptions) as MapInstanceInternal; // Props that are not part of constructor options if (viewState.padding) { map.setPadding(viewState.padding); @@ -582,6 +378,7 @@ export default class Mapbox { }; map.on('render', () => this._onAfterRepaint()); // Insert code into map's event pipeline + // eslint-disable-next-line @typescript-eslint/unbound-method const fireEvent = map.fire; map.fire = this._fireEvent.bind(this, fireEvent); @@ -621,7 +418,7 @@ export default class Mapbox { // render cycle, which is managed by Mapbox's animation loop. // This removes the synchronization issue caused by requestAnimationFrame. redraw() { - const map = this._map; + const map = this._map as any; // map._render will throw error if style does not exist // https://github.com/mapbox/mapbox-gl-js/blob/fb9fc316da14e99ff4368f3e4faa3888fb43c513 // /src/ui/map.js#L1834 @@ -647,7 +444,7 @@ export default class Mapbox { @param {object} nextProps @returns {bool} true if size has changed */ - _updateSize(nextProps: MapboxProps): boolean { + _updateSize(nextProps: MapboxProps): boolean { // Check if size is controlled const {viewState} = nextProps; if (viewState) { @@ -666,7 +463,7 @@ export default class Mapbox { @param {bool} triggerEvents - should fire camera events @returns {bool} true if anything is changed */ - _updateViewState(nextProps: MapboxProps, triggerEvents: boolean): boolean { + _updateViewState(nextProps: MapboxProps, triggerEvents: boolean): boolean { if (this._internalUpdate) { return false; } @@ -713,13 +510,14 @@ export default class Mapbox { @param {object} currProps @returns {bool} true if anything is changed */ - _updateSettings(nextProps: MapboxProps, currProps: MapboxProps): boolean { + _updateSettings(nextProps: MapboxProps, currProps: MapboxProps): boolean { const map = this._map; let changed = false; for (const propName of settingNames) { if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) { changed = true; - map[`set${propName[0].toUpperCase()}${propName.slice(1)}`](nextProps[propName]); + const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; + setter?.(nextProps[propName]); } } return changed; @@ -730,7 +528,7 @@ export default class Mapbox { @param {object} currProps @returns {bool} true if style is changed */ - _updateStyle(nextProps: MapboxProps, currProps: MapboxProps): boolean { + _updateStyle(nextProps: MapboxProps, currProps: MapboxProps): boolean { if (nextProps.cursor !== currProps.cursor) { this._map.getCanvas().style.cursor = nextProps.cursor; } @@ -740,6 +538,7 @@ export default class Mapbox { diff: styleDiffing }; if ('localIdeographFontFamily' in nextProps) { + // @ts-ignore Mapbox specific prop options.localIdeographFontFamily = nextProps.localIdeographFontFamily; } this._map.setStyle(normalizeStyle(mapStyle), options); @@ -753,19 +552,34 @@ export default class Mapbox { @param {object} currProps @returns {bool} true if anything is changed */ - _updateStyleComponents(nextProps: MapboxProps, currProps: MapboxProps): boolean { + _updateStyleComponents( + nextProps: MapboxProps & { + light?: Light; + fog?: Fog; + terrain?: Terrain; + }, + currProps: MapboxProps & { + light?: Light; + fog?: Fog; + terrain?: Terrain; + } + ): boolean { const map = this._map; let changed = false; - if (map.style.loaded()) { - if ('light' in nextProps && !deepEqual(nextProps.light, currProps.light)) { + if (map.isStyleLoaded()) { + if ('light' in nextProps && map.setLight && !deepEqual(nextProps.light, currProps.light)) { changed = true; map.setLight(nextProps.light); } - if ('fog' in nextProps && !deepEqual(nextProps.fog, currProps.fog)) { + if ('fog' in nextProps && map.setFog && !deepEqual(nextProps.fog, currProps.fog)) { changed = true; map.setFog(nextProps.fog); } - if ('terrain' in nextProps && !deepEqual(nextProps.terrain, currProps.terrain)) { + if ( + 'terrain' in nextProps && + map.setTerrain && + !deepEqual(nextProps.terrain, currProps.terrain) + ) { if (!nextProps.terrain || map.getSource(nextProps.terrain.source)) { changed = true; map.setTerrain(nextProps.terrain); @@ -780,7 +594,7 @@ export default class Mapbox { @param {object} currProps @returns {bool} true if anything is changed */ - _updateHandlers(nextProps: MapboxProps, currProps: MapboxProps): boolean { + _updateHandlers(nextProps: MapboxProps, currProps: MapboxProps): boolean { const map = this._map; let changed = false; for (const propName of handlerNames) { @@ -798,13 +612,13 @@ export default class Mapbox { return changed; } - _onEvent = (e: MapboxEvent) => { + _onEvent = (e: MapEvent) => { // @ts-ignore const cb = this.props[otherEvents[e.type]]; if (cb) { cb(e); } else if (e.type === 'error') { - console.error((e as ErrorEvent).error); // eslint-disable-line + console.error((e as ErrorEvent).error); // eslint-disable-line } }; @@ -821,7 +635,7 @@ export default class Mapbox { } } - _updateHover(e: MapMouseEvent) { + _updateHover(e: MapMouseEvent) { const {props} = this; const shouldTrackHoveredFeatures = props.interactiveLayerIds && (props.onMouseMove || props.onMouseEnter || props.onMouseLeave); @@ -847,7 +661,7 @@ export default class Mapbox { } } - _onPointerEvent = (e: MapLayerMouseEvent | MapLayerTouchEvent) => { + _onPointerEvent = (e: MapLayerMouseEvent | MapLayerTouchEvent) => { if (e.type === 'mousemove' || e.type === 'mouseout') { this._updateHover(e); } @@ -863,7 +677,7 @@ export default class Mapbox { } }; - _onCameraEvent = (e: ViewStateChangeEvent) => { + _onCameraEvent = (e: ViewStateChangeEvent) => { if (!this._internalUpdate) { // @ts-ignore const cb = this.props[cameraEvents[e.type]]; @@ -876,7 +690,7 @@ export default class Mapbox { } }; - _fireEvent(baseFire: Function, event: string | MapboxEvent, properties?: object) { + _fireEvent(baseFire: Function, event: string | MapEvent, properties?: object) { const map = this._map; const tr = map.transform; @@ -886,7 +700,7 @@ export default class Mapbox { } if (eventType in cameraEvents) { if (typeof event === 'object') { - (event as unknown as ViewStateChangeEvent).viewState = transformToViewState(tr); + (event as unknown as ViewStateChangeEvent).viewState = transformToViewState(tr); } if (this._map.isMoving()) { // Replace map.transform with ours during the callbacks diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 00000000..e1e8da6f --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,83 @@ +export type {IControl} from './lib'; + +/* Data types */ +export interface Point { + x: number; + y: number; +} +export type PointLike = Point | [number, number]; + +export interface LngLat { + lng: number; + lat: number; +} +export type LngLatLike = + | [number, number] + | LngLat + | {lng: number; lat: number} + | {lon: number; lat: number}; + +export interface LngLatBounds { + sw: LngLatLike; + ne: LngLatLike; + + contains(lnglat: LngLatLike): boolean; + + getCenter(): LngLat; + getSouthWest(): LngLat; + getNorthEast(): LngLat; + getNorthWest(): LngLat; + getSouthEast(): LngLat; + + getWest(): number; + getSouth(): number; + getEast(): number; + getNorth(): number; +} +export type LngLatBoundsLike = + | LngLatBounds + | [LngLatLike, LngLatLike] + | [number, number, number, number] + | LngLatLike; + +export type Anchor = + | 'center' + | 'left' + | 'right' + | 'top' + | 'bottom' + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right'; + +export type Alignment = 'map' | 'viewport' | 'auto'; + +export type PaddingOptions = { + top: number; + bottom: number; + left: number; + right: number; +}; + +/** Describes the camera's state */ +export type ViewState = { + /** Longitude at map center */ + longitude: number; + /** Latitude at map center */ + latitude: number; + /** Map zoom level */ + zoom: number; + /** Map rotation bearing in degrees counter-clockwise from north */ + bearing: number; + /** Map angle in degrees at which the camera is looking at the ground */ + pitch: number; + /** Dimensions in pixels applied on each side of the viewport for shifting the vanishing point. */ + padding: PaddingOptions; +}; + +export type ControlPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; + +export interface ImmutableLike { + toJS: () => T; +} diff --git a/src/types/events.ts b/src/types/events.ts new file mode 100644 index 00000000..26699961 --- /dev/null +++ b/src/types/events.ts @@ -0,0 +1,148 @@ +import type GeoJSON from 'geojson'; +import type {ViewState, Point, LngLat, LngLatBounds} from './common'; +import type { + MapInstance, + Evented, + MarkerInstance, + PopupInstance, + GeolocateControlInstance +} from './lib'; +import type {AnyLayer, AnySource} from './style-spec'; + +export type MapGeoJSONFeature = GeoJSON.Feature & { + layer: AnyLayer; + source: string; + sourceLayer: string; + state: {[key: string]: any}; +}; + +export interface MapEvent { + type: string; + target: SourceT; + originalEvent: OriginalEventT; +} + +export type ErrorEvent = MapEvent & { + type: 'error'; + error: Error; +}; + +export type MapStyleDataEvent = MapEvent & { + dataType: 'style'; +}; + +export type MapSourceDataEvent = MapEvent & { + dataType: 'source'; + isSourceLoaded: boolean; + source: AnySource; + sourceId: string; + sourceDataType: 'metadata' | 'content'; + tile: any; + coord: { + canonical: { + x: number; + y: number; + z: number; + key: number; + }; + wrap: number; + key: number; + }; +}; + +export type MapMouseEvent = MapEvent & { + type: + | 'mousedown' + | 'mouseup' + | 'click' + | 'dblclick' + | 'mousemove' + | 'mouseover' + | 'mouseenter' + | 'mouseleave' + | 'mouseout' + | 'contextmenu'; + + point: Point; + lngLat: LngLat; + + preventDefault(): void; + defaultPrevented: boolean; +}; + +export type MapLayerMouseEvent = MapMouseEvent & { + features?: MapGeoJSONFeature[] | undefined; +}; + +export type MapTouchEvent = MapEvent & { + type: 'touchstart' | 'touchend' | 'touchcancel'; + + point: Point; + lngLat: LngLat; + points: Point[]; + lngLats: LngLat[]; + + preventDefault(): void; + defaultPrevented: boolean; +}; + +export type MapLayerTouchEvent = MapTouchEvent & { + features?: MapGeoJSONFeature[] | undefined; +}; + +export type MapWheelEvent = MapEvent & { + type: 'wheel'; + + preventDefault(): void; + defaultPrevented: boolean; +}; + +export type MapBoxZoomEvent = MapEvent & { + type: 'boxzoomstart' | 'boxzoomend' | 'boxzoomcancel'; + + boxZoomBounds: LngLatBounds; +}; + +export type ViewStateChangeEvent = + | (MapEvent & { + type: 'movestart' | 'move' | 'moveend' | 'zoomstart' | 'zoom' | 'zoomend'; + viewState: ViewState; + }) + | (MapEvent & { + type: + | 'rotatestart' + | 'rotate' + | 'rotateend' + | 'dragstart' + | 'drag' + | 'dragend' + | 'pitchstart' + | 'pitch' + | 'pitchend'; + viewState: ViewState; + }); + +export type PopupEvent = { + type: 'open' | 'close'; + target: PopupT; +}; + +export type MarkerEvent = MapEvent< + MarkerT, + OriginalEventT +>; + +export type MarkerDragEvent = MarkerEvent & { + type: 'dragstart' | 'drag' | 'dragend'; + target: MarkerT; + lngLat: LngLat; +}; + +export type GeolocateEvent = + MapEvent; + +export type GeolocateResultEvent = + GeolocateEvent & GeolocationPosition; + +export type GeolocateErrorEvent = + GeolocateEvent & GeolocationPositionError; diff --git a/src/types/external.ts b/src/types/external.ts deleted file mode 100644 index 1d238af4..00000000 --- a/src/types/external.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type {PaddingOptions, MapboxEvent, Popup, Marker, GeolocateControl, LngLat} from 'mapbox-gl'; - -/** Describes the camera's state */ -export type ViewState = { - /** Longitude at map center */ - longitude: number; - /** Latitude at map center */ - latitude: number; - /** Map zoom level */ - zoom: number; - /** Map rotation bearing in degrees counter-clockwise from north */ - bearing: number; - /** Map angle in degrees at which the camera is looking at the ground */ - pitch: number; - /** Dimensions in pixels applied on each side of the viewport for shifting the vanishing point. */ - padding: PaddingOptions; -}; - -export type ControlPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; - -export interface ImmutableLike { - toJS: () => any; -} - -/* Events */ - -export type ViewStateChangeEvent = - | (MapboxEvent & { - type: 'movestart' | 'move' | 'moveend' | 'zoomstart' | 'zoom' | 'zoomend'; - viewState: ViewState; - }) - | (MapboxEvent & { - type: - | 'rotatestart' - | 'rotate' - | 'rotateend' - | 'dragstart' - | 'drag' - | 'dragend' - | 'pitchstart' - | 'pitch' - | 'pitchend'; - viewState: ViewState; - }); - -export type PopupEvent = { - type: 'open' | 'close'; - target: Popup; -}; - -export type MarkerDragEvent = { - type: 'dragstart' | 'drag' | 'dragend'; - target: Marker; - lngLat: LngLat; -}; - -export type GeolocateEvent = { - type: string; - target: GeolocateControl; -}; - -export type GeolocateResultEvent = GeolocateEvent & GeolocationPosition; - -export type GeolocateErrorEvent = GeolocateEvent & GeolocationPositionError; - -/* re-export mapbox types */ - -export type { - Point, - PointLike, - LngLat, - LngLatLike, - LngLatBounds, - LngLatBoundsLike, - Anchor, - Alignment, - PaddingOptions, - PositionOptions, - FitBoundsOptions, - DragPanOptions, - InteractiveOptions as ZoomRotateOptions, - TransformRequestFunction, - Projection, - Light, - Fog, - TerrainSpecification, - Style as MapboxStyle, - BackgroundLayer, - CircleLayer, - FillExtrusionLayer, - FillLayer, - HeatmapLayer, - HillshadeLayer, - LineLayer, - RasterLayer, - SymbolLayer, - CustomLayerInterface, - SkyLayer, - GeoJSONSourceRaw, - VideoSourceRaw, - ImageSourceRaw, - CanvasSourceRaw, - GeoJSONSource, - VideoSource, - ImageSource, - CanvasSource, - VectorSourceImpl as VectorTileSource, - VectorSource as VectorSourceRaw, - RasterSource, - RasterDemSource, - MapLayerMouseEvent, - MapLayerTouchEvent, - MapBoxZoomEvent, - MapWheelEvent, - MapStyleDataEvent, - MapSourceDataEvent, - MapboxEvent, - ErrorEvent, - MapboxGeoJSONFeature, - IControl, - Map as MapboxMap -} from 'mapbox-gl'; diff --git a/src/types/index.ts b/src/types/index.ts index 44797dbb..570e91dc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,43 +1,55 @@ -import {PaddingOptions, LngLat, Point, LngLatBounds} from 'mapbox-gl'; +export * from './public'; -export * from './external'; +import type GeoJSON from 'geojson'; +import type {CustomSourceImplementation} from './lib'; +import type {ImageSource} from './style-spec'; -// re-export mapbox types -export type { - AnyLayer, - AnySourceData, - AnySourceImpl, - MapMouseEvent, - Marker as MapboxMarker, - Popup as MapboxPopup, - AttributionControl as MapboxAttributionControl, - FullscreenControl as MapboxFullscreenControl, - GeolocateControl as MapboxGeolocateControl, - NavigationControl as MapboxNavigationControl, - ScaleControl as MapboxScaleControl, - ElevationQueryOptions -} from 'mapbox-gl'; +// Internal: source implementations -/** - * Stub for mapbox's Transform class - * https://github.com/mapbox/mapbox-gl-js/blob/main/src/geo/transform.js - */ -export type Transform = { - width: number; - height: number; - center: LngLat; - zoom: number; - bearing: number; - pitch: number; - padding: PaddingOptions; - elevation: any; - pixelsToGLUnits: [number, number]; - cameraElevationReference: 'ground' | 'sea'; +export interface GeoJSONSourceImplementation { + type: 'geojson'; + setData( + data: GeoJSON.Feature | GeoJSON.FeatureCollection | String + ): this; +} - clone: () => Transform; - resize: (width: number, height: number) => void; - isPaddingEqual: (value: PaddingOptions) => boolean; - getBounds: () => LngLatBounds; - locationPoint: (lngLat: LngLat) => Point; - pointLocation: (p: Point) => LngLat; -}; +export interface ImageSourceImplemtation { + type: 'image'; + updateImage(options: Omit): this; + setCoordinates(coordinates: number[][]): this; +} + +export interface CanvasSourceImplemtation { + type: 'canvas'; + play(): void; + pause(): void; + getCanvas(): HTMLCanvasElement; + setCoordinates(coordinates: number[][]): this; +} + +export interface VectorSourceImplementation { + type: 'vector'; + setTiles(tiles: ReadonlyArray): this; + setUrl(url: string): this; +} + +export interface RasterSourceImplementation { + type: 'raster' | 'raster-dem'; + setTiles(tiles: ReadonlyArray): this; + setUrl(url: string): this; +} + +export interface VideoSourceImplementation { + type: 'video'; + getVideo(): HTMLVideoElement; + setCoordinates(coordinates: number[][]): this; +} + +export type AnySourceImplementation = + | GeoJSONSourceImplementation + | VideoSourceImplementation + | ImageSourceImplemtation + | CanvasSourceImplemtation + | VectorSourceImplementation + | RasterSourceImplementation + | CustomSourceImplementation; diff --git a/src/types/lib.ts b/src/types/lib.ts new file mode 100644 index 00000000..24459137 --- /dev/null +++ b/src/types/lib.ts @@ -0,0 +1,278 @@ +import type {PaddingOptions, LngLat, Point, LngLatLike, PointLike} from './common'; + +export interface IControl { + onAdd(map: MapT): HTMLElement; + + onRemove(map: MapT): void; + + getDefaultPosition?: (() => string) | undefined; +} + +type Listener = (event?: any) => any; + +export interface Evented { + on(type: string, listener: Listener); + + off(type?: string | any, listener?: Listener); + + once(type: string, listener: Listener); +} + +/** + * A user-facing type that represents the minimal intersection between Mapbox.Map and Maplibre.Map + * User provided `mapLib.Map` is supposed to implement this interface + * Only losely typed for compatibility + */ +export interface MapInstance extends Evented { + // https://github.com/mapbox/mapbox-gl-js/issues/6522 + fire(type: string, properties?: {[key: string]: any}); + + addControl( + control: IControl, + position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' + ); + + removeControl(control: IControl); + + hasControl(control: IControl): boolean; + + resize(): this; + + queryRenderedFeatures(geometry?: any, options?: any): any[]; + + setStyle(style: any, options?: any); + + isMoving(): boolean; + + getStyle(): any; + + isStyleLoaded(): boolean | void; + + addSource(id: string, source: any); + + removeSource(id: string): this; + + getSource(id: string): any; + + addLayer(layer: any, before?: string); + + moveLayer(id: string, beforeId?: string); + + removeLayer(id: string); + + getLayer(id: string): any; + + setFilter(layer: string, filter?: any[] | boolean | null); + + setLayerZoomRange(layerId: string, minzoom: number, maxzoom: number); + + setPaintProperty(layer: string, name: string, value: any); + + setLayoutProperty(layer: string, name: string, value: any); + + project(lnglat: any): Point; + + unproject(point: any): LngLat; + + queryTerrainElevation?(lngLat: any, options?: any): number | null; + + getContainer(): HTMLElement; + + getCanvas(): HTMLCanvasElement; + + remove(): void; + + triggerRepaint(): void; + + setPadding(padding: PaddingOptions); + + fitBounds(bounds: any, options?: any); + + setFog?(fog: any); + + setLight?(options: any, lightOptions?: any); + + setTerrain?(terrain?: any); +} + +export interface MarkerInstance extends Evented { + addTo(map: MapInstance): this; + remove(): this; + + getLngLat(): LngLat; + setLngLat(lngLat: LngLatLike): this; + + getElement(): HTMLElement; + + setPopup(popup?: any): this; + getPopup(): any; + + getOffset(): PointLike; + setOffset(offset: PointLike): this; + + setDraggable(value: boolean): this; + isDraggable(): boolean; + + getRotation(): number; + setRotation(rotation: number): this; + + getRotationAlignment(): any; + setRotationAlignment(alignment: any): this; + + getPitchAlignment(): any; + setPitchAlignment(alignment: any): this; +} + +export interface PopupInstance extends Evented { + options?: any; + + addTo(map: MapInstance): this; + remove(): this; + + isOpen(): boolean; + + getLngLat(): LngLat; + setLngLat(lnglat: LngLatLike): this; + + getElement(): HTMLElement; + + setDOMContent(htmlNode: any): this; + + getMaxWidth(): any; + setMaxWidth(maxWidth: any): this; + + addClassName(className: string): void; + removeClassName(className: string): void; + + setOffset(offset?: any): this; +} + +export interface AttributionControlInstance extends IControl { + _container?: HTMLElement; +} + +export interface FullscreenControlInstance extends IControl { + _controlContainer?: HTMLElement; +} + +export interface GeolocateControlInstance extends IControl, Evented { + _container?: HTMLElement; + trigger(); +} + +export interface NavigationControlInstance extends IControl { + _container?: HTMLElement; +} + +export interface ScaleControlInstance extends IControl { + _container?: HTMLElement; + options?: any; + setUnit(unit: any): void; +} + +/** + * A user-facing type that represents the minimal intersection between Mapbox and Maplibre + * User provided `mapLib` is supposed to implement this interface + * Only losely typed for compatibility + */ +export interface MapLib { + supported?: (options: any) => boolean; + + Map: {new (options: any): MapT}; + + Marker: {new (...options: any[]): MarkerInstance}; + + Popup: {new (options: any): PopupInstance}; + + AttributionControl: {new (options: any): AttributionControlInstance}; + + FullscreenControl: {new (options: any): FullscreenControlInstance}; + + GeolocateControl: {new (options: any): GeolocateControlInstance}; + + NavigationControl: {new (options: any): NavigationControlInstance}; + + ScaleControl: {new (options: any): ScaleControlInstance}; +} + +/** + * Stub for mapbox's Transform class + * https://github.com/mapbox/mapbox-gl-js/blob/main/src/geo/transform.js + */ +export type Transform = { + width: number; + height: number; + center: LngLat; + zoom: number; + bearing: number; + pitch: number; + padding: PaddingOptions; + elevation: any; + pixelsToGLUnits: [number, number]; + cameraElevationReference: 'ground' | 'sea'; + + clone: () => Transform; + resize: (width: number, height: number) => void; + isPaddingEqual: (value: PaddingOptions) => boolean; + getBounds: () => any; + locationPoint: (lngLat: LngLat) => Point; + pointLocation: (p: Point) => LngLat; +}; + +export type MapInstanceInternal = MapT & { + transform: Transform; + + _render: Function; + + _renderTaskQueue: { + run: Function; + }; +}; + +// Custom layer +export interface CustomLayerInterface { + id: string; + type: 'custom'; + renderingMode?: '2d' | '3d'; + + onRemove?(map: MapInstance, gl: WebGLRenderingContext): void; + onAdd?(map: MapInstance, gl: WebGLRenderingContext): void; + + prerender?(gl: WebGLRenderingContext, matrix: number[]): void; + render(gl: WebGLRenderingContext, matrix: number[]): void; +} + +// Custom source + +export interface CustomSourceImplementation { + id: string; + type: 'custom'; + dataType: 'raster'; + minzoom?: number; + maxzoom?: number; + scheme?: string; + tileSize?: number; + attribution?: string; + bounds?: [number, number, number, number]; + hasTile?: (tileID: {z: number; x: number; y: number}) => boolean; + loadTile: ( + tileID: {z: number; x: number; y: number}, + options: {signal: AbortSignal} + ) => Promise; + prepareTile?: (tileID: {z: number; x: number; y: number}) => TileDataT | undefined; + unloadTile?: (tileID: {z: number; x: number; y: number}) => void; + onAdd?: (map: MapInstance) => void; + onRemove?: (map: MapInstance) => void; +} + +export interface CustomSource { + id: string; + type: 'custom'; + scheme: string; + minzoom: number; + maxzoom: number; + tileSize: number; + attribution: string; + + _implementation: CustomSourceImplementation; +} diff --git a/src/types/public.ts b/src/types/public.ts new file mode 100644 index 00000000..36192f3b --- /dev/null +++ b/src/types/public.ts @@ -0,0 +1,4 @@ +export * from './common'; +export * from './events'; +export * from './style-spec'; +export * from './lib'; diff --git a/src/types/style-spec.ts b/src/types/style-spec.ts new file mode 100644 index 00000000..fa3adcac --- /dev/null +++ b/src/types/style-spec.ts @@ -0,0 +1,127 @@ +/* + * Mapbox Style Specification types + * Note that these are NOT base map specific - all compatible map libraries implement the same spec + */ +import type { + FilterSpecification, + PropertyValueSpecification, + BackgroundLayerSpecification as BackgroundLayer, + CircleLayerSpecification as CircleLayer, + FillLayerSpecification as FillLayer, + FillExtrusionLayerSpecification as FillExtrusionLayer, + HeatmapLayerSpecification as HeatmapLayer, + HillshadeLayerSpecification as HillshadeLayer, + LineLayerSpecification as LineLayer, + RasterLayerSpecification as RasterLayer, + SymbolLayerSpecification as SymbolLayer, + GeoJSONSourceSpecification as GeoJSONSource, + VideoSourceSpecification as VideoSource, + ImageSourceSpecification as ImageSource, + VectorSourceSpecification as VectorSource, + RasterSourceSpecification as RasterSource, + RasterDEMSourceSpecification as RasterDemSource +} from '@maplibre/maplibre-gl-style-spec'; + +// Layers +export type { + BackgroundLayer, + CircleLayer, + FillLayer, + FillExtrusionLayer, + HeatmapLayer, + HillshadeLayer, + LineLayer, + RasterLayer, + SymbolLayer +}; + +/** + * @deprecated use `fog` instead + */ +export type SkyLayer = { + id: string; + type: 'sky'; + metadata?: unknown; + minzoom?: number; + maxzoom?: number; + filter?: FilterSpecification; + layout?: { + visibility?: 'visible' | 'none'; + }; + paint?: { + 'sky-atmosphere-color'?: PropertyValueSpecification; + 'sky-atmosphere-halo-color'?: PropertyValueSpecification; + 'sky-atmosphere-sun'?: PropertyValueSpecification; + 'sky-atmosphere-sun-intensity'?: PropertyValueSpecification; + 'sky-gradient'?: PropertyValueSpecification; + 'sky-gradient-center'?: PropertyValueSpecification; + 'sky-gradient-radius'?: PropertyValueSpecification; + 'sky-opacity'?: PropertyValueSpecification; + 'sky-type'?: 'gradient' | 'atmosphere'; + }; +}; + +export type AnyLayer = + | BackgroundLayer + | CircleLayer + | FillLayer + | FillExtrusionLayer + | HeatmapLayer + | HillshadeLayer + | LineLayer + | RasterLayer + | SymbolLayer + | SkyLayer; + +// Sources +export {GeoJSONSource, VideoSource, ImageSource, VectorSource, RasterSource, RasterDemSource}; + +// Not part of the style spec but a valid source +export interface CanvasSource { + type: 'canvas'; + coordinates: number[][]; + animate?: boolean; + canvas: string | HTMLCanvasElement; +} + +export type AnySource = + | GeoJSONSource + | VideoSource + | ImageSource + | VectorSource + | RasterSource + | RasterDemSource + | CanvasSource; + +// Other style types + +export type { + StyleSpecification as MapboxStyle, + LightSpecification as Light, + TerrainSpecification as Terrain +} from '@maplibre/maplibre-gl-style-spec'; + +// The following types are not yet supported by maplibre +export interface Fog { + color?: PropertyValueSpecification; + 'horizon-blend'?: PropertyValueSpecification; + range?: PropertyValueSpecification; +} + +type ProjectionNames = + | 'albers' + | 'equalEarth' + | 'equirectangular' + | 'lambertConformalConic' + | 'mercator' + | 'naturalEarth' + | 'winkelTripel' + | 'globe'; + +export type Projection = + | ProjectionNames + | { + name: ProjectionNames; + center?: [number, number]; + parallels?: [number, number]; + }; diff --git a/src/utils/style-utils.ts b/src/utils/style-utils.ts index 506623c0..ef4217da 100644 --- a/src/utils/style-utils.ts +++ b/src/utils/style-utils.ts @@ -5,7 +5,9 @@ const refProps = ['type', 'source', 'source-layer', 'minzoom', 'maxzoom', 'filte // Prepare a map style object for diffing // If immutable - convert to plain object // Work around some issues in older styles that would fail Mapbox's diffing -export function normalizeStyle(style: string | MapboxStyle | ImmutableLike): string | MapboxStyle { +export function normalizeStyle( + style: string | MapboxStyle | ImmutableLike +): string | MapboxStyle { if (!style) { return null; } @@ -13,7 +15,7 @@ export function normalizeStyle(style: string | MapboxStyle | ImmutableLike): str return style; } if ('toJS' in style) { - style = style.toJS() as MapboxStyle; + style = style.toJS(); } if (!style.layers) { return style; @@ -25,19 +27,21 @@ export function normalizeStyle(style: string | MapboxStyle | ImmutableLike): str } const layers = style.layers.map(layer => { - // @ts-expect-error - const layerRef = layerIndex[layer.ref]; - let normalizedLayer = null; + let normalizedLayer: typeof layer = null; if ('interactive' in layer) { - normalizedLayer = {...layer}; + normalizedLayer = Object.assign({}, layer); // Breaks style diffing :( + // @ts-ignore legacy field not typed delete normalizedLayer.interactive; } // Style diffing doesn't work with refs so expand them out manually before diffing. + // @ts-ignore legacy field not typed + const layerRef = layerIndex[layer.ref]; if (layerRef) { - normalizedLayer = normalizedLayer || {...layer}; + normalizedLayer = normalizedLayer || Object.assign({}, layer); + // @ts-ignore delete normalizedLayer.ref; // https://github.com/mapbox/mapbox-gl-js/blob/master/src/style-spec/deref.js for (const propName of refProps) { diff --git a/test/render/index.jsx b/test/render/index.jsx index 71be30fd..c1dfca62 100644 --- a/test/render/index.jsx +++ b/test/render/index.jsx @@ -99,9 +99,9 @@ test('Render test', async t => { goldenImage, region: boundingBox, tolerance: 0.05, - includeEmpty: false, + includeEmpty: false // Uncomment to save screenshot - saveOnFail: true + // saveOnFail: true }); error = result.error; diff --git a/tsconfig.es5.json b/tsconfig.es5.json index 1cb7e2c0..f1bb3c0c 100644 --- a/tsconfig.es5.json +++ b/tsconfig.es5.json @@ -3,6 +3,7 @@ "target": "es5", "jsx": "react", "moduleResolution": "node", + "allowSyntheticDefaultImports": true, "module": "commonjs", "downlevelIteration": true, "declaration": true, diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 2bf8ce7e..cdf82f26 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -3,6 +3,7 @@ "target": "es2018", "jsx": "react", "moduleResolution": "node", + "allowSyntheticDefaultImports": true, "module": "ES2020", "declaration": true, "sourceMap": true, diff --git a/yarn.lock b/yarn.lock index 71bb9577..a9adfd33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2138,10 +2138,10 @@ get-stream "^6.0.1" minimist "^1.2.6" -"@mapbox/jsonlint-lines-primitives@^2.0.2": +"@mapbox/jsonlint-lines-primitives@^2.0.2", "@mapbox/jsonlint-lines-primitives@~2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" - integrity sha1-zlblOfg1UrWNENZy6k1vya3HsjQ= + integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ== "@mapbox/mapbox-gl-supported@^2.0.1": version "2.0.1" @@ -2175,6 +2175,20 @@ resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz#497c67a1cef50d1a2459ba60f315e448d2ad87fe" integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q== +"@maplibre/maplibre-gl-style-spec@^19.2.1": + version "19.2.1" + resolved "https://registry.yarnpkg.com/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.2.1.tgz#eb78211151b93f4f9c0cf6cb908133dab7a438dd" + integrity sha512-ZVT5QlkVhlxlPav+ca0NO3Moc7EzbHDO2FXW4ic3Q0Vm+TDUw9I8A2EBws7xUUQZf7HQB3kQ+3Jsh5mFLRD4GQ== + dependencies: + "@mapbox/jsonlint-lines-primitives" "~2.0.2" + "@mapbox/point-geometry" "^0.1.0" + "@mapbox/unitbezier" "^0.0.1" + "@types/mapbox__point-geometry" "^0.1.2" + json-stringify-pretty-compact "^3.0.0" + minimist "^1.2.8" + rw "^1.3.3" + sort-object "^3.0.3" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -2413,7 +2427,7 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/mapbox-gl@^2.7.11": +"@types/mapbox-gl@>=1.0.0": version "2.7.11" resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-2.7.11.tgz#c9b9ed2ed24970aeef947609fdfcfcf826a3aa49" integrity sha512-4vSwPSTQIawZTFRiTY2R74aZwAiM9gE6KGj871xdyAPpa+DmEObXxQQXqL2PsMH31/rP9nxJ2Kv0boeTVJMXVw== @@ -3187,6 +3201,21 @@ byte-size@^5.0.1: resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-5.0.1.tgz#4b651039a5ecd96767e71a3d7ed380e48bed4191" integrity sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw== +bytewise-core@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/bytewise-core/-/bytewise-core-1.2.3.tgz#3fb410c7e91558eb1ab22a82834577aa6bd61d42" + integrity sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA== + dependencies: + typewise-core "^1.2" + +bytewise@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/bytewise/-/bytewise-1.1.0.tgz#1d13cbff717ae7158094aa881b35d081b387253e" + integrity sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ== + dependencies: + bytewise-core "^1.2.2" + typewise "^1.0.3" + c8@^7.12.0: version "7.13.0" resolved "https://registry.yarnpkg.com/c8/-/c8-7.13.0.tgz#a2a70a851278709df5a9247d62d7f3d4bcb5f2e4" @@ -5274,10 +5303,10 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" -get-value@^2.0.3, get-value@^2.0.6: +get-value@^2.0.2, get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== getpass@^0.1.1: version "0.1.7" @@ -6351,6 +6380,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json-stringify-pretty-compact@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz#f71ef9d82ef16483a407869556588e91b681d9ab" + integrity sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA== + json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -7002,7 +7036,7 @@ minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== -minimist@^1.2.6, minimist@~1.2.7: +minimist@^1.2.6, minimist@^1.2.8, minimist@~1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -8939,6 +8973,16 @@ socks@~2.3.2: ip "1.1.5" smart-buffer "^4.1.0" +sort-asc@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/sort-asc/-/sort-asc-0.2.0.tgz#00a49e947bc25d510bfde2cbb8dffda9f50eb2fc" + integrity sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA== + +sort-desc@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/sort-desc/-/sort-desc-0.2.0.tgz#280c1bdafc6577887cedbad1ed2e41c037976646" + integrity sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w== + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" @@ -8946,6 +8990,18 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" +sort-object@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sort-object/-/sort-object-3.0.3.tgz#945727165f244af9dc596ad4c7605a8dee80c269" + integrity sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ== + dependencies: + bytewise "^1.1.0" + get-value "^2.0.2" + is-extendable "^0.1.1" + sort-asc "^0.2.0" + sort-desc "^0.2.0" + union-value "^1.0.1" + source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -9722,6 +9778,18 @@ typescript@~4.6.0: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== +typewise-core@^1.2, typewise-core@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/typewise-core/-/typewise-core-1.2.0.tgz#97eb91805c7f55d2f941748fa50d315d991ef195" + integrity sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg== + +typewise@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typewise/-/typewise-1.0.3.tgz#1067936540af97937cc5dcf9922486e9fa284651" + integrity sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ== + dependencies: + typewise-core "^1.2.0" + uglify-js@^3.1.4: version "3.14.5" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.5.tgz#cdabb7d4954231d80cb4a927654c4655e51f4859" @@ -9788,7 +9856,7 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== -union-value@^1.0.0: +union-value@^1.0.0, union-value@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==