Skip to main content

Sahib Update

How Qibla Calculation & Compass Work


1. Qibla Direction Calculation

Library Used

  • adhan — JavaScript library for Islamic calculations
export const findNearestCity = async (
  latitude: number,
  longitude: number,
): Promise<NearestCityResult> => {
  const nearest = await db.query.cities.findFirst({
    columns: { id: true, name: true },
    orderBy: sql`abs(${cities.latitude} - ${latitude}) + abs(${cities.longitude} - ${longitude})`,
  });

  return nearest || null;
};

How It Works

const coordinates = new Coordinates(latitude, longitude);
const qiblaAngle = Qibla(coordinates); // Returns angle in degrees (0-360)

2. Device Heading (Compass)

Library Used

  • expo-location — Expo’s location services

How It Works

Location.watchHeadingAsync((heading) => {
  const headingValue = heading.trueHeading > 0 
    ? heading.trueHeading    // True North (GPS-based)
    : heading.magHeading;    // Magnetic North (compass-based)
  
  setDeviceHeading(headingValue); // 0-360 degrees
});
  • Listens to the device’s magnetometer/compass
  • Returns the device’s heading (0–360°)
  • trueHeading: GPS-based (more accurate, requires GPS)
  • magHeading: magnetic compass (works without GPS)

3. Compass Rotation Formula

targetRotation = -deviceHeading

currentNormalized = normalizeAngle(currentRotation)  // 0-360
targetNormalized = normalizeAngle(targetRotation)    // 0-360

diff = targetNormalized - currentNormalized
if (diff > 180) diff -= 360   // Goshortening the other waymovement
if (diff < -180) diff += 360  // Goshortening the other waymovement

finalTarget = currentRotation + diff
Why negative?
  • When device points North (0°), compass should show North at top (0° rotation)
  • When device rotates 90° East, compass rotates -90° to keep North at top

4. Visual Display

Compass Circle

  • Rotates with device movement
  • Formula: rotate(-deviceHeading)
  • Keeps North at the top visually

Qibla Arrow

  • Stays fixed, pointing to qibla direction
  • Formula: rotation = qiblaDirection (e.g., 58°)
  • Shows the direction to Mecca relative to the compass

Design patterns in prayer-times service


 

1. Template Method Pattern

What it is: The base class defines the algorithm steps, and subclasses fill in the specific parts.
How it works here:
export interface PrayerTimeInput {
  coordinates: Coordinates;
  date?: Date;
  timezone: string;
  format?: string;
  adjustments?: PrayerAdjustments;
}
export interface PrayerTimesResult {
  fajr: Date;
  sunrise: Date;
  dhuhr: Date;
  asr: Date;
  maghrib: Date;
  isha: Date;
  formatted?: FormattedPrayerTimes;
}

export interface FormattedPrayerTimes {
  fajr: string;
  sunrise: string;
  dhuhr: string;
  asr: string;
  maghrib: string;
  isha: string;
}
// Base class defines the template
abstract class PrayerTimeCalculator {
  public calculate(input: PrayerTimeInput): PrayerTimesResult {
    // Step 1: Prepare (subclass implements)
    const params = this.prepareCalculationParameters(input);
    
    // Step 2: Apply adjustments (subclass implements)
    const adjustedadjustedParams = this.applyAdjustments(params, input.adjustments);
    
    // Step 3: Calculate (subclass implements)
    const rawTimes = this.calculateRawPrayerTimes(input, adjusted)adjustedParams);
    
    // Step 4 & 5: Common steps (base class handles)
    const timezoneAware = this.convertToTimezone(rawTimes, input.timezone);
    const formatted = this.formatPrayerTimes(timezoneAware, input.timezone);
    
    return { ...timezoneAware, formatted };
  }
}
Analogy: A recipe template where steps 1–3 are fixed, and steps vary by method

2. Adapter Pattern

What it is: Translates one interface to another so different systems can work together.
Example: 
  • AdhanAdapter wraps the Adhan library to match the generic interface.
/**
 * Adapter for Adhan library
 * This adapter handles all Adhan-specific logic, making the calculator generic
 */
export class AdhanAdapter {
  /**
   * Create Adhan Coordinates from generic coordinates
   */
  static createCoordinates(input: PrayerTimeInput): AdhanCoordinates {
    return new AdhanCoordinates(input.coordinates.latitude, input.coordinates.longitude);
  }


  /**
   * Calculate prayer times using Adhan library
   */
  static calculatePrayerTimes(
    input: PrayerTimeInput,
    params: CalculationParameters,
  ): RawPrayerTimes {
    const coordinates = AdhanAdapter.createCoordinates(input);
    const date = input.date || new Date();
    const prayerTimes = new AdhanPrayerTimes(coordinates, date, params);

    return {
      fajr: prayerTimes.fajr,
      sunrise: prayerTimes.sunrise,
      dhuhr: prayerTimes.dhuhr,
      asr: prayerTimes.asr,
      maghrib: prayerTimes.maghrib,
      isha: prayerTimes.isha,
    };
  }
}

3. Facade Pattern

What it is: A simple interface that hides complexity behind the scenes.
/**
 * Factory function to create a calculator instance based on method name
 * @param methodName - Name of the calculation method (enum or 'Dummy' string)
 * @returns Calculator instance
 */
export const createCalculator = (methodName: CalculationMethodName | 'Dummy'): PrayerTimeCalculator => {
  switch (methodName) {
    case CalculationMethodName.MuslimWorldLeague:
      return new MuslimWorldLeagueCalculator();
    case CalculationMethodName.Egyptian:
      return new EgyptianCalculator();
      //....
    default:
      throw new Error(`Unknown calculation method: ${methodName}`);
  }
};

/**
 * Convenience function to calculate prayer times
 * @param methodName - Name of the calculation method
 * @param input - Prayer time calculation input
 * @returns Prayer times result
 */
export const calculatePrayerTimes = (
  methodName: CalculationMethodName,
  input: PrayerTimeInput,
): PrayerTimesResult => {
  const calculator = createCalculator(methodName);
  return calculator.calculate(input);
};

Usage
export enum CalculationMethodName {
  MuslimWorldLeague = 'MuslimWorldLeague',
  Egyptian = 'Egyptian',
  Karachi = 'Karachi',
  UmmAlQura = 'UmmAlQura',
  Dubai = 'Dubai',
  MoonsightingCommittee = 'MoonsightingCommittee',
  NorthAmerica = 'NorthAmerica',
  Kuwait = 'Kuwait',
  Qatar = 'Qatar',
  Singapore = 'Singapore',
  Tehran = 'Tehran',
  Turkey = 'Turkey',
}
const prayerTimesResult = calculatePrayerTimes(CalculationMethodName.MuslimWorldLeague, {
        coordinates: {
          latitude: state.latitude,
          longitude: state.longitude,
        },
        date: new Date(),
        timezone: state.timezone,
        format: 'h:mm A',
        adjustments: state.prayerAdjustments,
      });

Data Storage


 

I migrated from WatermelonDB

migrated from WatermelonDB to Drizzle ORM because:
  1. Prepopulated database support: WatermelonDB doesn't support prepopulated databases, which we need for cities data.
  2. Migration limitations: No built-in support for renaming or deleting columns, even using raw sql is pain.
  3. API extensibility: Hard to extend or customize.
  4. Documentation: Poor and outdated.
Drizzle ORM addresses these and works well with Expo SQLite.

Drizzle Features

1. Database instance (drizzle)

Feature: Creates a Drizzle ORM instance wrapping SQLite
import { drizzle } from 'drizzle-orm/expo-sqlite';
import { openDatabaseSync } from 'expo-sqlite';
import * as schema from './schemas';

const expoDb = openDatabaseSync('sahib.db', {
  enableChangeListener: true,
});

export const db = drizzle(expoDb, { schema });
Usage: Use db for all database operations.

2. Table definition (sqliteTable)

Feature: Defines SQLite tables with type-safe columns
import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';

export const cities = sqliteTable('cities', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  latitude: real('latitude').notNull(),
  longitude: real('longitude').notNull(),
});
Column types used:
  • integer() - Integer numbers
  • text() - Text strings
    real() - Floating point numbers

    3. Column modifiers

    Feature: Adds constraints and properties to columns
    export const cities = sqliteTable('cities', {
      // Primary key with auto-increment
      id: integer('id').primaryKey({ autoIncrement: true }),
      
      // Not null constraint
      name: text('name').notNull(),
      
      // Foreign key reference
      countryId: integer('country_id')
        .notNull()
        .references(() => countries.id),
      
      // Optional field (nullable)
      wikiDataId: text('wikiDataId'),
    });

    6.3. Query builder API (db.query)

    Feature: Type-safe query builder with relation support
    // Query 
    const cityWithCountry = await db.query.cities.findFirst({
      where: (cityTable, { eq }) => eq(cityTable.id, cityId),
      columns: {
        name: true,
        latitude: true,
      },
      with: {
        country: {
          columns: {
            name: true,
            timezones: true,
          },
        },
        state: {
          columns: {
            name: true,
          },
        },
      },
    });

    7. Raw SQL (sql template tag)

    import { sql } from 'drizzle-orm';
    
    const results = await db.query.cities.findMany({
      where: sql`lower(${cities.name}) LIKE lower(${`%${searchQuery}%`})`,
      limit: 40,
    });

    8. Reactive queries (useLiveQuery)

    import { useLiveQuery } from 'drizzle-orm/expo-sqlite';
    
    function CityList() {
      const { data, updatedAt } = useLiveQuery(
        getCitiesQuery(searchQuery, 40),
        [searchQuery], // Dependencies - re-runs when these change
      );
    
      // data automatically updates when database changes!
      return (
        <FlatList
          data={data ?? []}
          renderItem={({ item }) => <CityItem city={item} />}
        />
      );
    }

    Features:

    • Auto-updates when database changes
    • Returns data and updatedAt
    • Re-runs when dependencies change

    10. Migrations (useMigrations)

    import { useMigrations } from 'drizzle-orm/expo-sqlite/migrator';
    import migrations from '@/src/db/migrations/migrations';
    
    function MigratorComponent() {
      const sqlite = useSQLiteContext();
      const db = drizzle(sqlite, { schema });
      
      // Automatically runs pending migrations on app start
      useMigrations(db, migrations);
      
      return null;
    }

    11. Migration generation (drizzle-kit)

    # Generate migrations from schema changes
    npx drizzle-kit generate
    
    # This creates SQL migration files in src/db/migrations/

    12. Database studio (useDrizzleStudio)

    Feature: Visual database browser (development only)

    Expo SQLite Key-Value Storage


    1. Basic setup

    import Storage from 'expo-sqlite/kv-store';
    
    Storage.setItemSync('key', 'value');
    const value = Storage.getItemSync('key');
    
    // Asynchronous API
    await Storage.setItem('key', 'value');
    const value = await Storage.getItem('key');

    2. Wrapper implementation

    import Storage from 'expo-sqlite/kv-store';
    
    export const storage = {
      // ==================== Synchronous API ====================
      
      setString: (key: string, value: string): void => {
        Storage.setItemSync(key, value);
      },
      
      getString: (key: string): string | null => {
        return Storage.getItemSync(key);
      },
      
      setBoolean: (key: string, value: boolean): void => {
        Storage.setItemSync(key, String(value));
      },
      
      getBoolean: (key: string): boolean | null => {
        const value = Storage.getItemSync(key);
        if (value === null) return null;
        return value === 'true';
      },
      
      setNumber: (key: string, value: number): void => {
        Storage.setItemSync(key, String(value));
      },
      
      getNumber: (key: string): number | null => {
        const value = Storage.getItemSync(key);
        if (value === null) return null;
        const num = Number(value);
        return isNaN(num) ? null : num;
      },
      
      remove: (key: string): void => {
        Storage.removeItemSync(key);
      },
      
      clear: (): void => {
        Storage.clearSync();
      },
      
      // ==================== Asynchronous API ====================
      
      setStringAsync: async (key: string, value: string): Promise<void> => {
        await Storage.setItem(key, value);
      },
      
      getStringAsync: async (key: string): Promise<string | null> => {
        return await Storage.getItem(key);
      },
      
      // ... similar async methods for boolean, number, remove, clear
    };

    How Cities Are Handled


    Prepopulated database structure

    What's prepopulated:
    • Cities with coordinates (latitude/longitude)
    • Countries with timezones and translations
    • States, regions, and subregions
    • Relationships between all entities

    Screenshot 2026-01-23 at 23.50.27.png

    Nearest city detection

    Formula:
    drawing-1-1769208332.png
    export const findNearestCity = async (
      latitude: number,
      longitude: number,
    ): Promise<NearestCityResult> => {
      const nearest = await db.query.cities.findFirst({
        columns: { id: true, name: true },
        orderBy: sql`abs(${cities.latitude} - ${latitude}) + abs(${cities.longitude} - ${longitude})`,
      });
    
      return nearest || null;
    };