@neoncoder/geolocation-data
v0.0.7
Published
A Collection of utility functions, classes and data to help with building geolocation apps.
Downloads
8
Maintainers
Readme
Geolocation Data Utils
A Collection of utility functions, classes and data to help with building geolocation apps.
Features
- Up-to-date and customizable database of Regions (continents), Subregions, Countries, States, and Cities.
- Resource Repositories with methods for querying, filtering and CRUDing the data
- Drizzle ORM instance and schema for more complex queries and filtering conditions
- Utility functions for geolocation computations (distance, nearest cities, etc)
Installation
Usable on any nodejs project with either JavaScript or Typescript on the frontend or backend.
[!NOTE] To use, download this
sqlite.db
raw file from this repo and place at the root of your project, then install the package
# download sqlite.db file
wget -c https://github.com/Bankole2000/geo-data-store/raw/main/sqlite.db
# install package in your project
npm install @neoncoder/geolocation-data
Usage
The sqlite.db
consists of 5 tables - Region
, Subregion
, Country
, State
, City
, all related with foreign keys and typed thus:
type Region = { // Major Regions (6)
id: number;
name: string;
translations: {[key: string]: string};
wikiDataId: string | null;
}
type Subregion = { // Geographical Zones (22)
id: number;
name: string;
region_id: number; // Foreign key referencing Region.id
translations: unknown;
wikiDataId: string | null;
}
type Country = { // Countries (250)
id: number;
name: string;
iso3: string; // Unique 3 char country code
iso2: string; // Unique 2 char country code
numeric_code: string;
phone_code: string;
capital: string; // Country's capital state / city
currency: string; // Unique 3 Char currency code e.g. USD
currency_name: string;
currency_symbol: string;
tld: string; // e.g. .co.uk, .za, .pl etc
native: string;
region_id: number; // Foreign key referencing Region.id
subregion_id: number; // Foreign key referencing Subregion.id
nationality: string;
timezones: Array<Timezone>
translations: {[key:string]: string}
latitude: number;
longitude: number;
emojiU: string;
}
type State = { // States (5084)
id: number;
name: string;
latitude: number;
longitude: number;
country_id: number; // Foreign key referencing Country.id
country_code: string; // Same as Country.iso2
country_name: string; // Same as Country.name
state_code: string; // Only unique within same Country
type: string | null;
}
type City = { // Cities (150634)
id: number;
name: string;
wikiDataId: string | null;
latitude: number;
longitude: number;
country_id: number; // Foreign key references Country.id
country_code: string; // same as Country.iso2
country_name: string; // same as Country.name
state_id: number; // Foreign key referencing State.id
state_code: string; // same as State.code
state_name: string; // same as State.name
}
This package itself exposes 3 main resources:
- Drizzle ORM insance
db
- with schema and sql utility fxns for quering the database - Resource Classes - with readable methods for typical use cases
- Utility Functions - For geolocation computations
Using the db
drizzle orm instance
import { db } from '@neoncoder/geolocation-data'
// async IIFE (in case no top-level await)
(async() => {
// Get all countries - 250
const countries = await db.query.country.findMany()
// Get all cities in britain (iso2 - GB)
const citiesInTheUK = await db.query.city
.findMany({
where: (city, {eq}) => eq(city.country_code, 'GB'),
// include country and state data
with: {country: true, state: true}
})
// find state in the US that contains searchTerm
const searchTerm = 'flor'
const floridaSearch = await db.query.state
.findFirst({
where: (state, {eq, and, like}) => {
return and(
eq(state.country_code, 'US'),
like(state.name, `%${searchTerm}%`)
)
}, with: {country: true, cities: true}})
// find all cities in either Lagos or Abuja, Nigeria
// Order by city name, paginate results;
let page = 2, per_page = 10;
const citySearch = await db.query.city
.findMany({
where: (city, {eq, or, and, like}) => {
return and(
eq(city.country_code, 'NG'),
or(
like(city.state_name, `%Abuja%`),
like(city.state_name, `%Lagos%`)
)
)
},
limit: per_page,
offset: (page - 1) * per_page,
orderBy: ((city, {asc}) => asc(city.name))
})
})()
see the drizzle ORM documentation for more details on using the db instance
Using Resource Classes
5 repository classes are provided RegionRepository
, SubregionRepository
, CountryRepository
, StateRepository
, CityRepository
// import Classes and instantiate or
// import and rename already instantiated classes
import {
RegionRepository, // or { regionRepository as regionRepo }
SubregionRepository, // or { subregionRepository as subRepo }
CountryRepository, // or { countryRepository as countryRepo }
StateRepository, // or { stateRepository as stateRepo }
CityRepository // or { cityRepository as cityRepo }
} from '@neoncoder/geolocation-data'
const regionRepo = new RegionRepository()
const subRepo = new SubregionRepository()
const countryRepo = new CountryRepository()
const stateRepo = new StateRepository()
const cityRepo = new CityRepository()
All repository classes provide the same attendant methods to each class i.e.
get[PlacePlural]
- Paginated, takes inpagination
,filter
,sort
andinclude
options. returns paginated Array of[Place]
getAll[PlacePlural]
- NOT Paginated, takes infilter
,sort
andinclude
options. returns non-paginated Array of[Place]
find[Place]ById
- Takesid
and (optional)include
parameters. returns Array of single[Place]
record if found, else returns empty arraycreate[Place]
- creates new[Place]
record in the databaseupdate[Place]
- updates existing[Place]
record in the databasedelete[Place]
- deletes existing[Place]
record from the database
where [Place]
is one of Region
, Subregion
, Country
, State
, or City
, and
[Place_Plural]
is plural from of place (e.g. Country plural is Countries.)
Example using CountryRespository
class
import { countryRepository as cr } from '@neoncoder/geolocation-data'
// method examples using countryRepository
(async() => {
// get countries paginated
const countriesPaginated = await cr.getCountries(countryQueryOptions + pagination)
// get countries not paginated
const countriesNotPaginated = await cr.getAllCountries(countryQueryOptions)
// get singular country by Id
const countryById = await cr.findCountryById(id)
// create new country record
const newCountry = await cr.createCountry(createData)
// update existing country record
const updatedCountry = await cr.updateCountry(id, updateData)
// delete country record
const deleted = await cr.deleteCountry(id)
})()
// The same methods are available on all the other repositories, e.g.
// cityRepository.getAllCities(cityQueryOptions)
// stateRepository.getAllStates(stateQueryOptions)
Repository usage examples
import {
regionRepository as regionRepo,
countryRepository as countryRepo,
stateRepository as stateRepo,
cityRepository as cityRepo
} from '@neoncoder/geolocation-data'
(async() => {
// get all regions, counting subregions and countries in each region
const regions = await regionRepo.getAllRegions({include: {count: true}})
// get countries paginated, counting states and cities in each country
const countries = await countryRepo.getCountries({
page: 1, limit: 20
// also include region and subregion data for each country
include: {count: true, subregion: true, region: true}
})
// get first 20 cities in britain (iso2 - GB) - Paginated, sort by name in
// descending order, include country and state data
const {data: cities, meta} = await cityRepo.getCities({
page: 1, limit: 20,
filter: {country_code: 'GB'},
sort: {field: 'name', direction: 'desc'},
include: {country: true, state: true}
})
// For no pagination use the `cityRepo.getAllCities` method
// find state in the US that contains searchTerm
const searchTerm = 'flor'
const floridaSearch = await stateRepo.getStates({
page: 1, limit: 1
// filter operation is AND by default and fields can't be
// repeated on the top level (object unique key constraint)
filters: {country_code: 'US', name: searchTerm},
sort: {field: 'name', direction: 'asc'},
include: {count: true, country: true, cities: true}
})
// find all cities in either Lagos or Abuja, Nigeria
// Order by city name, paginate results;
let page = 2, per_page = 10;
const citySearch = await cityRepo.getCities({
page: page, limit: per_page,
filter: {
operation: 'and', // AND (country_code, subfilters)
country_code: 'NG',
suboperation: 'or',
subfilters: [ // OR suboperation is applied here
{state_name: 'Abuja'},
{state_name: 'Lagos'}
]
},
sort: {field: 'name', direction: 'asc'}
})
// // This above is essentially the same as
// db.query.city.findMany({
// where: (city, {eq, or, and, like}) => {
// return and(
// eq(city.country_code, 'NG'),
// or(
// like(city.state_name, `%Abuja%`),
// like(city.state_name, `%Lagos%`)
// )
// )
// },
// limit: per_page,
// offset: (page - 1) * per_page,
// orderBy: ((city, {asc}) => asc(city.name))
})()
[!TIP]
drizzle orm
db
instance vsClassRepository
. Which should you use and what's the difference?You can basically use either in most scenarios, but here are recommendations due to their different implementations
- To create, update, or delete records, use the Class Repositories
- To use filter operations other than
eq
orlike
(e.g.lte
,gte
,not
etc) use thedb
instance- To conveniently count related records (in 1-n relationships) use the classRepository
- To run custom SQL queries, or if you're very familiar with Drizzle orm, use the
db
instance
Data structures
Main resource types: Other Utility Types:
| Name | Structure | Description |
|------|------------------|---------------------|
| GeoPoint
| {lat: number, lng: number}
| Single geolocation coordinate |
|DistanceUnit
| one of km
, m
, mi
|Kilometers, meters or miles|
|BoundingBox
|{topLeft: GeoPoint, bottomRight: GeoPoint}
|Coordinates of a rectangulat area |
|Vector
| {angle: number, distance: number, unit: DistanceUnit, unitInWords?: string}
| Unit of distance with angular direction |
Utility Functions
11 Utility functions are provided - haversine
(or calculateDistance
),
calculateVectorDistance
,
isWithinBoundingBox
,
getMidwayPoint
,
isWithinRadius
,
isWithinPolygon
, findClosestCity
, findClosestCities
, findEntitiesWithinRadius
, getBoundingBox
, moveCoordsTo
- haversine - aliased as
calculateDistance
, given 2GeoPoint
s (i.e. geolocation coordinates{lat: number, lng: number}
) returns the straight line distance inmeters
(the unit can be changed) using the haversing formula - findClosestCity - given a
GeoPoint
returns the nearestCity
,State
andCountry
, also returns distance from the nearest city inmeters
(unit changeable) - findClosestCities - Given a
GeoPoint
and a numberx
, returnsx
number of cities closest to that location - findEntitiesWithinRadius - Given a
GeoPoint
and a numberx
, returns all cities inx
km radius around that location (the unit is customizable) - getBoundingBox - Calculates the bounding box for a given set of geographical points with an optional margin.
- calculateVectorDistance - Calculates the vector distance (angle and distance) between two geographical points.
- moveCoordsTo - Moves a geographical point by a specified vector (angle and distance).
- isWithinBoundingBox - Checks if a given
GeoPoint
is inside a specifiedBoundingBox
. returns boolean - getMidwayPoint - Given a list of
GeoPoint[]
s returns a point close to the center of all points - isWithinRadius - Checks if a given
GeoPoint
is within a specified radius from a centerGeoPoint
- isWithinPolygon - Given a
GeoPoint
X and list ofGeoPoint[]
s Y that make a polygon, check if X is withing polygon Y, returns true or false. Polygon must have at least 3 sides
Examples
import {
calculateDistance,
findClosestCity,
findClosestCities,
findEntitiesWithinRadius
} from '@neoncoder/geolocation-data'
// Heathrow Airport Coords
const pointA = {lat: 51.46852608063078, lng: -0.4548364232750391}
// Abuja Int'l Airport Coords
const pointB = {lat: 9.007318554723346, lng: 7.269119361911654}
// Straight line distance between Heathrow & Abuja Airport
const distance = calculateDistance(pointA, pointB)
// return { distance: 4774031.17315101, unit: 'm', unitInWords: 'meters' },
// Nearest city to Heathrow, return distance in miles
const nearestCity = findClosestCity(pointA, 'mi')
// return { name: 'West Drayton', unit: 'mi', unitInWords: 'miles', ...cityDetails}
// Nearest 3 cities to Abuja Airport, distance in kilometers
const nearest3Cities = findClosestCities(pointB, 3, 'km')
// returns {cities: City[3], states: State[], country: Country[]}
// 'Madala', 'Zuba', 'Kuje'
// Cities within a 3 mile radius of Heathrow Airport
const citiesIn3MileRadius = findEntitiesWithinRadius(pointA, 3, 'mi');
// returns {cities: City[3], states: State[], country: Country[]}
// 'West Drayton', 'Feltham', 'Iver'
// All City results are returned in order of distance
const locations = [
{ lat: 40.7128, lng: -74.0060 },
{ lat: 34.0522, lng: -118.2437 },
{ lat: 41.8781, lng: -87.6298 }
];
// Get Bounding Box Coordinates with a 10km margin
const boundingBox = getBoundingBox(locations, 10, 'km');
// boundingBox: {
// topLeft: { lat: 42.02283016952886, lng: -118.43808172393314 },
// bottomRight: { lat: 34.19693016952886, lng: -74.18068354701443 }
// }
// Find new coordinates if you move 100km at 45deg
const origin = { lat: 40.7128, lng: -74.0060 };
const vector = { angle: 45, distance: 100, unit: 'km' };
const destination = moveCoordsTo(origin, vector);
console.log(destination);
// { lat: 41.34871640601271, lng: -73.16704757394922 }
// Calculate distance between points with direction and unit
const origin = { lat: 40.7128, lng: -74.0060 };
const destination = { lat: 40.8128, lng: -74.0060 };
const vectorDistance = calculateVectorDistance(origin, destination, 'km');
/** returns {
angle: 0, // because the longitude did not change
distance: 11.11949266445603,
unit: 'km',
unitInWords: 'kilometers'
} */
// check if point is withing bounding box
const point = { lat: 40.7128, lng: -74.0060 };
const boundingBox = {
topLeft: { lat: 41.0, lng: -75.0 },
bottomRight: { lat: 40.0, lng: -73.0 }
};
isWithinBoundingBox(point, boundingBox); // true
// Get central point of locations
const locations = [
{ lat: 40.7128, lng: -74.0060 },
{ lat: 34.0522, lng: -118.2437 },
{ lat: 41.8781, lng: -87.6298 }
];
const midwayPoint = getMidwayPoint(locations);
// { lat: 38.881033333333335, lng: -93.29316666666666 }
// Check if point is within radial distance from center
const point = { lat: 40.7128, lng: -74.0060 };
const center = { lat: 40.730610, lng: -73.935242 };
const radius = 10; // in kilometers
isWithinRadius(point, center, radius, 'km'); // true
const point = { lat: 40.7128, lng: -74.0060 };
const polygon = [
{ lat: 40.7127, lng: -74.0059 },
{ lat: 40.7129, lng: -74.0059 },
{ lat: 40.7129, lng: -74.0061 },
{ lat: 40.7127, lng: -74.0061 }
];
isWithinPolygon(point, polygon); // true
// convert tuple to GeoPoint and vice-versa
const tuple = [40.7128, -74.0060];
const geoPoint: GeoPoint = tupleToGeoPoint(tuple);
// { lat: 40.7128, lng: -74.0060 }
const newTuple: number[] = geoPointToTuple(point);
// [40.7128, -74.0060]
Backgroun Information - The Geographical coordinate system
The location and orientation of ships and airplaines on the globe is typically expressed in terms of longitude, latitude, and heading.
- Longitude (horizontal axis) is zero degrees at Greenwich, and varies from -180 degrees West to 180 degrees East.
- Latitude (vertical axis) is 0 degrees at the equator, and varies from -90 degrees on the South pool to 90 degrees on the North pool.
- Heading (aliased as the angle property of the
Vector
type in this project) varies from 0 to 360 degrees. North is 0 degrees, East is 90 degrees, South is 180 degrees, and West is 270 degrees. Visit this link for more details
Acknowledgements
Special thanks to @dr5hn and all contributors to both the Countries-state-cities-database project and the geolocation-utils projects as well. This package would not be possible without their hard work 🙌.