Valentin Kononov
Full Stack Developer, Speaker
{
"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",
}
}
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>;
}
// rebuild layers and move map to the next tile
useEffect(() => {
if (!mapInstance) return;
rebuildLayersAfterCleanup(mapInstance, geoJsonFeature, options);
}, [mapInstance, geoJsonFeature, options]);
map.addSource("SourceId", data);
{
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
},
},
};
};
{
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
{
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',
},
}
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();
}
});
}
});
<div id="map" className="flex-child--grow"></div>
<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 });
};
// 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);
}
});
@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 };
}
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;
}
By Valentin Kononov
Slides for conference topic RAVEn - work with map for the first time in my life