maps icon indicating copy to clipboard operation
maps copied to clipboard

[Bug]: Offline pack not visible in mapview when device is offline

Open sahni01 opened this issue 10 months ago • 12 comments

Mapbox Implementation

Mapbox

Mapbox Version

default

React Native Version

0.76.0

Platform

Android

@rnmapbox/maps version

10.1.36

Standalone component to reproduce

import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import {
  View,
  Text,
  TextInput,
  StyleSheet,
  FlatList,
  Modal,
  Pressable,
  Alert,
  TouchableOpacity,
  ToastAndroid,
  KeyboardAvoidingView,
  Dimensions,
  Image,
} from 'react-native';
import Mapbox from '@rnmapbox/maps';
import { useFocusEffect } from '@react-navigation/native';
import IonIcons from 'react-native-vector-icons/Ionicons';
import Geolocation from '@react-native-community/geolocation';
import { Picker } from '@react-native-picker/picker';

const MAPBOX_API_KEY = 'API_KEY';
const MAPBOX_SATELITE_STYLE = 'mapbox://styles/mapbox/satellite-streets-v12';
const MAPBOX_OUTDOOR_STYLE = 'mapbox://styles/mapbox/outdoors-v12';

Mapbox.setAccessToken(MAPBOX_API_KEY);

const MapRegionCaching = () => {
  const [offlineRegions, setOfflineRegions] = useState([]);
  const [isModalVisible, setModalVisible] = useState(false);
  const [regionName, setRegionName] = useState('');
  const [isDownloading, setIsDownloading] = useState(false);
  const [downloadProgress, setDownloadProgress] = useState(0);
  const [centerCoordinates, setCenterCoordinates] = useState();
  const [selectedRegion, setSelectedRegion] = useState();
  const [selectedMap, setSelectedMap] = useState();
  const [selectedMapStyle, setSelectedMapStyle] = useState(MAPBOX_OUTDOOR_STYLE);
  const [showMap, setShowMap] = useState(false);
  const mapCameraRef = useRef();
  const [mapStyle, setMapStyle] = useState(MAPBOX_OUTDOOR_STYLE);
  const [zoomLevel, setZoomLevel] = useState(12);
  const [zoomOpen, setZoomOpen] = useState(false);

  const zoomLevels = Array.from({ length: 23 }, (_, i) => ({
    label: `Zoom Level ${i}${i === 12 ? ' (Standard)' : ''}`,
    value: i,
  }));

  // Save a new offline region
  const saveOfflineRegion = async () => {
    if (!regionName.trim() || !selectedRegion) {
      Alert.alert('Error', 'A region name is required and region is required to be saved.');
      return;
    }

    const isExists = offlineRegions.find(
      (item) => item?.metadata?.name === regionName
    );

    if (isExists) {
      ToastAndroid.show('Duplicate Region Name', ToastAndroid.SHORT);
      return;
    }

    console.log('Creating offline ...');
    try {
      setModalVisible(false);
      setIsDownloading(true);
      ToastAndroid.show('Starting Download...', ToastAndroid.SHORT);
      Mapbox.offlineManager.setTileCountLimit(9999999);
      const offlineRegion = await Mapbox.offlineManager.createPack(
        {
          bounds: selectedRegion,
          name: regionName,
          minZoom: 18,
          maxZoom: 20,
          styleURL: mapStyle,
        },
        (progress) => {
          console.log('PROGRESS ====> ', progress);
          progress.status().then((value) => {
            console.log('STATUS ====> ', value);
            setDownloadProgress(value.percentage.toFixed(2));
            if (value.percentage === 100) {
              setIsDownloading(false);
              setDownloadProgress(0);
              ToastAndroid.show('Downloaded Successfully', ToastAndroid.SHORT);
              setRegionName('');
              setZoomLevel(12);
              setSelectedRegion();
              loadOfflineMaps();
            }
          });
        },
        (error) => {
          Mapbox.offlineManager.deletePack(regionName);
          console.log('DOWNLOAD_ERROR ====> ', error);
          setIsDownloading(false);
          setDownloadProgress(0);
          setModalVisible(true);
          ToastAndroid.show("Couldn't Download! please try smaller regions", ToastAndroid.LONG);
          error.status().then((value) => {
            console.log('ERROR_STATUS ====> ', value);
          });
        }
      );

      console.log('OFFLINE REGION ====> ', offlineRegion);
    } catch (error) {
      Mapbox.offlineManager.deletePack(regionName);
      ToastAndroid.show(error.toString(), ToastAndroid.SHORT);
      console.log('ERROR ====> ', error);
    }
  };

  const loadOfflineMaps = async () => {
    try {
      const offlineMaps = await Mapbox.offlineManager.getPacks();
      setOfflineRegions(offlineMaps);
      console.log('OFFLINE_MAPS ===> ', JSON.stringify(offlineMaps));
    } catch (error) {
      console.log('ERROR ====> ', error.toString());
    }
  };

  const cancelDownload = async (packName) => {
    try {
      const deletedPack = await Mapbox.offlineManager.deletePack(packName);
      console.log('CANCELLED_DOWNLOAD ====> ', deletedPack);
      ToastAndroid.show('Download Cancelled', ToastAndroid.SHORT);
      setIsDownloading(false);
      setDownloadProgress(0);
      setModalVisible(true);
    } catch (error) {
      ToastAndroid.show("Couldn't Cancel Download", ToastAndroid.SHORT);
      console.log('ERROR ===> ', error);
    }
  };

  const deleteMap = async (packName) => {
    try {
      const deletedPack = await Mapbox.offlineManager.deletePack(packName);
      console.log('MAP_DELETED ====> ', deletedPack);
      ToastAndroid.show(packName + ' Deleted', ToastAndroid.SHORT);
      loadOfflineMaps();
    } catch (error) {
      ToastAndroid.show("Couldn't Delete " + packName, ToastAndroid.SHORT);
      console.log('ERROR ===> ', error);
    }
  };

  useFocusEffect(
    useCallback(() => {
      Geolocation.getCurrentPosition((info) => {
        console.log(info);
        setCenterCoordinates([info.coords.longitude, info.coords.latitude]);
      });
      loadOfflineMaps();
    }, [])
  );

  const rectangleGeoJSON = {
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        geometry: selectedMap?._metadata?._rnmapbox?.bounds,
        properties: {},
      },
    ],
  };

  // Render saved offline regions
  const renderOfflineRegion = ({ item }) => (
    <View style={styles.regionItem}>
      <View style={styles.regionItemContainer}>
        <View style={styles.infoContainer}>
          <IonIcons name="map" size={40} color="#4CAF50" style={styles.icon} />
          <View style={styles.textContainer}>
            <Text style={styles.regionName}>
              {item?.metadata?.name || 'Unknown Region'}
            </Text>
            <Text style={styles.regionInfo}>
              Expires on: {item?.pack?.expires}
            </Text>
          </View>
        </View>
        <View style={styles.actionIcons}>
          <TouchableOpacity
            onPress={() => {
              console.log('SELECTED_MAP ====> ', JSON.stringify(item));
              setSelectedMap(item);
              setShowMap(true);
              setTimeout(() => {
                mapCameraRef?.current?.fitBounds(
                  [item?.pack?.bounds[0], item?.pack?.bounds[1]],
                  [item?.pack?.bounds[2], item?.pack?.bounds[3]],
                  [50, 50],
                  2000
                );
              }, 1000);
            }}
            style={styles.iconButton}
          >
            <IonIcons name="eye" size={24} color="#4CAF50" />
          </TouchableOpacity>
          <TouchableOpacity
            onPress={() => deleteMap(item?.metadata?.name)}
            style={styles.iconButton}
          >
            <IonIcons name="trash" size={24} color="#F44336" />
          </TouchableOpacity>
        </View>
      </View>
    </View>
  );

  return (
    <View style={styles.container}>
      {/* List of Offline Regions */}
      <FlatList
        data={offlineRegions}
        renderItem={renderOfflineRegion}
        keyExtractor={(item, index) => index.toString()}
        contentContainerStyle={styles.regionList}
        ListEmptyComponent={
          <Text style={styles.emptyText}>No offline regions saved.</Text>
        }
      />

      {/* Floating Action Button */}
      <TouchableOpacity
        style={styles.fab}
        onPress={() => setModalVisible(true)}
      >
        <Text style={styles.fabText}>+</Text>
      </TouchableOpacity>

      <ProgressModal
        title="Downloading"
        progress={downloadProgress}
        cancelButtonTitle="Cancel Download"
        visible={isDownloading}
        onCancelPress={() => cancelDownload(regionName)}
      />

      <Modal
        visible={showMap}
        animationType="slide"
        transparent={true}
        onRequestClose={() => setShowMap(false)}
      >
        <KeyboardAvoidingView style={{ flex: 1 }}>
          <View style={styles.modalBackground}>
            <View style={styles.modalContent}>
              <Text style={styles.modalHeader}>View Offline Map</Text>
              <View style={styles.mapContainer}>
                <Mapbox.MapView
                  logoEnabled={false}
                  attributionEnabled={false}
                  style={{ ...styles.map, width: '100%', borderWidth: 0 }}
                  styleURL={selectedMap?._metadata?._rnmapbox?.styleURI}
                >
                  <Mapbox.Camera
                    animationDuration={2000}
                    zoomLevel={12}
                    ref={mapCameraRef}
                    bounds={{
                      ne: [
                        selectedMap?.pack?.bounds[0],
                        selectedMap?.pack?.bounds[1],
                      ],
                      sw: [
                        selectedMap?.pack?.bounds[2],
                        selectedMap?.pack?.bounds[3],
                      ],
                    }}
                  />
                  <Mapbox.ShapeSource id="rectangleSource" shape={rectangleGeoJSON}>
                    <Mapbox.FillLayer
                      id="rectangleFill"
                      style={{
                        fillColor: 'rgba(0, 150, 200, 0.5)',
                        fillOutlineColor: 'rgba(0, 150, 200, 1)',
                      }}
                    />
                  </Mapbox.ShapeSource>
                </Mapbox.MapView>
              </View>
              <View style={styles.modalActions}>
                <Pressable
                  style={[styles.modalButton, styles.cancelButton]}
                  onPress={() => {
                    setShowMap(false);
                    setSelectedMap();
                  }}
                >
                  <Text style={styles.buttonText}>Close</Text>
                </Pressable>
              </View>
            </View>
          </View>
        </KeyboardAvoidingView>
      </Modal>

      {/* Modal for Adding Offline Region */}
      <Modal
        visible={isModalVisible}
        animationType="slide"
        transparent={true}
        onRequestClose={() => setModalVisible(false)}
      >
        <KeyboardAvoidingView style={{ flex: 1 }}>
          <View style={styles.modalBackground}>
            <View style={styles.modalContent}>
              <Text style={styles.modalHeader}>Add Offline Region</Text>
              <View
                style={{
                  width: '100%',
                  flexDirection: 'row',
                  justifyContent: 'space-between',
                  alignItems: 'center',
                  marginBottom: 16,
                }}
              >
                <TextInput
                  style={[
                    styles.input,
                    {
                      width: '75%',
                      height: '100%',
                    },
                  ]}
                  placeholder="Region Name"
                  value={regionName}
                  onChangeText={setRegionName}
                />
                <View style={[styles.input, { width: '24%', height: 'auto' }]}>
                  <Picker
                    selectedValue={zoomLevel}
                    onValueChange={(itemValue) => setZoomLevel(itemValue)}
                    style={{ padding: 0 }}
                    placeholder="Zoom Level"
                  >
                    {zoomLevels.map((z) => (
                      <Picker.Item
                        key={`zoom-level-${z.value}`}
                        label={z.label}
                        value={z.value}
                      />
                    ))}
                  </Picker>
                </View>
              </View>
              <Text style={styles.label}>
                Select Map Region (Zoom and Pan to set region visible. Map area will be saved for offline use.)
              </Text>
              <View style={styles.mapContainer}>
                <TouchableOpacity
                  style={styles.mapStyle2}
                  onPress={() => {
                    if (mapStyle === MAPBOX_OUTDOOR_STYLE) {
                      setMapStyle(MAPBOX_SATELITE_STYLE);
                    } else {
                      setMapStyle(MAPBOX_OUTDOOR_STYLE);
                    }
                  }}
                >
                  {mapStyle === MAPBOX_OUTDOOR_STYLE ? (
                    <Image
                      source={require('../../assets/satellite_map_icon.png')}
                      style={styles.mapStyleIcon}
                    />
                  ) : (
                    <Image
                      source={require('../../assets/street_map_icon.png')}
                      style={styles.mapStyleIcon}
                    />
                  )}
                </TouchableOpacity>
                <Mapbox.MapView
                  logoEnabled={false}
                  attributionEnabled={false}
                  style={styles.map}
                  styleURL={mapStyle}
                  onRegionDidChange={(state) => {
                    setSelectedRegion(state.properties.visibleBounds);
                    console.log(
                      'MAP REGION CHANGED ====> ',
                      JSON.stringify(state.properties)
                    );
                  }}
                >
                  <Mapbox.Camera
                    zoomLevel={12}
                    centerCoordinate={centerCoordinates}
                  />
                </Mapbox.MapView>
              </View>
              <View style={styles.modalActions}>
                <Pressable
                  style={[styles.modalButton, styles.cancelButton]}
                  onPress={() => {
                    setModalVisible(false);
                    setRegionName('');
                    setZoomLevel(12);
                    setSelectedRegion();
                  }}
                >
                  <Text style={styles.buttonText}>Cancel</Text>
                </Pressable>
                <Pressable style={styles.modalButton} onPress={saveOfflineRegion}>
                  <Text style={styles.buttonText}>Save</Text>
                </Pressable>
              </View>
            </View>
          </View>
        </KeyboardAvoidingView>
      </Modal>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  regionList: {
    paddingHorizontal: 16,
    paddingBottom: 80,
  },
  regionItemContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  regionItem: {
    backgroundColor: '#fff',
    padding: 10,
    marginVertical: 8,
    borderRadius: 8,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  infoContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    flex: 1,
  },
  icon: {
    marginRight: 10,
  },
  textContainer: {
    flex: 1,
  },
  regionName: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#212121',
  },
  regionInfo: {
    fontSize: 14,
    color: '#757575',
    marginTop: 4,
  },
  actionIcons: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  iconButton: {
    marginHorizontal: 8,
  },
  label: {
    fontSize: 18,
    color: '#757575',
    marginTop: 4,
    marginBottom: 8,
    textAlign: 'center',
  },
  emptyText: {
    textAlign: 'center',
    marginTop: 20,
    fontSize: 16,
    color: '#757575',
  },
  fab: {
    position: 'absolute',
    right: 20,
    bottom: 30,
    backgroundColor: '#4CAF50',
    width: 56,
    height: 56,
    borderRadius: 8,
    justifyContent: 'center',
    alignItems: 'center',
    elevation: 5,
  },
  fabText: {
    fontSize: 30,
    color: '#FFF',
    fontWeight: 'bold',
  },
  modalBackground: {
    height: Dimensions.get('window').height,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
  },
  modalContent: {
    width: '90%',
    backgroundColor: '#FFF',
    padding: 16,
    borderRadius: 8,
    elevation: 4,
  },
  modalHeader: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#212121',
    marginBottom: 16,
    textAlign: 'left',
  },
  input: {
    height: 48,
    borderWidth: 1,
    borderColor: '#BDBDBD',
    borderRadius: 4,
    paddingHorizontal: 12,
    backgroundColor: '#FAFAFA',
  },
  mapContainer: {
    height: 400,
    width: '100%',
    borderRadius: 8,
    marginBottom: 16,
    overflow: 'hidden',
    justifyContent: 'center',
    alignItems: 'center',
    position: 'relative',
  },
  map: {
    flex: 1,
    height: 400,
    width: 400,
    borderWidth: 5,
    borderColor: '#4CAF50',
    position: 'relative',
  },
  mapStyle2: {
    position: 'absolute',
    bottom: 20,
    right: 220,
    width: 70,
    height: 70,
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 2,
    borderRadius: 10,
    overflow: 'hidden',
    zIndex: 1,
  },
  mapStyleIcon: {
    width: 70,
    height: 70,
  },
  modalActions: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  modalButton: {
    flex: 1,
    backgroundColor: '#4CAF50',
    paddingVertical: 12,
    borderRadius: 4,
    alignItems: 'center',
    marginHorizontal: 4,
  },
  cancelButton: {
    backgroundColor: '#757575',
  },
  buttonText: {
    color: '#FFF',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

export default MapRegionCaching;

const ProgressModal = ({
  visible,
  progress,
  onClose,
  cancelButtonTitle,
  onCancelPress,
  title,
}) => {
  return (
    <Modal
      transparent
      animationType="slide"
      visible={visible}
      onRequestClose={onClose}
    >
      <View style={styles1.modalBackground}>
        <View style={styles1.modalContainer}>
          <Text style={styles1.modalTitle}>{title}</Text>
          <View style={styles1.progressContainer}>
            <View style={styles1.progressBar}>
              <View
                style={[styles1.progressFill, { width: `${progress}%` }]}
              />
            </View>
            <Text style={styles1.progressText}>{`${progress}%`}</Text>
          </View>
          <View style={styles1.buttonContainer}>
            <TouchableOpacity
              style={[styles1.button, styles1.cancelButton]}
              onPress={onCancelPress}
            >
              <Text style={styles1.buttonText}>{cancelButtonTitle}</Text>
            </TouchableOpacity>
          </View>
        </View>
      </View>
    </Modal>
  );
};

ProgressModal.propTypes = {
  visible: PropTypes.bool.isRequired,
  progress: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
    .isRequired,
  onClose: PropTypes.func,
  cancelButtonTitle: PropTypes.string.isRequired,
  onCancelPress: PropTypes.func.isRequired,
  title: PropTypes.string.isRequired,
};

const styles1 = StyleSheet.create({
  modalBackground: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
  },
  modalContainer: {
    width: '80%',
    backgroundColor: '#FFFFFF',
    borderRadius: 8,
    padding: 16,
    alignItems: 'flex-start',
    elevation: 5,
  },
  modalTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  progressContainer: {
    width: '100%',
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 20,
  },
  progressBar: {
    width: '95%',
    height: 10,
    backgroundColor: '#E0E0E0',
    borderRadius: 5,
    overflow: 'hidden',
  },
  progressFill: {
    height: '100%',
    backgroundColor: '#4CAF50',
  },
  progressText: {
    textAlign: 'center',
    fontSize: 16,
    fontWeight: 'bold',
    color: '#333',
    width: '5%',
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'flex-end',
    width: '100%',
  },
  button: {
    flex: 1,
    padding: 12,
    backgroundColor: '#757575',
    borderRadius: 8,
    marginHorizontal: 8,
    alignItems: 'center',
  },
  cancelButton: {},
  buttonText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

Image Image

Observed behavior and steps to reproduce

we area download a area of map using createPack method and we getting that in packs by calling getPack() method but we are not able to see that area of map when there is internet connection besically we area downloading area of map at certain zoom level so that we can see that area when there is not internet connection we followed everything from dcoumentation and every issue reported nothing is working

LOGS i have recorded

Logs of Downloading offline pack.----- from zoom level 18,20

PROGRESS ====> {"_metadata": null, "pack": {"bounds": "{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-79.378419567841,43.665287295057084]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-79.40243501061582,43.64791235618151]}}]}", "metadata": "{"name":"Toronto sat "}"}} (NOBRIDGE) LOG STATUS ====> {"completedResourceCount": 0, "completedResourceSize": 0, "erroredResourceCount": 0, "loadedResourceCount": 0, "loadedResourceSize": 0, "metadata": {"_rnmapbox": {"bounds": [Object], "styleURI": "mapbox://styles/mapbox/satellite-streets-v12", "zoomRange": [Array]}, "name": "Toronto sat "}, "name": "Toronto sat ", "percentage": 0, "requiredResourceCount": 1, "state": "unkown"} (NOBRIDGE) LOG PROGRESS ====> {"_metadata": {"name": "Toronto sat "}, "pack": {"bounds": "{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-79.378419567841,43.665287295057084]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-79.40243501061582,43.64791235618151]}}]}", "metadata": "{"name":"Toronto sat "}"}} (NOBRIDGE) LOG STATUS ====> {"completedResourceCount": 1, "completedResourceSize": 8648487, "erroredResourceCount": 0, "loadedResourceCount": 0, "loadedResourceSize": 0, "metadata": {"_rnmapbox": {"bounds": [Object], "styleURI": "mapbox://styles/mapbox/satellite-streets-v12", "zoomRange": [Array]}, "name": "Toronto sat "}, "name": "Toronto sat ", "percentage": 100, "requiredResourceCount": 1, "state": "complete"} (NOBRIDGE) LOG OFFLINE_MAPS ===> [{"pack":{"expires":"Sun Mar 23 17:51:06 GMT+05:30 2025","percentage":100,"bounds":[-79.378419567841,43.66528729505709,-79.40243501061582,43.64791235618151],"completedResourceSize":8648487,"metadata":"{"name":"Toronto sat ","_rnmapbox":{"bounds":{"coordinates":[[[-79.3784196,43.6652873],[-79.402435,43.6652873],[-79.402435,43.6479124],[-79.3784196,43.6479124],[-79.3784196,43.6652873]]],"type":"Polygon"},"zoomRange":[18,20],"styleURI":"mapbox:\/\/styles\/mapbox\/satellite-streets-v12"}}","state":"complete","completedResourceCount":1,"requiredResourceCount":1},"_metadata":{"name":"Toronto sat ","_rnmapbox":{"bounds":{"coordinates":[[[-79.3784196,43.6652873],[-79.402435,43.6652873],[-79.402435,43.6479124],[-79.3784196,43.6479124],[-79.3784196,43.6652873]]],"type":"Polygon"},"zoomRange":[18,20],"styleURI":"mapbox://styles/mapbox/satellite-streets-v12"}}}]

when viewing map with no network its just cache is visible which was there when downloaded it at zoom 13 just those cache tiles are visible when i am zooming it to level 18-20 no detailed tiles are visible of that region i created pack for.

these log data which offline device trying to view offline pack in mapview

(NOBRIDGE) ERROR Mapbox [error] RNMBXMapView | Map load failed: {tile-id={x=293049, y=382662, z=20}, type=tile, message=Failed to load tile: Unable to resolve host "api.mapbox.com": No address associated with hostname, begin=86303505788, source-id=mapbox-satellite} (NOBRIDGE) LOG CAMERA_STATE ====> {"gestures": {"isGestureActive": true}, "properties": {"bounds": {"ne": [Array], "sw": [Array]}, "center": [-79.39018702067013, 43.65789523675591], "heading": 354.00065729767084, "pitch": 0, "zoom": 19.07918573547368}, "timestamp": 1740140840016}

SCREENSHOTS when device is offline for the downloaded area at same zoom levels which we have downloaded for.

Expected behavior

Downloaded area of map should be visible when there is no internet connection to device

Notes / preliminary analysis

No response

Additional links and references

No response

sahni01 avatar Feb 24 '25 16:02 sahni01

am also facing the same issue , i was downloading the app , but it was not showing as the zoom level of 20 , i thick it was only 13 zoom level only

sampathkumarch avatar Feb 27 '25 12:02 sampathkumarch

@sampathkumarch you mean offline packs are working till zoom level 13 only?

sahni01 avatar Feb 28 '25 07:02 sahni01

yes , I also worked on the same thing but was unable to get the clarity of tiles

On Fri, Feb 28, 2025 at 1:04 PM NITESH SAHNI @.***> wrote:

@sampathkumarch https://github.com/sampathkumarch you mean offline packs are working till zoom level 13 only?

— Reply to this email directly, view it on GitHub https://github.com/rnmapbox/maps/issues/3790#issuecomment-2689939117, or unsubscribe https://github.com/notifications/unsubscribe-auth/AL6WXQYM5XVUXISO6DN4IJD2SAGSDAVCNFSM6AAAAABXYNHLP2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMOBZHEZTSMJRG4 . You are receiving this because you were mentioned.Message ID: @.***> [image: sahni01]sahni01 left a comment (rnmapbox/maps#3790) https://github.com/rnmapbox/maps/issues/3790#issuecomment-2689939117

@sampathkumarch https://github.com/sampathkumarch you mean offline packs are working till zoom level 13 only?

— Reply to this email directly, view it on GitHub https://github.com/rnmapbox/maps/issues/3790#issuecomment-2689939117, or unsubscribe https://github.com/notifications/unsubscribe-auth/AL6WXQYM5XVUXISO6DN4IJD2SAGSDAVCNFSM6AAAAABXYNHLP2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMOBZHEZTSMJRG4 . You are receiving this because you were mentioned.Message ID: @.***>

sampathkumarch avatar Feb 28 '25 12:02 sampathkumarch

anybody got any solution for it or any working example? if its not working then why its even there in documentation ?

sahni01 avatar Mar 05 '25 08:03 sahni01

@sampathkumarch I've resolved the issue—a small bug in the Kotlin implementation was causing the problem. To isolate the issue, I first implemented a Kotlin-only version to determine whether it was related to Mapbox or our repository. It turned out that the problem was in the repository itself. I've created a PR with the fix, and now the tiles download correctly, render on the map view (even when the device is offline), and all Mapbox styles work as expected.

sahni01 avatar Mar 06 '25 18:03 sahni01

@sahni01, Can I know where I have to be changes are required like kotlin file , can you guide me in detail manner . It help to me also thank you for support

sampathkumarch avatar Mar 07 '25 03:03 sampathkumarch

@sahni01, What are the changes are required to get max zoom level can you explain it , i didn't see the any PR

sampathkumarch avatar Mar 08 '25 07:03 sampathkumarch

@sahni01, Hello, I have the same problem as above. I'd like to know what changes you've made to correct the implementation?

M4thi4s avatar Mar 27 '25 15:03 M4thi4s

Hello , i have the same problem as above

Leadola1 avatar Apr 01 '25 02:04 Leadola1

If you're interested in downloading high-resolution tiles (zoom 22 max), you'll need to use offlineManagerLegacy class. From what I understand, the new offlineManager class only allows downloading in the zoom range 1 to 16 (source).

Here's a sample code for downloading high-resolution tiles (with @rnmapbox : 10.1.37) :

import { Bounds } from "@models/bounds";
import Mapbox from "@rnmapbox/maps";

async function checkPackDownloading(zone: string, onProgress: (progress: number) => void, onFatalError: (error: Error) => void){
    const pack = await Mapbox.offlineManagerLegacy.getPack(zone);
    if (pack != undefined) {
        const status = await pack.status();
        console.log(status);

        // Mapbox.OfflinePackDownloadState is not fully compatible with offlineManagerLegacy, avoid using it directly
        if (status.state === 2) { // Completed
            onProgress(100);
        } else if (status.state === 0 && status.percentage != 100) { // Fatal Error
            console.error("Zone download paused", zone);
            onFatalError(new Error("TILE_DOWNLOAD_PAUSED"));
        } else if (status.state === 1) { // In progress
            onProgress(status.percentage);

            setTimeout(() => {
                checkPackDownloading(zone, onProgress, onFatalError);
            }, 2000);
        } else {
            onFatalError(new Error(`UNEXPECTED_STATE (${status.state})`));
        }
    } else {
        onFatalError(new Error("PACK_NOT_FOUND"));
    }
}

export async function fetchPack(zone: string, bounds: Bounds, onProgress: (progress: number) => void, onFatalError: (error: Error) => void) {
    let packInfo = await Mapbox.offlineManagerLegacy.getPack(zone);
    console.log(bounds);
    if(packInfo && (packInfo as any).pack.state != Mapbox.OfflinePackDownloadState.Complete) {
        // Download was interrupted, remove it and download again
        await removeAllPack();
        packInfo = undefined;
    }
    if (!packInfo) {
        await Mapbox.offlineManagerLegacy.createPack({
            name: zone,
            styleURL: Mapbox.StyleURL.SatelliteStreet,
            minZoom: 14,
            maxZoom: 20, // Maximum zoom level is 22, but 17 is the practical limit for certain zones. In USA and France, zoom level 22 is available.
            bounds: [  // [SW, NE]
                [bounds.sw[0], bounds.sw[1]],
                [bounds.ne[0], bounds.ne[1]],
            ]}
        );

        checkPackDownloading(zone, onProgress, onFatalError);

    } else {
        console.log("Zone already downloaded", zone);
        onProgress(100);
    }
}

For those who'd like an example in native kotlin code, here's a repo I made to understand how the library works:

Many bugs can occur, notably due to the pixel ratio or the coordinates of your bounding box. And don't forget the limit of 6,000 tiles per region... In some regions, satellite image quality doesn't increase after zoom level 17, but you should be able to download tiles at zoom level 22 everywhere.

To test on your device, I recommend that you try running a native code if the react native code doesn't work. This code has not been tested on iOS.

M4thi4s avatar Apr 01 '25 09:04 M4thi4s

someone have a solution to estimate download size to ask the user before download please? i tried many different way but nothing work correctly, the estimation are wrong 80% of the time and the rnmapbox module don't implemente the function to calculate

djangoamidala avatar Apr 12 '25 14:04 djangoamidala

Hey @djangoamidala @Leadola1 @sampathkumarch @M4thi4s — sorry folks, I got a bit caught up and couldn’t follow up on the issue earlier. the problem was in rbmapbox itself — I had to update some of its internal modules to fix it. I’m putting together a PR now, and once it’s accepted, the fix will be included in the next release.

sahni01 avatar Apr 12 '25 15:04 sahni01

@sahni01 Any updates?

nick-d2g avatar Aug 14 '25 10:08 nick-d2g

I'm closing this now, feel free to open this In a new issue if you can still reproduce it, or it's still relevant

mfazekas avatar Oct 13 '25 19:10 mfazekas