connect_errno ) die('Cannot connect to database: ('.$db->connect_errno.') '.$db->connect_error); $db->set_charset('utf8'); if( PHP_SAPI == 'cli' ) { db_dump(); exit; } if( !isset($_REQUEST['action']) ) { require('iodb-web.php'); exit; } $action = $_REQUEST['action']; if( $action == 'get' ) { do_get(); } elseif( $action == 'store' ) { post(); } elseif( $action == 'deprecate' ) { deprecate(); } elseif( $action == 'report' ) { report(); } elseif( $action == 'all' ) { db_dump_passthru(); exit; } else { // the rest of actions could be related to web require('iodb-web.php'); } // ----------------------------------------------------------------------------- // Check query parameter and return either it or the default value, or raise error if there's no default. function req( $param, $default = NULL ) { if( !isset($_REQUEST[$param]) || strlen($_REQUEST[$param]) == 0 ) { if( is_null($default) ) error("Missing required parameter \"$param\"."); else return $default; } return trim($_REQUEST[$param]); } // Validate float or integer number, and check for min/max. function validate_num( $f, $name, $float, $min = NULL, $max = NULL ) { if( !preg_match($float ? '/^-?\d+(?:\.\d+)?$/' : '/^-?\d+$/', $f) ) error("Parameter \"$name\" should be " .($float ? 'a floating-point number with a dot as a separator.' : 'an integer number')); if( (!is_null($min) && $f < $min) || (!is_null($max) && $f > $max) ) error("Parameter \"$name\" should be a number between $min and $max."); } // Print a message (respecting output format) and shut down. function print_msg( $type, $msg ) { $fmt = out_fmt(); if( $fmt == 'json' ) { print_json(array($type => $msg)); } elseif ( $fmt == 'xml' ) { print xmlheader()."\t<$type>".htmlspecialchars($msg)."\n".xmlfooter(); } else { // html print ''.htmlspecialchars($msg).''; } exit; } // Prints an error message. function error( $msg ) { print_msg('error', $msg); } // Prints a success message. function message( $msg ) { print_msg('message', $msg); } // Returns requested outpue format: 'json' or 'xml'. function out_fmt() { if( isset($GLOBALS['html']) ) return 'html'; return isset($_REQUEST['format']) && strtolower($_REQUEST['format']) == 'json' ? 'json' : 'xml'; } // Prints json respecting jsonp parameter. function print_json( $data ) { header('Content-type: application/json'); $str = json_encode($data); print isset($_REQUEST['jsonp']) ? $_REQUEST['jsonp']."($str);" : $str; } // Prints XML header. function xmlheader() { header('Content-type: application/xml'); return "\n\n"; } // Prints XML footer. function xmlfooter() { return ""; } // Checks for input parameters and calls get() or get_json() depending on requested format. function do_get() { $lat = req('lat'); $lon = req('lon'); validate_num($lat, 'lat', true, -90.0, 90.0); validate_num($lon, 'lon', true, -180.0, 180.0); $radius = req('radius', DEFAULT_RADIUS); validate_num($radius, 'radius', false); $query = get_query($lat, $lon, min($radius, MAX_RADIUS)); if( out_fmt() == 'json' ) get_json($query); else get($query); } // Queries the database and prints XML. function get( $query ) { global $db; $result = $db->query($query); if( !$result ) error("Database query failed: ".$db->error); print xmlheader(); if( isset($_REQUEST['query']) ) print "\t".htmlspecialchars($query)."\n"; while( $data = $result->fetch_assoc() ) { $obsolete = is_null($data['abandon_date']) ? '' : ' deprecated="yes"'; $flagged = is_null($data['flagged']) ? '' : ' flagged="yes"'; $tag = !is_null($data['imagery']) ? 'offset' : (!is_null($data['calibration']) ? 'calibration' : 'unknown'); print "\t<$tag id=\"".$data['offset_id']."\" lat=\"".$data['lat']."\" lon=\"".$data['lon']."\"$obsolete$flagged>\n"; print "\t\t".htmlspecialchars($data['author'])."\n"; print "\t\t".htmlspecialchars($data['description'])."\n"; print "\t\t".$data['offset_date']."\n"; if( !is_null($data['abandon_date']) ) { print "\t\t\n"; print "\t\t\t".htmlspecialchars($data['abandon_author'])."\n"; print "\t\t\t".htmlspecialchars($data['abandon_reason'])."\n"; print "\t\t\t".$data['abandon_date']."\n"; print "\t\t\n"; } if( !is_null($data['imagery']) ) { $minzoom = is_null($data['min_zoom']) ? '' : ' min-zoom="'.$data['min_zoom'].'"'; $maxzoom = is_null($data['max_zoom']) ? '' : ' max-zoom="'.$data['max_zoom'].'"'; print "\t\t".htmlspecialchars($data['imagery'])."\n"; print "\t\t\n"; } elseif( !is_null($data['calibration']) ) { $geom = parse_wkt($data['cal_text']); print "\t\t\n"; foreach( $geom as $lonlat ) print "\t\t\t\n"; print "\t\t\n"; } print "\t\n"; } print xmlfooter(); $result->free(); } // Queries the database and prints JSON. function get_json( $query ) { global $db; $result = $db->query($query); if( !$result ) error("Database query failed: ".$db->error); $list = array(); $list[] = array('type' => 'meta', 'timestamp' => date(DATE_ISO8601)); while( $data = $result->fetch_assoc() ) { $item = array(); $item['type'] = !is_null($data['imagery']) ? 'offset' : (!is_null($data['calibration']) ? 'calibration' : 'unknown'); $item['id'] = $data['offset_id']; $item['lat'] = $data['lat']; $item['lon'] = $data['lon']; $item['author'] = $data['author']; $item['description'] = $data['description']; $item['date'] = $data['offset_date']; if( !is_null($data['abandon_date']) ) { $dep = array(); $dep['author'] = $data['abandon_author']; $dep['reason'] = $data['abandon_reason']; $dep['date'] = $data['abandon_date']; $item['deprecated'] = $dep; } if( !is_null($data['flagged']) ) $item['flagged'] = 1; if( !is_null($data['imagery']) ) { if( !is_null($data['min_zoom']) ) $item['min-zoom'] = $data['min_zoom']; if( !is_null($data['max_zoom']) ) $item['max-zoom'] = $data['max_zoom']; $item['imagery'] = $data['imagery']; $item['imlat'] = $data['imlat']; $item['imlon'] = $data['imlon']; } elseif( !is_null($data['calibration']) ) { $item['geometry'] = parse_wkt($data['cal_text']); } $list[] = $item; } $result->free(); print_json($list); } // Prepares a database query given lat, lon, radius and $_REQUEST['imagery']. function get_query( $lat, $lon, $radius ) { global $db; $coslat = cos($lat * M_PI / 180.0); $region = region_where_clause($lat, $lon, $radius); $qimagery = !isset($_REQUEST['imagery']) ? '' : "and (imagery is null or imagery = '".$db->escape_string(trim($_REQUEST['imagery']))."')"; $query = "select *, AsText(calibration) as cal_text, " ."X(coord) as lon, Y(coord) as lat, X(im_coord) as imlon, Y(im_coord) as imlat, " ."(Y(coord) - $lat)*(Y(coord) - $lat) + (X(coord) - $lon)*(X(coord) - $lon)*$coslat*$coslat as distance " ."from iodb where $region $qimagery order by distance limit 30"; return $query; } // Returns where clause for offsets around a point with given radius in km function region_where_clause( $lat, $lon, $radius ) { // http://stackoverflow.com/questions/973363/mysql-not-using-my-indexes // http://en.wikipedia.org/wiki/Latitude#The_length_of_a_degree_of_latitude // http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates $basekm = 6371.0; // 111.1; $coslat = cos($lat * M_PI / 180.0); $dlat = $radius / $basekm; $dlon = asin(sin($dlat) / $coslat); // $dlat * $coslat $bbox = wkt_bbox_around($lat, $lon, $dlat * 180.0 / M_PI, $dlon * 180.0 / M_PI); return "MBRContains($bbox, coord)"; } // Transforms center point + radiuses to a WKT string. function wkt_bbox_around( $lat, $lon, $dlat, $dlon ) { $minlat = $lat - $dlat; $minlon = $lon - $dlon; $maxlat = $lat + $dlat; $maxlon = $lon + $dlon; return "GeomFromText('POLYGON(($minlon $minlat, $minlon $maxlat, $maxlon $maxlat, $maxlon $minlat, $minlon $minlat))')"; } // Returns an array of coordinate arrays [lon, lat] from WKT. function parse_wkt( $wkt ) { $geom = array(); $type = strtoupper(substr($wkt, strpos($wkt, '('))); // we don't need it actually $i = strrpos($wkt, '(') + 1; $points = explode(',', substr($wkt, $i, strpos($wkt, ')') - $i)); foreach( $points as $point ) { $geom[] = explode(' ', trim($point)); } return $geom; } // Adds an entry to the database. function post() { global $db; $author = $db->escape_string(req('author')); $message = $db->escape_string(req('description')); if( mb_strlen($author, 'UTF8') == 0 || mb_strlen($author, 'UTF8') > 100 ) error('Incorrect author name'); $messagelen = mb_strlen($message, 'UTF8'); if( $messagelen < 3 ) error('Description is too short'); if( $messagelen > 200 ) error("Description is too long ($messagelen), 200 chars max"); $ip = bin2hex(inet_pton($_SERVER['REMOTE_ADDR'])); validate_ip($ip); if( isset($_REQUEST['imagery']) ) { $imagery = $db->escape_string(req('imagery')); $lat = req('lat'); $lon = req('lon'); validate_num($lat, 'lat', true, -90.0, 90.0); validate_num($lon, 'lon', true, -180.0, 180.0); // test if there is already something at there coordinates validate_dup($lat, $lon, $author, true); $imlat = req('imlat'); $imlon = req('imlon'); $minzoom = req('minzoom', 0); $maxzoom = req('maxzoom', 30); validate_num($imlon, 'imlon', true); validate_num($imlat, 'imlat', true); validate_num($minzoom, 'minzoom', false, 0, 30); validate_num($maxzoom, 'maxzoom', false, 0, 30); $query = "insert into iodb (offset_date, coord, author, description, ip, " ."imagery, im_coord, min_zoom, max_zoom) " ."values (CURDATE(), POINT($lon, $lat), '$author', '$message', 0x$ip, " ."'$imagery', POINT($imlon, $imlat), $minzoom, $maxzoom)"; } elseif( isset($_REQUEST['geometry']) ) { $geom = parse_geometry(req('geometry')); validate_dup($geom[1], $geom[0], $author, false); $query = "insert into iodb (offset_date, coord, author, description, ip, calibration) " ."values (CURDATE(), POINT($geom[0], $geom[1]), '$author', '$message', 0x$ip, GeomFromText('$geom[2]'))"; } else { error("Not enough parameters"); } // everything's set, add this! $result = $db->query($query); if( !$result ) { error("Database query failed: ".$db->error); } message('Offset was added to the database, thank you.'); } // Request a single value from the database function request_one($query) { global $db; $result = $db->query($query); if( !$result ) error('Database query failed: '.$db->error); if( $result->num_rows > 0 ) { $tmp = $result->fetch_row(); $ret = $tmp[0]; } else { $ret = null; } $result->free(); return $ret; } // Check that there are no offset of a given type in this point. function validate_dup($lat, $lon, $author, $is_imagery) { $is_im_clause = $is_imagery ? 'is not null' : 'is null'; $query = "select offset_id from iodb where " ."MBRContains(".wkt_bbox_around($lat, $lon, 1e-6, 1e-6).", coord) " ."and author = '$author' and imagery $is_im_clause"; if( request_one($query) != null ) error("There is already an offset at coordinates ($lat, $lon), sorry."); } // Limit offsets per day for a single IP. function validate_ip( $ipenc ) { $query = "select count(1) from iodb where " ."(ip = 0x$ipenc or abandon_ip = 0x$ipenc) and (offset_date = CURDATE() or abandon_date = CURDATE())"; $cnt = request_one($query); if( $cnt > MAX_REQUEST_PER_IP_PER_DAY ) error('You have made '.MAX_REQUEST_PER_IP_PER_DAY.' requests today. That\'s the limit, sorry.'); } // Parses query parameter 'geometry', returns array of [lon, lat, wkt]. function parse_geometry( $geom ) { $points = explode(',', $geom); $g = array(); $alat = 0.0; $alon = 0.0; foreach( $points as $pt ) { if( !preg_match('/(-?\d{1,3}(?:\.\d+)?)\s+(-?\d{1,2}(?:\.\d+)?)/', $pt, $matches) ) error("Incorrect format of a geometry point: $pt"); $g[] = $matches[1].' '.$matches[2]; $alon += $matches[1]; $alat += $matches[2]; } $cnt = count($g); if( $cnt == 0 ) error("No points specified in the geometry."); if( $cnt > 100 ) error("Too many points (".count($g)." > 100) in the geometry."); $wkt_base = implode(',', $g); if( $cnt == 1 ) $wkt = "POINT($wkt_base)"; elseif( $g[0] != $g[$cnt-1] ) $wkt = "LINESTRING($wkt_base)"; else $wkt = "POLYGON(($wkt_base))"; return array($alon / $cnt, $alat / $cnt, $wkt); } // Mark a specified offset as deprecated. function deprecate() { global $db; $id = req('id'); deprecate_impl($id, req('author'), req('reason')); message("Offset $id was deprecated."); } // Mark a specified offset as deprecated (or not). function deprecate_impl( $id, $author, $reason ) { global $db; validate_num($id, 'id', false); if( mb_strlen($author, 'UTF8') == 0 || mb_strlen($author, 'UTF8') > 100 ) error('Incorrect author name'); $messagelen = mb_strlen($reason, 'UTF8'); if( $messagelen < 3 ) error('Reason is too short'); if( $messagelen > 200 ) error("Reason is too long ($messagelen), 200 chars max"); $ip = bin2hex(inet_pton($_SERVER['REMOTE_ADDR'])); validate_ip($ip); $result = $db->query("select abandon_date from iodb where offset_id = $id"); if( !$result ) error("Database query failed: ".$db->error); if( $result->num_rows < 1 ) error("No such offset"); $r = $result->fetch_row(); $result->free(); if( !is_null($r[0]) ) error("Offset $id is already deprecated"); $qset = "abandon_date = CURDATE(), abandon_author = '".$db->escape_string($author)."', abandon_reason = '".$db->escape_string($reason)."', abandon_ip = 0x$ip"; $result = $db->query("update iodb set $qset where offset_id = $id"); if( !$result ) error("Database query failed: ".$db->error); } // Flag a specified offset. function report() { global $db; $id = req('id'); report_impl($id, req('reason')); message("Offset $id was flagged."); } // Flag/unflag a specified offset. function report_impl($id, $message = null) { global $db; if( is_array($id) ) { foreach( $id as $tid ) validate_num($tid, 'id', false); $qid = 'in ('.implode(',', $id).')'; } else { validate_num($id, 'id', false); $result = $db->query("select flagged from iodb where offset_id = $id"); if( !$result ) error("Database query failed: ".$db->error); if( $result->num_rows < 1 ) error("No such offset"); $arr = $result->fetch_row(); if( !is_null($message) && !is_null($arr[0]) ) error('The offset is already reported.'); if( is_null($message) && is_null($arr[0]) ) error('The offset is not reported yet.'); $result->free(); $qid = '= '.$id; } $result = $db->query('update iodb set flagged = '.(is_null($message) ? 'null' : "'".$db->escape_string($message)."'")." where offset_id $qid"); if( !$result ) error("Database query failed: ".$db->error); } function undeprecate( $id ) { global $db; if( is_array($id) ) { foreach( $id as $tid ) validate_num($tid, 'id', false); $qid = 'in ('.implode(',', $id).')'; } else { validate_num($id, 'id', false); $result = $db->query("select abandon_date from iodb where offset_id = $id"); if( !$result ) error("Database query failed: ".$db->error); if( $result->num_rows < 1 ) error("No such offset"); $r = $result->fetch_row(); if( is_null($r[0]) ) error("Offset $id is not deprecated"); $result->free(); $qid = '= '.$id; } $result = $db->query("update iodb set abandon_date = null where offset_id $qid"); if( !$result ) error("Database query failed: ".$db->error); } function delete_impl( $id ) { global $db; if( !is_array($id) ) { $qid = '= '.$id; validate_num($id, 'id', false); } else { foreach( $id as $tid ) validate_num($tid, 'id', false); $qid = 'in ('.implode(',', $id).')'; } $result = $db->query("delete from iodb where offset_id $qid"); if( !$result ) error("Database query failed: ".$db->error); } // Just dump the whole database. Is called only from command line. function db_dump() { global $argc, $argv; $query = 'select *, AsText(calibration) as cal_text, ' .'X(coord) as lon, Y(coord) as lat, X(im_coord) as imlon, Y(im_coord) as imlat ' .'from iodb order by offset_id'; if( $argc > 1 ) { if( $argv[1] == 'loc' ) find_locations(); else if( $argv[1] == 'json' ) get_json($query); else if( $argv[1] == 'xml' ) get($query); else print "Unknown action\n"; } else { print "IODB Command line\nSyntax: php $argv[0] {xml|json|loc}\n"; } } // Pass the database dump file through, respecting requested output format. function db_dump_passthru() { header('Content-Encoding: gzip'); if( out_fmt() == 'json' ) { header('Content-type: application/json'); $file = 'iodb-latest.json.gz'; } else { header('Content-type: application/xml'); $file = 'iodb-latest.xml.gz'; } header('Content-Length: '.filesize('download/'.$file)); // header('Content-Disposition: attachment; filename='.$file); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Pragma: public'); readfile('download/'.$file); } // Query nominatim for unset locations in the database function find_locations() { global $db; $result = $db->query("select offset_id, X(coord) as lon, Y(coord) as lat from iodb where location is null order by offset_id limit 3"); if( !$result ) error("Database query failed: ".$db->error); $queue = array(); while( $line = $result->fetch_assoc() ) $queue[] = array('id' => $line['offset_id'], 'lat' => $line['lat'], 'lon' => $line['lon']); $result->free(); $lastrow = request_one("select max(offset_id) from iodb where location is not null"); $result = $db->query("select offset_id, X(coord) as lon, Y(coord) as lat from iodb where offset_id = ".rand(1, $lastrow)); if( !$result ) error("Database query failed: ".$db->error); while( $line = $result->fetch_assoc() ) $queue[] = array('id' => $line['offset_id'], 'lat' => $line['lat'], 'lon' => $line['lon']); $result->free(); foreach( $queue as $offset ) { $url = 'http://nominatim.openstreetmap.org/reverse?format=json&zoom=16&addressdetails=0' .'&email=zverik%40textual.ru&lat='.$offset['lat'].'&lon='.$offset['lon']; $response = json_decode(file_get_contents($url), true); if( isset($response) && (isset($response['display_name']) || isset($response['error'])) ) { $newval = $db->escape_string(isset($response['display_name']) ? $response['display_name'] : ''); $result = $db->query("update iodb set location='$newval' where offset_id = ".$offset['id']); if( !$result ) error("Database query failed: ".$db->error); } } } function create_table() { global $db; $query = <<query($query); if( !$result ) { print('Could not create iodb table: '.$db->error); exit; } } ?>