How to extend TileLayer component in react-leaflet v3?
Asked Answered
V

2

4

I am trying to extend the TileLayer component in 'react-leaflet' v3. It is necessary to override this function to provide custom tile URL naming scheme. An example of what I need, written in basic leaflet:

function initMap() {
    L.TileLayer.WebGis = L.TileLayer.extend({

        initialize: function (url, options) {
            options = L.setOptions(this, options);
            if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) {
                options.tileSize = Math.floor(options.tileSize / 2);
                options.zoomOffset++;
                if (options.minZoom > 0) {
                    options.minZoom--;
                }
                this.options.maxZoom--;
            }
            if (options.bounds) {
                options.bounds = L.latLngBounds(options.bounds);
            }
            this._url = url + "/gis_render/{x}_{y}_{z}/" + options.userId + "/tile.png";
            var subdomains = this.options.subdomains;
            if (typeof subdomains === 'string') {
                this.options.subdomains = subdomains.split('');
            }
        },

        getTileUrl: function (tilePoint) {
            return L.Util.template(this._url, L.extend({
                s: this._getSubdomain(tilePoint),
                z: 17 - this._map._zoom,
                x: tilePoint.x,
                y: tilePoint.y
            }, this.options));
        }
    });

    L.tileLayer.webGis = function (url, options) {
        return new L.TileLayer.WebGis(url, options);
    };

    // create a map in the "map" div, set the view to a given place and zoom
    var map = L.map('map').setView([53.9, 27.55], 10);

    // add an Gurtam Maps tile layer
    L.tileLayer.webGis(wialon.core.Session.getInstance().getBaseGisUrl('render'), {
        attribution: 'Gurtam Maps',
        minZoom: 4,
        userId: wialon.core.Session.getInstance().getCurrUser().getId()
    }).addTo(map);

}

If I just write a url of Gurtam maps to a 'url' prop of TileLayer component, then my map incorrectly displayed (zoom and tile errors).

I can't figure out what to use for the correct display:

  1. Use 'useRef' hook to get the current TileLayer instance and extend it.
  2. Use some hook (maybe createElementHook) from package 'react-leaflet/core' and create my own custom component
  3. Or something else

I would be grateful for any explanations.

Vanadous answered 11/1, 2021 at 8:55 Comment(0)
U
7

For anyone using react-leaflet and typescript: I recreated the kitten example from leaflet in LeafletJS style and typescript based on Seth Lutske's answer.

Javascript:

import L from "leaflet";
import { createLayerComponent } from "@react-leaflet/core";

// @see https://mcmap.net/q/1770200/-how-to-extend-tilelayer-component-in-react-leaflet-v3
// @ts-ignore
L.TileLayer.Kitten = L.TileLayer.extend({
    getTileUrl: function(coords: L.Coords) {
        var i = Math.ceil( Math.random() * 4 );
        return "https://placekitten.com/256/256?image=" + i;
    },
    getAttribution: function() {
        return "<a href='https://placekitten.com/attribution.html'>PlaceKitten</a>"
    }
});

// @ts-ignore
L.tileLayer.kitten = function() {
    // @ts-ignore
    return new L.TileLayer.Kitten();
}

// @ts-ignore
const createKittenLayer = (props, context) => {
    // @ts-ignore
    const instance = L.tileLayer.kitten(props.url, {...props});
    return {instance, context};
}

// @ts-ignore
const updateKittenLayer = (instance, props, prevProps) => {
    if (prevProps.url !== props.url) {
        if (instance.setUrl) instance.setUrl(props.url)
    }

    if (prevProps.userId !== props.userId) {
        if (instance.setUserId) instance.setUserId(props.userId)
    }
}

const KittenLayer = createLayerComponent(createKittenLayer, updateKittenLayer);
export default KittenLayer;

Typescript (practical but not leaflet convention see in this SO answer's comments):

import L, { Coords, DoneCallback, GridLayer } from "leaflet";
import {createLayerComponent, LayerProps } from "@react-leaflet/core";
import { ReactNode } from "react";

interface KittenProps extends LayerProps {
    userId: string,
    children?: ReactNode // PropsWithChildren is not exported by @react-leaflet/core
}

class Kitten extends L.TileLayer {

    getTileUrl(coords: L.Coords) {
        var i = Math.ceil( Math.random() * 4 );
        return "https://placekitten.com/256/256?image=" + i;
    }

    getAttribution() {
        return "<a href='https://placekitten.com/attribution.html'>PlaceKitten</a>"
    }

}

const createKittenLayer = (props: KittenProps, context:any) => {
    const instance = new Kitten("placeholder", {...props});
    return {instance, context};
}

const updateKittenLayer = (instance: any, props: KittenProps, prevProps: KittenProps) => {
      if (prevProps.userId !== props.userId) {
        if (instance.setUserId) instance.setUserId(props.userId)
      }
    
}

const KittenLayer = createLayerComponent(createKittenLayer, updateKittenLayer);
export default KittenLayer;

Unpleasant answered 27/5, 2021 at 8:55 Comment(0)
A
6

It sounds like you're trying to create a custom component in react-leaflet v3. If you're not super familiar with react-leaflet, this might be daunting. The docs on writing a custom component are a little hard to follow. I found this section helpful: Element hook factory

So you need to start with 2 basic functions. One function to create your element, and one to update it. To create it,

// Assuming you've already defined L.TileLayer.WebGis somewhere
// and attached it to the global L

const createWebGisLayer = (props, context) => {

  const instance = L.tileLayer.webGis(props.url, {...props})

  return { instance, context }

}

Then you need another function that will handle any updates you want to trickle down into your component. For example, if a certain prop of your component changes, you need to explicitly tell react-leaflet to update the underlying leaflet instance of that component:

const updateWebGisLayer = (instance, props, prevProps) => {

  if (prevProps.url !== props.url) {
    if (instance.setUrl) instance.setUrl(props.url)
  }

  if (prevProps.userId !== props.userId) {
    if (instance.setUserId) instance.setUserId(props.userId)
  }

}

You need these setter functions to tell react-leaflet that if the url or userId (or whatever) prop changes in react, it needs to rerender the leaflet layer. setUrl already exists on L.TileLayer, but you'll need to define a setUserId that updated the userId option of the L.TileLayer.WebGis instance. If you don't include these, your component will not update when its props changes.

To put it all together, you can use the createLayerComponent factory function:

const WebGisLayer = createLayerComponent(createWebGisLayer, updateWebGisLayer)
export WebGisLayer

WebGisLayer is now a react component that you can use as a child of a MapContainer

const App = () => {

  const [userId, setUserId] = useState('user123')

  return (
    <MapContainer center={center} zoom={zoom}>
      <WebGisLayer 
        url="some_url" 
        userId={userId}
        otherPropsYouNeed={otherProps} />
    </MapContainer>
  )

}

When the component loads it will run your leaflet code and add the leaflet component to the map. If the userId changes due to some setstate call, your updateWebGisLayer function tells react-leaflet to update the underlying leaflet component.

There's a lot of ways to do this, but this is the one I think is most straightforward. I haven't had a chance to test this code so you'll inevitably have to play around with it to get it working, but this should get you started.

Aluminium answered 14/1, 2021 at 5:25 Comment(4)
Tried following the steps mentioned here. I'm unable to render my custom component and am getting the error No context provided: useLeafletContext() can only be used in a descendant of <MapContainer>. Am I doing something wrong or did I miss any imports for it to work. I'm using react-leaflet ^3.2.0 and leaflet ^1.7.1Belomancy
I'd have to see the code to know what the issue is. Are you sure your custom component is being used as a child of the MapContainer?Aluminium
Ya. I'm using it as a child component of mapp container. Let me see if I can put a stackblitz link of this.Belomancy
I was getting that error because of using @react-leaflet/core ^1.0.2. Uggrading the version to @react-leaflet/core ^1.1.0 fixed it. Thanks for your help. Figured this issue out when setting up the stackblitz link.Belomancy

© 2022 - 2024 — McMap. All rights reserved.