0
votes

Sorry in advance for the long code sample. I'm making an API call(Mapbox Directions API) in my componentDidMount to get coordinates points and draw an itinerary line between multiple coordinates. I'm currently drawing a line on my map based on coordinates I get from this API call, the thing is, the API call is made based on data stored in my Redux store, if I change the store, the line drawn on the map stays there (because componentDidMount is not called again, so the data for the line stays the same). I'm no React expert and I'm looking for an elegant way to refresh line drawn on the map. Any help is appreciated ! Thanks in advance.

Dave

import React, { Component } from 'react';
import ReactMapboxGl, { Marker } from 'react-mapbox-gl';
import marker from '../../assets/images/marker.png';
import Loading from '../Loading';
import { connect } from 'react-redux';
import ItineraryCard from '../Itinerary/ItineraryCard';
import photo1 from '../../assets/images/nav_tourisme.jpg';

const Map = ReactMapboxGl({
  accessToken:
    '...',
});

class MapWithLine extends Component {
  constructor(props) {
    super(props);
    this.state = {
      error: null,
      data: [],
      isLoaded: false,
      zoom: [13],
    };
  }

  onMapLoad = map => {
    map.addLayer({
      id: 'route',
      type: 'line',
      source: {
        type: 'geojson',
        data: {
          type: 'Feature',
          properties: {},
          geometry: {
            type: 'LineString',
            coordinates: this.state.data,
          },
        },
      },
      layout: {
        'line-join': 'round',
        'line-cap': 'round',
      },
      paint: {
        'line-color': '#888',
        'line-width': 8,
      },
    });
    map.resize();
  };

  removeLastCharacter(str) {
    if (str != null && str.length > 0) {
      str = str.substring(0, str.length - 1);
    }
    return str;
  }

  getItineraryCoords() {
    let itineraryString = '';
    this.props.itineraryReducer.itinerary.map(exp => {
      const coords = exp.lng + ',' + exp.lat + ';';
      itineraryString = itineraryString + coords;
      return itineraryString;
    });
    return this.removeLastCharacter(itineraryString);
  }

  componentDidMount() {
    if (this.props.itineraryReducer.itinerary.length > 1) {
      const url =
        'https://api.mapbox.com/directions/v5/mapbox/' +
        'driving/' +
        this.getItineraryCoords() +
        '?geometries=geojson&' +
        'access_token='...';
      fetch(url)
        .then(response => response.json())
        .then(
          result => {
            result.routes
              ? this.setState({
                  data: result.routes[0].geometry.coordinates,
                  isLoaded: true,
                })
              : this.setState({
                  isLoaded: true,
                });
          },
          error => {
            this.setState({
              isLoaded: true,
              error: error,
            });
          }
        );
    }
  }

  componentWillUnmount() {
    //Resume scrolling on body when drawer is not maximized (component unmounted)
    document.body.style.overflow = 'visible';
  }
  render() {
    const { error, isLoaded } = this.state;
    if (error) {
      return <div>Error: {error.message}</div>;
    } else if (!isLoaded) {
      return <Loading />;
    } else {
      return (
        <div className="mapWrapper">
          <Map
            style="mapbox://styles/mapbox/streets-v8"
            className="mapContainer"
            center={this.state.data[0]}
            zoom={this.state.zoom}
            onStyleLoad={this.onMapLoad}
          >
            });
            {this.props.itineraryReducer.itinerary.map((exp, index) => {
              const coords = [exp.lng, exp.lat];
              return (
                <Marker coordinates={coords} anchor="bottom" key={index}>
                  <img src={marker} alt="marker-icon" className="mapMarker" />
                </Marker>
              );
            })}
          </Map>
        </div>
      );
    }
  }
}

const mapStateToProps = state => {
  return {
    itineraryReducer: state.itineraryReducer,
  };
};

export default connect(mapStateToProps)(MapWithLine);
1

1 Answers

0
votes

If I understand correctly, you want to display the response coming from the mapbox Directions API whenever props.itineraryReducer updates, and which you are storing in state.data. Therefore, you can use ComponentDidUpdate - which is a React lifecycle method that you can use to act on changes in your component (be it state or props) - to trigger an update on your map whenever those props and state variables change. You could do something of the like:

componentDidMount (prevProps, prevState) {
  const { data } = this.state
  const { itineraryReducer } = this.props

  if (prevProps.itineraryReducer !== itineraryReducer) {
    if (itineraryReducer.itinerary.length > 1) {
      const url =
        'https://api.mapbox.com/directions/v5/mapbox/' +
        'driving/' +
        this.getItineraryCoords() +
        '?geometries=geojson&' +
        'access_token='...';

      fetch(url)
        .then(response => response.json())
        .then(
          result => {
            result.routes
              ? this.setState({
                  data: result.routes[0].geometry.coordinates,
                  isLoaded: true,
                })
              : this.setState({
                  isLoaded: true,
                });
          },
          error => { // You should probably catch this error on a .catch() in your promise chain
            this.setState({
              isLoaded: true, 
              error: error,
            });
          }
        );
    }
  }

  if (prevState.data !== data) {
    this.addLineLayer(data) // Here is where we are actually drawing the line map layer
  }
}

addLineLayer = (coords) => {
  this.map.addLayer({
      id: 'route',
      type: 'line',
      source: {
        type: 'geojson',
        data: {
          type: 'Feature',
          properties: {},
          geometry: {
            type: 'LineString',
            coordinates: this.state.data,
          },
        },
      },
      layout: {
        'line-join': 'round',
        'line-cap': 'round',
      },
      paint: {
        'line-color': '#888',
        'line-width': 8,
      },
    });
    this.map.resize();
}

// ... (Rest of the Component)

Potentially depending on your goal, you'd have to remove the previous existing layer on the map, before you draw the next layer. You can check this link for reference on the method to call.

You may also have noticed that the map object is being accessed in this.map, so you'll need to save the map object in your class instance when the style loads. For this, you could pass the following method as the attribute onStyleLoad to <Map>:

onStyleLoad = (map) => {
  this.map = map // This saves the object map in your class instance, so that you can access it later
}

...

<Map
  style="mapbox://styles/mapbox/streets-v8"
  className="mapContainer"
  center={this.state.data[0]}
  zoom={this.state.zoom}
  onStyleLoad={this.onStyleLoad}
>

...