#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(all(doctest, feature = "bundled", feature = "export-geojson"))]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
use std::borrow::Cow;
use std::f64::consts::PI;
#[cfg(all(feature = "bundled", feature = "full"))]
compile_error!(
"feature `bundled` is mutually exclusive with the git-only data feature \
`full`; add `default-features = false` when enabling it"
);
mod finder;
mod tzb;
#[cfg(feature = "export-geojson")]
mod geojson;
#[cfg(feature = "export-geojson")]
pub use geojson::{
BoundaryFile, FeatureItem, GeometryDefine, MultiPolygonCoordinates, PolygonCoordinates,
PropertiesDefine,
};
pub use tzb::Error;
use finder::{DenseGrid, FuzzyIndex, PolyFinder, assemble_items};
use tzb::Reader;
#[cfg(feature = "bundled")]
use tzf_dist::load_lite_tzb;
#[cfg(all(not(feature = "bundled"), feature = "full"))]
use tzf_dist_git::load_lite_tzb;
pub struct DefaultFinder {
fuzzy: Option<FuzzyIndex>,
finder: PolyFinder,
}
impl DefaultFinder {
#[cfg(any(feature = "bundled", feature = "full"))]
#[must_use]
pub fn new() -> Self {
Self::from_tzb(load_lite_tzb()).expect("tzf-dist lite.tzb is validated at release")
}
#[cfg(feature = "full")]
#[cfg_attr(docsrs, doc(cfg(feature = "full")))]
#[must_use]
pub fn new_full() -> Self {
Self::from_tzb(tzf_dist_git::load_full_tzb())
.expect("tzf-dist full.tzb is validated at release")
}
pub fn from_tzb(data: &[u8]) -> Result<Self, Error> {
let reader = Reader::open(Cow::Borrowed(data))?;
let fuzzy = FuzzyIndex::from_reader(&reader)?;
let grid = DenseGrid::from_reader(&reader);
let expanded = reader.expand()?;
Ok(Self {
fuzzy,
finder: PolyFinder {
items: assemble_items(expanded.names, expanded.polygons),
grid,
version: expanded.version,
},
})
}
#[must_use]
pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
if let Some(fuzzy) = &self.fuzzy
&& let Some(idx) = fuzzy.get(lng, lat)
{
return &self.finder.items[usize::from(idx)].name;
}
self.finder.get_tz_name(lng, lat)
}
#[must_use]
pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
self.finder.get_tz_names(lng, lat)
}
#[must_use]
pub fn timezonenames(&self) -> Vec<&str> {
self.finder.timezonenames()
}
#[must_use]
pub fn data_version(&self) -> &str {
&self.finder.version
}
#[cfg(feature = "export-geojson")]
#[cfg_attr(docsrs, doc(cfg(feature = "export-geojson")))]
#[must_use]
pub fn to_geojson(&self) -> BoundaryFile {
geojson::collection(
self.finder
.items
.iter()
.map(geojson::feature_from_item)
.collect(),
)
}
#[cfg(feature = "export-geojson")]
#[cfg_attr(docsrs, doc(cfg(feature = "export-geojson")))]
#[must_use]
pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
let features: Vec<_> = self
.finder
.items
.iter()
.filter(|item| item.name == timezone_name)
.map(geojson::feature_from_item)
.collect();
if features.is_empty() {
None
} else {
Some(geojson::collection(features))
}
}
#[cfg(feature = "export-geojson")]
#[cfg_attr(docsrs, doc(cfg(feature = "export-geojson")))]
#[must_use]
pub fn get_tz_preindex_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
let fuzzy = self.fuzzy.as_ref()?;
let indices: Vec<u16> = self
.finder
.items
.iter()
.enumerate()
.filter(|(_, item)| item.name == timezone_name)
.filter_map(|(i, _)| u16::try_from(i).ok())
.collect();
if indices.is_empty() {
return None;
}
let keys = fuzzy.tile_keys_for(&indices);
if keys.is_empty() {
return None;
}
Some(geojson::collection(vec![geojson::feature_from_tile_keys(
timezone_name.to_string(),
&keys,
)]))
}
#[cfg(feature = "export-geojson")]
#[cfg_attr(docsrs, doc(cfg(feature = "export-geojson")))]
#[must_use]
pub fn to_preindex_geojson(&self) -> Option<BoundaryFile> {
let fuzzy = self.fuzzy.as_ref()?;
let mut grouped = fuzzy.tile_keys_grouped();
let features = self
.finder
.items
.iter()
.enumerate()
.filter_map(|(i, item)| {
let keys = grouped.remove(&u16::try_from(i).ok()?)?;
Some(geojson::feature_from_tile_keys(item.name.clone(), &keys))
})
.collect();
Some(geojson::collection(features))
}
}
#[cfg(any(feature = "bundled", feature = "full"))]
impl Default for DefaultFinder {
fn default() -> Self {
Self::new()
}
}
pub struct EmbeddedFinder {
reader: Reader<'static>,
names: Vec<String>,
}
impl EmbeddedFinder {
#[cfg(any(feature = "bundled", feature = "full"))]
#[must_use]
pub fn new() -> Self {
Self::from_tzb(load_lite_tzb()).expect("tzf-dist lite.tzb is validated at release")
}
pub fn from_tzb(data: impl Into<Cow<'static, [u8]>>) -> Result<Self, Error> {
let reader = Reader::open(data.into())?;
let names = (0..reader.timezone_count())
.map(|i| reader.name(i).map(str::to_string))
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { reader, names })
}
#[must_use]
pub fn get_tz_name(&self, lng: f64, lat: f64) -> &str {
if self.reader.has_fuzzy()
&& let Ok(Some(idx)) = self.reader.fuzzy_lookup(lng, lat)
{
return &self.names[idx as usize];
}
match self.reader.lookup(lng, lat) {
Ok(Some(idx)) => &self.names[idx as usize],
_ => "",
}
}
#[must_use]
pub fn get_tz_names(&self, lng: f64, lat: f64) -> Vec<&str> {
let mut indices = Vec::with_capacity(self.reader.lookup_buffer_size());
if self.reader.lookup_into(lng, lat, &mut indices).is_err() {
return Vec::new();
}
indices
.into_iter()
.map(|idx| self.names[idx as usize].as_str())
.collect()
}
#[must_use]
pub fn timezonenames(&self) -> Vec<&str> {
self.names.iter().map(String::as_str).collect()
}
#[must_use]
pub fn data_version(&self) -> &str {
self.reader.data_version()
}
#[cfg(feature = "export-geojson")]
#[cfg_attr(docsrs, doc(cfg(feature = "export-geojson")))]
#[must_use]
pub fn to_geojson(&self) -> BoundaryFile {
let features = (0..self.names.len())
.filter_map(|i| {
let polys = self.reader.expand_timezone(i as u32).ok()?;
Some(geojson::feature_from_expanded(
self.names[i].clone(),
&polys,
))
})
.collect();
geojson::collection(features)
}
#[cfg(feature = "export-geojson")]
#[cfg_attr(docsrs, doc(cfg(feature = "export-geojson")))]
#[must_use]
pub fn get_tz_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
let mut features = Vec::new();
for (i, name) in self.names.iter().enumerate() {
if name != timezone_name {
continue;
}
let polys = self.reader.expand_timezone(i as u32).ok()?;
features.push(geojson::feature_from_expanded(name.clone(), &polys));
}
if features.is_empty() {
None
} else {
Some(geojson::collection(features))
}
}
#[cfg(feature = "export-geojson")]
#[cfg_attr(docsrs, doc(cfg(feature = "export-geojson")))]
#[must_use]
pub fn get_tz_preindex_geojson(&self, timezone_name: &str) -> Option<BoundaryFile> {
if !self.reader.has_fuzzy() {
return None;
}
let indices: Vec<u16> = self
.names
.iter()
.enumerate()
.filter(|(_, name)| name.as_str() == timezone_name)
.filter_map(|(i, _)| u16::try_from(i).ok())
.collect();
if indices.is_empty() {
return None;
}
let entries = self.reader.fuzzy_entries().ok()?;
let keys: Vec<u64> = entries
.iter()
.filter(|(_, idxs)| idxs.iter().any(|i| indices.contains(i)))
.map(|(key, _)| *key)
.collect();
if keys.is_empty() {
return None;
}
Some(geojson::collection(vec![geojson::feature_from_tile_keys(
timezone_name.to_string(),
&keys,
)]))
}
#[cfg(feature = "export-geojson")]
#[cfg_attr(docsrs, doc(cfg(feature = "export-geojson")))]
#[must_use]
pub fn to_preindex_geojson(&self) -> Option<BoundaryFile> {
if !self.reader.has_fuzzy() {
return None;
}
let entries = self.reader.fuzzy_entries().ok()?;
let mut grouped: std::collections::HashMap<u16, Vec<u64>> =
std::collections::HashMap::new();
for (key, idxs) in &entries {
for &idx in idxs {
grouped.entry(idx).or_default().push(*key);
}
}
let features = (0..self.names.len())
.filter_map(|i| {
let keys = grouped.remove(&u16::try_from(i).ok()?)?;
Some(geojson::feature_from_tile_keys(
self.names[i].clone(),
&keys,
))
})
.collect();
Some(geojson::collection(features))
}
}
#[cfg(any(feature = "bundled", feature = "full"))]
impl Default for EmbeddedFinder {
fn default() -> Self {
Self::new()
}
}
#[must_use]
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::similar_names
)]
pub fn deg2num(lng: f64, lat: f64, zoom: i64) -> (i64, i64) {
let n = (1i64 << zoom) as f64;
let lat_rad = lat.to_radians();
let xtile = (lng / 360.0 + 0.5) * n;
let ytile = (1.0 - lat_rad.tan().asinh() / PI) / 2.0 * n;
(xtile as i64, ytile as i64)
}