oDB =& $oDB; } public function loadParamArray($oParams) { $aLangs = $oParams->getPreferredLanguages(); $this->aLangPrefOrderSql = 'ARRAY['.join(',', array_map('getDBQuoted', $aLangs)).']'; $this->bExtraTags = $oParams->getBool('extratags', false); $this->bNameDetails = $oParams->getBool('namedetails', false); $this->bIncludePolygonAsText = $oParams->getBool('polygon_text'); $this->bIncludePolygonAsGeoJSON = $oParams->getBool('polygon_geojson'); $this->bIncludePolygonAsKML = $oParams->getBool('polygon_kml'); $this->bIncludePolygonAsSVG = $oParams->getBool('polygon_svg'); $this->fPolygonSimplificationThreshold = $oParams->getFloat('polygon_threshold', 0.0); $iWantedTypes = ($this->bIncludePolygonAsText ? 1 : 0) + ($this->bIncludePolygonAsGeoJSON ? 1 : 0) + ($this->bIncludePolygonAsKML ? 1 : 0) + ($this->bIncludePolygonAsSVG ? 1 : 0); if ($iWantedTypes > CONST_PolygonOutput_MaximumTypes) { if (CONST_PolygonOutput_MaximumTypes) { userError("Select only ".CONST_PolygonOutput_MaximumTypes." polgyon output option"); } else { userError("Polygon output is disabled"); } } } public function setAnchorSql($sPoint) { $this->sAnchorSql = $sPoint; } public function setDeDupe($bDeDupe) { $this->bDeDupe = $bDeDupe; } public function setAddressRankList($aList) { $this->sAddressRankListSql = '('.join(',', $aList).')'; } public function setAllowedTypesSQLList($sSql) { $this->sAllowedTypesSQLList = $sSql; } public function setLanguagePreference($aLangPrefOrder) { $this->aLangPrefOrderSql = 'ARRAY['.join(',', array_map('getDBQuoted', $aLangPrefOrder)).']'; } public function setIncludeAddressDetails($bAddressDetails = true) { $this->bAddressDetails = $bAddressDetails; } public function setIncludeExtraTags($bExtraTags = false) { $this->bExtraTags = $bExtraTags; } public function setIncludeNameDetails($bNameDetails = false) { $this->bNameDetails = $bNameDetails; } public function setIncludePolygonAsPoints($b = true) { $this->bIncludePolygonAsPoints = $b; } public function setIncludePolygonAsText($b = true) { $this->bIncludePolygonAsText = $b; } public function setIncludePolygonAsGeoJSON($b = true) { $this->bIncludePolygonAsGeoJSON = $b; } public function setIncludePolygonAsKML($b = true) { $this->bIncludePolygonAsKML = $b; } public function setIncludePolygonAsSVG($b = true) { $this->bIncludePolygonAsSVG = $b; } public function setPolygonSimplificationThreshold($f) { $this->fPolygonSimplificationThreshold = $f; } private function addressImportanceSql($sGeometry, $sPlaceId) { if ($this->sAnchorSql) { $sSQL = 'ST_Distance('.$this->sAnchorSql.','.$sGeometry.')'; } else { $sSQL = '(SELECT max(ai_p.importance * (ai_p.rank_address + 2))'; $sSQL .= ' FROM place_addressline ai_s, placex ai_p'; $sSQL .= ' WHERE ai_s.place_id = '.$sPlaceId; $sSQL .= ' AND ai_p.place_id = ai_s.address_place_id '; $sSQL .= ' AND ai_s.isaddress '; $sSQL .= ' AND ai_p.importance is not null)'; } return $sSQL.' AS addressimportance,'; } private function langAddressSql($sHousenumber) { return 'get_address_by_language(place_id,'.$sHousenumber.','.$this->aLangPrefOrderSql.') AS langaddress,'; } public function lookupOSMID($sType, $iID) { $sSQL = "select place_id from placex where osm_type = '".$sType."' and osm_id = ".$iID; $iPlaceID = chksql($this->oDB->getOne($sSQL)); if (!$iPlaceID) { return null; } $aResults = $this->lookup(array($iPlaceID => new Result($iPlaceID))); return sizeof($aResults) ? reset($aResults) : null; } public function lookup($aResults, $iMinRank = 0, $iMaxRank = 30) { if (!sizeof($aResults)) { return array(); } $aSubSelects = array(); $sPlaceIDs = Result::joinIdsByTable($aResults, Result::TABLE_PLACEX); if (CONST_Debug) var_dump('PLACEX', $sPlaceIDs); if ($sPlaceIDs) { $sSQL = 'SELECT '; $sSQL .= ' osm_type,'; $sSQL .= ' osm_id,'; $sSQL .= ' class,'; $sSQL .= ' type,'; $sSQL .= ' admin_level,'; $sSQL .= ' rank_search,'; $sSQL .= ' rank_address,'; $sSQL .= ' min(place_id) AS place_id,'; $sSQL .= ' min(parent_place_id) AS parent_place_id,'; $sSQL .= ' housenumber,'; $sSQL .= ' country_code,'; $sSQL .= $this->langAddressSql('-1'); $sSQL .= ' get_name_by_language(name,'.$this->aLangPrefOrderSql.') AS placename,'; $sSQL .= " get_name_by_language(name, ARRAY['ref']) AS ref,"; if ($this->bExtraTags) { $sSQL .= 'hstore_to_json(extratags)::text AS extra,'; } if ($this->bNameDetails) { $sSQL .= 'hstore_to_json(name)::text AS names,'; } $sSQL .= ' avg(ST_X(centroid)) AS lon, '; $sSQL .= ' avg(ST_Y(centroid)) AS lat, '; $sSQL .= ' COALESCE(importance,0.75-(rank_search::float/40)) AS importance, '; $sSQL .= $this->addressImportanceSql( 'ST_Collect(centroid)', 'min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END)' ); $sSQL .= " (extratags->'place') AS extra_place "; $sSQL .= ' FROM placex'; $sSQL .= " WHERE place_id in ($sPlaceIDs) "; $sSQL .= ' AND ('; $sSQL .= " placex.rank_address between $iMinRank and $iMaxRank "; if (14 >= $iMinRank && 14 <= $iMaxRank) { $sSQL .= " OR (extratags->'place') = 'city'"; } if ($this->sAddressRankListSql) { $sSQL .= ' OR placex.rank_address in '.$this->sAddressRankListSql; } $sSQL .= " ) "; if ($this->sAllowedTypesSQLList) { $sSQL .= 'AND placex.class in '.$this->sAllowedTypesSQLList; } $sSQL .= ' AND linked_place_id is null '; $sSQL .= ' GROUP BY '; $sSQL .= ' osm_type, '; $sSQL .= ' osm_id, '; $sSQL .= ' class, '; $sSQL .= ' type, '; $sSQL .= ' admin_level, '; $sSQL .= ' rank_search, '; $sSQL .= ' rank_address, '; $sSQL .= ' housenumber,'; $sSQL .= ' country_code, '; $sSQL .= ' importance, '; if (!$this->bDeDupe) $sSQL .= 'place_id,'; $sSQL .= ' langaddress, '; $sSQL .= ' placename, '; $sSQL .= ' ref, '; if ($this->bExtraTags) $sSQL .= 'extratags, '; if ($this->bNameDetails) $sSQL .= 'name, '; $sSQL .= " extratags->'place' "; $aSubSelects[] = $sSQL; } // postcode table $sPlaceIDs = Result::joinIdsByTable($aResults, Result::TABLE_POSTCODE); if ($sPlaceIDs) { $sSQL = 'SELECT'; $sSQL .= " 'P' as osm_type,"; $sSQL .= " (SELECT osm_id from placex p WHERE p.place_id = lp.parent_place_id) as osm_id,"; $sSQL .= " 'place' as class, 'postcode' as type,"; $sSQL .= ' null as admin_level, rank_search, rank_address,'; $sSQL .= ' place_id, parent_place_id,'; $sSQL .= ' null as housenumber,'; $sSQL .= ' country_code,'; $sSQL .= $this->langAddressSql('-1'); $sSQL .= " postcode as placename,"; $sSQL .= " postcode as ref,"; if ($this->bExtraTags) $sSQL .= "null AS extra,"; if ($this->bNameDetails) $sSQL .= "null AS names,"; $sSQL .= " ST_x(geometry) AS lon, ST_y(geometry) AS lat,"; $sSQL .= " (0.75-(rank_search::float/40)) AS importance, "; $sSQL .= $this->addressImportanceSql('geometry', 'lp.parent_place_id'); $sSQL .= " null AS extra_place "; $sSQL .= "FROM location_postcode lp"; $sSQL .= " WHERE place_id in ($sPlaceIDs) "; $sSQL .= " AND lp.rank_address between $iMinRank and $iMaxRank"; $aSubSelects[] = $sSQL; } // All other tables are rank 30 only. if ($iMaxRank == 30) { // TIGER table if (CONST_Use_US_Tiger_Data) { $sPlaceIDs = Result::joinIdsByTable($aResults, Result::TABLE_TIGER); if ($sPlaceIDs) { $sHousenumbers = Result::sqlHouseNumberTable($aResults, Result::TABLE_TIGER); // Tiger search only if a housenumber was searched and if it was found // (realized through a join) $sSQL = " SELECT "; $sSQL .= " 'T' AS osm_type, "; $sSQL .= " (SELECT osm_id from placex p WHERE p.place_id=blub.parent_place_id) as osm_id, "; $sSQL .= " 'place' AS class, "; $sSQL .= " 'house' AS type, "; $sSQL .= ' null AS admin_level, '; $sSQL .= ' 30 AS rank_search, '; $sSQL .= ' 30 AS rank_address, '; $sSQL .= ' place_id, '; $sSQL .= ' parent_place_id, '; $sSQL .= ' housenumber_for_place as housenumber,'; $sSQL .= " 'us' AS country_code, "; $sSQL .= $this->langAddressSql('housenumber_for_place'); $sSQL .= " null AS placename, "; $sSQL .= " null AS ref, "; if ($this->bExtraTags) $sSQL .= "null AS extra,"; if ($this->bNameDetails) $sSQL .= "null AS names,"; $sSQL .= " st_x(centroid) AS lon, "; $sSQL .= " st_y(centroid) AS lat,"; $sSQL .= " -1.15 AS importance, "; $sSQL .= $this->addressImportanceSql('centroid', 'blub.parent_place_id'); $sSQL .= " null AS extra_place "; $sSQL .= " FROM ("; $sSQL .= " SELECT place_id, "; // interpolate the Tiger housenumbers here $sSQL .= " ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) AS centroid, "; $sSQL .= " parent_place_id, "; $sSQL .= " housenumber_for_place"; $sSQL .= " FROM ("; $sSQL .= " location_property_tiger "; $sSQL .= " JOIN (values ".$sHousenumbers.") AS housenumbers(place_id, housenumber_for_place) USING(place_id)) "; $sSQL .= " WHERE "; $sSQL .= " housenumber_for_place >= startnumber"; $sSQL .= " AND housenumber_for_place <= endnumber"; $sSQL .= " ) AS blub"; //postgres wants an alias here $aSubSelects[] = $sSQL; } } // osmline - interpolated housenumbers $sPlaceIDs = Result::joinIdsByTable($aResults, Result::TABLE_OSMLINE); if ($sPlaceIDs) { $sHousenumbers = Result::sqlHouseNumberTable($aResults, Result::TABLE_OSMLINE); // interpolation line search only if a housenumber was searched // (realized through a join) $sSQL = "SELECT "; $sSQL .= " 'W' AS osm_type, "; $sSQL .= " osm_id, "; $sSQL .= " 'place' AS class, "; $sSQL .= " 'house' AS type, "; $sSQL .= ' 15 AS admin_level, '; $sSQL .= ' 30 AS rank_search, '; $sSQL .= ' 30 AS rank_address, '; $sSQL .= ' place_id, '; $sSQL .= ' parent_place_id, '; $sSQL .= ' housenumber_for_place as housenumber,'; $sSQL .= ' country_code, '; $sSQL .= $this->langAddressSql('housenumber_for_place'); $sSQL .= ' null AS placename, '; $sSQL .= ' null AS ref, '; if ($this->bExtraTags) $sSQL .= 'null AS extra, '; if ($this->bNameDetails) $sSQL .= 'null AS names, '; $sSQL .= ' st_x(centroid) AS lon, '; $sSQL .= ' st_y(centroid) AS lat, '; // slightly smaller than the importance for normal houses $sSQL .= " -0.1 AS importance, "; $sSQL .= $this->addressImportanceSql('centroid', 'blub.parent_place_id'); $sSQL .= " null AS extra_place "; $sSQL .= " FROM ("; $sSQL .= " SELECT "; $sSQL .= " osm_id, "; $sSQL .= " place_id, "; $sSQL .= " country_code, "; $sSQL .= " CASE "; // interpolate the housenumbers here $sSQL .= " WHEN startnumber != endnumber "; $sSQL .= " THEN ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) "; $sSQL .= " ELSE ST_LineInterpolatePoint(linegeo, 0.5) "; $sSQL .= " END as centroid, "; $sSQL .= " parent_place_id, "; $sSQL .= " housenumber_for_place "; $sSQL .= " FROM ("; $sSQL .= " location_property_osmline "; $sSQL .= " JOIN (values ".$sHousenumbers.") AS housenumbers(place_id, housenumber_for_place) USING(place_id)"; $sSQL .= " ) "; $sSQL .= " WHERE housenumber_for_place >= 0 "; $sSQL .= " ) as blub"; //postgres wants an alias here $aSubSelects[] = $sSQL; } if (CONST_Use_Aux_Location_data) { $sPlaceIDs = Result::joinIdsByTable($aResults, Result::TABLE_AUX); if ($sPlaceIDs) { $sHousenumbers = Result::sqlHouseNumberTable($aResults, Result::TABLE_AUX); $sSQL = " SELECT "; $sSQL .= " 'L' AS osm_type, "; $sSQL .= " place_id AS osm_id, "; $sSQL .= " 'place' AS class,"; $sSQL .= " 'house' AS type, "; $sSQL .= ' null AS admin_level, '; $sSQL .= ' 30 AS rank_search,'; $sSQL .= ' 30 AS rank_address, '; $sSQL .= ' place_id,'; $sSQL .= ' parent_place_id, '; $sSQL .= ' housenumber,'; $sSQL .= " 'us' AS country_code, "; $sSQL .= $this->langAddressSql('-1'); $sSQL .= " null AS placename, "; $sSQL .= " null AS ref, "; if ($this->bExtraTags) $sSQL .= "null AS extra, "; if ($this->bNameDetails) $sSQL .= "null AS names, "; $sSQL .= " ST_X(centroid) AS lon, "; $sSQL .= " ST_Y(centroid) AS lat, "; $sSQL .= " -1.10 AS importance, "; $sSQL .= $this->addressImportanceSql( 'centroid', 'location_property_aux.parent_place_id' ); $sSQL .= " null AS extra_place "; $sSQL .= " FROM location_property_aux "; $sSQL .= " WHERE place_id in ($sPlaceIDs) "; $aSubSelects[] = $sSQL; } } } if (CONST_Debug) var_dump($aSubSelects); if (!sizeof($aSubSelects)) { return array(); } $aPlaces = chksql( $this->oDB->getAll(join(' UNION ', $aSubSelects)), "Could not lookup place" ); $aClassType = getClassTypes(); foreach ($aPlaces as &$aPlace) { if ($this->bAddressDetails) { // to get addressdetails for tiger data, the housenumber is needed $aPlace['aAddress'] = $this->getAddressNames( $aPlace['place_id'], $aPlace['housenumber'] ); } if ($this->bExtraTags) { if ($aPlace['extra']) { $aPlace['sExtraTags'] = json_decode($aPlace['extra']); } else { $aPlace['sExtraTags'] = (object) array(); } } if ($this->bNameDetails) { if ($aPlace['names']) { $aPlace['sNameDetails'] = json_decode($aPlace['names']); } else { $aPlace['sNameDetails'] = (object) array(); } } $sAddressType = ''; $sClassType = $aPlace['class'].':'.$aPlace['type'].':'.$aPlace['admin_level']; if (isset($aClassType[$sClassType]) && isset($aClassType[$sClassType]['simplelabel'])) { $sAddressType = $aClassType[$aClassType]['simplelabel']; } else { $sClassType = $aPlace['class'].':'.$aPlace['type']; if (isset($aClassType[$sClassType]) && isset($aClassType[$sClassType]['simplelabel'])) $sAddressType = $aClassType[$sClassType]['simplelabel']; else $sAddressType = $aPlace['class']; } $aPlace['addresstype'] = $sAddressType; } if (CONST_Debug) var_dump($aPlaces); return $aPlaces; } private function getAddressDetails($iPlaceID, $bAll, $sHousenumber) { $sSQL = 'SELECT *,'; $sSQL .= ' get_name_by_language(name,'.$this->aLangPrefOrderSql.') as localname'; $sSQL .= ' FROM get_addressdata('.$iPlaceID.','.$sHousenumber.')'; if (!$bAll) { $sSQL .= " WHERE isaddress OR type = 'country_code'"; } $sSQL .= ' ORDER BY rank_address desc,isaddress DESC'; return chksql($this->oDB->getAll($sSQL)); } public function getAddressNames($iPlaceID, $sHousenumber = null) { $aAddressLines = $this->getAddressDetails( $iPlaceID, false, $sHousenumber === null ? -1 : $sHousenumber ); $aAddress = array(); $aFallback = array(); $aClassType = getClassTypes(); foreach ($aAddressLines as $aLine) { $bFallback = false; $aTypeLabel = false; if (isset($aClassType[$aLine['class'].':'.$aLine['type'].':'.$aLine['admin_level']])) { $aTypeLabel = $aClassType[$aLine['class'].':'.$aLine['type'].':'.$aLine['admin_level']]; } elseif (isset($aClassType[$aLine['class'].':'.$aLine['type']])) { $aTypeLabel = $aClassType[$aLine['class'].':'.$aLine['type']]; } elseif (isset($aClassType['boundary:administrative:'.((int)($aLine['rank_address']/2))])) { $aTypeLabel = $aClassType['boundary:administrative:'.((int)($aLine['rank_address']/2))]; $bFallback = true; } else { $aTypeLabel = array('simplelabel' => 'address'.$aLine['rank_address']); $bFallback = true; } if ($aTypeLabel && ((isset($aLine['localname']) && $aLine['localname']) || (isset($aLine['housenumber']) && $aLine['housenumber']))) { $sTypeLabel = strtolower(isset($aTypeLabel['simplelabel'])?$aTypeLabel['simplelabel']:$aTypeLabel['label']); $sTypeLabel = str_replace(' ', '_', $sTypeLabel); if (!isset($aAddress[$sTypeLabel]) || (isset($aFallback[$sTypeLabel]) && $aFallback[$sTypeLabel]) || $aLine['class'] == 'place') { $aAddress[$sTypeLabel] = $aLine['localname']?$aLine['localname']:$aLine['housenumber']; } $aFallback[$sTypeLabel] = $bFallback; } } return $aAddress; } /* returns an array which will contain the keys * aBoundingBox * and may also contain one or more of the keys * asgeojson * askml * assvg * astext * lat * lon */ public function getOutlines($iPlaceID, $fLon = null, $fLat = null, $fRadius = null) { $aOutlineResult = array(); if (!$iPlaceID) return $aOutlineResult; if (CONST_Search_AreaPolygons) { // Get the bounding box and outline polygon $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,"; $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,"; $sSQL .= "ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,"; $sSQL .= "ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon"; if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson"; if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml"; if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg"; if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext"; $sFrom = " from placex where place_id = ".$iPlaceID; if ($this->fPolygonSimplificationThreshold > 0) { $sSQL .= " from (select place_id,centroid,ST_SimplifyPreserveTopology(geometry,".$this->fPolygonSimplificationThreshold.") as geometry".$sFrom.") as plx"; } else { $sSQL .= $sFrom; } $aPointPolygon = chksql($this->oDB->getRow($sSQL), "Could not get outline"); if ($aPointPolygon['place_id']) { if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null) { $aOutlineResult['lat'] = $aPointPolygon['centrelat']; $aOutlineResult['lon'] = $aPointPolygon['centrelon']; } if ($this->bIncludePolygonAsGeoJSON) $aOutlineResult['asgeojson'] = $aPointPolygon['asgeojson']; if ($this->bIncludePolygonAsKML) $aOutlineResult['askml'] = $aPointPolygon['askml']; if ($this->bIncludePolygonAsSVG) $aOutlineResult['assvg'] = $aPointPolygon['assvg']; if ($this->bIncludePolygonAsText) $aOutlineResult['astext'] = $aPointPolygon['astext']; if ($this->bIncludePolygonAsPoints) $aOutlineResult['aPolyPoints'] = geometryText2Points($aPointPolygon['astext'], $fRadius); if (abs($aPointPolygon['minlat'] - $aPointPolygon['maxlat']) < 0.0000001) { $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius; $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius; } if (abs($aPointPolygon['minlon'] - $aPointPolygon['maxlon']) < 0.0000001) { $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius; $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius; } $aOutlineResult['aBoundingBox'] = array( (string)$aPointPolygon['minlat'], (string)$aPointPolygon['maxlat'], (string)$aPointPolygon['minlon'], (string)$aPointPolygon['maxlon'] ); } } // as a fallback we generate a bounding box without knowing the size of the geometry if ((!isset($aOutlineResult['aBoundingBox'])) && isset($fLon)) { // if ($this->bIncludePolygonAsPoints) { $sGeometryText = 'POINT('.$fLon.','.$fLat.')'; $aOutlineResult['aPolyPoints'] = geometryText2Points($sGeometryText, $fRadius); } $aBounds = array(); $aBounds['minlat'] = $fLat - $fRadius; $aBounds['maxlat'] = $fLat + $fRadius; $aBounds['minlon'] = $fLon - $fRadius; $aBounds['maxlon'] = $fLon + $fRadius; $aOutlineResult['aBoundingBox'] = array( (string)$aBounds['minlat'], (string)$aBounds['maxlat'], (string)$aBounds['minlon'], (string)$aBounds['maxlon'] ); } return $aOutlineResult; } }