React MapboxGL rendering issue with function component - reactjs

I'm very new at programming and react. Currently trying to bring a layer to MapboxGL Map but I become error msg it says:
'mapContainer' is not defined no-undef
What am I doing wrong?
import React, {useEffect} from 'react';
import mapboxgl from 'mapbox-gl';
mapboxgl.accessToken = 'PUBLIC TOKEN'
const getMap = () => {
return new mapboxgl.Map({
container: mapContainer,
style: 'mapbox://styles/mapbox/streets-v11',
center: [9, 47],
zoom: 10
});
const Map = () => {
useEffect(() => {
const map = getMap();
map.on('move', () => {
this.setState({
lng: map.getCenter().lng.toFixed(4),
lat: map.getCenter().lat.toFixed(4),
zoom: map.getZoom().toFixed(0)
});
});
map.on('load', () => {
map.addLayer({
id: 'streets',
type: 'line',
source: {
type: 'geojson',
data:
'http://someWFSAPIdata=application/json'
},
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': '#08363e',
'line-width': 0.8
}
});
});
}, []);
And rendering as followed
return (
<div>
<div ref={el => (this.mapContainer = el)} className='mapContainer' />
<h1>Hello there geoReact</h1>
</div>
);
Since I'm using functional component no longer need to render but I'm not sure what is wrong with it.
Thanks a lot

I found this explanation which was very helpful for my case
https://sparkgeo.com/blog/build-a-react-mapboxgl-component-with-hooks/

Related

I can not render here map in react

I am try to integrate here map into our app but looks like it is getting an issue when rendering map view
This is my code
import React, { useLayoutEffect, useRef } from 'react';
import H from "#here/maps-api-for-javascript";
function MapView(): JSX.Element {
const ref = useRef();
useLayoutEffect(() => {
// `mapRef.current` will be `undefined` when this hook first runs; edge case that
if (!ref.current) return;
// instantiate a platform, default layers and a map as usual
const platform = new H.service.Platform({
apikey: process.env.REACT_APP_HERE_MAP_API_KEY,
});
const defaultLayers = platform.createDefaultLayers();
const mapView = new H.Map(
ref.current,
defaultLayers.vector.normal.map,
{
pixelRatio: window.devicePixelRatio || 1,
center: { lat: 0, lng: 0 },
zoom: 2,
},
);
// This will act as a cleanup to run once this hook runs again.
// This includes when the component un-mounts
return () => {
// eslint-disable-next-line #typescript-eslint/no-unsafe-call
mapView.dispose();
};
}, []);
return (
<div
style={{ width: '300px', height: '300px' }}
ref={ref}
/>
);
}
export default MapView;
I am using react 18 with typescript. Any idea to resolve this?

Add tiles from sentinel-hub to mapbox-gl

I tried to add tiles from sentinel-hub to mapbox-gl.
This is the first time I'm doing this and I didn't succeed.
import mapboxgl from 'mapbox-gl';
import "./Map.css"
mapboxgl.accessToken = 'your token';
export default class MapComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
lng: 7.56198,
lat: 47.47607,
zoom: 9,
}
}
componentDidMount() {
const { lat, lng, zoom } = this.state;
map = new mapboxgl.Map({
container: this.mapDiv,
style: 'mapbox://styles/mapbox/streets-v11',
center: [lng, lat],
zoom: zoom
});
map.on('load', () => {
map.addSource('portland', {
'type': 'raster',
'url': 'https://services.sentinel-hub.com/ogc/wmts/{INSTANCE_ID}?REQUEST=GetTile&bbox=bbox-epsg-3857&RESOLUTION=10&LAYER=TRUE-COLOR-S2L2A&TILEMATRIXSET=PopularWebMercator256&TILEMATRIX=10&TILEROW=307&TILECOL=775'
});
map.addLayer({
'id': 'portland',
'source': 'portland',
'type': 'raster'
});
});
};
render() {
return (
<div>
<div ref={e => this.mapDiv = e} className="map"></div>
<nav id="menu"></nav>
</div>
)
}
}
Maybe I need to do something with 'url' address or 'addSourse/addLayer'. Tell me, what is the error and how to fix it?

Changing color on layer React-Mapbox-gl

I am new to React-mapbox GL. I have tried for a while now to look at the examples but can't figure out how to change the layer's color on enter/hover. I have 2 questions so far.
map.on('mouseenter', 'clusters', () => {
map.getCanvas().style.cursor = 'pointer';
});
How can I define the the cluster element for each function in Reactmapbox gl? I don't quite understand how the interactiveLayerIds works I suppose?
question 2.
const onMouseEnter = useCallback(event =>{
if (event.features[0].layer.id==="unclustered-point"){
/* console.log(event.features[0].layer.paint.'circle-color') */
}
})
I have attempted this so far(the whole code is below) but it tells me that circle-color is a unexpected token. OnEnter this unclustered-point layer I want to change the color of the element so the user can clearly see what element they are hovering over? How would I go about doing this in React mapbox gl if I cant change the circle color?
THE WHOLE CODE:
import React, { useContext, useEffect, useRef,useState,useCallback } from 'react';
import './MapViews.css';
import { useNavigate } from 'react-router-dom';
import ReactMapGL, { Marker, Layer, Source } from 'react-map-gl';
import SourceFinder from '../../Apis/SourceFinder';
import { SourceContext } from '../../context/SourceContext';
import { clusterLayer, clusterCountLayer, unclusteredPointLayer } from './Layers';
const MapView = () => {
const navigate = useNavigate()
const { sources, setSources } = useContext(SourceContext)
const [viewport, setViewport] = React.useState({
longitude: 10.757933,
latitude: 59.91149,
zoom: 12,
bearing: 0,
pitch: 0
});
const mapRef = useRef(null);
function getCursor({isHovering, isDragging}) {
return isDragging ? 'grabbing' : isHovering ? 'pointer' : 'default';
}
useEffect(() => {
const fetchData = async () => {
try {
const response = await SourceFinder.get("/sources");
setSources(response.data.data.plass);
} catch (error) { }
};
fetchData();
}, [])
const onMouseEnter = useCallback(event =>{
if (event.features[0].layer.id==="unclustered-point"){
/* console.log(event.features[0].layer.paint.'circle-color') */
}
})
const ShowMore = event => {
if(event.features[0].layer.id==="unclustered-point"){
const feature = event.features[0];
console.log(feature)
mapRef.current.getMap().getCanvas().style.cursor="pointer"
}else{
const feature = event.features[0];
const clusterId = feature.properties.cluster_id;
const mapboxSource = mapRef.current.getMap().getSource('stasjoner');
mapboxSource.getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) {
return;
}
setViewport({
...viewport,
longitude: feature.geometry.coordinates[0],
latitude: feature.geometry.coordinates[1],
zoom,
transitionDuration: 500
});
});
}
};
return (
<ReactMapGL {...viewport} width="100%" height="100%" getCursor={getCursor} onMouseEnter={onMouseEnter} interactiveLayerIds={[clusterLayer.id,unclusteredPointLayer.id]} mapboxApiAccessToken={"SECRET"} clickRadius={2} onViewportChange={setViewport} mapStyle="mapbox://styles/mapbox/streets-v11" onClick={ShowMore} ref={mapRef}>
<Source id="stasjoner" type="geojson" data={sources} cluster={true} clusterMaxZoom={14} clusterRadius={50} >
<Layer {...clusterLayer} />
<Layer {...clusterCountLayer} />
<Layer {...unclusteredPointLayer}/>
</Source>
</ReactMapGL>
);
};
export default MapView;
LAYERS.JS
//Hvergang vi skal ha 2 eller flere baller
export const clusterLayer = {
id: 'clusters',
type: 'circle',
source: 'stasjoner',
filter: ['has', 'point_count'],
paint: {
'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 100, '#f1f075', 500, '#f28cb1'],
'circle-radius': ['step', ['get', 'point_count'], 20, 100, 30, 750, 40],
}
};
//Dette er tallene som er inne i ballene
export const clusterCountLayer = {
id: 'cluster-count',
type: 'symbol',
source: 'stasjoner',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12,
}
};
//Per punkt
export const unclusteredPointLayer = {
id: 'unclustered-point',
type: 'circle',
source: 'stasjoner',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 8,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff',
}
};

Mapbox map is getting loaded before useEffect returns data

I have started learning react hooks and I want to load map on initial render with cooridnates which I get from the backend, but somehow my map is getting rendered before my api give the data back,
how do I make sure my map will wait till data is return ? do I need to put any condition on that?
below is api code
import React, { useState, useEffect, useRef } from "react";
import { useDispatch, useSelector } from 'react-redux';
import mapboxgl from 'mapbox-gl';
import { getBikeInfo, mapDetails } from './features/counter/getInfo/getDetails.js'
function App() {
const dispatch = useDispatch();
const dataToDisplay = useSelector(mapDetails);
const mapContainer = useRef(null);
mapboxgl.accessToken = 'pk.eyJ1IjoidmloYW5nMTYiLCJhIjoiY2ttOHowc2ZhMWN2OTJvcXJ0dGpiY21pNyJ9.hK5Wxwby89E7tKWoBoY5bg';
useEffect(() => {
dispatch(getBikeInfo())
var map = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/light-v10',
center: [-96, 37.8],
zoom: 3
});
console.log('mapdetails:' + mapDetails)
console.log('data display:' + dataToDisplay)
map.on('load', function () {
// Add an image to use as a custom marker
map.loadImage(
'https://docs.mapbox.com/mapbox-gl-js/assets/custom_marker.png',
function (error, image) {
if (error) throw error;
map.addImage('custom-marker', image);
// Add a GeoJSON source with 2 points
map.addSource('points', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': dataToDisplay
}
});
// Add a symbol layer
map.addLayer({
'id': 'points',
'type': 'symbol',
'source': 'points',
'layout': {
'icon-image': 'custom-marker',
// get the title name from the source's "title" property
'text-field': ['get', 'title'],
'text-font': [
'Open Sans Semibold',
'Arial Unicode MS Bold'
],
'text-offset': [0, 1.25],
'text-anchor': 'top'
}
});
}
);
});
}, []);
return (
<div className="district-map-wrapper">
<div id="districtDetailMap" className="map">
<div style={{ height: "100%" }} ref={mapContainer}>
</div>
</div>
</div>
);
}
export default App;
below is my updated code:
useEffect(() => {
if (dataToDisplay.length != 0) {
loadtheMap
}
}, []);
but after some refresh I see data is not getting populated and setting dataDisplay to [].
update 1:
based on suggestion I have updated my code like this
if(!dataLoaded) { // do not render anything if you're not ready
return (
<div className="hidden">
<div id="districtDetailMap" className="map">
<div style={{ height: "100%" }} ref={mapContainer}>
</div>
</div>
</div>);
}
where className="hidden" is define in App.css like below
.hidden{
display: none;
}
but still I am on same issue
as requested PFB my sandbox link
codesandbox
Since you're using useEffect you are basically guaranteed that your component will render once before it even initiates the query
If you want to ensure your component won't be rendered, use some flag to indicate that data is ready, set it to true after you set up your Map and conditionally render some fallback ui while this flag is false
function App() {
const [dataLoaded, setDataLoaded] = useState(false); // introduce the flag
const dispatch = useDispatch();
const dataToDisplay = useSelector(mapDetails);
const mapContainer = useRef(null);
mapboxgl.accessToken = 'pk.eyJ1IjoidmloYW5nMTYiLCJhIjoiY2ttOHowc2ZhMWN2OTJvcXJ0dGpiY21pNyJ9.hK5Wxwby89E7tKWoBoY5bg';
useEffect(() => {
dispatch(getBikeInfo())
var map = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/light-v10',
center: [-96, 37.8],
zoom: 3
});
console.log('mapdetails:' + mapDetails)
console.log('data display:' + dataToDisplay)
map.on('load', function () {
// Add an image to use as a custom marker
map.loadImage(
'https://docs.mapbox.com/mapbox-gl-js/assets/custom_marker.png',
function (error, image) {
if (error) throw error;
map.addImage('custom-marker', image);
// Add a GeoJSON source with 2 points
map.addSource('points', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': dataToDisplay
}
});
// Add a symbol layer
map.addLayer({
'id': 'points',
'type': 'symbol',
'source': 'points',
'layout': {
'icon-image': 'custom-marker',
// get the title name from the source's "title" property
'text-field': ['get', 'title'],
'text-font': [
'Open Sans Semibold',
'Arial Unicode MS Bold'
],
'text-offset': [0, 1.25],
'text-anchor': 'top'
}
});
setDataLoaded(true); // set it to true when you're done
}
);
});
}, []);
return (
<div className="district-map-wrapper" style={dataLoaded ? undefined : {display: 'none'}}>
<div id="districtDetailMap" className="map">
<div style={{ height: "100%" }} ref={mapContainer}>
</div>
</div>
</div>
);
}

TypeError: _mapboxGl.default.Map is not a constructor

Trying to write a unit test for react.js component. The component implements a map rendered with mapbox API. But unfortunately i bumped into a series of problems:
The firs one was: TypeError: window.URL.createObjectURL is not a function
I have solved this thanks to this: https://github.com/mapbox/mapbox-gl-js/issues/3436, by adding this code:
jest.mock('mapbox-gl/dist/mapbox-gl', () => ({
Map: () => ({})
}))
Then the secound one was: ReferenceError: shallow is not defined
To solve this problem in the base of this: ReferenceError on enzyme import,
1) npm istall enzyme,
2) add the line:
import { shallow, render, mount } from 'enzyme'
The third problem was: import Adapter from 'enzyme-adapter-react-15'
Thanks to this article: Could not find declaration file for enzyme-adapter-react-16?, the next step was adding this code:
import Adapter from 'enzyme-adapter-react-16'
import enzyme from 'enzyme'
enzyme.configure({ adapter: new Adapter() })
And now finally a have: TypeError: _mapboxGl.default.Map is not a constructor
And now, unfortunately, I couldn't find a meaningful solution on the web.
Did anyone have a similar problem?
Why unit testing the mapbox API is so hard?
Maybe i'm doing it completely wrong and the whole solution is to the trash? if so, can anyone suggest an alternative?
Below the whole test code:
import React, { Component } from 'react'
import { shallow, render, mount } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import enzyme from 'enzyme'
enzyme.configure({ adapter: new Adapter() })
import Map from '../Map'
jest.mock('mapbox-gl/dist/mapbox-gl', () => ({
Map: () => ({})
}))
describe('<Map />', ()=>{
let mapWrapper
let mapInstance
const map = (disableLifecycleMethods = false)=>shallow(<Map />,{disableLifecycleMethods})
beforeEach(()=>{
mapWrapper = map()
mapInstance = mapWrapper.instance()
})
afterEach(() => {
mapWrapper = undefined;
mapInstance = undefined;
})
it('renders without crashing', () => {
expect(map().exists()).toBe(true);
})
})
Below the tested component code:
import React, { Component } from 'react'
import mapboxgl from 'mapbox-gl'
//Mechanics
import {importContours} from './utilities/importContours'
import {addData} from './utilities/addData'
import {setLegend} from './utilities/setLegend'
//Components
import Searchbar from '../search/Searchbar'
import Tabbar from '../tabbar/Tabbar'
import Legend from '../legend/Legend'
//import Popup from '../popup/Popup'
class Map extends Component {
map
constructor(){
super()
this.state = {
active: null,
fetchData: null,
mapType: 0,
searchedPhrase: ''
}
}
componentDidUpdate() {
this.setMapLayer()
}
componentDidMount() {
mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_TOKEN
this.map = new mapboxgl.Map({
container: 'Map',
style: 'mapbox://styles/mapbox/streets-v9',
center: [16.145136, 51.919437],
maxZoom: 13,
minZoom: 3,
zoom: 5.7,
})
this.map.once('load', () => {})
}
setMapLayer(){
if (!this.map.loaded() || this.state.searchedPhrase === '') return
var contours = importContours(this.state.mapType)
var contoursWithData = addData(contours, this.state.mapType, this.state.searchedPhrase)
contoursWithData.then((data)=>{
var mpSource = this.map.getSource("contours")
if (typeof mpSource === 'undefined')
this.map.addSource('contours', { type: 'geojson', data })
else
this.map.getSource("contours").setData(data)
var mpLayer = this.map.getLayer("contours")
if (typeof mpLayer === 'undefined') {
this.map.addLayer({
id: 'contours',
type: 'fill',
source: 'contours',
layout: {},
paint: {
'fill-opacity': [
'case',
['boolean', ['feature-state', 'hover'], false],
0.8,
0.4
]
}
}, 'country-label-lg')
this.map.addLayer({
id: 'state-borders',
type: 'line',
source: 'contours',
layout: {},
paint: {
'line-color': '#c44cc0',
'line-width': 0.01
}
})
}
var hoveredStateId = null
// When the user moves their mouse over the state-fill layer, we'll update the
// feature state for the feature under the mouse.
this.map.on('mousemove', 'contours', (e) => {
if (e.features.length > 0) {
if (hoveredStateId) {
this.map.setFeatureState(
{ source: 'contours', id: hoveredStateId },
{ hover: false }
)
}
hoveredStateId = e.features[0].id
this.map.setFeatureState(
{ source: 'contours', id: hoveredStateId },
{ hover: true }
)
}
})
// When the mouse leaves the state-fill layer, update the feature state of the
// previously hovered feature.
this.map.on('mouseleave', 'contours', () => {
if (hoveredStateId) {
this.map.setFeatureState(
{ source: 'contours', id: hoveredStateId },
{ hover: false }
)
}
hoveredStateId = null
})
// When the user click their mouse over the layer, we'll update the
this.map.on('click', 'contours', (e) => {
var popupHTML = `<Popover
style = { zIndex: 2, position: 'absolute' }
anchorOrigin={{ vertical: 'center',horizontal: 'center'}}
transformOrigin={{vertical: 'center',horizontal: 'center'}}
>
${e.features[0].id}
</Popover>`
if (e.features.length > 0) {
new mapboxgl.Popup(
{style:"zIndex: 2"},
{closeButton: false, closeOnClick: true}
)
.setLngLat(e.lngLat)
.setHTML(popupHTML)
.addTo(this.map);
}
})
this.setState({
active: setLegend(data)
})
//Set fill
if(this.state.active == null) return
const { property, stops } = this.state.active
this.map.setPaintProperty('contours', 'fill-color', {
property,
stops
})
})
}
handleChange = (newMapType) => {
if (this.state.mapType === newMapType) return
const { searchedPhrase } = this.state
if (typeof searchedPhrase === 'undefined')return
this.setState({mapType:newMapType})
}
handleSearch = (newSearchPhrase) => {
if (typeof newSearchPhrase === 'undefined') return
this.setState({searchedPhrase:newSearchPhrase.toUpperCase()})
}
render(){
return (
<div id="Map">
<Searchbar click={this.handleSearch.bind(this)}/>
<Tabbar click={this.handleChange.bind(this)}/>
<Legend active={this.state.active}/>
</div>
)
}
}
export default Map
Your can add the code below to your test entry file for me it was src/setupTests.ts
jest.mock('mapbox-gl/dist/mapbox-gl', () => ({
GeolocateControl: jest.fn(),
Map: jest.fn(() => ({
addControl: jest.fn(),
on: jest.fn(),
remove: jest.fn(),
})),
NavigationControl: jest.fn(),
}));
Thanks to Morlo's answer, I succeeded in fixing directly in my test file:
import Map from '#/components/modules/Home/Map/Map'
jest.mock('mapbox-gl/dist/mapbox-gl', () => ({
Map: jest.fn(),
Marker: jest.fn().mockReturnValue({
setLngLat: jest.fn().mockReturnValue({
setPopup: jest.fn().mockReturnValue({
addTo: jest.fn().mockReturnValue({})
})
})
}),
Popup: jest.fn().mockReturnValue({
setHTML: jest.fn().mockReturnValue({ on: jest.fn() })
})
}))
describe('Map', () => {
it('should match snapshot', () => {
// When
const wrapper = shallowMount(Map)
// Then
expect(wrapper).toMatchSnapshot()
})
})

Resources