Location: PHPKode > scripts > Geobaza PHP API > geobaza/geobaza.php
<?php
# Copyright (c) 2013, CN-Software Ltd.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#     1. Redistributions of source code must retain the above copyright notice,
#        this list of conditions and the following disclaimer.
#
#     2. Redistributions in binary form must reproduce the above copyright
#        notice, this list of conditions and the following disclaimer in the
#        documentation and/or other materials provided with the distribution.
#
#     3. Neither the name of CN-Software Ltd. nor the names of its contributors
#        may be used to endorse or promote products derived from this software
#        without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

require_once 'utils.php';

define('VERSION', '1.0.4.13.03');

define('FILE_PATH', dirname(__FILE__).'/data/geobaza.dat');
define('NO_CACHE', 0);
define('MEMORY_CACHE', 1);

/**
 * Exceptions
 */

/**
 * Base module exception
 */
class GeobazaException extends Exception {}

class GeobazaIPException extends GeobazaException {}

/**
 * Binary files
 */

class FileSingleton {
    protected static $instances = array();

    public function __clone() {
        trigger_error('Clone is not allowed.', E_USER_ERROR);
    }

    public function __wakeup() {
        trigger_error('Unserializing is not allowed.', E_USER_ERROR);
    }

    public function unpack($bytes, $fmt) {
        $data = $this->read($bytes);
        $unp = unpack($fmt, $data);
        return $unp;
    }
}

class BinaryFile extends FileSingleton {
    public static function init($path) {
        if (!array_key_exists($path, self::$instances)) {
            self::$instances[$path] = new self($path);
        }
        return self::$instances[$path];
    }

    private function __construct($path) {
        $this->file = fopen($path, 'r');
    }

    public function read($bytes) {
        return fread($this->file, $bytes);
    }

    public function seek($offset, $whence=SEEK_SET) {
        return fseek($this->file, $offset, $whence);
    }

    public function tell() {
        return ftell($this->file);
    }
}

class BinaryBlob extends FileSingleton {
    private $position;
    private $blob;

    public static function init($path) {
        if (!array_key_exists($path, self::$instances)) {
            self::$instances[$path] = new self($path);
        }
        return self::$instances[$path];
    }

    private function __construct($path) {
        $this->blob = file_get_contents($path);
        $this->position = 0;
    }

    public function read($bytes) {
        $chunk = substr($this->blob, $this->position, $bytes);
        $this->position += $bytes;
        return $chunk;
    }

    public function seek($offset, $whence=0) {
        switch ($whence) {
            case 1:
                $this->position += $offset;
            case 2:
                $this->position = count($this->blob) + $offset;
            default:
                $this->position = $offset;
        }
    }

    public function tell() {
        return $this->position;
    }
}

/**
 * Languages
 */

class Language {
    public $id, $name;

    function __construct($id, $name) {
        $this->id = $id;
        $this->name = $name;
    }
}

class Languages {
    static $languages = array(
        'aa' => 'Afar',
        'ab' => 'Abkhazian',
        'ae' => 'Avestan',
        'af' => 'Afrikaans',
        'ak' => 'Akan',
        'am' => 'Amharic',
        'an' => 'Aragonese',
        'ar' => 'Arabic',
        'as' => 'Assamese',
        'av' => 'Avaric',
        'ay' => 'Aymara',
        'az' => 'Azerbaijani',
        'ba' => 'Bashkir',
        'be' => 'Belarusian',
        'bg' => 'Bulgarian',
        'bh' => 'Bihari',
        'bi' => 'Bislama',
        'bm' => 'Bambara',
        'bn' => 'Bengali',
        'bo' => 'Tibetan',
        'br' => 'Breton',
        'bs' => 'Bosnian',
        'ca' => 'Catalan; Valencian',
        'ce' => 'Chechen',
        'ch' => 'Chamorro',
        'co' => 'Corsican',
        'cr' => 'Cree',
        'cs' => 'Czech',
        'cu' => 'Church Slavic',
        'cv' => 'Chuvash',
        'cy' => 'Welsh',
        'da' => 'Danish',
        'de' => 'German',
        'dv' => 'Divehi; Dhivehi; Maldivian',
        'dz' => 'Dzongkha',
        'ee' => 'Ewe',
        'el' => 'Greek, Modern',
        'en' => 'English',
        'eo' => 'Esperanto',
        'es' => 'Spanish; Castilian',
        'et' => 'Estonian',
        'eu' => 'Basque',
        'fa' => 'Persian',
        'ff' => 'Fulah',
        'fi' => 'Finnish',
        'fj' => 'Fijian',
        'fo' => 'Faroese',
        'fr' => 'French',
        'fy' => 'Western Frisian',
        'ga' => 'Irish',
        'gd' => 'Gaelic; Scottish Gaelic',
        'gl' => 'Galician',
        'gn' => 'Guarani',
        'gu' => 'Gujarati',
        'gv' => 'Manx',
        'ha' => 'Hausa',
        'he' => 'Hebrew',
        'hi' => 'Hindi',
        'ho' => 'Hiri Motu',
        'hr' => 'Croatian',
        'ht' => 'Haitian; Haitian Creole',
        'hu' => 'Hungarian',
        'hy' => 'Armenian',
        'hz' => 'Herero',
        'ia' => 'Interlingua',
        'id' => 'Indonesian',
        'ie' => 'Interlingue; Occidental',
        'ig' => 'Igbo',
        'ii' => 'Sichuan Yi; Nuosu',
        'ik' => 'Inupiaq',
        'io' => 'Ido',
        'is' => 'Icelandic',
        'it' => 'Italian',
        'iu' => 'Inuktitut',
        'ja' => 'Japanese',
        'jv' => 'Javanese',
        'ka' => 'Georgian',
        'kg' => 'Kongo',
        'ki' => 'Kikuyu; Gikuyu',
        'kj' => 'Kuanyama; Kwanyama',
        'kk' => 'Kazakh',
        'kl' => 'Kalaallisut; Greenlandic',
        'km' => 'Central Khmer',
        'kn' => 'Kannada',
        'ko' => 'Korean',
        'kr' => 'Kanuri',
        'ks' => 'Kashmiri',
        'ku' => 'Kurdish',
        'kv' => 'Komi',
        'kw' => 'Cornish',
        'ky' => 'Kirghiz; Kyrgyz',
        'la' => 'Latin',
        'lb' => 'Luxembourgish; Letzeburgesch',
        'lg' => 'Ganda',
        'li' => 'Limburgan; Limburger; Limburgish',
        'ln' => 'Lingala',
        'lo' => 'Lao',
        'lt' => 'Lithuanian',
        'lu' => 'Luba-Katanga',
        'lv' => 'Latvian',
        'mg' => 'Malagasy',
        'mh' => 'Marshallese',
        'mi' => 'Maori',
        'mk' => 'Macedonian',
        'ml' => 'Malayalam',
        'mn' => 'Mongolian',
        'mo' => 'Moldavian; Moldovan',
        'mr' => 'Marathi',
        'ms' => 'Malay',
        'mt' => 'Maltese',
        'my' => 'Burmese',
        'na' => 'Nauru',
        'nb' => 'Bokmål, Norwegian; Norwegian Bokmål',
        'nd' => 'Ndebele, North; North Ndebele',
        'ne' => 'Nepali',
        'ng' => 'Ndonga',
        'nl' => 'Dutch (Flemish)',
        'nn' => 'Norwegian Nynorsk; Nynorsk, Norwegian',
        'no' => 'Norwegian',
        'nr' => 'Ndebele, South; South Ndebele',
        'nv' => 'Navajo; Navaho',
        'ny' => 'Chichewa; Chewa; Nyanja',
        'oc' => 'Occitan (post 1500)',
        'oj' => 'Ojibwa',
        'om' => 'Oromo',
        'or' => 'Oriya',
        'os' => 'Ossetian; Ossetic',
        'pa' => 'Panjabi; Punjabi',
        'pi' => 'Pali',
        'pl' => 'Polish',
        'ps' => 'Pushto; Pashto',
        'pt' => 'Portuguese',
        'qu' => 'Quechua',
        'rm' => 'Romansh',
        'rn' => 'Rundi',
        'ro' => 'Romanian',
        'ru' => 'Russian',
        'rw' => 'Kinyarwanda',
        'sa' => 'Sanskrit',
        'sc' => 'Sardinian',
        'sd' => 'Sindhi',
        'se' => 'Northern Sami',
        'sg' => 'Sango',
        'si' => 'Sinhala; Sinhalese',
        'sk' => 'Slovak',
        'sl' => 'Slovenian',
        'sm' => 'Samoan',
        'sn' => 'Shona',
        'so' => 'Somali',
        'sq' => 'Albanian',
        'sr' => 'Serbian',
        'ss' => 'Swati',
        'st' => 'Sotho, Southern',
        'su' => 'Sundanese',
        'sv' => 'Swedish',
        'sw' => 'Swahili',
        'ta' => 'Tamil',
        'te' => 'Telugu',
        'tg' => 'Tajik',
        'th' => 'Thai',
        'ti' => 'Tigrinya',
        'tk' => 'Turkmen',
        'tl' => 'Tagalog',
        'tn' => 'Tswana',
        'to' => 'Tonga (Tonga Islands)',
        'tr' => 'Turkish',
        'ts' => 'Tsonga',
        'tt' => 'Tatar',
        'tw' => 'Twi',
        'ty' => 'Tahitian',
        'ug' => 'Uighur; Uyghur',
        'uk' => 'Ukrainian',
        'ur' => 'Urdu',
        'uz' => 'Uzbek',
        've' => 'Venda',
        'vi' => 'Vietnamese',
        'vo' => 'Volapük',
        'wa' => 'Walloon',
        'wo' => 'Wolof',
        'xh' => 'Xhosa',
        'yi' => 'Yiddish',
        'yo' => 'Yoruba',
        'za' => 'Zhuang; Chuang',
        'zh' => 'Chinese',
        'zu' => 'Zulu');

    static function list_languages() {
        foreach (self::$languages as $id => $name) {
            echo $id, $name, EOL;
        }
    }

    static function get_language($id) {
        $lang = self::$languages[$id];
        if (empty($lang)) {
            throw GeobazaException(sprintf('Language with ISO ID %s does not exists!', $id));
        }
        return new Language($id, $lang);
    }
}

/**
 * Geography
 */

class LatLon {
    public $latitude, $longitude;

    function __construct($lat=NULL, $lon=NULL) {
        $this->latitude = $lat;
        $this->longitude = $lon;
    }
}

class Geography extends Serializer {
    public $center;

    function __construct($args) {
        foreach ($args as $key => $value) {
            $this->$key = $value;
        }
    }

    function as_xml() {
        $xml = new DOMDocument('1.0', 'utf-8');
        $root = $xml->createElement('center');
        $root->setAttribute('latitude', $this->center->latitude);
        $root->setAttribute('longitude', $this->center->longitude);
        $xml->appendChild($root);
        return $xml;
    }

    function as_array() {
        return array('center' => array('latitude' => $this->center->latitude, 'longitude' => $this->center->longitude));
    }
}

/**
 * Geographical objects
 */

class AbstractObject extends Serializer {
    protected $encoding = 'utf-8';
    protected $utf;
}

class GeobazaObject extends AbstractObject {
    protected $parent = NULL;
    protected $query, $encoding;

    public $id, $level, $type, $iso_id, $name, $translations, $child, $population, $geography;

    const COUNTRY = 'country';
    const REGION = 'region';
    const LOCALITY = 'locality';
    const SPECIAL = 'special';

    function __construct($data, $encoding='utf-8') {
        $this->encoding = strtolower($encoding);
        $this->id = (int)$data->id;
        $this->level = (int)$data->level;
        $this->type = $data->type;
        if (isset($data->iso_id)) {
            $this->iso_id = $data->iso_id;
        }

        $this->set_geography($data);
        $this->set_population($data);
        $this->set_translation($data);
        $this->set_name();
        $this->encode();
    }

    function __set($name, $value) {
        switch ($name) {
            case 'query':
                $this->query = $value;
            case 'parent':
                $this->parent = $value;
        }
    }

    function __get($name) {
        switch ($name) {
            case 'parent':
                $this->fetch_parent();
                return $this->parent;
        }
    }

    protected function fetch_parent() {
        if (is_int($this->parent)) {
            $data = $this->query->get_from($this->parent);

            if (count($data) == 2) {
                $obj = $data[0];
                $parent = $data[1]->parent;
            }
            else {
                $obj = $data[0];
                $parent = NULL;
            }
            $this->parent = Geobaza::create_instance($obj, $this->encoding, $childs=$this, $parent=$parent);
            $this->parent->query = $this->query;
        }
    }

    protected function set_geography($data) {
        $lat = $lon = NULL;
        if (isset($data->lat)) {
            $lat = $data->lat;
        }
        if (isset($data->lon)) {
            $lon = $data->lon;
        }
        $this->geography = new Geography(array('center' => new LatLon($lat, $lon)));
    }

    protected function set_population($data) {
        $this->population = NULL;
        if (isset($data->population)) {
            $this->population = (int)$data->population;
        }
    }

    protected function set_translation($data) {
        $RAW_MAP = array(
            'official' => array(0, 'name_official'),
            'alt' => array(1, 'name'),
        );

        $this->translations = array();
        foreach ($RAW_MAP as $type => $value) {
            $priority = $value[0];
            $raw_key = $value[1];

            $obj = array('type' => $type);
            foreach ($data->$raw_key as $lang => $name) {
                $obj[strtolower($lang)] = $name;
            }
            $this->translations[] = (object)$obj;
        }
    }

    protected function set_name() {
        $this->name = $this->translations[0]->en;
    }

    protected function encode() {
        $this->utf = new stdClass();
        $this->utf->translations = array();
        foreach ($this->translations as $item) {
            $this->utf->translations[] = clone($item);
        }
        $this->utf->name = $this->name;

        for ($i = 0; $i < count($this->translations); $i++) {
            foreach ($this->translations[$i] as $key => $value) {
                if ($key != 'type') {
                    $value = convert_encoding($value, $this->encoding);
                }
                $this->translations[$i]->$key = $value;
            }
        }
    }

    function as_xml() {
        $xml = new DOMDocument('1.0', $this->encoding);
        $root = $xml->createElement('object');
        $xml->appendChild($root);
        $root->setAttribute('type', $this->type);
        $root->setAttribute('id', $this->id);
        $root->setAttribute('level', $this->level);
        $this->fetch_parent();
        if (!empty($this->parent)) {
            $root->setAttribute('parent', $this->parent->id);
        }
        if (!empty($this->child)) {
            $root->setAttribute('child', $this->child->id);
        }

        $name = $xml->createElement('name');
        $name->appendChild($xml->createTextNode($this->utf->name));
        $root->appendChild($name);

        $iso_id = $xml->createElement('iso-id');
        $iso_id->appendChild($xml->createTextNode($this->iso_id));
        $root->appendChild($iso_id);

        $population = $xml->createElement('population');
        $population->appendChild($xml->createTextNode($this->population));
        $root->appendChild($population);

        $translations = $xml->createElement('translations');
        foreach($this->utf->translations as $item) {
            $group = $xml->createElement('group');
            $translations->appendChild($group);
            $group->setAttribute('type', $item->type);
            foreach($item as $key => $value) {
                if ($key != 'type') {
                    $translation = $xml->createElement('item');
                    $translation->setAttribute('language', $key);
                    $translation->appendChild($xml->createTextNode($value));
                    $group->appendChild($translation);
                }
            }
        }
        $root->appendChild($translations);

        $geography = $xml->createElement('geography');
        $geography_xml = $this->geography->as_xml();
        $node = $xml->importNode($geography_xml->documentElement, true);
        $geography->appendChild($node);
        $root->appendChild($geography);

        return $xml;
    }

    function as_array() {
        $object = array(
            'type' => $this->type,
            'name' => $this->utf->name,
            'iso_id' => $this->iso_id,
            'id' => $this->id,
            'level' => $this->level,
        );

        $this->fetch_parent();
        if (!empty($this->parent)) {
            $object['parent'] = $this->parent->id;
        }
        if (!empty($this->child)) {
            $object['child'] = $this->child->id;
        }

        $object['geography'] = $this->geography->as_array();
        $object['population'] = $this->population;
        $object['translations'] = $this->utf->translations;

        return $object;
    }
}

class Region extends GeobazaObject {
    public $language;

    function __construct($data, $encoding='utf-8') {
        $this->set_lang($data);
        parent::__construct($data, $encoding);
    }

    protected function set_lang($data) {
        $this->language = NULL;
        if (isset($data->lang)) {
            $lang = strtolower($data->lang);
            if ($lang) {
                $this->language = Languages::get_language($lang);
            }
        }
    }

    protected function encode() {
        parent::encode();
        if ($this->language instanceof Language) {
            $this->utf->language = clone($this->language);
        } else {
            $this->utf->language = $this->language;
        }

        if ($this->encoding != 'utf-8') {
            $this->language->name = convert_encoding($this->language->name, $this->encoding);
        }
    }

    function as_xml() {
        $xml = parent::as_xml();
        $language = $xml->createElement('language');
        $xml->documentElement->appendChild($language);
        if (!empty($this->utf->language)) {
            $language->setAttribute('id', $this->utf->language->id);
            $language->appendChild($xml->createTextNode($this->utf->language->name));
        }
        return $xml;
    }

    function as_array() {
        $object = parent::as_array();
        $language = NULL;
        if (!empty($this->utf->language)) {
            $language = array(
                'name' => $this->utf->language->name,
                'id' => $this->utf->language->id
            );
        }
        $object['language'] = $language;
        return $object;
    }
}

class Country extends Region {
    public $tld;

    function __construct($data, $encoding='utf-8') {
        parent::__construct($data, $encoding);
        $this->set_tld($data);
    }

    protected function set_tld() {
        $this->tld = NULL;
        if (isset($this->iso_id)) {
            if ($this->iso_id == 'GB') {
                $this->tld = 'uk';
            }
            else {
                $this->tld = strtolower($this->iso_id);
            }
        }
    }

    function as_xml() {
        $xml = parent::as_xml();
        $tld = $xml->createElement('tld');
        $xml->documentElement->appendChild($tld);
        $tld->appendChild($xml->createTextNode($this->tld));
        return $xml;
    }

    function as_array() {
        $object = parent::as_array();
        $object['tld'] = $this->tld;
        return $object;
    }
}

class Locality extends GeobazaObject {}

class SpecialRange extends AbstractObject {
    function __construct($data, $encoding='utf-8') {
        $this->utf = new stdClass();
        $this->type = GeobazaObject::SPECIAL;
        $this->encoding = strtolower($encoding);
        $name = $data->special ? $data->special : NULL;
        $this->name = $this->utf->name = $name;
        if (!empty($name) && $this->encoding != 'utf-8') {
            $this->name = convert_encoding($name, $this->encoding);
        }
    }

    function as_xml() {
        $xml = new DOMDocument('1.0', $this->encoding);
        $root = $xml->createElement('object');
        $root->setAttribute('type', $this->type);
        $name = $xml->createElement('name');
        $name->appendChild($xml->createTextNode($this->utf->name));
        $root->appendChild($name);
        $xml->appendChild($root);
        return $xml;
    }

    function as_array() {
        $object = array('type' => $this->type, 'name' => $this->utf->name);

        return $object;
    }
}

/**
 * Query
 */

class Geobaza extends Serializer implements Iterator, Countable, ArrayAccess {
    /**
     * Iterator position
     * @var integer
     */
    private $position = 0;

    private $is_special = NULL;

    private $list = array();
    private $country;
    private $regions = array();
    private $localities = array();

    function __construct($meta) {
        $this->meta = $meta;
        $this->headers = $meta->headers;
    }

    function current() {
        return $this->list[$this->position];
    }

    function key() {
        return $this->position;
    }

    function next() {
        $this->position++;
    }

    function rewind() {
        $this->position = 0;
    }

    function valid() {
        if ($this->position < count($this->list)) {
            return true;
        }
        return false;
    }

    function offsetSet($offset, $value) {
        throw new Exception('You can not assign values to Geobaza!');
    }

    function offsetExists($offset) {
        return isset($this->list[$offset]);
    }

    function offsetUnset($offset) {
        throw new Exception('You can not change Geobaza object list!');
    }

    function offsetGet($offset) {
        if (isset($this->list[$offset])) {
            return $this->list[$offset];
        }
        throw new OutOfRangeException('Invalid offset!');
    }

    protected function set_special_flag() {
        if (!isset($this->is_special)) {
            foreach ($this->list as $obj) {
                if ($obj->type == GeobazaObject::SPECIAL) {
                    $this->is_special = true;
                    return;
                }
            }
            $this->is_special = false;
        }
    }

    function __get($name) {
        switch ($name) {
            case 'is_special':
                $this->set_special_flag();
                return $this->is_special;
            case 'localities':
                return self::from_list($this->localities, $this->meta);
            case 'regions':
                return self::from_list($this->regions, $this->meta);
            case 'country':
                return $this->country;
        }
    }

    function append($item) {
        $this->add_by_type($item);
        $this->list[] = $item;
    }

    function extend($items) {
        foreach ($items as $item) {
            $this->append($item);
        }
    }

    static function from_array($data, $meta) {
        $geobaza = new self($meta);
        $data = array_reverse($data);
        $parent = NULL;

        foreach ($data as $obj) {
            if (isset($obj->special)) {
                $geobaza->append(new SpecialRange($obj, $meta->encoding));
            }
            else if (isset($obj->parent)) {
                # If object only contains link
                $parent = $obj->parent;
            }
            else {
                $inst = Geobaza::create_instance($obj, $meta->encoding);
                $inst->query = $meta->query;
                $geobaza->append($inst);
                $inst->parent = $parent;
                if ($parent instanceof AbstractObject) {
                    $parent->child = $inst;
                }
                $parent = $inst;
            }
        }
        return $geobaza;
    }

    static function from_list($items, $meta) {
        $geobaza = new self($meta);
        $geobaza->extend($items);
        return $geobaza;
    }

    static function create_instance($obj, $encoding, $child=NULL, $parent=NULL) {
        $type_map = array(
            GeobazaObject::COUNTRY => 'Country',
            GeobazaObject::REGION => 'Region',
            GeobazaObject::LOCALITY => 'Locality'
        );
        $type = $obj->type;
        $item_class = $type_map[$type];
        $item = new $item_class($obj, $encoding);
        $item->child = $child;
        if (!empty($parent)) {
            $item->parent = $parent;
        }
        return $item;
    }

    protected function add_by_type($obj) {
        switch ($obj->type) {
            case GeobazaObject::COUNTRY:
                $this->country = $obj;
                break;
            case GeobazaObject::REGION:
                $this->regions[] = $obj;
                break;
            case GeobazaObject::LOCALITY:
                $this->localities[] = $obj;
                break;
        }
    }

    function name_path($delimiter=', ') {
        return join($delimiter, $this->name_list());
    }

    function name_list() {
        $list = array();
        foreach ($this as $item) {
            $list[] = $item->name;
        }
        return $list;
    }

    function id_list() {
        $list = array();
        foreach ($this as $item) {
            $list[] = $item->id;
        }
        return $list;
    }

    function first() {
        return $this->list[0];
    }

    function last() {
        return $this->list[count($this->list) - 1];
    }

    function count() {
        return count($this->list);
    }

    function as_xml() {
        $xml = new DOMDocument('1.0', $this->meta->encoding);
        $root = $xml->createElement('geobaza');
        $root->setAttribute('api-version', $this->headers->api_version);
        $root->setAttribute('release', $this->headers->release);
        $root->setAttribute('build-timestamp', $this->headers->build_timestamp);
        $root->setAttribute('build-date', $this->headers->build_date);
        $root->setAttribute('lite', (int)$this->headers->lite);
        $root->setAttribute('query-timestamp', time(true));
        $root->setAttribute('ip', $this->meta->ip);
        $this->set_special_flag();
        $root->setAttribute('is-special', (int)$this->is_special);

        $objects = $xml->createElement('objects');
        foreach ($this as $item) {
            $item_xml = $item->as_xml();
            $node = $xml->importNode($item_xml->documentElement, true);
            $objects->appendChild($node);
        }
        $root->appendChild($objects);
        $xml->appendChild($root);
        return $xml;
    }

    function as_array() {
        $this->set_special_flag();
        $object = array(
            'api_version' => $this->headers->api_version,
            'release' => $this->headers->release,
            'build_timestamp' => $this->headers->build_timestamp,
            'build_date' => $this->headers->build_date,
            'lite' => (int)$this->headers->lite,
            'query_timestamp' => time(true),
            'ip' => $this->meta->ip,
            'is_special' => (int)$this->is_special,
        );
        $object['objects'] = array();
        foreach ($this as $item) {
            $object['objects'][] = $item->as_array();
        }
        return $object;
    }
}

class GeobazaQuery extends Serializer {
    private $level = array();
    private $offset;
    private $f;
    private $meta;
    private $encoding;

    function __get($name) {
        if ($name == 'encoding') {
            return $this->encoding;
        }
    }

    function __construct($path=FILE_PATH, $cache=NO_CACHE, $encoding='utf-8') {
        $this->encoding = strtolower($encoding);

        if ($cache == MEMORY_CACHE) {
            $this->f = BinaryBlob::init($path);
            $this->f->seek(0);
        }
        else {
            $this->f = BinaryFile::init($path);
            $this->f->seek(0);
        }

        if ($this->f->read(7) != 'GEOBAZA') {
            throw new GeobazaException('Invalid datafile signature!');
        }

        # Headers
        $unpack = $this->f->unpack(2, 'nlen');
        $this->headers = json_decode($this->f->read($unpack['len']));
        $this->headers->lite = (isset($this->headers->lite) ? true : false);
        $this->headers->release = trim($this->headers->release);

        while (true) {
            $data = $this->f->unpack(1, 'Cdata');
            $hi = ($data['data'] >> 4) & 0x0f;
            $this->level[] = $hi;
            if ($hi == 0) {
                break;
            }
            $lo = $data['data'] & 0x0f;
            $this->level[] = $lo;
            if ($lo == 0) {
                break;
            }
        }

        $this->offset = $this->f->tell();
    }

    protected function get_json($offset, $path=false) {
        $json_str = array();
        while ($offset) {
            $length = $this->get_length($offset);
            if ($length > 0) {
                $obj_str = $this->f->read($length);
                $unp_offset = $this->f->unpack(4, 'Ndata');
                $offset = $unp_offset['data'];
                $json_str[] = $obj_str;
                if ($path == false) {
                    $length = $this->get_length($offset);
                    if ($length && $offset) {
                        $json_str[] = sprintf('{"parent": %d}', $offset);
                    }
                    break;
                }
            }
            else {
                return;
            }
        }
        return sprintf('[%s]', join(',', $json_str));
    }

    protected function get_length($offset) {
        $position = $offset & 0x7fffffff;
        $this->f->seek($position, 0);
        $unpack = $this->f->unpack(2, 'nlen');
        return $unpack['len'] & 0xffff;
    }

    protected function get_data($ip, $path=false) {
        $offset = $this->offset;
        $ip_int = ip2long($ip);
        $shift = 32;
        for ($i = 0; $i < sizeof($this->level); $i++) {
            $shift -= $this->level[$i];
            $index = (($ip_int >> $shift)) & ((1 << $this->level[$i]) - 1);
            $tell = $offset + $index * 4;
            $this->f->seek($tell, 0);
            $unpack = $this->f->unpack(4, 'Ndata');
            $offset = $unpack['data'] & 0xffffffff;
            if ($offset & 0x80000000) {
                return $this->get_json($offset, $path);
            }
        }
    }

    protected function get_meta($ip) {
        $meta = array(
            'ip' => $ip,
            'query' => $this,
            'headers' => $this->headers,
            'encoding' => $this->encoding
        );
        return (object)$meta;
    }

    function get($ip) {
        $json_str = $this->get_data($ip);
        if (!empty($json_str)) {
            $geobaza = Geobaza::from_array(json_decode($json_str), $this->get_meta($ip));
            return $geobaza->first();
        }
    }

    function get_list($ip) {
        $json_str = $this->get_data($ip);
        if (!empty($json_str)) {
            $geobaza = Geobaza::from_array(json_decode($json_str), $this->get_meta($ip));
            return $geobaza;
        }
    }

    function get_path($ip) {
        $json_str = $this->get_data($ip, $path=true);
        if (!empty($json_str)) {
            $geobaza = Geobaza::from_array(json_decode($json_str), $this->get_meta($ip));
            return $geobaza;
        }
    }

    function get_from($offset) {
        $json_str = $this->get_json($offset);
        if (!empty($json_str)) {
            return json_decode($json_str);
        }
    }
}

/**
 * Shortcut functions
 */

function lookup($ip) {
    try {
        $geobaza = new GeobazaQuery();
        return $geobaza->get_path($ip);
    }
    catch (GeobazaException $e) {
        throw $e;
    }
}

function lookup_xml($ip, $pretty=false) {
    $geobaza = lookup($ip);
    if (!empty($geobaza)) {
        if ($pretty == true) {
            return $geobaza->to_pretty_xml();
        }
        return $geobaza->to_xml();
    }
}

function lookup_json($ip, $pretty=false) {
    $geobaza = lookup($ip);
    if (!empty($geobaza)) {
        if ($pretty == true) {
            return $geobaza->to_pretty_json();
        }
        return $geobaza->to_json();
    }
}
?>
Return current item: Geobaza PHP API