maplibre-react-native icon indicating copy to clipboard operation
maplibre-react-native copied to clipboard

Crash on Android - TextureViewRend

Open kkx64 opened this issue 3 months ago • 3 comments

Describe and reproduce the Bug

Summary

I haven't pinned down exactly what, but on Android when rendering Heatmaps and Markers, zooming out causes this error.

--------- beginning of crash
08-07 18:36:04.663   417   514 F libc    : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x2827a96c79fe71 in tid 514 (TextureViewRend), pid 417 (om.six33.first2)

Here's some code for the event markers we use

Single Marker

import { MarkerView, PointAnnotation } from '@maplibre/maplibre-react-native';
import { Pressable, View, StyleSheet } from 'react-native';
// Animation imports removed
import CustomText from '../../../UI/CustomText';

import { FontAwesome6 as Icon } from '@expo/vector-icons';
import SocialEvent from '../../../../redux/models/Event/Event';
import { useMap } from '../../../Map/MapContext';
import { useTranslation } from 'react-i18next';
import { memo, useEffect, useMemo } from 'react';
import { colors } from '../../../../utils/colors';
import { MAP_FRIEND_COLOR } from '../../../../utils/constants';
import { dayjsWithCoords } from '../../../../utils/dayjsUtils';
import { getColorPalette } from '../../../../utils/getColorPalette';
import CrewLogo from '../../../UI/Crews/CrewLogo';
import NameText from '../../../UI/NameText';
import { Image } from 'expo-image';

const underlayImg = require('../../../../assets/images/event-underlay.png');

const THRESHOLD_ICON_ZOOM = 8;
const THRESHOLD_DETAILS_ZOOM = 12;

interface EventMarkerProps {
  event: SocialEvent;
  onPressZoomIn?: () => void;
  onPressEvent?: (eventId: string) => void;
}

export const EventMarker = memo(
  ({ event, onPressEvent, onPressZoomIn }: EventMarkerProps) => {
    const { mapZoom } = useMap();
    const { t } = useTranslation();

    const isCrewEvent = useMemo(() => Boolean(event.crew?.id), [event]);

    const colorPalette = useMemo(() => {
      const primaryColor =
        event.crew?.primaryColor ||
        (event.private ? MAP_FRIEND_COLOR : colors.accent);
      const secondaryColor =
        event.crew?.secondaryColor ||
        (event.private ? MAP_FRIEND_COLOR : colors.accent);

      return getColorPalette(primaryColor, secondaryColor);
    }, [event.crew?.primaryColor, event.crew?.secondaryColor, event.private]);

    const colorOnDark = useMemo(() => {
      return colorPalette.isPrimaryLight ? colorPalette.primary : '#ffffff';
    }, [colorPalette]);

    const markerStyle = useMemo(
      () => [
        styles.markerContainer,
        event.crew ? styles.largeMarker : styles.smallMarker,
        { backgroundColor: `${colorPalette.colorOnDark}99` },
      ],
      [event.crew, colorPalette.colorOnDark],
    );

    const dotStyle = useMemo(
      () => [styles.dot, { backgroundColor: colorPalette.secondary }],
      [colorPalette.secondary],
    );

    if (!event.location?.coordinates) {
      return null;
    }

    return (
      <PointAnnotation
        id={event.id.toString()}
        coordinate={[
          event.location.coordinates[0], // longitude
          event.location.coordinates[1], // latitude
        ]}
      >
        <Pressable
          onPress={() => {
            if (mapZoom < THRESHOLD_DETAILS_ZOOM) {
              onPressZoomIn?.();
              return;
            }
            onPressEvent?.(event.id);
          }}
          className="w-56 h-56 flex items-center justify-center"
          pointerEvents="box-none"
        >
          <View className="rounded-full overflow-hidden" style={[markerStyle]}>
            {event.crew?.logo && (
              <CrewLogo className="w-5 h-5" logoUrl={event.crew.logo} />
            )}
            {!event.crew && <View style={dotStyle} />}
          </View>
          {mapZoom >= THRESHOLD_DETAILS_ZOOM && (
            <View className="absolute top-0 left-0 w-56 h-56 flex items-center justify-center -z-[1]">
              <Image
                source={underlayImg}
                className="absolute -top-10 -left-10 -right-10 -bottom-10"
                contentFit="contain"
                style={{
                  transform: [{ rotate: '-90deg' }],
                }}
              />
              <View
                className="flex-1 justify-end flex-col"
                pointerEvents="none"
              >
                <NameText
                  isVerified={event.creator.verified}
                  type={isCrewEvent ? 'crew' : 'user'}
                  name={event.crew?.name || event.creator.username}
                  color={colorOnDark}
                  textStyle={{
                    fontSize: 10,
                  }}
                />
              </View>
              <View
                className="w-full h-16 flex-row items-center justify-center"
                pointerEvents="none"
              >
                <View className="flex-1 justify-end items-center flex-row space-x-1 opacity-80">
                  <CustomText className="text-xs text-right text-white" bold>
                    {dayjsWithCoords(
                      event.date,
                      event.location.coordinates[1],
                      event.location.coordinates[0],
                    ).format('D MMM\nHH:mm')}
                  </CustomText>
                </View>
                <View className="w-20 h-full" />
                <View className="flex-1 flex-col items-start">
                  <View className="flex flex-row items-center space-x-1 opacity-80">
                    <Icon
                      name={event.private ? 'people-group' : 'earth-americas'}
                      size={10}
                      color="white"
                    />
                    <CustomText className="text-xs text-white" bold>
                      {event.private
                        ? t('events.event.private')
                        : t('events.event.public')}
                    </CustomText>
                  </View>
                  <View className="flex flex-row items-center space-x-1 opacity-80">
                    <Icon name="user" solid size={9} color="white" />
                    <CustomText className="text-xs text-white" bold>
                      {event.attendeesCount || event.attending?.length || 0}
                    </CustomText>
                  </View>
                </View>
              </View>
              <View
                className="flex-1 w-full flex items-center"
                pointerEvents="box-none"
              >
                <View className="w-full" pointerEvents="none">
                  <CustomText
                    numberOfLines={2}
                    uppercase
                    className="text-center text-white w-full text-lg"
                    bold
                    style={{ color: colorOnDark }}
                  >
                    {event.name}
                  </CustomText>
                </View>

                {onPressEvent && (
                  <View className="p-2">
                    <CustomText
                      bold
                      uppercase
                      className="text-xs text-darkBlue-400"
                    >
                      {t('events.event.viewDetails')}
                    </CustomText>
                  </View>
                )}
              </View>
            </View>
          )}
        </Pressable>
      </PointAnnotation>
    );
  },
);

const styles = StyleSheet.create({
  markerContainer: {
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 40,
  },
  smallMarker: {
    width: 10,
    height: 10,
  },
  largeMarker: {
    width: 24,
    height: 24,
  },
  dot: {
    width: 4,
    height: 4,
    borderRadius: 4,
  },
});

Rendering the multiple markers and heatmap when zoomed out

import {
  CircleLayer,
  HeatmapLayer,
  ShapeSource,
} from '@maplibre/maplibre-react-native';
import React, { memo, useEffect, useMemo, useRef } from 'react';
import { useGetNearbyEventsQuery } from '../../../../redux/api/events';
import { useGetNearbyEventsGuestQuery } from '../../../../redux/api/events-guest';
import { useTypedSelector } from '../../../../redux/store';
import { colors } from '../../../../utils/colors';

import { useMap } from '../../../Map/MapContext';

import { isPointInBounds } from '../../../../utils/mapUtils';
import dayjs from 'dayjs';
import Color from 'color';
import { EventMarker } from './EventMarker';
import { useGridSearchCoords } from '../../useGridSearchCoords';

interface EventMarkersProps {
  onPressEvent?: (eventId: string, type: 'zoom' | 'event') => void;
}

const HEATMAP_ZOOM_THRESHOLD = 9;

const EventMarkers = memo(({ onPressEvent }: EventMarkersProps) => {
  const { mapRegion, mapZoom } = useMap();

  const guestMode = useTypedSelector((state) => state.config.guestMode);

  const gridCoords = useGridSearchCoords();

  // Use appropriate API based on guest mode
  const { data: nearbyEvents } = guestMode
    ? useGetNearbyEventsGuestQuery(
        {
          latitude: gridCoords.latitude,
          longitude: gridCoords.longitude,
          radius: gridCoords.radius,
        },
        {
          skip: !gridCoords.latitude || !gridCoords.longitude,
        },
      )
    : useGetNearbyEventsQuery(
        {
          latitude: gridCoords.latitude,
          longitude: gridCoords.longitude,
          radius: gridCoords.radius,
        },
        {
          skip: !gridCoords.latitude || !gridCoords.longitude,
        },
      );

  const validEvents = useMemo(
    () =>
      [...(nearbyEvents || [])]

        .filter((event) => dayjs(event.date).isBefore(dayjs().add(1, 'month')))
        .filter(
          (event) =>
            event.location?.coordinates?.[0] &&
            event.location?.coordinates?.[1],
        ),
    [nearbyEvents, mapRegion],
  );

  const validEventsForDisplay = useMemo(
    () =>
      validEvents
        .filter((event) => {
          if (!mapRegion || !mapRegion.properties.visibleBounds) return true;
          return isPointInBounds(
            {
              latitude: event.location.coordinates[1],
              longitude: event.location.coordinates[0],
            },
            mapRegion.properties.visibleBounds,
          );
        })
        .sort((a, b) =>
          a.participants
            ? a.participants.length - (b.participants?.length || 0)
            : 0,
        )
        .slice(0, 7),
    [validEvents, mapRegion],
  );

  // Create GeoJSON feature collection for heatmap
  const heatmapData = useMemo(() => {
    return {
      type: 'FeatureCollection' as const,
      features: (validEvents || []).map((event) => ({
        type: 'Feature' as const,
        properties: {
          weight: 0.1,
        },
        geometry: {
          type: 'Point' as const,
          coordinates: [
            event.location.coordinates[0], // longitude
            event.location.coordinates[1], // latitude
          ],
        },
      })),
    };
  }, [validEvents]);

  if (!validEvents.length) {
    return null;
  }

  return (
    <>
      <ShapeSource
        key="event-heatmap-source"
        id="event-heatmap-source"
        shape={heatmapData}
      >
        <HeatmapLayer
          id="event-heatmap"
          style={{
            heatmapIntensity: [
              'interpolate',
              ['linear'],
              ['zoom'],
              HEATMAP_ZOOM_THRESHOLD - 1,
              1,
              HEATMAP_ZOOM_THRESHOLD,
              0,
            ],
            heatmapColor: [
              'interpolate',
              ['linear'],
              ['heatmap-density'],
              0,
              Color(colors.accent).alpha(0).rgb().string(),
              1,
              Color(colors.accent).alpha(1).rgb().string(),
            ],
            heatmapRadius: ['interpolate', ['linear'], ['zoom'], 5, 2, 14, 4],
            heatmapOpacity: [
              'interpolate',
              ['linear'],
              ['zoom'],
              HEATMAP_ZOOM_THRESHOLD - 1,
              0.7,
              HEATMAP_ZOOM_THRESHOLD,
              0,
            ],
          }}
        />
      </ShapeSource>

      {mapZoom > HEATMAP_ZOOM_THRESHOLD &&
        validEventsForDisplay.map((event) => {
          return (
            <EventMarker
              onPressEvent={() => {
                onPressEvent?.(event.id, 'event');
              }}
              onPressZoomIn={() => {
                onPressEvent?.(event.id, 'zoom');
              }}
              key={event.id}
              event={event}
            />
          );
        })}
    </>
  );
});

export default memo(EventMarkers);

@maplibre/maplibre-react-native Version

10.2.0

Which platforms does this occur on?

Android Device

Which frameworks does this occur on?

Expo

Which architectures does this occur on?

Old Architecture

Environment

expo-env-info 1.3.4 environment info:
    System:
      OS: macOS 15.5
      Shell: 5.9 - /bin/zsh
    Binaries:
      Node: 22.9.0 - /usr/local/bin/node
      Yarn: 1.22.18 - /opt/homebrew/bin/yarn
      npm: 11.5.1 - /opt/homebrew/bin/npm
      Watchman: 2024.12.02.00 - /opt/homebrew/bin/watchman
    Managers:
      CocoaPods: 1.15.2 - /Users/kirilkrsteski/.rbenv/shims/pod
    SDKs:
      iOS SDK:
        Platforms: DriverKit 24.2, iOS 18.2, macOS 15.2, tvOS 18.2, visionOS 2.2, watchOS 11.2
    IDEs:
      Android Studio: 2024.2 AI-242.23339.11.2421.12550806
      Xcode: 16.2/16C5032a - /usr/bin/xcodebuild
    npmPackages:
      expo: ~52.0.47 => 52.0.47 
      react: 18.3.1 => 18.3.1 
      react-native: 0.76.9 => 0.76.9 
    npmGlobalPackages:
      eas-cli: 16.17.3
      expo-cli: 6.3.12
    Expo Workflow: bare

kkx64 avatar Aug 07 '25 16:08 kkx64

Thanks for the report. Please find a minimal reproduction we can simply copy/paste as a single file to reproduce.

KiwiKilian avatar Aug 07 '25 16:08 KiwiKilian

Thanks for the report. Please find a minimal reproduction we can simply copy/paste as a single file to reproduce.

@KiwiKilian I'm trying to pinpoint the exact issue myself, I thought it was some of the markers but I'm not sure that's the case anymore. I noticed using a mapStyle (specifically those from CARTO) would lock the map/app to 60fps (instead of 120) and tank performance.

Will comment here if I find anything more useful, unless this stuff with the styles is anything you already know

kkx64 avatar Aug 07 '25 19:08 kkx64

Can you try if this also occurs with surfaceView set to true on the MapView? Surface view shall be the default in the following releases. It has long be the default from MLN, but this library was never properly updated to reflect this.

KiwiKilian avatar Sep 24 '25 06:09 KiwiKilian