惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
Scott Helme
Scott Helme
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
C
Cyber Attacks, Cyber Crime and Cyber Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Schneier on Security
I
Intezer
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
V
Vulnerabilities – Threatpost
Cloudbric
Cloudbric
V2EX - 技术
V2EX - 技术
Google Online Security Blog
Google Online Security Blog
L
Lohrmann on Cybersecurity
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
LINUX DO - 热门话题
S
Secure Thoughts
T
The Exploit Database - CXSecurity.com
博客园 - 【当耐特】
Recent Announcements
Recent Announcements
Security Archives - TechRepublic
Security Archives - TechRepublic
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
K
Kaspersky official blog
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Latest news
Latest news
B
Blog
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
L
LangChain Blog
GbyAI
GbyAI
Last Week in AI
Last Week in AI
S
Security Affairs
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
Security Latest
Security Latest
Vercel News
Vercel News
Y
Y Combinator Blog
G
GRAHAM CLULEY
S
Securelist
T
Troy Hunt's Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
雷峰网
雷峰网

Supabase Blog

AI Agents Know About Supabase. They Don't Always Use It Right. Custom OIDC Providers for Supabase Auth 100,000 GitHub stars Supabase docs over SSH Navigating Regional Network Blocks Supabase Joins the Stripe Projects Developer Preview Log Drains: Now available on Pro Supabase Storage: major performance, security, and reliability updates Supabase incident on February 12, 2026 Hydra joins Supabase X / Twitter OAuth 2.0 is now available for Supabase Auth BKND joins Supabase Supabase is now an official Claude connector Supabase PrivateLink is now available Introducing: Postgres Best Practices When to use Read Replicas vs. bigger compute Introducing TRAE SOLO integration with Supabase Supabase Security Retro: 2025 Sync Stripe Data to Your Supabase Database in One Click Building ChatGPT Apps with Supabase Edge Functions and mcp-use Own Your Observability: Supabase Metrics API Introducing iceberg-js: A JavaScript Client for Apache Iceberg Introducing Supabase for Platforms Adding Async Streaming to Postgres Foreign Data Wrappers Build "Sign in with Your App" using Supabase Auth Introducing Seven New Email Templates for Supabase Auth The new Supabase power for Kiro Introducing Supabase ETL Introducing Analytics Buckets Introducing Vector Buckets Snap, Inc. Launches Snap Cloud, Powered by Supabase Triplit joins Supabase Supabase Series E 1000 Y Combinator Founders Choose Supabase gm 👋 web3, welcome aboard to Sign in with Web3 (Solana, Ethereum) Announcing the Supabase Remote MCP Server Enterprise speed, enterprise standards with Bolt Cloud + Supabase PostgREST 13 Lovable Cloud + Supabase: The Default Platform for AI Builders Processing large jobs with Edge Functions, Cron, and Queues Defense in Depth for MCP Servers OrioleDB Patent: now freely available to the Postgres community Supabase Launch Week 15 Hackathon Winner Announcement The Vibe Coder's Guide to Supabase Environments Testing for Vibe Coders: From Zero to Production Confidence The Vibe Coding Master Checklist Vibe Coding: Best Practices for Prompting Supabase Auth: Build vs. Buy Top 10 Launches of Launch Week 15 Supabase Launch Week 15 Hackathon Storage: 10x Larger Uploads, 3x Cheaper Cached Egress, and 2x Egress Quota Persistent Storage and 97% Faster Cold Starts for Edge Functions Algolia Connector for Supabase New Observability Features in Supabase Improved Security Controls and A New Home for Security Introducing Branching 2.0 Stripe-To-Postgres Sync Engine as standalone Library Supabase Analytics Buckets with Iceberg Support Create a Supabase backend using Figma Make Introducing JWT Signing Keys Supabase UI: Platform Kit Build a Personalized AI Assistant with Postgres Announcing Multigres: Vitess for Postgres Building on open table formats Open Data Standards: Postgres, OTel, and Iceberg Simplifying back-end complexity with Supabase Data APIs PostgreSQL Event Triggers without superuser access Top 10 Launches of Launch Week 14 Supabase MCP Server Data API Routes to Nearest Read Replica Declarative Schemas for Simpler Database Management Realtime: Broadcast from Database Keeping Tabs on What's New in Supabase Studio Edge Functions: Deploy from the Dashboard + Deno 2.1 Automatic Embeddings in Postgres Introducing the Supabase UI Library Supabase Auth: Bring Your Own Clerk Postgres Language Server: Initial Release Migrating from Fauna to Supabase Migrating from the MongoDB Data API to Supabase Dedicated Poolers Postgres as a Graph Database: (Ab)using pgRouting AI Hackathon at Y Combinator Calendars in Postgres using Foreign Data Wrappers Supabase Launch Week 13 Hackathon Winners How to Hack the Base! Running Durable Workflows in Postgres using DBOS database.build v2: Bring-your-own-LLM Restore to a New Project Hack the Base! with Supabase Top 10 Launches of Launch Week 13 Supabase Queues High Performance Disk Supabase Cron Supabase CLI v2: Config as Code Supabase Edge Functions: Introducing Background Tasks, Ephemeral Storage, and WebSockets Supabase AI Assistant v2 OrioleDB Public Alpha Executing Dynamic JavaScript Code on Supabase with Edge Functions ClickHouse Partnership, improved Postgres Replication, and Disk Management
Geo Queries with PostGIS in Ionic Angular
Simon Grimm · 2023-03-01 · via Supabase Blog

Geo Queries with PostGIS in Ionic Angular

Does your app need to handle geo data like latitude, longitude, or distance between geographic locations?

Then Supabase got you covered again as you can unlock all of this with the PostGIS extension!

In this tutorial you will learn to:

Since there are quite some code snippets we need I've put together the full source code on GitHub so you can easily run the project yourself!

Ready for some action?

Let's start within Supabase.

To get started we need a new Supabase project. If you don't have a Supabase account yet, you can get started for free!

In your dashboard, click "New Project" and leave it to the default settings, but make sure you keep a copy o your Database password!

After a minute your project should be ready, and we can configure our tables and extensions with SQL.

Why PostGIS Extension?#

Why do we actually need the PostGIS extension for our Postgres database?

Turns out storing lat/long coordinates and querying them isn't very effective and doesn't scale well.

By enabling this extension, we get access to additional data types like Point or Polygon, and we can easily add an index to our data that makes retrieving locations within certain bounds super simpler.

It's super easy to use PostGIS with Supabase as we just need to enable the extension - which is just one of many other Postgres extensions that you can toggle on with just a click!

Adding the PostGIS Extensions#

We could enable PostGIS from the Supabase project UI but we can actually do it with SQL as well, so let's navigate to the SQL Editor from the menu and run the following:


_10

-- enable the PostGIS extension

_10

create extension postgis with schema extensions;


You can now find this and many other extensions under Database -> Extensions:

It's as easy as that, and we can now create the rest of our table structure.

Creating the SQL Tables#

For our example, we need one Stores table so we can add stores with some text and their location.

Additionally, we create a spartial index on the location of our store to make our queries more performant.

Finally, we can also create a new storage bucket for file upload, so go ahead and run the following in the SQL Editor:


_19

-- create our table

_19

create table if not exists public.stores (

_19

id int generated by default as identity primary key,

_19

name text not null,

_19

description text,

_19

location geography(POINT) not null

_19

);

_19

_19

-- add the spatial index

_19

create index stores_geo_index

_19

on public.stores

_19

using GIST (location);

_19

_19

-- create a storage bucket and allow file upload/download

_19

insert into storage.buckets (id, name)

_19

values ('stores', 'stores');

_19

_19

CREATE POLICY "Select images" ON storage.objects FOR SELECT TO public USING (bucket_id = 'stores');

_19

CREATE POLICY "Upload images" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = 'stores');


For our tests, I also added some dummy data. Feel free to use mine or use coordinates closer to you:


_12

-- add some dummy data

_12

insert into public.stores

_12

(name, description, location)

_12

values

_12

(

_12

'The Galaxies.dev Shop',

_12

'Galaxies.dev - your favourite place to learn',

_12

st_point(7.6005702, 51.8807174)

_12

),

_12

('The Local Dev', 'Local people, always best', st_point(7.614454, 51.876565)),

_12

('City Store', 'Get the supplies a dev needs', st_point(7.642581, 51.945606)),

_12

('MEGA Store', 'Everything you need', st_point(13.404315, 52.511640));


To wrap this up we define 2 database functions:

  • nearby_stores will return a list of all stores and their distance to a lat/long place
  • stores_in_view uses more functions like ST_MakeBox2D to find all locations in a specific box of coordinates

Those are some powerful calculations, and we can easily use them through the PostGIS extension and by defining database functions like this:


_20

-- create database function to find nearby stores

_20

create or replace function nearby_stores(lat float, long float)

_20

returns table (id public.stores.id%TYPE, name public.stores.name%TYPE, description public.stores.description%TYPE, lat float, long float, dist_meters float)

_20

language sql

_20

as $$

_20

select id, name, description, st_y(location::geometry) as lat, st_x(location::geometry) as long, st_distance(location, st_point(long, lat)::geography) as dist_meters

_20

from public.stores

_20

order by location <-> st_point(long, lat)::geography;

_20

$$;

_20

_20

_20

-- create database function to find stores in a specific box

_20

create or replace function stores_in_view(min_lat float, min_long float, max_lat float, max_long float)

_20

returns table (id public.stores.id%TYPE, name public.stores.name%TYPE, lat float, long float)

_20

language sql

_20

as $$

_20

select id, name, ST_Y(location::geometry) as lat, ST_X(location::geometry) as long

_20

from public.stores

_20

where location && ST_SetSRID(ST_MakeBox2D(ST_Point(min_long, min_lat), ST_Point(max_long, max_lat)),4326)

_20

$$;


With all of that in place we are ready to build a powerful app with geo-queries based on our Supabase geolocation data!

Setting up the Project#

We are not bound to any framework, but in this article, we are using Ionic Angular to build a cross-platform application.

Additionally we use Capacitor to include a native Google Maps component and to retrieve the user location.

Get started by bringing up a new Ionic project, then add two pages and a service and run the first build so we can generate the native platforms with Capacitor.

Finally we can install the Supabase JS package, so go ahead and run:


_22

ionic start supaMap blank --type=angular

_22

cd ./supaMap

_22

_22

ionic g page store

_22

ionic g page nearby

_22

ionic g service services/stores

_22

_22

ionic build

_22

ionic cap add ios

_22

ionic cap add android

_22

_22

_22

# Add Maps and Geolocation plugins

_22

npm install @capacitor/google-maps

_22

npm install @capacitor/geolocation

_22

_22

# Install Supabase

_22

npm install @supabase/supabase-js

_22

_22

# Ionic 7 wasn't released so I installed the next version

_22

# not required if you are already on Ionic 7

_22

npm install @ionic/core@next @ionic/angular@next


Within the new project we need to add our Supabase credentials and a key for the Google Maps API to the src/environments/environment.ts like this:


_10

export const environment = {

_10

production: false,

_10

mapsKey: 'YOUR-GOOGLE-MAPS-KEY',

_10

supabaseUrl: 'YOUR-URL',

_10

supabaseKey: 'YOUR-ANON-KEY',

_10

}


You can find those values in your Supabase project by clicking on the Settings icon and then navigating to API where it shows your Project API keys.

The Google Maps API key can be obtained from the Google Cloud Platform where you can add a new project and then create credentials for the Maps Javascript API.

Native Project Configuration#

To use the Capacitor plugin we also need to update the permissions of our native projects, so within the ios/App/App/Info.plist we need to include these:


_10

<key>NSLocationAlwaysUsageDescription</key>

_10

<string>We want to show your nearby places</string>

_10

<key>NSLocationWhenInUseUsageDescription</key>

_10

<string>We want to show your nearby places</string>

_10

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>

_10

<string>To show your location</string>


Additionally, we need to add our Maps Key to the android/app/src/main/AndroidManifest.xml:


_10

<meta-data android:name="com.google.android.geo.API_KEY" android:value="YOUR_API_KEY_HERE"/>


Finally also add the required permissions for Android in the android/app/src/main/AndroidManifest.xml at the bottom:


_10

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

_10

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

_10

<uses-feature android:name="android.hardware.location.gps" />


You can also find more information about using Capacitor maps with Ionic in my Ionic Academy!

Finding Nearby Places with Database Functions#

Now the fun begins, and we can start by adding a function to our src/app/services/stores.service.ts that calls the database function (Remote Procedure Call) that we defined in the beginning:


_40

import { Injectable } from '@angular/core'

_40

import { DomSanitizer, SafeUrl } from '@angular/platform-browser'

_40

import { SupabaseClient, User, createClient } from '@supabase/supabase-js'

_40

import { environment } from 'src/environments/environment'

_40

_40

export interface StoreEntry {

_40

lat?: number

_40

long?: number

_40

name: string

_40

description: string

_40

image?: File

_40

}

_40

export interface StoreResult {

_40

id: number

_40

lat: number

_40

long: number

_40

name: string

_40

description: string

_40

image?: SafeUrl

_40

dist_meters?: number

_40

}

_40

@Injectable({

_40

providedIn: 'root',

_40

})

_40

export class StoresService {

_40

private supabase: SupabaseClient

_40

_40

constructor(private sanitizer: DomSanitizer) {

_40

this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)

_40

}

_40

_40

// Get all places with calculated distance

_40

async getNearbyStores(lat: number, long: number) {

_40

const { data, error } = await this.supabase.rpc('nearby_stores', {

_40

lat,

_40

long,

_40

})

_40

return data

_40

}

_40

}


This should return a nice list of StoreResult items that we can render in a list.

For that, let's display a modal from our src/app/home/home.page.ts:


_24

import { Component } from '@angular/core'

_24

import { ModalController } from '@ionic/angular'

_24

import { NearbyPage } from '../nearby/nearby.page'

_24

_24

export interface StoreMarker {

_24

markerId: string

_24

storeId: number

_24

}

_24

_24

@Component({

_24

selector: 'app-home',

_24

templateUrl: 'home.page.html',

_24

styleUrls: ['home.page.scss'],

_24

})

_24

export class HomePage {

_24

constructor(private modalCtrl: ModalController) {}

_24

_24

async showNearby() {

_24

const modal = await this.modalCtrl.create({

_24

component: NearbyPage,

_24

})

_24

modal.present()

_24

}

_24

}


We also need a button to present that modal, so change the src/app/home/home.page.html to include one:


_13

<ion-header>

_13

<ion-toolbar color="primary">

_13

<ion-buttons slot="start">

_13

<ion-button (click)="showNearby()">

_13

<ion-icon name="location" slot="start"></ion-icon> Nearby</ion-button

_13

>

_13

</ion-buttons>

_13

_13

<ion-title> Supa Stores </ion-title>

_13

</ion-toolbar>

_13

</ion-header>

_13

_13

<ion-content> </ion-content>


Now we are able to use the getNearbyStores from our service on that modal page, and we also load the current user location using Capacitor.

Once we got the user coordinates, we pass them to our function and PostGIS will do its magic to calculate the distance between us and the stores of our database!

Go ahead and change the src/app/nearby/nearby.page.ts to this now:


_38

import { Component, OnInit } from '@angular/core'

_38

import { Geolocation } from '@capacitor/geolocation'

_38

import { StoresService, StoreResult } from '../services/stores.service'

_38

import { LoadingController, ModalController } from '@ionic/angular'

_38

_38

@Component({

_38

selector: 'app-nearby',

_38

templateUrl: './nearby.page.html',

_38

styleUrls: ['./nearby.page.scss'],

_38

})

_38

export class NearbyPage implements OnInit {

_38

stores: StoreResult[] = []

_38

_38

constructor(

_38

private storesService: StoresService,

_38

public modalCtrl: ModalController,

_38

private loadingCtrl: LoadingController

_38

) {}

_38

_38

async ngOnInit() {

_38

// Show loading while getting data from Supabase

_38

const loading = await this.loadingCtrl.create({

_38

message: 'Loading nearby places...',

_38

})

_38

loading.present()

_38

_38

const coordinates = await Geolocation.getCurrentPosition()

_38

_38

if (coordinates) {

_38

// Get nearby places sorted by distance using PostGIS

_38

this.stores = await this.storesService.getNearbyStores(

_38

coordinates.coords.latitude,

_38

coordinates.coords.longitude

_38

)

_38

loading.dismiss()

_38

}

_38

}

_38

}


At this point, you can already log the values, but we can also quickly display them in a nice list by updating the src/app/nearby/nearby.page.html to:


_22

<ion-header>

_22

<ion-toolbar color="primary">

_22

<ion-buttons slot="start">

_22

<ion-button (click)="modalCtrl.dismiss()">

_22

<ion-icon slot="icon-only" name="close"></ion-icon>

_22

</ion-button>

_22

</ion-buttons>

_22

<ion-title>Nearby Places</ion-title>

_22

</ion-toolbar>

_22

</ion-header>

_22

_22

<ion-content>

_22

<ion-list>

_22

<ion-item *ngFor="let store of stores">

_22

<ion-label>

_22

{{ store.name }}

_22

<p>{{store.description }}</p>

_22

</ion-label>

_22

<ion-note slot="end">{{store.dist_meters!/1000 | number:'1.0-2' }} km</ion-note>

_22

</ion-item>

_22

</ion-list>

_22

</ion-content>


If you open the modal, you should now see a list like this after your position was loaded:

It looks so easy - but so many things are already coming together at this point:

  • Capacitor geolocation inside the browser
  • Supabase RPC to a stored database function
  • PostGIS geolocation calculation

We will see more of this powerful extension soon, but let's quickly add another modal to add our own data.

Add Stores with Coordinates to Supabase#

To add data to Supabase we create a new function in our src/app/services/stores.service.ts:


_19

async addStore(info: StoreEntry) {

_19

// Add a new database entry using the POINT() syntax for the coordinates

_19

const { data } = await this.supabase

_19

.from('stores')

_19

.insert({

_19

name: info.name,

_19

description: info.description,

_19

location: `POINT(${info.long} ${info.lat})`,

_19

})

_19

.select()

_19

.single();

_19

_19

if (data && info.image) {

_19

// Upload the image to Supabase

_19

const foo = await this.supabase.storage

_19

.from('stores')

_19

.upload(`/images/${data.id}.png`, info.image);

_19

}

_19

}


Notice how we convert the lat/long information of an entry to a string.

This is how PostGIS expects those values!

We use our Supabase storage bucket to upload an image file if it's included in the new StoreEntry. It's almost too easy and feels like cheating to upload a file to cloud storage in just three lines...

Now we need a simple modal, so just like before we add a new function to the src/app/home/home.page.ts:


_10

async addStore() {

_10

const modal = await this.modalCtrl.create({

_10

component: StorePage,

_10

});

_10

modal.present();

_10

}


That function get's called from another button in our src/app/home/home.page.html:


_16

<ion-header>

_16

<ion-toolbar color="primary">

_16

<ion-buttons slot="start">

_16

<ion-button (click)="showNearby()">

_16

<ion-icon name="location" slot="start"></ion-icon> Nearby</ion-button

_16

>

_16

</ion-buttons>

_16

_16

<ion-title> Supa Stores </ion-title>

_16

<ion-buttons slot="end">

_16

<ion-button (click)="addStore()">

_16

<ion-icon name="add" slot="start"></ion-icon> Store</ion-button

_16

>

_16

</ion-buttons>

_16

</ion-toolbar>

_16

</ion-header>


Back in this new modal, we will define an empty StoreEntry object and then connect it to the input fields in our view.

Because we defined the rest of the functionality in our service, we can simply update the src/app/store/store.page.ts to:


_34

import { Component, OnInit } from '@angular/core'

_34

import { ModalController } from '@ionic/angular'

_34

import { StoreEntry, StoresService } from '../services/stores.service'

_34

_34

@Component({

_34

selector: 'app-store',

_34

templateUrl: './store.page.html',

_34

styleUrls: ['./store.page.scss'],

_34

})

_34

export class StorePage implements OnInit {

_34

store: StoreEntry = {

_34

name: '',

_34

description: '',

_34

image: undefined,

_34

lat: undefined,

_34

long: undefined,

_34

}

_34

_34

constructor(

_34

public modalCtrl: ModalController,

_34

private storesService: StoresService

_34

) {}

_34

_34

ngOnInit() {}

_34

_34

imageSelected(ev: any) {

_34

this.store.image = ev.detail.event.target.files[0]

_34

}

_34

_34

async addStore() {

_34

this.storesService.addStore(this.store)

_34

this.modalCtrl.dismiss()

_34

}

_34

}


The view is not really special and simply holds a bunch of input fields that are connected to the new store entry, so bring up the src/app/store/store.page.html and change it to:


_41

<ion-header>

_41

<ion-toolbar color="primary">

_41

<ion-buttons slot="start">

_41

<ion-button (click)="modalCtrl.dismiss()">

_41

<ion-icon slot="icon-only" name="close"></ion-icon>

_41

</ion-button>

_41

</ion-buttons>

_41

<ion-title>Add Store</ion-title>

_41

</ion-toolbar>

_41

</ion-header>

_41

_41

<ion-content class="ion-padding">

_41

<ion-input

_41

label="Store name"

_41

label-placement="stacked"

_41

placeholder="Joeys"

_41

[(ngModel)]="store.name"

_41

/>

_41

<ion-textarea

_41

rows="3"

_41

label="Store description"

_41

label-placement="stacked"

_41

placeholder="Some about text"

_41

[(ngModel)]="store.description"

_41

/>

_41

<ion-input type="number" label="Latitude" label-placement="stacked" [(ngModel)]="store.lat" />

_41

<ion-input type="number" label="Longitude" label-placement="stacked" [(ngModel)]="store.long" />

_41

<ion-input

_41

label="Select store image"

_41

(ionChange)="imageSelected($event)"

_41

type="file"

_41

accept="image/*"

_41

></ion-input>

_41

_41

<ion-button

_41

expand="full"

_41

(click)="addStore()"

_41

[disabled]="!store.lat || !store.long || store.name === ''"

_41

>Add Store</ion-button

_41

>

_41

</ion-content>


As a result, you should have a clean input modal:

Give your storage inserter a try and add some places around you - they should be available in your nearby list immediately!

Adding a Map#

Now we have some challenges ahead: adding a map, loading data, and creating markers.

But if you've come this far, I'm sure you can do it!

Get started by adding the CUSTOM_ELEMENTS_SCHEMA to the src/app/home/home.module.ts which is required to use Capacitor native maps:


_15

import { NgModule } from '@angular/core'

_15

import { CommonModule } from '@angular/common'

_15

import { IonicModule } from '@ionic/angular'

_15

import { FormsModule } from '@angular/forms'

_15

import { HomePage } from './home.page'

_15

_15

import { HomePageRoutingModule } from './home-routing.module'

_15

import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'

_15

_15

@NgModule({

_15

imports: [CommonModule, FormsModule, IonicModule, HomePageRoutingModule],

_15

declarations: [HomePage],

_15

schemas: [CUSTOM_ELEMENTS_SCHEMA],

_15

})

_15

export class HomePageModule {}


In our src/app/home/home.page.ts we can now create the map by passing in a reference to a DOM element and some initial settings for the map and of course your key.

Update the page with our first step that adds some new variables:


_68

import { Component, ElementRef, ViewChild } from '@angular/core'

_68

import { GoogleMap } from '@capacitor/google-maps'

_68

import { LatLngBounds } from '@capacitor/google-maps/dist/typings/definitions'

_68

import { ModalController } from '@ionic/angular'

_68

import { BehaviorSubject } from 'rxjs'

_68

import { environment } from 'src/environments/environment'

_68

import { NearbyPage } from '../nearby/nearby.page'

_68

import { StoreResult, StoresService } from '../services/stores.service'

_68

import { StorePage } from '../store/store.page'

_68

_68

export interface StoreMarker {

_68

markerId: string

_68

storeId: number

_68

}

_68

_68

@Component({

_68

selector: 'app-home',

_68

templateUrl: 'home.page.html',

_68

styleUrls: ['home.page.scss'],

_68

})

_68

export class HomePage {

_68

@ViewChild('map') mapRef!: ElementRef<HTMLElement>

_68

map!: GoogleMap

_68

mapBounds = new BehaviorSubject<LatLngBounds | null>(null)

_68

activeMarkers: StoreMarker[] = []

_68

selectedMarker: StoreMarker | null = null

_68

selectedStore: StoreResult | null = null

_68

_68

constructor(

_68

private storesService: StoresService,

_68

private modalCtrl: ModalController

_68

) {}

_68

_68

ionViewDidEnter() {

_68

this.createMap()

_68

}

_68

_68

async createMap() {

_68

this.map = await GoogleMap.create({

_68

forceCreate: true, // Prevent issues with live reload

_68

id: 'my-map',

_68

element: this.mapRef.nativeElement,

_68

apiKey: environment.mapsKey,

_68

config: {

_68

center: {

_68

lat: 51.8,

_68

lng: 7.6,

_68

},

_68

zoom: 7,

_68

},

_68

})

_68

this.map.enableCurrentLocation(true)

_68

}

_68

_68

async showNearby() {

_68

const modal = await this.modalCtrl.create({

_68

component: NearbyPage,

_68

})

_68

modal.present()

_68

}

_68

_68

async addStore() {

_68

const modal = await this.modalCtrl.create({

_68

component: StorePage,

_68

})

_68

modal.present()

_68

}

_68

}


The map needs a place to render, so we can now add it to our src/app/home/home.page.html and wrap it in a div to add some additional styling later:


_22

<ion-header>

_22

<ion-toolbar color="primary">

_22

<ion-buttons slot="start">

_22

<ion-button (click)="showNearby()">

_22

<ion-icon name="location" slot="start"></ion-icon> Nearby</ion-button

_22

>

_22

</ion-buttons>

_22

_22

<ion-title> Supa Stores </ion-title>

_22

<ion-buttons slot="end">

_22

<ion-button (click)="addStore()">

_22

<ion-icon name="add" slot="start"></ion-icon> Store</ion-button

_22

>

_22

</ion-buttons>

_22

</ion-toolbar>

_22

</ion-header>

_22

_22

<ion-content>

_22

<div class="container">

_22

<capacitor-google-map #map></capacitor-google-map>

_22

</div>

_22

</ion-content>


Because the Capacitor map essentially renders behind your webview inside a native app, we need to make the background of our current page invisible.

For this, simply add the following to the src/app/home/home.page.scss:


_14

ion-content {

_14

--background: none;

_14

}

_14

_14

.container {

_14

width: 100%;

_14

height: 100%;

_14

}

_14

_14

capacitor-google-map {

_14

display: inline-block;

_14

width: 100%;

_14

height: 100%;

_14

}


Now the map should fill the whole screen.

This brings us to the last missing piece…

Loading Places in a Box of Coordinates#

Getting all stores is usually too much - you want to show what's nearby to a user, and you can do this by sending basically a box of coordinates to our previously stored database function.

For this, we first add another call in our src/app/services/stores.service.ts:


_15

// Get all places in a box of coordinates

_15

async getStoresInView(

_15

min_lat: number,

_15

min_long: number,

_15

max_lat: number,

_15

max_long: number

_15

) {

_15

const { data } = await this.supabase.rpc('stores_in_view', {

_15

min_lat,

_15

min_long,

_15

max_lat,

_15

max_long,

_15

});

_15

return data;

_15

}


Nothing fancy, just passing those values to the database function.

The challenging part is now listening to map boundary updates, which happen whenever you slightly touch the list.

Because we don't want to call our function 100 times in one second, we use a bit of RxJS to delay the update of our coordinates so the updateStoresInView function is called after the user finished swiping the list.

At that point, we grab the map bounds and call our function, so go ahead and update the src/app/home/home.page.ts with the following:


_52

async createMap() {

_52

this.map = await GoogleMap.create({

_52

forceCreate: true, // Prevent issues with live reload

_52

id: 'my-map',

_52

element: this.mapRef.nativeElement,

_52

apiKey: environment.mapsKey,

_52

config: {

_52

center: {

_52

lat: 51.8,

_52

lng: 7.6,

_52

},

_52

zoom: 7,

_52

},

_52

});

_52

this.map.enableCurrentLocation(true);

_52

_52

// Listen to biew changes and emit to our Behavior Subject

_52

this.map.setOnBoundsChangedListener((ev) => {

_52

this.mapBounds.next(ev.bounds);

_52

});

_52

_52

// React to changes of our subject with a 300ms delay so we don't trigger a reload all the time

_52

this.mapBounds.pipe(debounce((i) => interval(300))).subscribe((res) => {

_52

this.updateStoresInView();

_52

});

_52

_52

// Get the current user coordinates

_52

this.loadUserLocation();

_52

}

_52

_52

async updateStoresInView() {

_52

const bounds = await this.map.getMapBounds();

_52

_52

// Get stores in our bounds using PostGIS

_52

const stores = await this.storesService.getStoresInView(

_52

bounds.southwest.lat,

_52

bounds.southwest.lng,

_52

bounds.northeast.lat,

_52

bounds.northeast.lng

_52

);

_52

_52

// Update markers for elements

_52

this.addMarkers(stores);

_52

}

_52

_52

async loadUserLocation() {

_52

// TODO

_52

}

_52

_52

async addMarkers(stores: StoreResult[]) {

_52

// TODO

_52

}


We can also fill one of our functions with some code as we already used the Geolocation plugin to load users' coordinates before, so update the function to:


_15

async loadUserLocation() {

_15

// Get location with Capacitor Geolocation plugin

_15

const coordinates = await Geolocation.getCurrentPosition();

_15

_15

if (coordinates) {

_15

// Focus the map on user and zoom in

_15

this.map.setCamera({

_15

coordinate: {

_15

lat: coordinates.coords.latitude,

_15

lng: coordinates.coords.longitude,

_15

},

_15

zoom: 14,

_15

});

_15

}

_15

}


Now we are loading the user location and zooming in to the current place, which will then cause our updateStoresInView function to be triggered and we receive a list of places that we just need to render!

Displaying Marker on our Google Map#

You can already play around with the app and log the stores after moving the map - it really feels magical how PostGIS returns only the elements that are within the box of coordinates.

To actually display them we can add the following function to our src/app/home/home.page.ts now:


_45

async addMarkers(stores: StoreResult[]) {

_45

// Skip if there are no results

_45

if (stores.length === 0) {

_45

return;

_45

}

_45

_45

// Find marker that are outside of the view

_45

const toRemove = this.activeMarkers.filter((marker) => {

_45

const exists = stores.find((item) => item.id === marker.storeId);

_45

return !exists;

_45

});

_45

_45

// Remove markers

_45

if (toRemove.length) {

_45

await this.map.removeMarkers(toRemove.map((marker) => marker.markerId));

_45

}

_45

_45

// Create new marker array

_45

const markers: Marker[] = stores.map((store) => {

_45

return {

_45

coordinate: {

_45

lat: store.lat,

_45

lng: store.long,

_45

},

_45

title: store.name,

_45

};

_45

});

_45

_45

// Add markers, store IDs

_45

const newMarkerIds = await this.map.addMarkers(markers);

_45

_45

// Crate active markers by combining information

_45

this.activeMarkers = stores.map((store, index) => {

_45

return {

_45

markerId: newMarkerIds[index],

_45

storeId: store.id,

_45

};

_45

});

_45

_45

this.addMarkerClicks();

_45

}

_45

_45

addMarkerClicks() {

_45

// TODO

_45

}


This function got a bit longer because we need to manage our marker information. If we just remove and repaint all markers, it looks and feels horrible so we always keep track of existing markers and only render new markers.

Additionally, these Marker have limited information, and if we click a marker we want to present a modal with information about the store from Supabase.

That means we also need the real ID of that object, and so we create an array activeMarkers that basically connects the information of a store ID with the marker ID!

At this point, you should be able to see markers on your map. If you can't see them, zoom out and you might find them.

To wrap this up, let's take a look at one more cool Supabase feature.

Presenting Marker with Image Transform#

We have the marker and store ID, so we can simply load the information from our Supabase database.

Now a store might have an image, and while we download the image from our storage bucket we can use image transformations to get an image exactly in the right dimensions to save time and bandwidth!

For this, add two new functions to our src/app/services/stores.service.ts:


_21

// Load data from Supabase database

_21

async loadStoreInformation(id: number) {

_21

const { data } = await this.supabase

_21

.from('stores')

_21

.select('*')

_21

.match({ id })

_21

.single();

_21

return data;

_21

}

_21

_21

async getStoreImage(id: number) {

_21

// Get image for a store and transform it automatically!

_21

return this.supabase.storage

_21

.from('stores')

_21

.getPublicUrl(`images/${id}.png`, {

_21

transform: {

_21

width: 300,

_21

resize: 'contain',

_21

},

_21

}).data.publicUrl;

_21

}


To use image transformations we only need to add an object to the getPublicUrl() function and define the different properties we want to have.

Again, it's that easy.

Now we just need to load this information when we click on a marker, so add the following function to our src/app/home/home.page.ts which handles the click on a map marker:


_25

addMarkerClicks() {

_25

// Handle marker clicks

_25

this.map.setOnMarkerClickListener(async (marker) => {

_25

// Find our local object based on the marker ID

_25

const info = this.activeMarkers.filter(

_25

(item) => item.markerId === marker.markerId.toString()

_25

);

_25

if (info.length) {

_25

this.selectedMarker = info[0];

_25

_25

// Load the store information from Supabase Database

_25

this.selectedStore = await this.storesService.loadStoreInformation(

_25

info[0].storeId

_25

);

_25

_25

// Get the iamge from Supabase Storage

_25

const img = await this.storesService.getStoreImage(

_25

this.selectedStore!.id

_25

);

_25

if (img) {

_25

this.selectedStore!.image = img;

_25

}

_25

}

_25

});

_25

}


We simply load the information and image and set this to our selectedStore variable.

This will now be used to trigger an inline modal, so we don't need to come up with another component and can simply define our Ionic modal right inside the src/app/home/home.page.html like this:


_41

<ion-header>

_41

<ion-toolbar color="primary">

_41

<ion-buttons slot="start">

_41

<ion-button (click)="showNearby()">

_41

<ion-icon name="location" slot="start"></ion-icon> Nearby</ion-button

_41

>

_41

</ion-buttons>

_41

_41

<ion-title> Supa Stores </ion-title>

_41

<ion-buttons slot="end">

_41

<ion-button (click)="addStore()">

_41

<ion-icon name="add" slot="start"></ion-icon> Store</ion-button

_41

>

_41

</ion-buttons>

_41

</ion-toolbar>

_41

</ion-header>

_41

_41

<ion-content>

_41

<div class="container">

_41

<capacitor-google-map #map></capacitor-google-map>

_41

</div>

_41

_41

<ion-modal

_41

[isOpen]="selectedMarker !== null"

_41

[breakpoints]="[0, 0.4, 1]"

_41

[initialBreakpoint]="0.4"

_41

(didDismiss)="selectedMarker = null;"

_41

>

_41

<ng-template>

_41

<ion-content class="ion-padding">

_41

<ion-label class="ion-texst-wrap">

_41

<h1>{{selectedStore?.name}}</h1>

_41

<ion-note>{{selectedStore?.description}}</ion-note>

_41

</ion-label>

_41

<div class="ion-text-center ion-margin-top">

_41

<img [src]="selectedStore?.image" *ngIf="selectedStore?.image" />

_41

</div>

_41

</ion-content>

_41

</ng-template>

_41

</ion-modal>

_41

</ion-content>


Because we also used breakpoints and the initialBreakpoint properties of the modal we get this nice bottom sheet modal UI whenever we click on a marker:

And with that, we have finished our Ionic app with Supabase geo-queries using PostGIS!

I was fascinated by the power of this simple PostGIS extension that we enabled with just one command (or click).

Building apps based on geolocation data is a very common scenario, and with PostGIS we can build these applications easily on the back of a Supabase database (and auth ), and storage, and so much more..)

You can find the full code of this tutorial on GitHub where you just need to insert your own Supabase instance. your Google Maps key and then create the tables with the included SQL file.

If you enjoyed the tutorial, you can find many more tutorials and courses on Galaxies.dev where I help modern web and mobile developers build epic apps 🚀

Until next time and happy coding with Supabase!