public function test() { $this->initContext(); $this->assertEquals(false, RestoGeometryUtil::isValidGeoJSONFeature(null)); $this->assertEquals(false, RestoGeometryUtil::isValidGeoJSONFeature(array('type' => 'toto'))); $this->assertEquals(false, RestoGeometryUtil::isValidGeoJSONFeature(array('type' => 'Feature', 'geometry' => 'toto'))); $this->assertEquals(false, RestoGeometryUtil::isValidGeoJSONFeature(array('type' => 'Feature', 'geometry' => array()))); $this->assertEquals(false, RestoGeometryUtil::isValidGeoJSONFeature(array('type' => 'Feature', 'geometry' => array(), 'properties' => 'toto'))); $this->assertEquals(true, RestoGeometryUtil::isValidGeoJSONFeature(array('type' => 'Feature', 'geometry' => array(), 'properties' => array()))); $this->assertEquals(null, RestoGeometryUtil::geoJSONGeometryToWKT(array('type' => 'toto', 'coordinates' => array()))); $this->assertEquals('POINT()', RestoGeometryUtil::geoJSONGeometryToWKT(array('type' => 'POINT', 'coordinates' => array()))); $this->assertEquals('MULTIPOINT()', RestoGeometryUtil::geoJSONGeometryToWKT(array('type' => 'MULTIPOINT', 'coordinates' => array()))); $this->assertEquals('LINESTRING()', RestoGeometryUtil::geoJSONGeometryToWKT(array('type' => 'LINESTRING', 'coordinates' => array()))); $this->assertEquals('MULTILINESTRING()', RestoGeometryUtil::geoJSONGeometryToWKT(array('type' => 'MULTILINESTRING', 'coordinates' => array()))); $this->assertEquals('POLYGON()', RestoGeometryUtil::geoJSONGeometryToWKT(array('type' => 'POLYGON', 'coordinates' => array()))); $this->assertEquals('MULTIPOLYGON()', RestoGeometryUtil::geoJSONGeometryToWKT(array('type' => 'MULTIPOLYGON', 'coordinates' => array()))); $this->assertEquals(8.863358410694E-5, RestoGeometryUtil::radiusInDegrees(10, 10)); $this->assertEquals(array(8.9831528424457E-5, 0.00017966305685804), RestoGeometryUtil::inverseMercator(array(10, 20))); $this->assertEquals(null, RestoGeometryUtil::inverseMercator(array(10))); $this->assertEquals(null, RestoGeometryUtil::inverseMercator('toto')); $this->assertEquals(null, RestoGeometryUtil::forwardMercator('toto')); $this->assertEquals(null, RestoGeometryUtil::forwardMercator(array(90, -90))); $this->assertEquals(null, RestoGeometryUtil::forwardMercator(array(90, 90))); $this->assertEquals(null, RestoGeometryUtil::bboxToMercator(null)); $this->assertEquals('1113194.9077778,1118889.9747022,1113194.9077778,1118889.9747022', RestoGeometryUtil::bboxToMercator('10,10,10,10')); }
/** * Create JSON feature from new resource xml string * * <product> <title>S1A_IW_OCN__2SDV_20150727T044706_20150727T044731_006992_0097D1_F6DA</title> <resourceSize>6317404</resourceSize> <startTime>2015-07-27T04:47:06.611</startTime> <stopTime>2015-07-27T04:47:31.061</stopTime> <productType>OCN</productType> <missionId>S1A</missionId> <processingLevel>1</processingLevel> <mode>IW</mode> <absoluteOrbitNumber>6992</absoluteOrbitNumber> <orbitDirection>ASCENDING</orbitDirection> <s2takeid>38865</s2takeid> <cloudcover>0.0</cloudcover> <instrument>Multi-Spectral Instrument</instrument> <footprint>POLYGON ((-161.306549 21.163258,-158.915909 21.585093,-158.623169 20.077986,-160.989746 19.652864,-161.306549 21.163258))</footprint> </product> * * @param {DOMDocument} $dom : $dom DOMDocument */ private function parseNew($dom) { /* * Retrieves orbit direction */ $orbitDirection = strtolower($dom->getElementsByTagName('orbitDirection')->item(0)->nodeValue); $polygon = RestoGeometryUtil::WKTPolygonToArray($dom->getElementsByTagName('footprint')->item(0)->nodeValue); /* * Initialize feature */ $feature = array('type' => 'Feature', 'geometry' => array('type' => 'Polygon', 'coordinates' => array($polygon)), 'properties' => array('productIdentifier' => $dom->getElementsByTagName('title')->item(0)->nodeValue, 'title' => $dom->getElementsByTagName('title')->item(0)->nodeValue, 'resourceSize' => $dom->getElementsByTagName('resourceSize')->item(0)->nodeValue, 'authority' => 'ESA', 'startDate' => $dom->getElementsByTagName('startTime')->item(0)->nodeValue, 'completionDate' => $dom->getElementsByTagName('stopTime')->item(0)->nodeValue, 'productType' => $dom->getElementsByTagName('productType')->item(0)->nodeValue, 'processingLevel' => $dom->getElementsByTagName('processingLevel')->item(0)->nodeValue, 'platform' => $dom->getElementsByTagName('missionId')->item(0)->nodeValue, 'sensorMode' => $dom->getElementsByTagName('mode')->item(0)->nodeValue, 'orbitNumber' => $dom->getElementsByTagName('absoluteOrbitNumber')->item(0)->nodeValue, 'orbitDirection' => $orbitDirection, 'instrument' => $dom->getElementsByTagName('instrument')->item(0)->nodeValue, 'quicklook' => $this->getLocation($dom), 's2TakeId' => $dom->getElementsByTagName('s2takeid')->item(0)->nodeValue, 'cloudCover' => $dom->getElementsByTagName('cloudCover')->item(0)->nodeValue)); return $feature; }
/** * Generic code to transform input coordinates array to WKT string * * @param array $coordinates * @param function $functionName * @return type */ private static function coordinatesToString($coordinates, $functionName = null) { $output = array(); for ($i = 0, $l = count($coordinates); $i < $l; $i++) { switch ($functionName) { case 'toPoint': $output[] = RestoGeometryUtil::toPoint($coordinates[$i]); break; case 'toLineString': $output[] = RestoGeometryUtil::toLineString($coordinates[$i]); break; case 'toPolygon': $output[] = RestoGeometryUtil::toPolygon($coordinates[$i]); break; default: $output[] = join(' ', $coordinates[$i]); } } return '(' . join(',', $output) . ')'; }
/** * Create JSON feature from new resource xml string * * <product> <title>S1A_IW_OCN__2SDV_20150727T044706_20150727T044731_006992_0097D1_F6DA</title> <resourceSize>6317404</resourceSize> <startTime>2015-07-27T04:47:06.611</startTime> <stopTime>2015-07-27T04:47:31.061</stopTime> <productType>OCN</productType> <missionId>S1A</missionId> <processingLevel>2</processingLevel> <mode>IW</mode> <absoluteOrbitNumber>6992</absoluteOrbitNumber> <orbitDirection>ASCENDING</orbitDirection> <swath>IW</swath> <polarisation>VV VH</polarisation> <missiontakeid>38865</missiontakeid> <instrument>Multi-Spectral Instrument</instrument> <footprint>POLYGON ((-161.306549 21.163258,-158.915909 21.585093,-158.623169 20.077986,-160.989746 19.652864,-161.306549 21.163258))</footprint> </product> * * @param {DOMDocument} $dom : $dom DOMDocument */ private function parseNew($dom) { /* * Retreives orbit direction */ $orbitDirection = strtolower($dom->getElementsByTagName('orbitDirection')->item(0)->nodeValue); /* * Performs an inversion of the specified Sentinel-1 quicklooks footprint (inside the ZIP files, i.e SAFE product). * The datahub systematically performs an inversion of the Sentinel-1 quicklooks taking as input the quicklook images (.png) inside * the ZIP files (i.e. as produced by the S1 ground segment). */ $polygon = array($this->reorderSafeFootprintToDhus(RestoGeometryUtil::WKTPolygonToArray($dom->getElementsByTagName('footprint')->item(0)->nodeValue), $orbitDirection)); /* * Initialize feature */ $feature = array('type' => 'Feature', 'geometry' => array('type' => 'Polygon', 'coordinates' => $polygon), 'properties' => array('productIdentifier' => $dom->getElementsByTagName('title')->item(0)->nodeValue, 'title' => $dom->getElementsByTagName('title')->item(0)->nodeValue, 'resourceSize' => $dom->getElementsByTagName('resourceSize')->item(0)->nodeValue, 'authority' => 'ESA', 'startDate' => $dom->getElementsByTagName('startTime')->item(0)->nodeValue, 'completionDate' => $dom->getElementsByTagName('stopTime')->item(0)->nodeValue, 'productType' => $dom->getElementsByTagName('productType')->item(0)->nodeValue, 'processingLevel' => $dom->getElementsByTagName('processingLevel')->item(0)->nodeValue, 'platform' => $dom->getElementsByTagName('missionId')->item(0)->nodeValue, 'sensorMode' => $dom->getElementsByTagName('mode')->item(0)->nodeValue, 'orbitNumber' => $dom->getElementsByTagName('absoluteOrbitNumber')->item(0)->nodeValue, 'orbitDirection' => $orbitDirection, 'swath' => $dom->getElementsByTagName('swath')->item(0)->nodeValue, 'polarisation' => $dom->getElementsByTagName('polarisation')->item(0)->nodeValue, 'missionTakeId' => $dom->getElementsByTagName('missiontakeid')->item(0)->nodeValue, 'instrument' => $dom->getElementsByTagName('instrument')->item(0)->nodeValue, 'quicklook' => $this->getLocation($dom), 'cloudCover' => 0)); return $feature; }
/** * * PostgreSQL output columns are treated as string * thus they need to be converted to their true type * * @param Array $rawFeatureArray * @return array */ private function correctTypes($rawFeatureArray) { $corrected = array(); foreach ($rawFeatureArray as $key => $value) { switch ($key) { case 'bbox4326': $corrected[$key] = str_replace(' ', ',', substr(substr($rawFeatureArray[$key], 0, strlen($rawFeatureArray[$key]) - 1), 4)); $corrected['bbox3857'] = RestoGeometryUtil::bboxToMercator($rawFeatureArray[$key]); break; case 'keywords': $corrected[$key] = $this->correctKeywords(json_decode($value, true), $this->collections[$rawFeatureArray['collection']]); break; case 'licenseId': $corrected['license'] = isset($this->licenses[$rawFeatureArray['licenseId']]) ? $this->licenses[$rawFeatureArray['licenseId']] : null; break; default: $corrected[$key] = $this->castExplicit($key, $value, $this->collections[$rawFeatureArray['collection']]); } } if (!isset($corrected['license'])) { $corrected['license'] = $this->collections[$rawFeatureArray['collection']]->license->toArray(); } return $corrected; }
/** * Store feature within {collection}.features table following the class model * * @param array $data : array (MUST BE GeoJSON in abstract Model) * @param RestoCollection $collection * */ public function storeFeature($data, $collection) { /* * Assume input file or stream is a JSON Feature */ if (!RestoGeometryUtil::isValidGeoJSONFeature($data)) { RestoLogUtil::httpError(500, 'Invalid feature description'); } /* * Remap properties between RESTo model and input * GeoJSON Feature file */ $properties = $this->mapInputProperties($data); /* * Add collection to $properties to initialize facet counts on collection */ $properties['collection'] = isset($properties['collection']) ? $properties['collection'] : $collection->name; /* * Compute unique identifier */ if (!isset($data['id']) || !RestoUtil::isValidUUID($data['id'])) { $featureIdentifier = $collection->toFeatureId(isset($properties['productIdentifier']) ? $properties['productIdentifier'] : md5(microtime() . rand())); } else { $featureIdentifier = $data['id']; } /* * First check if feature is already in database * (do this before getKeywords to avoid iTag process) */ if ($collection->context->dbDriver->check(RestoDatabaseDriver::FEATURE, array('featureIdentifier' => $featureIdentifier))) { RestoLogUtil::httpError(500, 'Feature ' . $featureIdentifier . ' already in database'); } /* * Tag module */ $keywords = array(); if (isset($collection->context->modules['Tag'])) { $tagger = RestoUtil::instantiate($collection->context->modules['Tag']['className'], array($collection->context, $collection->user)); $keywords = $tagger->getKeywords($properties, $data['geometry']); } /* * Store feature */ $collection->context->dbDriver->store(RestoDatabaseDriver::FEATURE, array('collection' => $collection, 'featureArray' => array('type' => 'Feature', 'id' => $featureIdentifier, 'geometry' => $data['geometry'], 'properties' => array_merge($properties, array('keywords' => $keywords))))); return new RestoFeature($collection->context, $collection->user, array('featureIdentifier' => $featureIdentifier)); }
/** * Return a RESTo keywords array from an iTag Hierarchical feature * * @param array $properties * @param array $geometry (GeoJSON) */ private function keywordsFromITag($properties, $geometry) { /* * Initialize keywords array from faceted properties */ $keywords = array(); /* * Compute keywords from iTag */ if (isset($this->options['iTag'])) { $iTag = new iTag(array('dbh' => $this->getDatabaseHandler(isset($this->options['iTag']['database']) ? $this->options['iTag']['database'] : null))); $metadata = array('footprint' => RestoGeometryUtil::geoJSONGeometryToWKT($geometry), 'timestamp' => isset($properties['startDate']) ? $properties['startDate'] : null); $iTagFeature = $iTag->tag($metadata, isset($this->options['iTag']['taggers']) ? $this->options['iTag']['taggers'] : array()); } if (!isset($iTagFeature) || !isset($iTagFeature['content'])) { return $keywords; } /* * Continents, countries, regions and states */ if (isset($iTagFeature['content']['political'])) { $keywords = $this->getPoliticalKeywords($iTagFeature['content']['political']); } /* * Physical data */ if (isset($iTagFeature['content']['physical'])) { $keywords = array_merge($keywords, $this->getPhysicalKeywords($iTagFeature['content']['physical'])); } /* * Landuse and landuse details */ if (isset($iTagFeature['content']['landCover'])) { $keywords = array_merge($keywords, $this->getLandCoverKeywords($iTagFeature['content']['landCover'])); } /* * Population */ if (isset($iTagFeature['content']['population'])) { $keywords = array_merge($keywords, $this->getPopulationKeywords($iTagFeature['content']['population'])); } /* * Keywords */ if (isset($iTagFeature['content']['keywords'])) { $keywords = array_merge($keywords, $this->getAlwaysKeywords($iTagFeature['content']['keywords'])); } return $keywords; }
/** * Prepare SQL query for spatial operation ST_Distance (Input bbox or polygon) * * @param RestoModel $model * @param string $filterName * @param array $requestParams * @param boolean $exclusion * @return type */ private function prepareFilterQuery_distance($model, $filterName, $requestParams, $exclusion) { /* * WARNING ! Quick benchmark show that st_distance is 100x slower than st_intersects * TODO - check if st_distance performance can be improved. */ $use_distance = false; /* * geo:lon and geo:lat have preseance to geo:name * (avoid double call to Gazetteer) */ if (isset($requestParams['geo:lon']) && isset($requestParams['geo:lat'])) { $radius = RestoGeometryUtil::radiusInDegrees(isset($requestParams['geo:radius']) ? floatval($requestParams['geo:radius']) : 10000, $requestParams['geo:lat']); if ($use_distance) { return 'ST_distance(' . $model->getDbKey($model->searchFilters[$filterName]['key']) . ', ST_GeomFromText(\'' . pg_escape_string('POINT(' . $requestParams['geo:lon'] . ' ' . ($lat = $requestParams['geo:lat'] . ')')) . '\', 4326)) < ' . $radius; } else { $lonmin = $requestParams['geo:lon'] - $radius; $latmin = $requestParams['geo:lat'] - $radius; $lonmax = $requestParams['geo:lon'] + $radius; $latmax = $requestParams['geo:lat'] + $radius; return ($exclusion ? 'NOT ' : '') . 'ST_intersects(' . $model->getDbKey($model->searchFilters[$filterName]['key']) . ", ST_GeomFromText('" . pg_escape_string('POLYGON((' . $lonmin . ' ' . $latmin . ',' . $lonmin . ' ' . $latmax . ',' . $lonmax . ' ' . $latmax . ',' . $lonmax . ' ' . $latmin . ',' . $lonmin . ' ' . $latmin . '))') . "', 4326))"; } } }
/** * Convert feature array to database column/value pairs * * @param RestoCollection $collection * @param array $featureArray * @throws Exception */ private function getColumnsAndValues($collection, $featureArray) { /* * Initialize columns array */ $wkt = RestoGeometryUtil::geoJSONGeometryToWKT($featureArray['geometry']); $extent = RestoGeometryUtil::getExtent($wkt); /* * Compute "in house centroid" to avoid -180/180 date line issue */ $factor = 1; if (abs($extent[2] - $extent[0]) >= 180) { $factor = -1; } $columns = array_merge(array($collection->model->getDbKey('identifier') => '\'' . $featureArray['id'] . '\'', $collection->model->getDbKey('collection') => '\'' . $collection->name . '\'', $collection->model->getDbKey('geometry') => 'ST_GeomFromText(\'' . $wkt . '\', 4326)', '_geometry' => 'ST_SplitDateLine(ST_GeomFromText(\'' . $wkt . '\', 4326))', $collection->model->getDbKey('centroid') => 'ST_GeomFromText(\'POINT(' . ($extent[2] + $extent[0] * $factor) / 2.0 . ' ' . ($extent[3] + $extent[1]) / 2.0 . ')\', 4326)', 'updated' => 'now()', 'published' => 'now()'), $this->propertiesToColumns($collection, $featureArray['properties'])); return $columns; }