Skip to content

Instantly share code, notes, and snippets.

@mpontus
Created December 11, 2017 13:56
Show Gist options
  • Save mpontus/a6a3c69154715f2932349f0746ff81f2 to your computer and use it in GitHub Desktop.
Save mpontus/a6a3c69154715f2932349f0746ff81f2 to your computer and use it in GitHub Desktop.
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import SphericalMercator from "@mapbox/sphericalmercator";
import { MapView, Constants } from "expo";
const merc = new SphericalMercator();
const getZoomLevelFromRegion = (region, viewport) => {
const { longitudeDelta } = region;
const { width } = viewport;
// Normalize longitudeDelta which can assume negative values near central meridian
const lngD = (360 + longitudeDelta) % 360;
// Calculate the number of tiles currently visible in the viewport
const tiles = width / 256;
// Calculate the currently visible portion of the globe
const portion = lngD / 360;
// Calculate the portion of the globe taken up by each tile
const tilePortion = portion / tiles;
// Return the zoom level which splits the globe into that number of tiles
return Math.log2(1 / tilePortion);
};
const getRegionByZoomLevel = (center, zoomLevel, viewport) => {
const { latitude, longitude } = center;
const { width, height } = viewport;
// Calculate the projected coordinates center coordinates
const [x, y] = merc.px([lng, lat], zoomLevel);
// Subtract screen dimensions to get projected boundaries
const xmin = Math.floor(x - width / 2);
const xmax = xmin + width;
const ymin = Math.floor(y - height / 2);
const ymax = ymin + height;
// Use reverse projection to convert boundaries to geographical coordinates
const nw = merc.ll([xmin, ymin], zoomLevel);
const se = merc.ll([xmax, ymax], zoomLevel);
// Calculate latitude and longitude deltas as difference between boundary coordinates
const latitudeDelta = nw[1] - se[1];
const longitudeDelta = se[0] - nw[0];
// Return react-native-maps compatible region
return {
latitude,
longitude,
latitudeDelta,
longitudeDelta
};
};
const getRegionBoundaries = (center, zoomLevel, viewport) => {
const { latitude, longitude, latitudeDelta, longitudeDelta } = center;
const { width, height } = viewport;
const [x, y] = merc.px([longitude, latitude], zoomLevel);
const xmin = Math.floor(x - width / 2);
const xmax = xmin + width;
const ymin = Math.floor(y - height / 2);
const ymax = ymin + height;
const [westLongitude, northLatitude] = merc.ll([xmin, ymin], zoomLevel);
const [eastLongitude, southLatitude] = merc.ll([xmax, ymax], zoomLevel);
return {
northLatitude,
southLatitude,
westLongitude,
eastLongitude
};
};
export default class App extends React.Component {
state = {
region: null,
layout: null
};
getZoomLevel() {
const { region, layout } = this.state;
if (!region || !layout) return null;
return getZoomLevelFromRegion(region, layout);
}
renderNextZoomRegion() {
const zoomLevel = this.getZoomLevel();
if (!zoomLevel) {
return null;
}
const { latitude, longitude } = this.state.region;
const { width, height } = this.state.layout;
const {
northLatitude,
southLatitude,
westLongitude,
eastLongitude
} = getRegionBoundaries(
{ latitude, longitude },
Math.floor(zoomLevel) + 1,
{ width, height }
);
return (
<MapView.Polygon
coordinates={[
{ latitude: northLatitude, longitude: westLongitude },
{ latitude: northLatitude, longitude: eastLongitude },
{ latitude: southLatitude, longitude: eastLongitude },
{ latitude: southLatitude, longitude: westLongitude }
]}
/>
);
}
render() {
const zoomLevel = this.getZoomLevel();
return (
<View style={{ flex: 1, marginTop: Constants.statusBarHeight }}>
<MapView
style={{ flex: 1 }}
onRegionChange={region => this.setState({ region })}
onLayout={e => this.setState({ layout: e.nativeEvent.layout })}
>
{this.renderNextZoomRegion()}
</MapView>
{this.state.layout && (
<View
style={{
height: 48,
alignItems: "center",
justifyContent: "center"
}}
>
<Text>Width: {this.state.layout.width}</Text>
</View>
)}
{this.state.region && (
<View
style={{
height: 48,
alignItems: "center",
justifyContent: "center"
}}
>
<Text>LngD: {this.state.region.longitudeDelta}</Text>
</View>
)}
<View
style={{ height: 48, alignItems: "center", justifyContent: "center" }}
>
{zoomLevel !== null ? (
<Text>Current Zoom Level: {zoomLevel.toFixed(5)}</Text>
) : (
<Text>Drag the map around</Text>
)}
</View>
</View>
);
}
}
@JonathanPiquard
Copy link

JonathanPiquard commented Mar 3, 2025

Thank you ! I have updated the code to use React function components and Typescript :

import React, { useMemo, useState } from "react";
import { LayoutChangeEvent, LayoutRectangle, StyleSheet, Text, View } from "react-native";

import MapView, { LatLng, Polygon, Region } from "react-native-maps";
import { SphericalMercator, type Zoom, type XY, type LonLat } from "@mapbox/sphericalmercato";

type Viewport = { width: number, height: number };


const merc = new SphericalMercator();

const getZoomLevelFromRegion = (region: Region, viewport: Viewport): Zoom => {
    const { longitudeDelta } = region;
    const { width } = viewport;

    // Normalize longitudeDelta which can assume negative values near central meridian
    const lngD: number = (360 + longitudeDelta) % 360;

    // Calculate the number of tiles currently visible in the viewport
    const tiles: number = width / 256;

    // Calculate the currently visible portion of the globe
    const portion: number = lngD / 360;

    // Calculate the portion of the globe taken up by each tile
    const tilePortion: number = portion / tiles;

    // Return the zoom level which splits the globe into that number of tiles
    const zoomLevel: Zoom = Math.log2(1 / tilePortion);

    return zoomLevel;
};

const getRegionByZoomLevel = (center: LatLng, zoomLevel: Zoom, viewport: Viewport): Region => {
    const { latitude, longitude } = center;
    const { width, height } = viewport;

    // Calculate the projected coordinates center coordinates
    const [x, y]: XY = merc.px([longitude, latitude], zoomLevel);

    // Subtract screen dimensions to get projected boundaries
    const xmin: number = Math.floor(x - width / 2);
    const xmax: number = xmin + width;
    const ymin: number = Math.floor(y - height / 2);
    const ymax: number = ymin + height;

    // Use reverse projection to convert boundaries to geographical coordinates
    const nw: LonLat = merc.ll([xmin, ymin], zoomLevel);
    const se: LonLat = merc.ll([xmax, ymax], zoomLevel);

    // Calculate latitude and longitude deltas as difference between boundary coordinates
    const latitudeDelta: number = nw[1] - se[1];
    const longitudeDelta: number = se[0] - nw[0];

    // Return react-native-maps compatible region
    return {
        latitude,
        longitude,
        latitudeDelta,
        longitudeDelta
    };
};

const getRegionBoundaries = (center: LatLng, zoomLevel: Zoom, viewport: Viewport) => {
    const { latitude, longitude } = center;
    const { width, height } = viewport;

    const [x, y]: XY = merc.px([longitude, latitude], zoomLevel);
    const xmin: number = Math.floor(x - width / 2);
    const xmax: number = xmin + width;
    const ymin: number = Math.floor(y - height / 2);
    const ymax: number = ymin + height;

    const [westLongitude, northLatitude]: LonLat = merc.ll([xmin, ymin], zoomLevel);
    const [eastLongitude, southLatitude]: LonLat = merc.ll([xmax, ymax], zoomLevel);

    return {
        northLatitude,
        southLatitude,
        westLongitude,
        eastLongitude
    };
};

export default function MapTest() {
    const [region, setRegion] = useState<Region>();
    const [layout, setLayout] = useState<LayoutRectangle>();

    function getZoomLevel(): Zoom {
        if (!region || !layout)
            return null;

        return getZoomLevelFromRegion(region, layout);
    }

    function renderNextZoomRegion() {
        const zoomLevel: Zoom = getZoomLevel();

        if (!zoomLevel)
            return null;

        const {
            northLatitude,
            southLatitude,
            westLongitude,
            eastLongitude
        } = getRegionBoundaries(region, zoomLevel, layout);

        return (
            <Polygon
                coordinates={[
                    { latitude: northLatitude, longitude: westLongitude },
                    { latitude: northLatitude, longitude: eastLongitude },
                    { latitude: southLatitude, longitude: eastLongitude },
                    { latitude: southLatitude, longitude: westLongitude }
                ]}
            />
        );
    }

    const zoomLevel: Zoom = useMemo(
        () => getZoomLevel(),
        [region, layout]
    );

    return (
        <View style={{ flex: 1 }}>
            <MapView
                style={{ flex: 1 }}
                onRegionChange={setRegion}
                onLayout={(e: LayoutChangeEvent) => setLayout(e.nativeEvent.layout)}
            >
                {renderNextZoomRegion()}
            </MapView>

            <Display
                layout={layout}
                region={region}
                zoomLevel={zoomLevel}
            />
        </View>
    );
}

type DisplayProps = {
    layout: LayoutRectangle | undefined,
    region: Region | undefined,
    zoomLevel: Zoom
}

function Display({ layout, region, zoomLevel }: DisplayProps) {
    return (
        <View
            style={{
                alignItems: "center",
                justifyContent: "center"
            }}
        >
            {layout && (
                <View
                    style={{
                        height: 48,
                        alignItems: "center",
                        justifyContent: "center"
                    }}
                >
                    <Text>Width: {layout.width}</Text>
                </View>
            )}
            {region && (
                <View
                    style={{
                        height: 48,
                        alignItems: "center",
                        justifyContent: "center"
                    }}
                >
                    <Text>LngD: {region.longitudeDelta}</Text>
                </View>
            )}
            <View
                style={{ height: 48, alignItems: "center", justifyContent: "center" }}
            >
                {zoomLevel !== null ? (
                    <Text>Current Zoom Level: {zoomLevel.toFixed(5)}</Text>
                ) : (
                    <Text>Drag the map around</Text>
                )}
            </View>
        </View>
    );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment