<commit_before><!doctype html>
<!--
Canvas Vector Map

Demo of client-side map rendering, using GeoJSON vector tiles as the remote source,
and the HTML Canvas API as the rendering engine. Uses Leaflet for standard slippy map functionality.

<NAME>
-->
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Canvas vector map</title>
<style>

body {
    margin: 0px;
    border: 0px;
    padding: 0px;
}

.label {
    width: 200px;
    margin-left: -100px;
    position: absolute;
    z-index: 1;
    text-align: center;
}

.labelInner {
    padding: 2px;
    background: white;
    opacity: 0.75;
}

</style>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.css" />
</head>

<body>

<div id="map"></div>
<script src="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.js"></script>

<script>

var tiles = {};

// Makeshift style-sheet
var styles = {
    water: {
        color: {
            default: [25, 150, 220]
        }
    },
    land: {
        color: {
            default: [32, 200, 125]
        }
    },
    buildings: {
        color: {
            default: [150, 150, 150]
        },
        border: [75, 75, 75],
        // dash: {
        //     default: [4, 2]
        // }
    },
    roads: {
        color: {
            'highway': [0, 0, 0],
            'major_road': [100, 100, 100],
            'minor_road': [150, 150, 150],
            'path': [255, 255, 255],
            'rail': [0, 0, 0]
        },
        size: {
            'highway': 5,
            'major_road': 2.5,
            'minor_road': 1.5,
            'path': 0.5,
            'rail': 0.5
        },
        dash: {
            // 'rail': [4, 2]
        }
    }
};

// Point
function Point (x, y)
{
    return {
        x: x,
        y: y
    };
}

Point.copy = function (p)
{
    if (p == null) {
        return null;
    }
    return { x: p.x, y: p.y };
};

// Projection constants
var half_circumference_meters = 20037508.34;
var map_origin_meters = Point(-half_circumference_meters, half_circumference_meters);
var min_zoom_meters_per_pixel = 156543.0339;
var meters_per_pixel = [];
var max_zoom = 18;
for (var z=1; z <= max_zoom; z++) {
    meters_per_pixel[z] = min_zoom_meters_per_pixel / Math.pow(2, z);
}

// Convert tile location to mercator meters - multiply by pixels per tile, then by meters per pixel, adjust for map origin
var tile_size = Point(256, 256);
function metersForTile (tile)
{
    return Point(
        (tile.x * tile_size.x * meters_per_pixel[tile.z]) + map_origin_meters.x,
        ((tile.y * tile_size.y * meters_per_pixel[tile.z]) * -1) + map_origin_meters.y
    );
}

// Convert mercator meters to lat-lng
function metersToLatLng (meters)
{
    var c = Point.copy(meters);

    c.x /= half_circumference_meters;
    c.y /= half_circumference_meters;

    c.y = (2 * Math.atan(Math.exp(c.y * Math.PI)) - (Math.PI / 2)) / Math.PI;

    c.x *= 180;
    c.y *= 180;

    return c;
}

// Run a transform function on each cooordinate in a GeoJSON geometry
function transformGeometry (geometry, transform)
{
    if (geometry.type == 'Point') {
        return transform(geometry.coordinates);
    }
    else if (geometry.type == 'LineString' || geometry.type == 'MultiPoint') {
        return geometry.coordinates.map(transform);
    }
    else if (geometry.type == 'Polygon' || geometry.type == 'MultiLineString') {
        return geometry.coordinates.map(function (coordinates) {
            return coordinates.map(transform);
        });
    }
    else if (geometry.type == 'MultiPolygon') {
        return geometry.coordinates.map(function (polygon) {
            return polygon.map(function (coordinates) {
                return coordinates.map(transform);
            });
        });
    }
    // TODO: support GeometryCollection
    return {};
}

// Scale a GeoJSON coordinate (2-element array) from [min, max] to tile pixels
// returns a copy of geometry.coordinates transformed into Points
function scaleGeometryToPixels (geometry, min, max)
{
    return transformGeometry(geometry, function (coordinates) {
        return Point(
            Math.round((coordinates[0] - min.x) * tile_size.x / (max.x - min.x)), // rounding removes seams but causes aliasing
            Math.round((coordinates[1] - min.y) * tile_size.y / (max.y - min.y))
        );
    });
}

// Renders a line given as an array of Points
// line = [Point, Point, ...]
function renderLine (line, properties, style, context)
{
    var segments = line;
    var style = style || {};
    var color = (style.color && (style.color[properties.kind] || style.color.default)) || [255, 0, 0];
    var size = (style.size && (style.size[properties.kind] || style.size.default)) || 1;
    var dash = (style.dash && (style.dash[properties.kind] || style.dash.default));

    var c = context;
    c.beginPath();
    c.strokeStyle = 'rgb(' + color.join(',') + ')';
    c.lineCap = 'round';
    c.lineWidth = size;
    if (dash) {
        c.setLineDash(dash.map(function (d) { return d * size; }));
    }
    else {
        c.setLineDash([]);
    }

    for (var r=0; r < segments.length - 1; r ++) {
        var segment = [
            segments[r].x, segments[r].y,
            segments[r + 1].x, segments[r + 1].y
        ];

        c.moveTo(segment[0], segment[1]);
        c.lineTo(segment[2], segment[3]);
    };

    c.closePath();
    c.stroke();
}

// Renders a polygon given as an array of Points
// polygon = [Point, Point, ...]
function renderPolygon (polygon, properties, style, context)
{
    var segments = polygon;
    var color = (style.color && (style.color[properties.kind] || style.color.default)) || [255, 0, 0];
    var size = (style.size && (style.size[properties.kind] || style.size.default)) || 1;
    var dash = (style.dash && (style.dash[properties.kind] || style.dash.default));

    var c = context;
    c.beginPath();
    c.fillStyle = 'rgb(' + color.join(',') + ')';
    c.moveTo(segments[0].x, segments[0].y);

    for (var r=1; r < segments.length; r ++) {
        c.lineTo(segments[r].x, segments[r].y);
    };

    c.closePath();
    c.fill();

    // Border
    if (style.border) {
        c.strokeStyle = 'rgb(' + style.border.join(',') + ')';
        c.lineCap = 'round';
        c.lineWidth = size;
        if (dash) {
            c.setLineDash(dash.map(function (d) { return d * size; }));
        }
        else {
            c.setLineDash([]);
        }
        c.stroke();
    }
}

function renderGeometry (geometry, properties, style, context)
{
    var g;
    if (geometry.type == 'LineString') {
        renderLine(geometry.pixels, properties, style, context);
    }
    else if (geometry.type == 'MultiLineString') {
        for (g=0; g < geometry.pixels.length; g++) {
            renderLine(geometry.pixels[g], properties, style, context);
        }
    }
    else if (geometry.type == 'Polygon') {
        renderPolygon(geometry.pixels[0], properties, style, context); // TODO: support polygons w/holes
    }
    else if (geometry.type == 'MultiPolygon') {
        for (g=0; g < geometry.pixels.length; g++) {
            renderPolygon(geometry.pixels[g][0], properties, style, context); // TODO: support polygons w/holes
        }
    }
    // TODO: support Point, MultiPoint
}

// Generates a random color not yet present in the provided hash of colors
function generateColor (color_map)
{
    var r, g, b, key;
    color_map = color_map || {};
    while (true) {
        r = ~~(Math.random() * 256);
        g = ~~(Math.random() * 256);
        b = ~~(Math.random() * 256);
        key = (r + (g << 8) + (b << 16) + (255 << 24)) >>> 0; // need unsigned right shift to convert to positive #

        if (color_map[key] === undefined) {
            color_map[key] = { color: [r, g, b] };
            break;
        }
    }
    return color_map[key];
}

// Selection info shown on hover
var selection_info = document.createElement('div');
selection_info.setAttribute('class', 'label');
selection_info.style['z-index'] = 1;
selection_info.style.display = 'none';

// Render a GeoJSON tile onto canvas
function renderTile (tile, tile_min, tile_max, context)
{
    // Selection rendering - off-screen canvas to render a collision map for feature selection
    var selection = { colors: {} };
    var selection_canvas = document.createElement('canvas');
    selection_canvas.width = tile_size.x;
    selection_canvas.height = tile_size.y;
    var selection_context = selection_canvas.getContext('2d');
    var selection_color;
    var selection_count = 0;

    // Land
    var land = tile['land-usages'].features;
    land.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile_min, tile_max);

        if (feature.properties.name != null && feature.properties.name != '') {
            selection_color = generateColor(selection.colors);
            selection_color.properties = feature.properties;
            selection_count++;

            // Draw named land areas in a darker shade
            renderGeometry(feature.geometry, feature.properties, { color: { default: styles.land.color.default.map(function (c) { return Math.max(c - 30, 0); }) } }, context);
            renderGeometry(feature.geometry, feature.properties, { color: { default: selection_color.color } }, selection_context);
        }
        else {
            renderGeometry(feature.geometry, feature.properties, styles.land, context);
            renderGeometry(feature.geometry, feature.properties, { color: { default: [0, 0, 0] } }, selection_context);
        }
    });

    // Water
    var waters = tile['water-areas'].features;
    waters.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile_min, tile_max);
        renderGeometry(feature.geometry, feature.properties, styles.water, context);

        if (feature.properties.name != null && feature.properties.name != '') {
            selection_color = generateColor(selection.colors);
            selection_color.properties = feature.properties;
            selection_count++;
            renderGeometry(feature.geometry, feature.properties, { color: { default: selection_color.color } }, selection_context);
        }
        else {

            renderGeometry(feature.geometry, feature.properties, { color: { default: [0, 0, 0] } }, selection_context);
        }
    });

    // Roads
    var roads = tile['highroad'].features;
    roads.sort(function(a, b) { return (a.properties.sort_key > b.properties.sort_key); });
    roads.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile_min, tile_max);
        renderGeometry(feature.geometry, feature.properties, styles.roads, context);
    });

    // Road labels
    var road_labels = tile['skeletron'].features;
    road_labels.sort(function(a, b) { return (a.properties.sort_key > b.properties.sort_key); });
    road_labels.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile_min, tile_max);

        if (feature.properties.name != null && feature.properties.name != '') {
            selection_color = generateColor(selection.colors);
            selection_color.properties = feature.properties;
            selection_count++;

            renderGeometry(feature.geometry, feature.properties,
                {
                    color: { default: selection_color.color },
                    size: { default: 15 }
                },
                selection_context
            );
        }
    });

    // Buildings
    var buildings = tile['buildings'].features;
    buildings.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile_min, tile_max);
        renderGeometry(feature.geometry, feature.properties, styles.buildings, context);
    });

    // POIs
    var pois = tile['pois'].features;
    pois.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile_min, tile_max);

        if (feature.properties.name != null && feature.properties.name != '') {
            context.fillStyle = 'rgb(240, 0, 0)';
            context.strokeStyle = 'rgb(240, 240, 240)';
            context.lineWidth = 2;
            context.beginPath();
            context.arc(feature.geometry.pixels.x, feature.geometry.pixels.y, 5, 0, 2 * Math.PI);
            context.closePath();
            context.fill();
            context.stroke();

            selection_color = generateColor(selection.colors);
            selection_color.properties = feature.properties;
            selection_count++;

            selection_context.fillStyle = 'rgb(' + selection_color.color.join(',') + ')';
            selection_context.beginPath();
            selection_context.arc(feature.geometry.pixels.x, feature.geometry.pixels.y, 5, 0, 2 * Math.PI);
            selection_context.closePath();
            selection_context.fill();
        }
    });

    // Selection events
    if (selection_count > 0) {
        if (tiles[tile.key] == null) {
            tiles[tile.key] = {};
        }
        tiles[tile.key].selection = selection;

        selection.pixels = new Uint32Array(selection_context.getImageData(0, 0, selection_canvas.width, selection_canvas.height).data.buffer);

        context.canvas.onmousemove = function (event) {
            var off = event.offsetY * tile_size.x + event.offsetX;
            var color = selection.pixels[off];
            var feature = selection.colors[color];
            if (feature != null) {
                context.canvas.style.cursor = 'crosshair';
                selection_info.style.left = (parseInt(context.canvas.style.left) + event.offsetX + 5) + 'px';
                selection_info.style.top = (parseInt(context.canvas.style.top) + event.offsetY + 5) + 'px';
                selection_info.innerHTML = '<span class="labelInner">' + feature.properties.name + /*' [' + feature.properties.kind + ']*/'</span>';
                selection_info.style.display = 'block';
                context.canvas.parentNode.appendChild(selection_info);
            }
            else {
                context.canvas.style.cursor = null;
                selection_info.style.display = 'none';
                if (selection_info.parentNode == context.canvas.parentNode) {
                    context.canvas.parentNode.removeChild(selection_info);
                }
            }
        };
    }
    else {
        context.canvas.onmousemove = function (event) {
            context.canvas.style.cursor = null;
            selection_info.style.display = 'none';
            if (selection_info.parentNode == context.canvas.parentNode) {
                context.canvas.parentNode.removeChild(selection_info);
            }
        };
    }
}

// Leaflet map
function resizeMap () {
    document.getElementById('map').style.height = window.innerHeight + 'px';
}
window.onresize = resizeMap;
resizeMap();

var map = L.map('map');
map.attributionControl.setPrefix('');

// Get location from URL or use default
var map_default_location = [51.508, -0.105]; // [40.784702, -73.965452], [40.7180764, -73.9790335]
var map_default_zoom = 15;
var url_latlng = window.location.hash.slice(1, window.location.hash.length).split(',');
if (url_latlng.length == 3) {
    map.setView([url_latlng[0], url_latlng[1]], url_latlng[2]);
}
else {
    map.setView(map_default_location, map_default_zoom);
}

map.on('moveend', function () {
    var map_latlng = map.getCenter();
    window.location.hash = [map_latlng.lat, map_latlng.lng, map.getZoom()].join(',');
});

var layer = L.tileLayer.canvas({
    async: true,
    maxZoom: max_zoom,
    attribution: '<a href="https://github.com/bcamper/canvas-map">Source Code</a>'
});

layer.on('tileunload', function (event) {
    var canvas = event.tile;
    var key = canvas.getAttribute('data-tile-key');
    delete tiles[key];
});

layer.drawTile = function(canvas, tilePoint, zoom) {
    tile_key = [tilePoint.x, tilePoint.y, zoom].join('/');
    var context = canvas.getContext('2d');
    if (!context.setLineDash) {
        context.setLineDash = function () {};
    }
    canvas.style.background = 'rgb(200, 200, 200)';

    // Tile info for debugging
    canvas.setAttribute('data-tile-key', tile_key);

    // Calculate tile bounds
    // NB: This is a huge/fast hack since we only do projection at the tile corners and then linearly interpolate the main
    //     geometry to pixels within the tile. This isn't really visible at high zoom levels, but causes serious warping at lower zooms.
    var tile_min = metersToLatLng(metersForTile({ x: tilePoint.x, y: tilePoint.y, z: zoom }));
    var tile_max = metersToLatLng(metersForTile({ x: tilePoint.x + 1, y: tilePoint.y + 1, z: zoom }));

    // Load tile
    var tile_url = 'http://tile.openstreetmap.us/vectiles-all/' + zoom + '/' + tilePoint.x + '/' + tilePoint.y + '.json';
    var req = new XMLHttpRequest();
    req.onload = function () {
        var tile = JSON.parse(req.response);
        tile.key = tile_key;
        renderTile(tile, tile_min, tile_max, context);
        layer.tileDrawn(canvas);
    };
    req.open('GET', tile_url, true /* async flag */);
    req.send();
}

layer.addTo(map);
<commit_msg>make tile_min and tile_max properties of tile<commit_after><!doctype html>
<!--
Canvas Vector Map

Demo of client-side map rendering, using GeoJSON vector tiles as the remote source,
and the HTML Canvas API as the rendering engine. Uses Leaflet for standard slippy map functionality.

<NAME>
-->
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Canvas vector map</title>
<style>

body {
    margin: 0px;
    border: 0px;
    padding: 0px;
}

.label {
    width: 200px;
    margin-left: -100px;
    position: absolute;
    z-index: 1;
    text-align: center;
}

.labelInner {
    padding: 2px;
    background: white;
    opacity: 0.75;
}

</style>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.css" />
</head>

<body>

<div id="map"></div>
<script src="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.js"></script>

<script>

var tiles = {};

// Makeshift style-sheet
var styles = {
    water: {
        color: {
            default: [25, 150, 220]
        }
    },
    land: {
        color: {
            default: [32, 200, 125]
        }
    },
    buildings: {
        color: {
            default: [150, 150, 150]
        },
        border: [75, 75, 75],
        // dash: {
        //     default: [4, 2]
        // }
    },
    roads: {
        color: {
            'highway': [0, 0, 0],
            'major_road': [100, 100, 100],
            'minor_road': [150, 150, 150],
            'path': [255, 255, 255],
            'rail': [0, 0, 0]
        },
        size: {
            'highway': 5,
            'major_road': 2.5,
            'minor_road': 1.5,
            'path': 0.5,
            'rail': 0.5
        },
        dash: {
            // 'rail': [4, 2]
        }
    }
};

// Point
function Point (x, y)
{
    return {
        x: x,
        y: y
    };
}

Point.copy = function (p)
{
    if (p == null) {
        return null;
    }
    return { x: p.x, y: p.y };
};

// Projection constants
var tile_size = Point(256, 256);
var half_circumference_meters = 20037508.34;
var map_origin_meters = Point(-half_circumference_meters, half_circumference_meters);
var min_zoom_meters_per_pixel = 156543.0339;
var meters_per_pixel = [];
var max_zoom = 18;
for (var z=1; z <= max_zoom; z++) {
    meters_per_pixel[z] = min_zoom_meters_per_pixel / Math.pow(2, z);
}

// Convert tile location to mercator meters - multiply by pixels per tile, then by meters per pixel, adjust for map origin
function metersForTile (tile)
{
    return Point(
        (tile.x * tile_size.x * meters_per_pixel[tile.z]) + map_origin_meters.x,
        ((tile.y * tile_size.y * meters_per_pixel[tile.z]) * -1) + map_origin_meters.y
    );
}

// Convert mercator meters to lat-lng
function metersToLatLng (meters)
{
    var c = Point.copy(meters);

    c.x /= half_circumference_meters;
    c.y /= half_circumference_meters;

    c.y = (2 * Math.atan(Math.exp(c.y * Math.PI)) - (Math.PI / 2)) / Math.PI;

    c.x *= 180;
    c.y *= 180;

    return c;
}

// Run a transform function on each cooordinate in a GeoJSON geometry
function transformGeometry (geometry, transform)
{
    if (geometry.type == 'Point') {
        return transform(geometry.coordinates);
    }
    else if (geometry.type == 'LineString' || geometry.type == 'MultiPoint') {
        return geometry.coordinates.map(transform);
    }
    else if (geometry.type == 'Polygon' || geometry.type == 'MultiLineString') {
        return geometry.coordinates.map(function (coordinates) {
            return coordinates.map(transform);
        });
    }
    else if (geometry.type == 'MultiPolygon') {
        return geometry.coordinates.map(function (polygon) {
            return polygon.map(function (coordinates) {
                return coordinates.map(transform);
            });
        });
    }
    // TODO: support GeometryCollection
    return {};
}

// Scale a GeoJSON coordinate (2-element array) from [min, max] to tile pixels
// returns a copy of geometry.coordinates transformed into Points
function scaleGeometryToPixels (geometry, min, max)
{
    return transformGeometry(geometry, function (coordinates) {
        return Point(
            Math.round((coordinates[0] - min.x) * tile_size.x / (max.x - min.x)), // rounding removes seams but causes aliasing
            Math.round((coordinates[1] - min.y) * tile_size.y / (max.y - min.y))
        );
    });
}

// Renders a line given as an array of Points
// line = [Point, Point, ...]
function renderLine (line, properties, style, context)
{
    var segments = line;
    var style = style || {};
    var color = (style.color && (style.color[properties.kind] || style.color.default)) || [255, 0, 0];
    var size = (style.size && (style.size[properties.kind] || style.size.default)) || 1;
    var dash = (style.dash && (style.dash[properties.kind] || style.dash.default));

    var c = context;
    c.beginPath();
    c.strokeStyle = 'rgb(' + color.join(',') + ')';
    c.lineCap = 'round';
    c.lineWidth = size;
    if (dash) {
        c.setLineDash(dash.map(function (d) { return d * size; }));
    }
    else {
        c.setLineDash([]);
    }

    for (var r=0; r < segments.length - 1; r ++) {
        var segment = [
            segments[r].x, segments[r].y,
            segments[r + 1].x, segments[r + 1].y
        ];

        c.moveTo(segment[0], segment[1]);
        c.lineTo(segment[2], segment[3]);
    };

    c.closePath();
    c.stroke();
}

// Renders a polygon given as an array of Points
// polygon = [Point, Point, ...]
function renderPolygon (polygon, properties, style, context)
{
    var segments = polygon;
    var color = (style.color && (style.color[properties.kind] || style.color.default)) || [255, 0, 0];
    var size = (style.size && (style.size[properties.kind] || style.size.default)) || 1;
    var dash = (style.dash && (style.dash[properties.kind] || style.dash.default));

    var c = context;
    c.beginPath();
    c.fillStyle = 'rgb(' + color.join(',') + ')';
    c.moveTo(segments[0].x, segments[0].y);

    for (var r=1; r < segments.length; r ++) {
        c.lineTo(segments[r].x, segments[r].y);
    };

    c.closePath();
    c.fill();

    // Border
    if (style.border) {
        c.strokeStyle = 'rgb(' + style.border.join(',') + ')';
        c.lineCap = 'round';
        c.lineWidth = size;
        if (dash) {
            c.setLineDash(dash.map(function (d) { return d * size; }));
        }
        else {
            c.setLineDash([]);
        }
        c.stroke();
    }
}

function renderGeometry (geometry, properties, style, context)
{
    var g;
    if (geometry.type == 'LineString') {
        renderLine(geometry.pixels, properties, style, context);
    }
    else if (geometry.type == 'MultiLineString') {
        for (g=0; g < geometry.pixels.length; g++) {
            renderLine(geometry.pixels[g], properties, style, context);
        }
    }
    else if (geometry.type == 'Polygon') {
        renderPolygon(geometry.pixels[0], properties, style, context); // TODO: support polygons w/holes
    }
    else if (geometry.type == 'MultiPolygon') {
        for (g=0; g < geometry.pixels.length; g++) {
            renderPolygon(geometry.pixels[g][0], properties, style, context); // TODO: support polygons w/holes
        }
    }
    // TODO: support Point, MultiPoint
}

// Generates a random color not yet present in the provided hash of colors
function generateColor (color_map)
{
    var r, g, b, key;
    color_map = color_map || {};
    while (true) {
        r = ~~(Math.random() * 256);
        g = ~~(Math.random() * 256);
        b = ~~(Math.random() * 256);
        key = (r + (g << 8) + (b << 16) + (255 << 24)) >>> 0; // need unsigned right shift to convert to positive #

        if (color_map[key] === undefined) {
            color_map[key] = { color: [r, g, b] };
            break;
        }
    }
    return color_map[key];
}

// Selection info shown on hover
var selection_info = document.createElement('div');
selection_info.setAttribute('class', 'label');
selection_info.style['z-index'] = 1;
selection_info.style.display = 'none';

// Render a GeoJSON tile onto canvas
function renderTile (tile, context)
{
    // Selection rendering - off-screen canvas to render a collision map for feature selection
    var selection = { colors: {} };
    var selection_canvas = document.createElement('canvas');
    selection_canvas.width = tile_size.x;
    selection_canvas.height = tile_size.y;
    var selection_context = selection_canvas.getContext('2d');
    var selection_color;
    var selection_count = 0;

    // Land
    var land = tile['land-usages'].features;
    land.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile.min, tile.max);

        if (feature.properties.name != null && feature.properties.name != '') {
            selection_color = generateColor(selection.colors);
            selection_color.properties = feature.properties;
            selection_count++;

            // Draw named land areas in a darker shade
            renderGeometry(feature.geometry, feature.properties, { color: { default: styles.land.color.default.map(function (c) { return Math.max(c - 30, 0); }) } }, context);
            renderGeometry(feature.geometry, feature.properties, { color: { default: selection_color.color } }, selection_context);
        }
        else {
            renderGeometry(feature.geometry, feature.properties, styles.land, context);
            renderGeometry(feature.geometry, feature.properties, { color: { default: [0, 0, 0] } }, selection_context);
        }
    });

    // Water
    var waters = tile['water-areas'].features;
    waters.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile.min, tile.max);
        renderGeometry(feature.geometry, feature.properties, styles.water, context);

        if (feature.properties.name != null && feature.properties.name != '') {
            selection_color = generateColor(selection.colors);
            selection_color.properties = feature.properties;
            selection_count++;
            renderGeometry(feature.geometry, feature.properties, { color: { default: selection_color.color } }, selection_context);
        }
        else {

            renderGeometry(feature.geometry, feature.properties, { color: { default: [0, 0, 0] } }, selection_context);
        }
    });

    // Roads
    var roads = tile['highroad'].features;
    roads.sort(function(a, b) { return (a.properties.sort_key > b.properties.sort_key); });
    roads.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile.min, tile.max);
        renderGeometry(feature.geometry, feature.properties, styles.roads, context);
    });

    // Road labels
    var road_labels = tile['skeletron'].features;
    road_labels.sort(function(a, b) { return (a.properties.sort_key > b.properties.sort_key); });
    road_labels.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile.min, tile.max);

        if (feature.properties.name != null && feature.properties.name != '') {
            selection_color = generateColor(selection.colors);
            selection_color.properties = feature.properties;
            selection_count++;

            renderGeometry(feature.geometry, feature.properties,
                {
                    color: { default: selection_color.color },
                    size: { default: 15 }
                },
                selection_context
            );
        }
    });

    // Buildings
    var buildings = tile['buildings'].features;
    buildings.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile.min, tile.max);
        renderGeometry(feature.geometry, feature.properties, styles.buildings, context);
    });

    // POIs
    var pois = tile['pois'].features;
    pois.forEach(function(feature) {
        feature.geometry.pixels = scaleGeometryToPixels(feature.geometry, tile.min, tile.max);

        if (feature.properties.name != null && feature.properties.name != '') {
            context.fillStyle = 'rgb(240, 0, 0)';
            context.strokeStyle = 'rgb(240, 240, 240)';
            context.lineWidth = 2;
            context.beginPath();
            context.arc(feature.geometry.pixels.x, feature.geometry.pixels.y, 5, 0, 2 * Math.PI);
            context.closePath();
            context.fill();
            context.stroke();

            selection_color = generateColor(selection.colors);
            selection_color.properties = feature.properties;
            selection_count++;

            selection_context.fillStyle = 'rgb(' + selection_color.color.join(',') + ')';
            selection_context.beginPath();
            selection_context.arc(feature.geometry.pixels.x, feature.geometry.pixels.y, 5, 0, 2 * Math.PI);
            selection_context.closePath();
            selection_context.fill();
        }
    });

    // Selection events
    if (selection_count > 0) {
        if (tiles[tile.key] == null) {
            tiles[tile.key] = {};
        }
        tiles[tile.key].selection = selection;

        selection.pixels = new Uint32Array(selection_context.getImageData(0, 0, selection_canvas.width, selection_canvas.height).data.buffer);

        context.canvas.onmousemove = function (event) {
            var off = event.offsetY * tile_size.x + event.offsetX;
            var color = selection.pixels[off];
            var feature = selection.colors[color];
            if (feature != null) {
                context.canvas.style.cursor = 'crosshair';
                selection_info.style.left = (parseInt(context.canvas.style.left) + event.offsetX + 5) + 'px';
                selection_info.style.top = (parseInt(context.canvas.style.top) + event.offsetY + 5) + 'px';
                selection_info.innerHTML = '<span class="labelInner">' + feature.properties.name + /*' [' + feature.properties.kind + ']*/'</span>';
                selection_info.style.display = 'block';
                context.canvas.parentNode.appendChild(selection_info);
            }
            else {
                context.canvas.style.cursor = null;
                selection_info.style.display = 'none';
                if (selection_info.parentNode == context.canvas.parentNode) {
                    context.canvas.parentNode.removeChild(selection_info);
                }
            }
        };
    }
    else {
        context.canvas.onmousemove = function (event) {
            context.canvas.style.cursor = null;
            selection_info.style.display = 'none';
            if (selection_info.parentNode == context.canvas.parentNode) {
                context.canvas.parentNode.removeChild(selection_info);
            }
        };
    }
}

// Leaflet map
function resizeMap () {
    document.getElementById('map').style.height = window.innerHeight + 'px';
}
window.onresize = resizeMap;
resizeMap();

var map = L.map('map');
map.attributionControl.setPrefix('');

// Get location from URL or use default
var map_default_location = [51.508, -0.105]; // [40.784702, -73.965452], [40.7180764, -73.9790335]
var map_default_zoom = 15;
var url_latlng = window.location.hash.slice(1, window.location.hash.length).split(',');
if (url_latlng.length == 3) {
    map.setView([url_latlng[0], url_latlng[1]], url_latlng[2]);
}
else {
    map.setView(map_default_location, map_default_zoom);
}

map.on('moveend', function () {
    var map_latlng = map.getCenter();
    window.location.hash = [map_latlng.lat, map_latlng.lng, map.getZoom()].join(',');
});

var layer = L.tileLayer.canvas({
    async: true,
    maxZoom: max_zoom,
    attribution: '<a href="https://github.com/bcamper/canvas-map">Source Code</a>'
});

layer.on('tileunload', function (event) {
    var canvas = event.tile;
    var key = canvas.getAttribute('data-tile-key');
    delete tiles[key];
});

layer.drawTile = function(canvas, tilePoint, zoom) {
    var context = canvas.getContext('2d');
    if (!context.setLineDash) {
        context.setLineDash = function () {};
    }
    canvas.style.background = 'rgb(200, 200, 200)';

    // Load tile
    var tile_url = 'http://tile.openstreetmap.us/vectiles-all/' + zoom + '/' + tilePoint.x + '/' + tilePoint.y + '.json';
    var req = new XMLHttpRequest();
    req.onload = function () {
        var tile = JSON.parse(req.response);
        tile.key = [tilePoint.x, tilePoint.y, zoom].join('/');
        canvas.setAttribute('data-tile-key', tile.key); // tile info for debugging

        // Calculate tile bounds
        // NB: This is a huge/fast hack since we only do projection at the tile corners and then linearly interpolate the main
        //     geometry to pixels within the tile. This isn't really visible at high zoom levels, but causes serious warping at lower zooms.
        tile.min = metersToLatLng(metersForTile({ x: tilePoint.x, y: tilePoint.y, z: zoom }));
        tile.max = metersToLatLng(metersForTile({ x: tilePoint.x + 1, y: tilePoint.y + 1, z: zoom }));

        renderTile(tile, context);
        layer.tileDrawn(canvas);
    };
    req.open('GET', tile_url, true /* async flag */);
    req.send();
}

layer.addTo(map);

{'fraction_non_alphanumeric': 0.1455475000722105, 'fraction_numerical': 0.016752837872967274, 'mean_word_length': 2.4841501459192914, 'pattern_counts': {'":': 0, '<': 71, '<?xml version=': 0, '>': 61, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6216502', 'n_tokens_mistral': 11096, 'n_tokens_neox': 10543, 'n_words': 2386}