Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.13] — 2026-02-22

### Fixed
- Corrected preview contact interpolation so C1/C2/C3/C4 remain geometrically consistent even when the moon track has a non-zero closest-approach offset.
- Improved satellite/hybrid map readability by reducing eclipse overlay opacity in photo map modes and forcing map remount when switching map type to refresh tile detail loading.

### Changed
- Bumped `apps/mobile` version to `1.1.13`.

### Tests
- Added a regression test that verifies C1/C4 tangency remains exact for partial eclipse preview geometry with non-zero closest approach and directional travel.

## [1.1.12] — 2026-02-22

### Changed
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ if (typeof packageVersion !== "string" || !/^\d+\.\d+\.\d+$/.test(packageVersion

expo.version = packageVersion;
expo.runtimeVersion = packageVersion;
expo.extra = {
...expo.extra,
googleMapsAndroidApiKeyConfigured: Boolean(googleMapsAndroidApiKey),
};

if (!googleMapsAndroidApiKey) {
console.warn(
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eclipse-timer/mobile",
"version": "1.1.12",
"version": "1.1.13",
"private": true,
"main": "index.js",
"scripts": {
Expand Down
61 changes: 50 additions & 11 deletions apps/mobile/src/screens/TimerScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ function normalizeLongitudeDeg(lonDeg: number) {
return (((lonDeg % 360) + 540) % 360) - 180;
}

function mapTypeLabel(mapType: TimerState["mapType"]) {
if (mapType === "satellite") return "Satellite";
if (mapType === "hybrid") return "Hybrid";
return "Standard";
}

function destinationPoint(
latDeg: number,
lonDeg: number,
Expand Down Expand Up @@ -182,6 +188,13 @@ export default function TimerScreen({
: activeKindCode === "H"
? "Central Path"
: "Totality Path";
const isPhotoMapMode = timer.mapType !== "standard";
const visibleOverlayColor = isPhotoMapMode ? "rgba(79, 195, 247, 0.12)" : VISIBLE_PATH_COLOR;
const activeCentralOverlayColor = isPhotoMapMode
? activeKindCode === "A"
? "rgba(255, 167, 38, 0.16)"
: "rgba(255, 82, 82, 0.16)"
: centralOverlayColor;
const favoriteAtCurrentPin = useMemo(
() =>
favoriteLocations.find((location) =>
Expand Down Expand Up @@ -242,6 +255,7 @@ export default function TimerScreen({
timer.result,
]);
const hasDirectionsData = contactDirectionOverlays.length > 0;
const mapTypeText = mapTypeLabel(timer.mapType);

const closeAddFavoriteModal = () => {
setIsAddFavoriteModalOpen(false);
Expand Down Expand Up @@ -341,19 +355,24 @@ export default function TimerScreen({

<View style={styles.mapWrap}>
<MapView
key={`map-${timer.mapType}`}
ref={timer.mapRef}
style={styles.map}
region={timer.region}
onRegionChangeComplete={timer.onRegionChangeComplete}
onPress={timer.onMapPress}
mapType={timer.mapType}
showsBuildings
showsCompass
showsIndoors
showsPointsOfInterest
>
{timer.showVisibleOverlay
? timer.overlayVisiblePolygons.map((coordinates, idx) => (
<Polygon
key={`visible-${idx}`}
coordinates={coordinates}
fillColor={VISIBLE_PATH_COLOR}
fillColor={visibleOverlayColor}
strokeColor="rgba(79, 195, 247, 0.05)"
strokeWidth={0.5}
/>
Expand All @@ -364,7 +383,7 @@ export default function TimerScreen({
<Polygon
key={`central-${idx}`}
coordinates={coordinates}
fillColor={centralOverlayColor}
fillColor={activeCentralOverlayColor}
strokeColor="rgba(255,255,255,0.08)"
strokeWidth={0.5}
/>
Expand Down Expand Up @@ -438,15 +457,18 @@ export default function TimerScreen({
</Pressable>

<Pressable style={styles.mapOverlayBtn} onPress={timer.cycleMapType}>
<Text style={styles.mapOverlayBtnText}>
{timer.mapType === "standard"
? "Standard"
: timer.mapType === "satellite"
? "Satellite"
: "Hybrid"}
</Text>
<Text style={styles.mapOverlayBtnText}>{mapTypeText}</Text>
</Pressable>

{timer.mapType !== "standard" ? (
<View style={styles.mapModeHint}>
<Text style={styles.mapModeHintText}>
If satellite/hybrid tiles look old or blank on Android, rebuild with
GOOGLE_MAPS_ANDROID_API_KEY in apps/mobile/.env.local.
</Text>
</View>
) : null}

<View style={styles.mapLegend}>
<Pressable
style={[
Expand All @@ -464,7 +486,7 @@ export default function TimerScreen({
}
>
<View style={styles.mapLegendRow}>
<View style={[styles.mapLegendSwatch, { backgroundColor: VISIBLE_PATH_COLOR }]} />
<View style={[styles.mapLegendSwatch, { backgroundColor: visibleOverlayColor }]} />
<Text style={styles.mapLegendText}>Eclipse Visible</Text>
</View>
<Text style={styles.mapLegendState}>{timer.showVisibleOverlay ? "On" : "Off"}</Text>
Expand All @@ -484,7 +506,9 @@ export default function TimerScreen({
}
>
<View style={styles.mapLegendRow}>
<View style={[styles.mapLegendSwatch, { backgroundColor: centralOverlayColor }]} />
<View
style={[styles.mapLegendSwatch, { backgroundColor: activeCentralOverlayColor }]}
/>
<Text style={styles.mapLegendText}>{centralLegendLabel}</Text>
</View>
<Text style={styles.mapLegendState}>{timer.showCentralOverlay ? "On" : "Off"}</Text>
Expand Down Expand Up @@ -948,6 +972,21 @@ const styles = StyleSheet.create({
fontWeight: "700",
fontSize: 12,
},
mapModeHint: {
position: "absolute",
top: 52,
right: 10,
maxWidth: 230,
borderRadius: 10,
paddingHorizontal: 10,
paddingVertical: 6,
backgroundColor: "rgba(0,0,0,0.72)",
},
mapModeHintText: {
color: "#d2d2d2",
fontSize: 11,
lineHeight: 14,
},
mapGpsBtn: {
position: "absolute",
top: 10,
Expand Down
20 changes: 17 additions & 3 deletions apps/mobile/src/utils/previewGeometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,18 @@ function buildMotionAnchors(
contacts: PreviewMotionContacts,
sunRadius: number,
moonRadius: number,
moonClosestOffset: number,
): Array<{ progress: number; offsetX: number }> {
const externalTouchOffset = sunRadius + moonRadius;
const internalTouchOffset = Math.abs(sunRadius - moonRadius);
const axisDistanceForTouchOffset = (touchOffset: number) => {
const radialSq = touchOffset * touchOffset;
const closestSq = moonClosestOffset * moonClosestOffset;
const axisSq = radialSq - closestSq;
if (!Number.isFinite(axisSq) || axisSq <= 0) return 0;
return Math.sqrt(axisSq);
};

const externalTouchOffset = axisDistanceForTouchOffset(sunRadius + moonRadius);
const internalTouchOffset = axisDistanceForTouchOffset(Math.abs(sunRadius - moonRadius));
Comment on lines +141 to +142
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve motion anchors for non-eclipse previews

For kindAtLocation: "none", determineApproachOffset yields a moonClosestOffset larger than sunRadius + moonRadius, so axisDistanceForTouchOffset(...) collapses to 0 here; that makes both endpoint anchors offsetX = 0, and interpolateOffsetX returns a constant value for all progress values. In practice, no-eclipse previews now show a stationary moon instead of a pass-by track, which is a user-visible regression for locations where no contact occurs.

Useful? React with 👍 / 👎.


const anchors: Array<{ progress: number; offsetX: number }> = [
{ progress: 0, offsetX: -externalTouchOffset },
Expand Down Expand Up @@ -195,7 +204,12 @@ export function calculatePreviewMoonGeometry(params: {
sunRadius,
);

const anchors = buildMotionAnchors(params.contacts ?? {}, sunRadius, moonRadius);
const anchors = buildMotionAnchors(
params.contacts ?? {},
sunRadius,
moonRadius,
moonClosestOffset,
);
const axisOffset = interpolateOffsetX(params.progress, anchors);
const travelVector = params.travelVector ?? { x: 1, y: 0 };
const moonOffsetX = axisOffset * travelVector.x - moonClosestOffset * travelVector.y;
Expand Down
35 changes: 35 additions & 0 deletions apps/mobile/tests/preview-geometry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,39 @@ describe("preview moon geometry", () => {
);
expect(describePreviewTravelDirection({ x: -0.7, y: 0.1 })).toBe("right to left");
});

it("keeps C1 and C4 as tangency even with non-zero closest approach", () => {
const travelVector = determinePreviewTravelVector({
c1BearingDeg: 230,
c4BearingDeg: 30,
});

const c1Geometry = calculatePreviewMoonGeometry({
progress: 0,
kindAtLocation: "partial",
magnitude: 0.72,
contacts: { c1: 0, max: 0.5, c4: 1 },
travelVector,
});
const c4Geometry = calculatePreviewMoonGeometry({
progress: 1,
kindAtLocation: "partial",
magnitude: 0.72,
contacts: { c1: 0, max: 0.5, c4: 1 },
travelVector,
});

const stageCenter = 150;
const c1Distance = Math.hypot(
c1Geometry.moonCenterX - stageCenter,
c1Geometry.moonCenterY - stageCenter,
);
const c4Distance = Math.hypot(
c4Geometry.moonCenterX - stageCenter,
c4Geometry.moonCenterY - stageCenter,
);

expect(c1Distance).toBeCloseTo(PREVIEW_SUN_RADIUS + c1Geometry.moonRadius, 6);
expect(c4Distance).toBeCloseTo(PREVIEW_SUN_RADIUS + c4Geometry.moonRadius, 6);
});
});