How RAVEn met mapboxgl.js

https://github.com/valentinkononov

http://kononov.space/

 

@ValentinKononov

 

Developer, Speaker

 

 

  1. Anybody at Mapbox
  2. Javascript / Typescript developers
  3. Ones involved into Roads Enrichments 

Target Audience

Agenda

  1. About the project and why we need map
  2. React Component with Map - proc and cons
  3. Styles samples - not obvious cases
  4. Tricks I learned
  5. Map Feature indexing with KDBush algorithm

 

RAVEn - about project

* Annotate Images

* Segmentation

* Filtration

* Detection (bbox)

* Faster UI

RAVEn - about project

* Task Management

* Metrics / Quality

* Verification

* Export

* Design

RAVEn - not planned...

* Meta Annotation

* Arrows/Direction

* Roads Enrichment

* GeoJson Validation

* Imagery

Roads Enrichment

* Annotate Aerial Images

* Run Model

* JOSM

* Update Enrichment Layer

* Lanes

* Road Painting

* GeoJson

* Validate

* RAVEn

RAVEn === mini Streets Review

* Mapboxgl.js map

* Custom Arrows

* UX for editing

* No manual work

* Show static data

{
      "type": "Feature",
      "geometry": {
        "type": "LineString",
        "coordinates": []
      },
      "properties": {
        "lanes:forward": "2",
        "lanes": "5",
        "lanes:backward": "3",
        "turn:lanes:backward": "left||right",
        "oneway": "no",
        "osm:turn:lanes:backward": "None",
        "osm:lanes:forward": "None",
        "osm:lanes": "5",
        "osm:lanes:backward": "None",
        "osm:turn:lanes:forward": "None",
        "correct": "no",
        "id": "11576607;0",
      }
    }

GeoJson

Dive into Code

mapboxgl.accessToken = getMapboxGlToken();

const SimpleMap = ({ options }: SimpleMapProps): ReactElement => {
    const geoFeatureItem = useSelector(getGeoFeatureItem);
    const [mapInstance, setMapInstance] = useState(null);
    
    useEffect(() => {
        const map = new Map({
            container: 'map',
            style: 'mapbox://styles/mapbox/streets-v11',
            bounds: mapBounds,
        });

        map.on('load', () => {
            // prepare map settings
            setMapInstance(map);
        });

        return () => map.remove();
    }, []);
    return <div id="map" className="flex-child--grow"></div>;
}

React Component with map

// rebuild layers and move map to the next tile
useEffect(() => {
    if (!mapInstance) return;
    
    rebuildLayersAfterCleanup(mapInstance, geoJsonFeature, options);
}, [mapInstance, geoJsonFeature, options]);

React vs Mapboxgl

useEffect can cause map refresh

useEffect is NOT async, map is

map.addSource("SourceId", data);

Adding Data and Layers

{
    type: 'geojson',
    data: { type: 'FeatureCollection', features },
}
map.addLayer(getFeatureLineLayer("LayerId", "SourceId"));
const getFeatureLineLayer = (id: string, source: string):LineLayer => {
    return {
        id,
        type: 'line',
        source,
        filter: ['==', '$type', 'LineString'],
        layout: {
            'line-join': 'round',
            'line-cap': 'round',
        },
        paint: {
            'line-color': '#FF2181',
            'line-width': {
                base: 1,
                stops: [
                    [10, 1],   // from zoom 1 to 10 size is 1
                    [24, 4],   // from zoom 10 to 24 size is
                ],             // interpolated from 1 to 4
            },
        },
    };
};

Styles - Line

{
    paint: {
        'line-color': '#FF2181',
        'line-opacity': {
            stops: [
                [10, 0],   
                [16, 1],
            ],             
        },                 
    },
};
// from zoom 1 to 10 line is invisible
// from zoom 10 to 16 opacity is increasing
// line is getting visible
// from zoom 16 and on line is visible

Styles - Opacity

{
        type: 'symbol',
        ource: "sourceId",
        filter: ['==', '$type', 'Point'],
        paint: {
            'icon-opacity': {...},
        },
        layout: {
            'icon-image': "imageName",
            'icon-offset': {
                stops: [
                    [12, [0, offset / 20]],
                    [24, [0, offset]],
                ],
            },
            'icon-rotate': rotateAngle,
            'icon-rotation-alignment': 'map',
            'icon-size': 
                ['interpolate', ['linear'], ['zoom'], 12, 0.01, 24, 0.5],
            visibility: 'visible',
        },
    }

Image Layer

new Promise((resolve, reject) => {
    if (map.hasImage(imageName)) {
        map.addLayer(getImageLayer(layerId, GEOJSON_SOURCE, imageName));
        resolve();
    } else {
        map.loadImage(imageFileName, function (err: any, image: any) {
            if (err) {
                 console.error(`image loading error: ${imageFileName}`, err);
                 reject(err);
            } else {
                if (!map.hasImage(imageName)) {
                    map.addImage(imageName, image);
                }
                if (!map.getLayer(layerId)) {
                    map.addLayer(getImageLayer(layerId, GEOJSON_SOURCE, imageName));
                }

                resolve();
            }
        });
    }
});

Adding Image to Map

Adding Image to Map

<div id="map" className="flex-child--grow"></div>

container ID

Map is the only one element with such ID in DOM

<div id="mapboxgl-container" className="flex-child--grow"></div>
const rotateMap = (map: Map, feature: GeoJsonFeature): void => {
    const coords = feature.geometry.coordinates;
    const segment = coords.slice(coords.length - 2, coords.length);
  
    const rotate = angleFromWay(segment);
    // rotate is called with angle in degrees
    map.rotateTo(rotate, { animate: false });
};

Rotation

Map is refreshed even if rotateTo was called with the same angle!

Angle Formula (for lat/lon)

// lon = x, lat = y
// β = atan2 (X,Y)
// For variable Y = sin (toRadians (lo2-lo1)) * cos (toRadians (la2))
// variable X = cos (toRadians (la1))*sin (toRadians (la2)) 
//  – sin (toRadians (la1))*cos (toRadians (la2))*cos (toRadians (lo2-lo1))
export const angleFromWay = (coords: number[][]): number => {
    const end = coords[coords.length - 1];
    const lon2 = end[0];
    const lat2 = end[1];
    const start = coords[coords.length - 2];
    const lon1 = start[0];
    const lat1 = start[1];

    const y = Math.sin(toRadians(lon2 - lon1)) 
        * Math.cos(toRadians(lat2));
    const x =
        Math.cos(toRadians(lat1)) * Math.sin(toRadians(lat2)) -
        Math.sin(toRadians(lat1)) * Math.cos(toRadians(lat2) 
                                  * Math.cos(toRadians(lon2 - lon1)));
    return toDegrees(Math.atan2(y, x));
};
map.getStyle().layers.forEach(layer => {
    if (canRemoveLayer(layer.id)) {
        map.removeLayer(layer.id);
    }
});

CLEAN_UP_SOURCES.forEach(source => {
    if (map.getSource(source) !== undefined) {
        map.removeSource(source);
    }
});

Clear Layers

Takeaways

Less map updates

Clear layers everytime

Add layers in the right order

DataSource should be not too big

Double check things in async cases

Nearly Big Data

* All ways of San Fransisco in one file

* Map slows down

* 1.5 GB of RAM is consumed (300Mb before)

* User needs only one street

Houston, we have a problem

Geo Feature Index

Build Indexed

Request  by bounds

https://github.com/mourner/kdbush

Cache and GeoIndex

@Cached({ ttl: 365 * 24 * 60 })
async getIndexedGeoJson(key: string, bucket: string): 
                                   Promise<IndexedFeatures> {

    const geojson: GeoJson = await this.getGeoJsonFile(key, bucket);
    const { getX, getY } = getXYFunctions(geojson.features[0]);

    const index = new KDBush<GeoJsonFeature>(
          geojson.features, getX, getY, 64, Array);

    return { features: geojson.features, index };
}

KDBush does not store data - just indexes

Get Range of indexes

async getFeaturesByBounds(
        key: string,
        bucket: string,
        { minX, minY, maxX, maxY }: BoundsDto,
    ): Promise<GeoJsonFeature[]> {
        
    const indexedFeatures = await this.getIndexedGeoJson(key, bucket);
    const featuresByBounds: GeoJsonFeature[]
      = indexedFeatures.index
            .range(minX, minY, maxX, maxY)
             // returns number[] of indexes in initial array
            .map(id => indexedFeatures.features[id]);

    return featuresByBounds;
}

Lightning-fast!

UI and Demo

CREDITS

 * Maria Brutskaya

 * Yulia Alkhimonionak

 * Evgeniya Karnaukhava

 * Lina Shiryaeva

 * Ilya Bohaslauchyk

 * Sergei Pakhuta

 * Alexandr Buslaev

 * Nikita Klushnikov

 * Denis Sheka

THANKS!

RAVEn - work with map for the first time in my life

By Valentin Kononov

RAVEn - work with map for the first time in my life

Slides for conference topic RAVEn - work with map for the first time in my life

  • 276