All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.questdb.cairo.GeoHashes Maven / Gradle / Ivy

/*******************************************************************************
 *     ___                  _   ____  ____
 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
 *   | | | | | | |/ _ \/ __| __| | | |  _ \
 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
 *    \__\_\\__,_|\___||___/\__|____/|____/
 *
 *  Copyright (c) 2014-2019 Appsicle
 *  Copyright (c) 2019-2022 QuestDB
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 ******************************************************************************/

package io.questdb.cairo;

import io.questdb.cairo.sql.Function;
import io.questdb.cairo.sql.Record;
import io.questdb.std.LongList;
import io.questdb.std.Numbers;
import io.questdb.std.NumericException;
import io.questdb.std.Unsafe;
import io.questdb.std.str.CharSink;

public class GeoHashes {

    // geohash null value: -1
    // we use the highest bit of every storage size (byte, short, int, long)
    // to indicate null value. When a null value is cast down, nullity is
    // preserved, i.e. highest bit remains set:
    //     long nullLong = -1L;
    //     short nullShort = (short) nullLong;
    //     nullShort == nullLong;
    // in addition, -1 is the first negative non geohash value.
    public static final byte BYTE_NULL = -1;
    public static final short SHORT_NULL = -1;
    public static final int INT_NULL = -1;
    public static final long NULL = -1L;
    public static final int MAX_STRING_LENGTH = 12;
    // we fill this array with BYTE_NULL (-1)
    static final byte[] base32Indexes = {
            0, 1, 2, 3, 4, 5, 6, 7,         // 30-37, '0'..'7'
            8, 9, -1, -1, -1, -1, -1, -1,   // 38-2F, '8','9'
            -1, -1, 10, 11, 12, 13, 14, 15, // 40-47, 'B'..'G'
            16, -1, 17, 18, -1, 19, 20, -1, // 48-4F, 'H','J','K','M','N'
            21, 22, 23, 24, 25, 26, 27, 28, // 50-57, 'P'..'W'
            29, 30, 31, -1, -1, -1, -1, -1, // 58-5F, 'X'..'Z'
            -1, -1, 10, 11, 12, 13, 14, 15, // 60-67, 'b'..'g'
            16, -1, 17, 18, -1, 19, 20, -1, // 68-6F, 'h','j','k','m','n'
            21, 22, 23, 24, 25, 26, 27, 28, // 70-77, 'p'..'w'
            29, 30, 31, -1, -1, -1, -1, -1  // 78-7A, 'x'..'z'
    };

    private static final char[] base32 = {
            '0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'b', 'c', 'd', 'e', 'f', 'g',
            'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r',
            's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
    };

    public static void appendBinary(long hash, int bits, CharSink sink) {
        if (hash != NULL) {
            appendBinaryStringUnsafe(hash, bits, sink);
        }
    }

    public static void appendBinaryStringUnsafe(long hash, int bits, CharSink sink) {
        // Below assertion can happen if there is corrupt metadata
        // which should not happen in production code since reader and writer check table metadata
        assert bits > 0 && bits <= ColumnType.GEO_HASH_MAX_BITS_LENGTH;
        for (int i = bits - 1; i >= 0; --i) {
            sink.put(((hash >> i) & 1) == 1 ? '1' : '0');
        }
    }

    public static void appendChars(long hash, int chars, CharSink sink) {
        if (hash != NULL) {
            appendCharsUnsafe(hash, chars, sink);
        }
    }

    public static void appendCharsUnsafe(long hash, int chars, CharSink sink) {
        // Below assertion can happen if there is corrupt metadata
        // which should not happen in production code since reader and writer check table metadata
        assert chars > 0 && chars <= MAX_STRING_LENGTH;
        for (int i = chars - 1; i >= 0; --i) {
            sink.put(base32[(int) ((hash >> i * 5) & 0x1F)]);
        }
    }

    public static long bitmask(int count, int shift) {
        // e.g. 3, 4 -> 1110000
        return ((1L << count) - 1) << shift;
    }

    public static byte encodeChar(char c) {
        // element at index 11 is -1
        return base32Indexes[c > 47 && c < 123 ? c - 48 : 11];
    }

    public static long fromBitString(CharSequence bits, int start) throws NumericException {
        return fromBitString(bits, start, Math.min(bits.length(), ColumnType.GEO_HASH_MAX_BITS_LENGTH + start));
    }

    public static long fromBitStringNl(CharSequence bits, int start) throws NumericException {
        int len = bits.length();
        if (len - start <= 0) {
            return NULL;
        }
        return fromBitString(bits, start, Math.min(bits.length(), ColumnType.GEO_HASH_MAX_BITS_LENGTH + start));
    }

    public static long fromCoordinatesDeg(double lat, double lon, int bits) throws NumericException {
        if (lat < -90.0 || lat > 90.0) {
            throw NumericException.INSTANCE;
        }
        if (lon < -180.0 || lon > 180.0) {
            throw NumericException.INSTANCE;
        }
        if (bits < 0 || bits > ColumnType.GEO_HASH_MAX_BITS_LENGTH) {
            throw NumericException.INSTANCE;
        }
        return fromCoordinatesDegUnsafe(lat, lon, bits);
    }

    public static long fromCoordinatesDegUnsafe(double lat, double lon, int bits) {
        long latq = (long)Math.scalb((lat + 90.0) / 180.0, 32);
        long lngq = (long)Math.scalb((lon + 180.0) / 360.0, 32);
        return Numbers.interleaveBits(latq, lngq) >>> (64 - bits);
    }

    public static long fromString(CharSequence hash) throws NumericException {
        return fromString(hash, 0, hash.length());
    }

    public static long fromString(CharSequence hash, int start, int end) throws NumericException {
        // no bounds/length/nullity checks
        long geohash = 0;
        for (int i = start; i < end; ++i) {
            geohash = appendChar(geohash, hash.charAt(i));
        }
        return geohash;
    }

    public static long fromStringNl(CharSequence geohash, int start, int length) throws NumericException {
        if (length <= 0 || geohash == null || geohash.length() == 0) {
            return GeoHashes.NULL;
        }
        return fromString(geohash, start, start + Math.min(length, MAX_STRING_LENGTH));
    }

    public static void addNormalizedGeoPrefix(long hash, int prefixType, int columnType, final LongList prefixes) throws NumericException {
        final int bits = ColumnType.getGeoHashBits(prefixType);
        final int columnSize = ColumnType.sizeOf(columnType);
        final int columnBits = ColumnType.getGeoHashBits(columnType);

        if (hash == NULL || bits > columnBits) {
            throw NumericException.INSTANCE;
        }

        final int shift = columnBits - bits;
        long norm = hash << shift;
        long mask = GeoHashes.bitmask(bits, shift);
        mask |= 1L << (columnSize * 8 - 1); // set the most significant bit to ignore null from prefix matching

        prefixes.add(norm);
        prefixes.add(mask);
    }

    public static long fromStringTruncatingNl(CharSequence hash, int start, int end, int toBits) throws NumericException {
        if (start == end) {
            return NULL;
        }
        final int chars = Math.min(end - start, MAX_STRING_LENGTH);
        int fromBits = 5 * chars;
        if (fromBits < toBits) {
            throw NumericException.INSTANCE;
        }
        return ColumnType.truncateGeoHashBits(fromString(hash, start, start + chars), fromBits, toBits);
    }

    public static long fromStringTruncatingNl(long lo, long hi, int bits) throws NumericException {
        if (lo == hi) {
            return NULL;
        }
        final int chars = Math.min((int) (hi - lo), MAX_STRING_LENGTH);
        int actualBits = 5 * chars;
        if (actualBits < bits || bits == 0) {
            throw NumericException.INSTANCE;
        }
        long geohash = 0;
        for (long p = lo, limit = p + chars; p < limit; p++) {
            geohash = appendChar(geohash, (char) Unsafe.getUnsafe().getByte(p)); // base32
        }
        return ColumnType.truncateGeoHashBits(geohash, actualBits, bits);
    }

    public static long getGeoLong(int type, Function func, Record rec) {
        switch (ColumnType.tagOf(type)) {
            case ColumnType.GEOBYTE:
                return func.getGeoByte(rec);
            case ColumnType.GEOSHORT:
                return func.getGeoShort(rec);
            case ColumnType.GEOINT:
                return func.getGeoInt(rec);
            default:
                return func.getGeoLong(rec);
        }
    }

    public static boolean isValidBits(CharSequence tok, int start) {
        int idx;
        int len = tok.length();
        for (int i = start; i < len; i++) {
            idx = tok.charAt(i);
            if (idx < '0' || idx > '1') {
                return false;
            }
        }
        return start < len;
    }

    public static boolean isValidChars(CharSequence tok, int start) {
        char c;
        int len = tok.length();
        for (int i = start; i < len; i++) {
            c = tok.charAt(i);
            if (encodeChar(c) == -1) {
                return false;
            }
        }
        return start < len;
    }

    public static int getBitFlags(int columnType) {
        if (!ColumnType.isGeoHash(columnType)) {
            return 0;
        }
        final int bits = ColumnType.getGeoHashBits(columnType);
        if (bits > 0 && bits % 5 == 0) {
            // It's 5 bit per char. If it's integer number of chars value to be serialized as chars
            return -bits / 5;
        }
        // Value to be serialized as bit array.
        return bits;
    }

    private static long fromBitString(CharSequence bits, int start, int limit) throws NumericException {
        long result = 0;
        for (int i = start; i < limit; i++) {
            switch (bits.charAt(i)) {
                case '0':
                    result = result << 1;
                    break;
                case '1':
                    result = (result << 1) | 1;
                    break;
                default:
                    throw NumericException.INSTANCE;
            }
        }
        return result;
    }

    private static long appendChar(long hash, char c) throws NumericException {
        final byte idx = encodeChar(c);
        if (idx > -1) {
            return (hash << 5) | idx;
        }
        throw NumericException.INSTANCE;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy