PoC
This commit is contained in:
commit
8b20c84638
20
db.sql
Normal file
20
db.sql
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
create extension fuzzystrmatch;
|
||||||
|
|
||||||
|
CREATE TABLE trees (
|
||||||
|
id text primary key,
|
||||||
|
coord point,
|
||||||
|
score float,
|
||||||
|
botanic text,
|
||||||
|
botanic_pfaf text,
|
||||||
|
german text,
|
||||||
|
planted date
|
||||||
|
);
|
||||||
|
CREATE INDEX trees_coord ON trees USING GIST (coord);
|
||||||
|
|
||||||
|
CREATE TABLE areas (
|
||||||
|
coords polygon,
|
||||||
|
src text,
|
||||||
|
attrs jsonb
|
||||||
|
);
|
||||||
|
CREATE INDEX areas_coords ON areas USING GIST (coords);
|
||||||
|
CREATE INDEX areas_box_coords ON areas USING GIST (box(coords));
|
46
default.nix
Normal file
46
default.nix
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
|
rec {
|
||||||
|
geojsons = {
|
||||||
|
# https://opendata.dresden.de/informationsportal/#app/mainpage//Stadtb%C3%A4ume
|
||||||
|
trees = "https://kommisdd.dresden.de/net4/public/ogcapi/collections/L1261/items?limit=10000000";
|
||||||
|
# Natürliche Funktionen des Bodens: https://kommisdd.dresden.de/net4/public/ogcapi/collections/L859
|
||||||
|
boden_fkt = "https://kommisdd.dresden.de/net4/public/ogcapi/collections/L859/items?limit=10000000";
|
||||||
|
# Gebietstypen des natürlichen Wasserhaushaltes: https://kommisdd.dresden.de/net4/public/ogcapi/collections/L1106
|
||||||
|
wasserhaushalt = "https://kommisdd.dresden.de/net4/public/ogcapi/collections/L1106/items?limit=10000000";
|
||||||
|
# Wasserspeichervermögen: https://kommisdd.dresden.de/net4/public/ogcapi/collections/L849
|
||||||
|
wasserspeicher = "https://kommisdd.dresden.de/net4/public/ogcapi/collections/L849/items?limit=10000000";
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# im Grundwasser
|
||||||
|
# UmweltWasser
|
||||||
|
# Gebietstypen des natürlichen Wasserhaushaltes
|
||||||
|
# UmweltWasser
|
||||||
|
# Grundwasser - aktuelle Messwerte
|
||||||
|
|
||||||
|
# Plants For A Future
|
||||||
|
pfaf_sql = pkgs.requireFile {
|
||||||
|
name = "PlantsForAFuture.sql";
|
||||||
|
sha256 = "1wya1yb3hyybr71znmag7kq0jdc1cwazaqfp375jbfmaql3r92mq";
|
||||||
|
message = "PlantsForAFuture.sql, compatible with PostgreSQL";
|
||||||
|
};
|
||||||
|
|
||||||
|
init-database = with pkgs; writeShellScript "populate-database" ''
|
||||||
|
set -e
|
||||||
|
|
||||||
|
${lib.concatMapStrings (name: ''
|
||||||
|
F=${name}.geojson
|
||||||
|
[ -e $F ] || ${wget}/bin/wget -O $F "${geojsons.${name}}"
|
||||||
|
ls -l $F
|
||||||
|
# ${gdal}/bin/ogr2ogr -f "PostgreSQL" PG:"dbname=treeadvisor user=treeadvisor" -nln ${name} $F
|
||||||
|
'') (builtins.attrNames geojsons)}
|
||||||
|
|
||||||
|
${postgresql}/bin/psql -f ${./db.sql} treeadvisor
|
||||||
|
${deno}/bin/deno run --allow-read --allow-net ${./import_geojson.ts} GEOENV boden_fkt.geojson wasserhaushalt.geojson wasserspeicher.geojson
|
||||||
|
${deno}/bin/deno run --allow-read --allow-net ${./import_geojson.ts} TREES trees.geojson
|
||||||
|
|
||||||
|
${postgresql}/bin/psql -f ${pfaf_sql} treeadvisor
|
||||||
|
'';
|
||||||
|
}
|
174
import_geojson.ts
Normal file
174
import_geojson.ts
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
import { Pool } from "https://deno.land/x/postgres/mod.ts";
|
||||||
|
|
||||||
|
const GEBIETSTYPES: any = {
|
||||||
|
"Gewässer": 1,
|
||||||
|
"versickerungsdominiert": 1,
|
||||||
|
"verdunstungs- und versickerungsbestimmt": 0.8,
|
||||||
|
"verdunstungsdominiert": 0.5,
|
||||||
|
"verdunstungs- und abflussbestimmt": 0.2,
|
||||||
|
"abflussdominiert": 0.1,
|
||||||
|
"ausgewogen": 0.8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MODE = Deno.args[0];
|
||||||
|
if (!MODE) {
|
||||||
|
throw "No MODE arg";
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbPool = new Pool({
|
||||||
|
user: "treeadvisor",
|
||||||
|
password: "123",
|
||||||
|
database: "treeadvisor",
|
||||||
|
|
||||||
|
hostname: "10.233.1.2",
|
||||||
|
}, 12);
|
||||||
|
async function withDb(f: any): Promise<any> {
|
||||||
|
const client = await dbPool.connect();
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await f(client);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertGeoenv = async (src: string, coords: any, attrs: any) => {
|
||||||
|
const coords_s = "(" + coords.map((coords: any) => `(${coords[0]},${coords[1]})`).join(",") + ")";
|
||||||
|
const attrs_s = JSON.stringify(attrs);
|
||||||
|
await withDb(async (db: any) => db.queryObject`INSERT INTO areas (src, coords, attrs) VALUES (${src}, ${coords_s}::polygon, ${attrs_s})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
let pfafCache: any = {};
|
||||||
|
const insertTree = async (coords: any, props: any) => { await withDb(async (db: any) => {
|
||||||
|
const coords_s = `(${coords[0]},${coords[1]})`;
|
||||||
|
let botanic = props.art_botanisch
|
||||||
|
.replace(/,\s*/g, ", ")
|
||||||
|
.replace(/\.\s+/g, ". ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.replace(/'/g, "\"")
|
||||||
|
.replace(/\s+$/, "");
|
||||||
|
const german = props.art_deutsch && props.art_deutsch
|
||||||
|
.replace(/\s+-\s+/, "-")
|
||||||
|
.replace(/\.\s+/g, ". ")
|
||||||
|
.replace(/\s+x\s+/g, " × ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.replace(/'/g, "\"")
|
||||||
|
.replace(/\s+$/, "");
|
||||||
|
|
||||||
|
const age = parseInt(props.jalter, 10);
|
||||||
|
let planted = null;
|
||||||
|
if (age > 0) {
|
||||||
|
const m = props.aend_dat.match(/^(\d+)\.(\d+)\.(\d+) /);
|
||||||
|
if (!m) throw `Invalid date: ${props.aend_dat}`;
|
||||||
|
const y = parseInt(m[3], 10) - age;
|
||||||
|
planted = `${y}-${m[2]}-${m[1]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let botanic_pfaf;
|
||||||
|
if (botanic == "Baumart noch nicht bestimmt" || botanic == "unbekannt" || !botanic) {
|
||||||
|
botanic = null;
|
||||||
|
} else if (!pfafCache[botanic]) {
|
||||||
|
const res: any = await db.queryObject`SELECT "Latin name" as botanic FROM "PlantsForAFuture" WHERE "Latin name" ILIKE CONCAT(${botanic.slice(0, 3)}::text, '%') ORDER BY levenshtein("Latin name", ${botanic}) ASC LIMIT 1`;
|
||||||
|
if (res.rows[0]) {
|
||||||
|
botanic_pfaf = pfafCache[botanic] = res.rows[0].botanic;
|
||||||
|
if (botanic != botanic_pfaf) console.log(botanic + " = " + botanic_pfaf);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
botanic_pfaf = pfafCache[botanic];
|
||||||
|
}
|
||||||
|
|
||||||
|
let areaSrcSeen: any = {};
|
||||||
|
let areaInfo: any = {};
|
||||||
|
let res = await db.queryObject`SELECT src, attrs FROM areas WHERE coords @> $1::point ORDER BY coords <-> ${coords_s}::point ASC`;
|
||||||
|
res.rows.forEach(function(area: any) {
|
||||||
|
if (areaSrcSeen[area.src]) return;
|
||||||
|
|
||||||
|
Object.keys(area.attrs).forEach(function(k) {
|
||||||
|
areaInfo[k] = area.attrs[k];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let scores = [];
|
||||||
|
if (areaInfo.hasOwnProperty("versiegelung")) scores.push(1 - areaInfo.versiegelung);
|
||||||
|
if (areaInfo.hasOwnProperty("schutt")) scores.push(1 - areaInfo.schutt);
|
||||||
|
if (areaInfo.bodenqualitaet) scores.push((areaInfo.bodenqualitaet - 1) / 5);
|
||||||
|
if (areaInfo.wasserspeicher && areaInfo.wasserspeicher > 10) scores.push((50 - areaInfo.wasserspeicher) / 40)
|
||||||
|
else if (areaInfo.wasserspeicher) scores.push((5 - areaInfo.wasserspeicher) / 4);
|
||||||
|
if (areaInfo.gebietstyp) scores.push(GEBIETSTYPES[areaInfo.gebietstyp]);
|
||||||
|
let score: number | null = 0;
|
||||||
|
for(const s of scores) {
|
||||||
|
score += s;
|
||||||
|
}
|
||||||
|
if (scores.length > 0) {
|
||||||
|
score /= scores.length;
|
||||||
|
score = Math.max(0, Math.min(1, score));
|
||||||
|
} else {
|
||||||
|
score = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.queryObject`INSERT INTO trees (id, coord, score, botanic, botanic_pfaf, german, planted) VALUES (${props.id}, ${coords_s}, ${score}, ${botanic}, ${botanic_pfaf}, ${german}, ${planted})`;
|
||||||
|
}) };
|
||||||
|
|
||||||
|
const importFeatures = async (src: string, geojson: any) => {
|
||||||
|
if (geojson.type != "FeatureCollection") {
|
||||||
|
console.warn("Not a FeatureCollection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// var progress = 0, i = 0;
|
||||||
|
let promises = [];
|
||||||
|
for(const feature of geojson.features) { promises.push((async () => {
|
||||||
|
if (MODE == "GEOENV") {
|
||||||
|
if (feature.type == "Feature" &&
|
||||||
|
feature.geometry.type == "Polygon" &&
|
||||||
|
feature.geometry.crs.properties.name == "urn:ogc:def:crs:EPSG::4326") {
|
||||||
|
|
||||||
|
for(const coords of feature.geometry.coordinates) {
|
||||||
|
await insertGeoenv(src, coords, feature.properties);
|
||||||
|
}
|
||||||
|
// await insertGeoenv(feature.geometry.coordinates, feature.properties);
|
||||||
|
} else if (feature.type == "FeatureCollection") {
|
||||||
|
await importFeatures(src, feature);
|
||||||
|
} else if (feature.type == "Feature" &&
|
||||||
|
feature.geometry.type == "MultiPolygon" &&
|
||||||
|
feature.geometry.crs.properties.name == "urn:ogc:def:crs:EPSG::4326") {
|
||||||
|
|
||||||
|
for(const coordinates of feature.geometry.coordinates) {
|
||||||
|
for(const coords of coordinates) {
|
||||||
|
await insertGeoenv(src, coords, feature.properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Not recognized:", feature);
|
||||||
|
// console.warn(`Not recognized: ${JSON.stringify(geojson)}`);
|
||||||
|
}
|
||||||
|
} else if (MODE == "TREES") {
|
||||||
|
if (feature.type == "Feature" &&
|
||||||
|
feature.geometry.type == "Point" &&
|
||||||
|
feature.geometry.crs.properties.name == "urn:ogc:def:crs:EPSG::4326") {
|
||||||
|
|
||||||
|
await insertTree(feature.geometry.coordinates, feature.properties);
|
||||||
|
} else {
|
||||||
|
console.warn("Not recognized:", feature);
|
||||||
|
// console.warn(`Not recognized: ${JSON.stringify(geojson)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// i += 1;
|
||||||
|
// var newProgress = Math.floor(100 * i / geojson.features.length);
|
||||||
|
// if (newProgress != progress) {
|
||||||
|
// progress = newProgress;
|
||||||
|
// console.log(progress + "%");
|
||||||
|
// }
|
||||||
|
})()); }
|
||||||
|
|
||||||
|
console.log("wait for " + promises.length + " promises");
|
||||||
|
await Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
|
for(const filename of Deno.args.slice(1)) {
|
||||||
|
console.log(`Load ${filename}`)
|
||||||
|
const geojson = JSON.parse(await Deno.readTextFile(filename));
|
||||||
|
|
||||||
|
await importFeatures(filename, geojson);
|
||||||
|
}
|
54
nixos-module.nix
Normal file
54
nixos-module.nix
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{ pkgs, config, lib, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = {
|
||||||
|
networking.hostName = "treeadvisor";
|
||||||
|
|
||||||
|
users.users.treeadvisor = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "treeadvisor";
|
||||||
|
};
|
||||||
|
users.groups.treeadvisor = {};
|
||||||
|
|
||||||
|
services.postgresql = {
|
||||||
|
enable = true;
|
||||||
|
extraPlugins = with pkgs; [ postgis ];
|
||||||
|
# TODO: tmp
|
||||||
|
enableTCPIP = true;
|
||||||
|
authentication = ''
|
||||||
|
host all all 0.0.0.0/0 md5
|
||||||
|
'';
|
||||||
|
|
||||||
|
ensureDatabases = [ "treeadvisor" ];
|
||||||
|
ensureUsers = [ {
|
||||||
|
name = "treeadvisor";
|
||||||
|
ensurePermissions = {
|
||||||
|
"DATABASE treeadvisor" = "ALL PRIVILEGES";
|
||||||
|
};
|
||||||
|
} ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/treeadvisor 0755 treeadvisor treeadvisor -"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.treeadvisor-db-init = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
unitConfig.ConditionPathExists = [ "!/var/lib/treeadvisor/.db-init" ];
|
||||||
|
serviceConfig = {
|
||||||
|
User = "treeadvisor";
|
||||||
|
Group = "treeadvisor";
|
||||||
|
};
|
||||||
|
environment.HOME = "/tmp";
|
||||||
|
script = ''
|
||||||
|
cd /var/lib/treeadvisor
|
||||||
|
|
||||||
|
${(import ./. { inherit pkgs; }).init-database}
|
||||||
|
|
||||||
|
touch /var/lib/treeadvisor/.db-init
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
18
server/Cargo.toml
Normal file
18
server/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "treeadvisor-server"
|
||||||
|
version = "0.0.0"
|
||||||
|
authors = ["Astro <astro@spaceboyz.net>"]
|
||||||
|
edition = "2018"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gotham = "0.5"
|
||||||
|
gotham_derive = "0.5"
|
||||||
|
postgres = { version = "0.19", features = ["with-geo-types-0_7", "with-serde_json-1", "with-chrono-0_4"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
mime = "0.3"
|
||||||
|
http = "0.2"
|
||||||
|
geo = "0.18"
|
||||||
|
cairo-rs = { version = "0.14", features = ["png"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
162
server/src/area.rs
Normal file
162
server/src/area.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use gotham::{
|
||||||
|
helpers::http::response::create_response,
|
||||||
|
hyper::{Body, Response},
|
||||||
|
state::{FromState, State},
|
||||||
|
};
|
||||||
|
use geo::{Rect, LineString};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use mime::{APPLICATION_JSON, IMAGE_PNG};
|
||||||
|
use http::StatusCode;
|
||||||
|
use cairo::{ImageSurface, Context};
|
||||||
|
use crate::{AppState, AreaExtractor, PointExtractor};
|
||||||
|
|
||||||
|
|
||||||
|
pub fn get_details(state: State) -> (State, Response<Body>) {
|
||||||
|
let pe = PointExtractor::borrow_from(&state);
|
||||||
|
let app_state = AppState::borrow_from(&state);
|
||||||
|
let mut seen_srcs = HashSet::new();
|
||||||
|
let result = {
|
||||||
|
let mut db = app_state.db.lock().unwrap();
|
||||||
|
db.query("SELECT src, attrs FROM areas WHERE coords @> $1::point ORDER BY coords <-> $1::point ASC", &[
|
||||||
|
&pe.to_point()
|
||||||
|
]).unwrap()
|
||||||
|
}.into_iter().filter(|row| {
|
||||||
|
let src: &str = row.get(0);
|
||||||
|
if seen_srcs.contains(src) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
seen_srcs.insert(src.to_owned());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}).map(|row| {
|
||||||
|
let attrs: serde_json::Value = row.get(1);
|
||||||
|
attrs
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let body = serde_json::to_string(&serde_json::Value::Array(result)).unwrap();
|
||||||
|
let res = create_response(&state, StatusCode::OK, APPLICATION_JSON, body);
|
||||||
|
(state, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_heatmap(state: State) -> (State, Response<Body>) {
|
||||||
|
let pe = AreaExtractor::borrow_from(&state);
|
||||||
|
let app_state = AppState::borrow_from(&state);
|
||||||
|
let result = {
|
||||||
|
let mut db = app_state.db.lock().unwrap();
|
||||||
|
db.query("SELECT path(coords), src, attrs FROM areas WHERE box(coords) ?# $1::box", &[
|
||||||
|
&pe.to_rect()
|
||||||
|
]).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
const TILE_MAX: i32 = 256;
|
||||||
|
let (w, h) = if pe.w() > pe.h() {
|
||||||
|
(TILE_MAX, (TILE_MAX as f64 * pe.h() / pe.w()) as i32)
|
||||||
|
} else {
|
||||||
|
(((TILE_MAX as f64 * pe.w() / pe.h()) as i32), TILE_MAX)
|
||||||
|
};
|
||||||
|
println!("{}x{}", w, h);
|
||||||
|
let s = ImageSurface::create(cairo::Format::ARgb32, w, h).unwrap();
|
||||||
|
let ctx = Context::new(&s).unwrap();
|
||||||
|
ctx.set_antialias(cairo::Antialias::Fast);
|
||||||
|
|
||||||
|
for row in result {
|
||||||
|
// let src: String = row.get(1);
|
||||||
|
let attrs: serde_json::Value = row.get(2);
|
||||||
|
// println!("src: {:?} attrs: {:?}", src, attrs);
|
||||||
|
|
||||||
|
let coords: LineString<f64> = row.get(0);
|
||||||
|
let points = coords.into_points();
|
||||||
|
// println!("{} ps", points.len());
|
||||||
|
ctx.move_to((points[0].x() - pe.x1) * (w as f64) / pe.w(), (pe.y2 - points[0].y()) * (h as f64) / pe.h());
|
||||||
|
for point in &points[1..] {
|
||||||
|
ctx.line_to((point.x() - pe.x1) * (w as f64) / pe.w(), (pe.y2 - point.y()) * (h as f64) / pe.h());
|
||||||
|
}
|
||||||
|
ctx.close_path();
|
||||||
|
|
||||||
|
let (r, g, b, a) = area_color(&attrs);
|
||||||
|
ctx.set_source_rgba(r, g, b, a);
|
||||||
|
ctx.fill().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = Cursor::new(vec![]);
|
||||||
|
s.write_to_png(&mut buffer).unwrap();
|
||||||
|
let res = create_response(&state, StatusCode::OK, IMAGE_PNG, buffer.into_inner());
|
||||||
|
(state, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0.0: green, 0.5: yellow, 1.0: red
|
||||||
|
fn hue(mut x: f64) -> (f64, f64, f64) {
|
||||||
|
x = x.max(0.).min(1.);
|
||||||
|
|
||||||
|
if x < 0.5 {
|
||||||
|
(2.0 * x, 1.0, 0.0)
|
||||||
|
} else {
|
||||||
|
(1.0, 2.0 - 2.0 * x, 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn area_color(attrs: &serde_json::Value) -> (f64, f64, f64, f64) {
|
||||||
|
if let serde_json::Value::Object(attrs) = attrs {
|
||||||
|
// bodenquali
|
||||||
|
if let Some(serde_json::Value::Number(q)) = attrs.get("bodenqualitaet") {
|
||||||
|
if let Some(q) = q.as_f64() {
|
||||||
|
let (r, g, b) = hue(1.0 - q / 6.0);
|
||||||
|
return (r, g, b, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(serde_json::Value::Number(q)) = attrs.get("wasserspeicher") {
|
||||||
|
if let Some(mut q) = q.as_f64() {
|
||||||
|
if q > 10. {
|
||||||
|
q /= 10.;
|
||||||
|
}
|
||||||
|
let (r, g, b) = hue((5. - q) / 4.);
|
||||||
|
return (r, g, b, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wasserhaushalt
|
||||||
|
if let Some(serde_json::Value::String(typ)) = attrs.get("gebietstyp") {
|
||||||
|
match typ.as_str() {
|
||||||
|
"Gewässer" => return (0., 1., 0., 0.2),
|
||||||
|
"versickerungsdominiert" => return (0., 1., 0., 0.1),
|
||||||
|
"verdunstungs- und versickerungsbestimmt" => return (0.5, 1., 0., 0.05),
|
||||||
|
"verdunstungsdominiert" => return (1.0, 1.0, 0., 0.1),
|
||||||
|
"verdunstungs- und abflussbestimmt" => return (1.0, 0.5, 0., 0.05),
|
||||||
|
"abflussdominiert" => return (1., 0., 0., 0.1),
|
||||||
|
"ausgewogen" => return (0., 0., 0., 0.),
|
||||||
|
_ => println!("typ: {}", typ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// boden_fkt
|
||||||
|
let mut x = 0.0;
|
||||||
|
let mut a = 0.0;
|
||||||
|
if let Some(serde_json::Value::Number(q)) = attrs.get("schutt") {
|
||||||
|
if let Some(q) = q.as_f64() {
|
||||||
|
if q > 1. {
|
||||||
|
println!("schutt: {}", q);
|
||||||
|
}
|
||||||
|
x += q / 1.5;
|
||||||
|
a += 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(serde_json::Value::Number(q)) = attrs.get("versiegelung") {
|
||||||
|
if let Some(q) = q.as_f64() {
|
||||||
|
if q > 5. {
|
||||||
|
println!("versiegelung: {}", q);
|
||||||
|
}
|
||||||
|
x += q / 5.0;
|
||||||
|
a += 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (r, g, b) = hue(x);
|
||||||
|
return (r, g, b, a);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("not painted: {:?}", attrs);
|
||||||
|
(0., 0., 0., 0.)
|
||||||
|
}
|
117
server/src/main.rs
Normal file
117
server/src/main.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
// #![recursion_limit="2048"]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_derive;
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use gotham::{
|
||||||
|
handler::assets::FileOptions,
|
||||||
|
router::builder::{DefineSingleRoute, DrawRoutes},
|
||||||
|
middleware::state::StateMiddleware,
|
||||||
|
pipeline::single::single_pipeline,
|
||||||
|
pipeline::single_middleware,
|
||||||
|
router::builder::*,
|
||||||
|
};
|
||||||
|
use std::io::Cursor;
|
||||||
|
use gotham::{
|
||||||
|
helpers::http::response::create_response,
|
||||||
|
hyper::{Body, Response},
|
||||||
|
state::{FromState, State},
|
||||||
|
};
|
||||||
|
use geo::{Rect, Point};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
mod area;
|
||||||
|
mod trees;
|
||||||
|
|
||||||
|
#[derive(Clone, StateData)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub db: Arc<Mutex<postgres::Client>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, StateData, StaticResponseExtender)]
|
||||||
|
pub struct IdExtractor {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, StateData, StaticResponseExtender)]
|
||||||
|
pub struct AreaExtractor {
|
||||||
|
x1: f64,
|
||||||
|
y1: f64,
|
||||||
|
x2: f64,
|
||||||
|
y2: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AreaExtractor {
|
||||||
|
fn grow(&self, a: f64) -> Self {
|
||||||
|
AreaExtractor {
|
||||||
|
x1: self.x1 - a * self.w(),
|
||||||
|
y1: self.y1 - a * self.h(),
|
||||||
|
x2: self.x2 + a * self.w(),
|
||||||
|
y2: self.y2 + a * self.h(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_rect(&self) -> Rect<f64> {
|
||||||
|
Rect::new((self.x1, self.y1), (self.x2, self.y2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn w(&self) -> f64 {
|
||||||
|
self.x2 - self.x1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn h(&self) -> f64 {
|
||||||
|
self.y2 - self.y1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, StateData, StaticResponseExtender)]
|
||||||
|
pub struct PointExtractor {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointExtractor {
|
||||||
|
fn to_point(&self) -> Point<f64> {
|
||||||
|
Point::new(self.x, self.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
const DB_URL: &str = "host=10.233.1.2 dbname=treeadvisor user=treeadvisor password=123";
|
||||||
|
|
||||||
|
let db = postgres::Client::connect(DB_URL, postgres::NoTls)
|
||||||
|
.expect("DB");
|
||||||
|
|
||||||
|
let state = AppState {
|
||||||
|
db: Arc::new(Mutex::new(db)),
|
||||||
|
};
|
||||||
|
let (chain, pipelines) = single_pipeline(
|
||||||
|
single_middleware(
|
||||||
|
StateMiddleware::new(state)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let router = build_router(chain, pipelines, |route| {
|
||||||
|
route.get("/heatmap/:x1/:y1/:x2/:y2")
|
||||||
|
.with_path_extractor::<AreaExtractor>()
|
||||||
|
.to(trees::get_heatmap);
|
||||||
|
route.get("/area/:x/:y")
|
||||||
|
.with_path_extractor::<PointExtractor>()
|
||||||
|
.to(area::get_details);
|
||||||
|
route.get("/trees/:x1/:y1/:x2/:y2")
|
||||||
|
.with_path_extractor::<AreaExtractor>()
|
||||||
|
.to(trees::get_trees);
|
||||||
|
route.get("/tree/:id")
|
||||||
|
.with_path_extractor::<IdExtractor>()
|
||||||
|
.to(trees::get_tree);
|
||||||
|
route.get("static/*").to_dir(
|
||||||
|
FileOptions::new(&"static")
|
||||||
|
.with_cache_control("no-cache")
|
||||||
|
.with_gzip(true)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
gotham::start("0.0.0.0:8400", router);
|
||||||
|
}
|
167
server/src/trees.rs
Normal file
167
server/src/trees.rs
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use gotham::{
|
||||||
|
helpers::http::response::create_response,
|
||||||
|
hyper::{Body, Response},
|
||||||
|
state::{FromState, State},
|
||||||
|
};
|
||||||
|
use geo::Point;
|
||||||
|
use serde::Serialize;
|
||||||
|
use mime::{APPLICATION_JSON, IMAGE_PNG};
|
||||||
|
use http::StatusCode;
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use cairo::{ImageSurface, Context};
|
||||||
|
use crate::{AppState, AreaExtractor, IdExtractor};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct Tree {
|
||||||
|
id: String,
|
||||||
|
coords: [f64; 2],
|
||||||
|
german: Option<String>,
|
||||||
|
botanic: Option<String>,
|
||||||
|
details: Option<HashMap<String, String>>,
|
||||||
|
planted: Option<NaiveDate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_trees(state: State) -> (State, Response<Body>) {
|
||||||
|
let pe = AreaExtractor::borrow_from(&state);
|
||||||
|
let app_state = AppState::borrow_from(&state);
|
||||||
|
let result = {
|
||||||
|
let mut db = app_state.db.lock().unwrap();
|
||||||
|
db.query("SELECT id, coord, botanic, german, planted FROM trees WHERE coord <@ $1::box ORDER BY coord <-> center($1::box) ASC LIMIT 2000", &[
|
||||||
|
&pe.to_rect()
|
||||||
|
]).unwrap()
|
||||||
|
}.into_iter().map(|row| {
|
||||||
|
let point: Point<f64> = row.get(1);
|
||||||
|
Tree {
|
||||||
|
id: row.get(0),
|
||||||
|
coords: [point.x(), point.y()],
|
||||||
|
botanic: row.get(2),
|
||||||
|
details: None,
|
||||||
|
german: row.get(3),
|
||||||
|
planted: row.get(4),
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let body = serde_json::to_string(&result).unwrap();
|
||||||
|
let res = create_response(&state, StatusCode::OK, APPLICATION_JSON, body);
|
||||||
|
(state, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PFAF_COLS: &[&str] = &[
|
||||||
|
"Latin name", "Common name", "Family", "Synonyms",
|
||||||
|
"Known hazards", "Range", "Habitat", "Rating",
|
||||||
|
"Medicinal Rating", "Habit", "Height", "Width",
|
||||||
|
"Hardyness", "FrostTender", "In leaf", "Scented",
|
||||||
|
"Flowering time", "Seed ripens", "Flower Type", "Pollinators",
|
||||||
|
"Self-fertile", "Nitrogen fixer", "Wildlife", "Soil",
|
||||||
|
"Well-drained", "pH", "Shade", "Moisture",
|
||||||
|
"Saline", "Wind", "Edible uses", "Medicinal",
|
||||||
|
"Uses notes", "In cultivation?", "Cultivation details", "Growth rate",
|
||||||
|
"Pollution", "Propagation 1", "Poor soil", "Drought",
|
||||||
|
"Woodland", "Meadow", "Wall", "Acid",
|
||||||
|
"Alkaline", "Heavy clay", "Cultivars", "Cultivars in cultivation",
|
||||||
|
"Pull-out", "Last update", "Record checked", "SiteSpecificNotes",
|
||||||
|
"Deciduous/Evergreen", "Author", "Botanical references"
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn get_tree(state: State) -> (State, Response<Body>) {
|
||||||
|
let ie = IdExtractor::borrow_from(&state);
|
||||||
|
let app_state = AppState::borrow_from(&state);
|
||||||
|
let result = {
|
||||||
|
let mut db = app_state.db.lock().unwrap();
|
||||||
|
let row = db.query("SELECT id, coord, botanic, botanic_pfaf, german, planted FROM trees WHERE id=$1 LIMIT 1", &[&ie.id]).unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.next();
|
||||||
|
if let Some(row) = row {
|
||||||
|
let botanic_pfaf: &str = row.get(3);
|
||||||
|
let mut query = format!("SELECT ");
|
||||||
|
for (i, col) in PFAF_COLS.iter().enumerate() {
|
||||||
|
query = format!("{}{}\"{}\"::text", query, if i == 0 { "" } else { ", " }, col);
|
||||||
|
}
|
||||||
|
query = format!("{} FROM \"PlantsForAFuture\" WHERE \"Latin name\"=$1 LIMIT 1", query);
|
||||||
|
let pfaf_row = db.query(query.as_str(), &[&botanic_pfaf]).unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.next();
|
||||||
|
pfaf_row.map(|pfaf_row| {
|
||||||
|
let point: Point<f64> = row.get(1);
|
||||||
|
let mut details: HashMap<String, String> = HashMap::with_capacity(PFAF_COLS.len());
|
||||||
|
for (i, col) in PFAF_COLS.iter().enumerate() {
|
||||||
|
details.insert(col.to_string(), pfaf_row.get(i));
|
||||||
|
}
|
||||||
|
Tree {
|
||||||
|
id: row.get(0),
|
||||||
|
coords: [point.x(), point.y()],
|
||||||
|
botanic: row.get(2),
|
||||||
|
german: row.get(4),
|
||||||
|
details: Some(details),
|
||||||
|
planted: row.get(5),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = match result {
|
||||||
|
Some(result) => {
|
||||||
|
let body = serde_json::to_string(&result).unwrap();
|
||||||
|
create_response(&state, StatusCode::OK, APPLICATION_JSON, body)
|
||||||
|
}
|
||||||
|
None =>
|
||||||
|
create_response(&state, StatusCode::NOT_FOUND, APPLICATION_JSON, "{}"),
|
||||||
|
};
|
||||||
|
(state, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_heatmap(state: State) -> (State, Response<Body>) {
|
||||||
|
let pe = AreaExtractor::borrow_from(&state);
|
||||||
|
let app_state = AppState::borrow_from(&state);
|
||||||
|
let result = {
|
||||||
|
let mut db = app_state.db.lock().unwrap();
|
||||||
|
db.query("SELECT coord, score FROM trees WHERE coord <@ $1::box AND SCORE IS NOT NULL", &[
|
||||||
|
&pe.grow(0.2).to_rect()
|
||||||
|
]).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
const TILE_SIZE: i32 = 256;
|
||||||
|
// let (w, h) = if pe.w() / 2. > pe.h() {
|
||||||
|
// (TILE_MAX / 2, (TILE_MAX as f64 * pe.h() / pe.w()) as i32)
|
||||||
|
// } else {
|
||||||
|
// (((TILE_MAX as f64 / 2. * pe.w() / pe.h()) as i32), TILE_MAX)
|
||||||
|
// };
|
||||||
|
let (w, h) = (TILE_SIZE, TILE_SIZE);
|
||||||
|
// println!("{}x{} ({}x{})", w, h, pe.w(), pe.h());
|
||||||
|
let s = ImageSurface::create(cairo::Format::ARgb32, w, h).unwrap();
|
||||||
|
let ctx = Context::new(&s).unwrap();
|
||||||
|
ctx.set_antialias(cairo::Antialias::Fast);
|
||||||
|
|
||||||
|
for row in result {
|
||||||
|
let point: Point<f64> = row.get(0);
|
||||||
|
let score: f64 = row.get(1);
|
||||||
|
|
||||||
|
let (r, g, b) = temperature(1. - score);
|
||||||
|
ctx.set_source_rgba(r, g, b, 0.1);
|
||||||
|
let radius = 1.5 + 0.03 / pe.w();
|
||||||
|
// println!("radius: {}", radius);
|
||||||
|
ctx.arc((point.x() - pe.x1) * (w as f64) / pe.w(), (pe.y2 - point.y()) * (h as f64) / pe.h(),
|
||||||
|
radius, 0., 2. * core::f64::consts::PI);
|
||||||
|
ctx.fill().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = Cursor::new(vec![]);
|
||||||
|
s.write_to_png(&mut buffer).unwrap();
|
||||||
|
let res = create_response(&state, StatusCode::OK, IMAGE_PNG, buffer.into_inner());
|
||||||
|
(state, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0.0: green, 0.5: yellow, 1.0: red
|
||||||
|
fn temperature(mut x: f64) -> (f64, f64, f64) {
|
||||||
|
x = x.max(0.).min(1.);
|
||||||
|
|
||||||
|
if x < 0.5 {
|
||||||
|
(2.0 * x, 1.0, 0.0)
|
||||||
|
} else {
|
||||||
|
(1.0, 2.0 - 2.0 * x, 0.0)
|
||||||
|
}
|
||||||
|
}
|
151
server/static/app.js
Normal file
151
server/static/app.js
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
const TREES_MIN_ZOOM = 17;
|
||||||
|
|
||||||
|
var map = L.map('map').setView([51.05, 13.75], TREES_MIN_ZOOM, {
|
||||||
|
maxZoom: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
var tiles = L.tileLayer('https://stamen-tiles.a.ssl.fastly.net/terrain/{z}/{x}/{y}.jpg', {
|
||||||
|
attribution: '© <a href="https://osm.org/copyright">OpenStreetMap</a> contributors',
|
||||||
|
maxZoom: 17,
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
var HeatmapLayer = L.TileLayer.extend({
|
||||||
|
getTileUrl: function (coords) {
|
||||||
|
var cs = this._tileCoordsToNwSe(coords);
|
||||||
|
var x1 = Math.min(cs[0].lng, cs[1].lng);
|
||||||
|
var y1 = Math.min(cs[0].lat, cs[1].lat);
|
||||||
|
var x2 = Math.max(cs[0].lng, cs[1].lng);
|
||||||
|
var y2 = Math.max(cs[0].lat, cs[1].lat);
|
||||||
|
return ["", "heatmap", x1, y1, x2, y2].join("/");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(new HeatmapLayer({
|
||||||
|
maxZoom: TREES_MIN_ZOOM - 1,
|
||||||
|
opacity: 0.4,
|
||||||
|
})).addTo(map);
|
||||||
|
|
||||||
|
var treeIcon = L.icon({
|
||||||
|
iconUrl: "tree.png",
|
||||||
|
iconSize: [12, 12],
|
||||||
|
iconAnchor: [6, 6],
|
||||||
|
popupAnchor: [0, -6],
|
||||||
|
});
|
||||||
|
|
||||||
|
function treePopup(coords, entry) {
|
||||||
|
var popup = L.popup()
|
||||||
|
.setLatLng(coords);
|
||||||
|
popup.on('add', function() {
|
||||||
|
var info = {};
|
||||||
|
function update(newInfo) {
|
||||||
|
Object.keys(newInfo).forEach(function(k) {
|
||||||
|
info[k] = newInfo[k];
|
||||||
|
});
|
||||||
|
|
||||||
|
popup.setContent(function() {
|
||||||
|
var div = document.createElement("div");
|
||||||
|
var h2 = document.createElement("h2");
|
||||||
|
h2.textContent = entry.german;
|
||||||
|
if (entry.age)
|
||||||
|
h2.textContent += " (" + entry.age + ")";
|
||||||
|
div.appendChild(h2);
|
||||||
|
var p = document.createElement("p");
|
||||||
|
p.textContent = entry.botanic;
|
||||||
|
div.appendChild(p);
|
||||||
|
|
||||||
|
if (info.area) {
|
||||||
|
p = document.createElement("p");
|
||||||
|
p.textContent = JSON.stringify(info.area);
|
||||||
|
div.appendChild(p);
|
||||||
|
}
|
||||||
|
if (info.tree) {
|
||||||
|
p = document.createElement("p");
|
||||||
|
p.style.maxHeight = "10em";
|
||||||
|
p.style.overflow = "scroll";
|
||||||
|
p.textContent = JSON.stringify(info.tree);
|
||||||
|
div.appendChild(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return div;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
update({});
|
||||||
|
|
||||||
|
fetch(["", "area", coords[1], coords[0]].join("/"))
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
update({ area: data });
|
||||||
|
});
|
||||||
|
fetch(["", "tree", entry.id].join("/"))
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log("tree data", data);
|
||||||
|
update({ tree: data });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return popup;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trees;
|
||||||
|
var trees_pending = false;
|
||||||
|
var visible_trees;
|
||||||
|
|
||||||
|
function updateTrees() {
|
||||||
|
if (map.getZoom() < TREES_MIN_ZOOM) {
|
||||||
|
if (trees) {
|
||||||
|
trees.remove();
|
||||||
|
trees = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var bounds = map.getBounds();
|
||||||
|
if (!trees) {
|
||||||
|
trees = L.layerGroup().addTo(map);
|
||||||
|
visible_trees = {};
|
||||||
|
} else {
|
||||||
|
trees.eachLayer(function(marker) {
|
||||||
|
var ll = marker.getLatLng();
|
||||||
|
if (ll.lng < bounds.getWest() || ll.lng > bounds.getEast() ||
|
||||||
|
ll.lat < bounds.getSouth() || ll.lat > bounds.getNorth()) {
|
||||||
|
|
||||||
|
delete visible_trees[marker.options.id];
|
||||||
|
trees.removeLayer(marker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trees_pending) return;
|
||||||
|
trees_pending = true;
|
||||||
|
|
||||||
|
fetch(["", "trees", bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()].join("/"))
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (map.getZoom() < TREES_MIN_ZOOM) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.forEach(function(entry) {
|
||||||
|
var coords = [entry.coords[1], entry.coords[0]];
|
||||||
|
if (entry.planted) {
|
||||||
|
entry.age = Math.round((Date.now() - Date.parse(entry.planted)) / (365.25 * 24 * 60 * 60 * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visible_trees[entry.id]) {
|
||||||
|
visible_trees[entry.id] = true;
|
||||||
|
|
||||||
|
var marker = L.marker(coords, {
|
||||||
|
id: entry.id,
|
||||||
|
title: entry.german,
|
||||||
|
icon: treeIcon,
|
||||||
|
}).bindPopup(treePopup(coords, entry));
|
||||||
|
trees.addLayer(marker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
trees_pending = false;
|
||||||
|
})
|
||||||
|
.catch (error => {
|
||||||
|
console.error('Error:' + error)
|
||||||
|
trees_pending = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
map.on('moveend', updateTrees);
|
||||||
|
map.on('zoomend', updateTrees);
|
||||||
|
updateTrees();
|
19
server/static/index.html
Normal file
19
server/static/index.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Dresden Tree Advisor</title>
|
||||||
|
<link rel="stylesheet" href="leaflet.css" />
|
||||||
|
<script src="leaflet.js"></script>
|
||||||
|
<style>
|
||||||
|
#map { margin: 0; padding: 0; width: 100vw; height: 100vh; }
|
||||||
|
body { margin: 0; padding: 0; font-family: sans-serif; }
|
||||||
|
</style>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
640
server/static/leaflet.css
Normal file
640
server/static/leaflet.css
Normal file
|
@ -0,0 +1,640 @@
|
||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-pane > svg,
|
||||||
|
.leaflet-pane > canvas,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
/* Prevents IE11 from highlighting tiles in blue */
|
||||||
|
.leaflet-tile::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||||
|
.leaflet-safari .leaflet-tile {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||||
|
.leaflet-safari .leaflet-tile-container {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1600px;
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||||
|
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container .leaflet-overlay-pane svg,
|
||||||
|
.leaflet-container .leaflet-marker-pane img,
|
||||||
|
.leaflet-container .leaflet-shadow-pane img,
|
||||||
|
.leaflet-container .leaflet-tile-pane img,
|
||||||
|
.leaflet-container img.leaflet-image-layer,
|
||||||
|
.leaflet-container .leaflet-tile {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag {
|
||||||
|
-ms-touch-action: pinch-zoom;
|
||||||
|
/* Fallback for FF which doesn't support pinch-zoom */
|
||||||
|
touch-action: none;
|
||||||
|
touch-action: pinch-zoom;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 800;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pane { z-index: 400; }
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 200; }
|
||||||
|
.leaflet-overlay-pane { z-index: 400; }
|
||||||
|
.leaflet-shadow-pane { z-index: 500; }
|
||||||
|
.leaflet-marker-pane { z-index: 600; }
|
||||||
|
.leaflet-tooltip-pane { z-index: 650; }
|
||||||
|
.leaflet-popup-pane { z-index: 700; }
|
||||||
|
|
||||||
|
.leaflet-map-pane canvas { z-index: 100; }
|
||||||
|
.leaflet-map-pane svg { z-index: 200; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 800;
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-tile {
|
||||||
|
will-change: opacity;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-animated {
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
-ms-transform-origin: 0 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.leaflet-crosshair,
|
||||||
|
.leaflet-crosshair .leaflet-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-grab,
|
||||||
|
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||||
|
.leaflet-dragging .leaflet-marker-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* marker & overlays interactivity */
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-pane > svg path,
|
||||||
|
.leaflet-tile-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon.leaflet-interactive,
|
||||||
|
.leaflet-image-layer.leaflet-interactive,
|
||||||
|
.leaflet-pane > svg path.leaflet-interactive,
|
||||||
|
svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-active {
|
||||||
|
outline: 2px solid orange;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-bar a:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-scrollbar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default icon URLs */
|
||||||
|
.leaflet-default-icon-path {
|
||||||
|
background-image: url(images/marker-icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-control-attribution,
|
||||||
|
.leaflet-container .leaflet-control-scale {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 19px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 18px 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 4px 4px 0 0;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 14px;
|
||||||
|
font: 16px/14px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #c3c3c3;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
-ms-zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip-container {
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
/* Base styles for the element that has a tooltip */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #222;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip.leaflet-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directions */
|
||||||
|
|
||||||
|
.leaflet-tooltip-bottom {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top {
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
top: 0;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
right: 0;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
left: 0;
|
||||||
|
margin-left: -12px;
|
||||||
|
border-right-color: #fff;
|
||||||
|
}
|
6
server/static/leaflet.js
Normal file
6
server/static/leaflet.js
Normal file
File diff suppressed because one or more lines are too long
BIN
server/static/tree.png
Normal file
BIN
server/static/tree.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
Loading…
Reference in New Issue
Block a user