2025-05-26 11:55:44 +02:00
< ? php
namespace App\Service ;
use Symfony\Contracts\HttpClient\HttpClientInterface ;
2025-05-26 12:57:10 +02:00
use Doctrine\ORM\EntityManagerInterface ;
2025-05-26 11:55:44 +02:00
class Motocultrice
{
private $overpassApiUrl = 'https://overpass-api.de/api/interpreter' ;
2025-05-26 16:22:01 +02:00
private $osmApiUrl = 'https://www.openstreetmap.org/api/0.6' ;
2025-05-26 11:55:44 +02:00
2025-06-03 16:19:07 +02:00
public $overpass_base_places = '
(
2025-06-05 15:16:21 +02:00
nw [ " amenity " ~ " ^(cafe|bar|restaurant|library|cinema|fast_food|post_office|marketplace|community_centre|theatre|bank|townhall|animal_boarding|animal_breeding|animal_shelter|animal_training|archive|arts_centre|bank|bar|bicycle_rental|biergarten|boat_rental|boat_storage|bureau_de_change|cafe|canteen|car_rental|car_wash|casino|childcare|clinic|college|conference_centre|courthouse|coworking_space|crematorium|dancing_school|dentist|dive_centre|doctors|dojo|driver_training|driving_school|events_venue|financial_advice|fire_station|flight_school|food_court|fuel|funeral_hall|hookah_lounge|hospital|ice_cream|internet_cafe|karaoke_box|kindergarten|language_school|love_hotel|medical_supply|monastery|money_transfer|motorcycle_rental|music_school|music_venue|nightclub|nursing_home|pharmacy|place_of_mourning|place_of_worship|police|post_depot|prep_school|prison|pub|public_bath|ranger_station|research_institute|sailing_school|school|ski_rental|ski_school|social_centre|social_facility|stripclub|student_accommodation|studio|surf_school|swingerclub|theatre|toy_library|university|vehicle_inspection|veterinary|waste_transfer_station|workshop|art_school|atm|bar||bicycle_parking|bicycle_repair_station|boat_school|bus_station|clock|clubhouse|coffee|construction_equipment_rental|cooking_school|deposit_sale|dog_toilet|drive_school|estate_agent|event|fixme|florist|groundskeeping|group_home|health_facility|healthcare|hearing_aid|hearing_aids|herbalist|laboratory|office|parcel_locker|personal_service|photo_booth|printer|professional_school|public_building|public_service|recycling|scuba_diving_school|septic_tank|software_engineering|spa|swingerclub for swinger clubs|taxi|teahouse|toilets|training|underprivileged|vehicule_inspection|vending_machine|warehouse) $ " ]( area . searchArea );
2025-06-03 16:19:07 +02:00
nw [ " shop " ]( area . searchArea );
nw [ " healthcare " ]( area . searchArea );
nw [ " office " ]( area . searchArea );
);
2025-06-05 15:16:21 +02:00
2025-06-03 16:19:07 +02:00
' ;
2025-05-27 12:17:46 +02:00
// ne pas lister les tags qui utilisent des morceaux particuliers de formulaire pour éviter que les gens aient besoin de connaître le tag OSM
public $excluded_tags_to_render = [
'name' ,
'wheelchair' ,
'harassment_prevention' ,
2025-05-29 16:50:25 +02:00
'image' ,
'panoramax' ,
2025-05-27 12:17:46 +02:00
];
// les tags OSM que l'on estime nécessaires pour un commerce
2025-05-26 23:51:46 +02:00
public $base_tags = [
'name' ,
'opening_hours' ,
'contact:email' ,
'contact:phone' ,
2025-06-03 11:37:27 +02:00
'contact:housenumber' ,
'contact:street' ,
2025-05-26 23:51:46 +02:00
'contact:website' ,
'contact:mastodon' ,
2025-05-27 19:13:35 +02:00
'image' ,
2025-05-27 12:17:46 +02:00
'note'
2025-05-26 23:51:46 +02:00
];
2025-05-27 19:13:35 +02:00
// quand un commerce a fermé, on peut supprimer ces tags
2025-06-05 15:09:28 +02:00
public $obsolete_tags = [
" phone " , " website " , " email " , " description " , " brand " , " opening_hours " ,
2025-05-27 19:13:35 +02:00
" check_date:opening_hours " , " internet_access " ,
" indoor_seating " , " takeaway " , " female " , " male " , " unisex " ,
" ref:FR:NAF " , " ref:FR:FINESS " , " ref:FR:SIRET " , " ref:FR:SIREN " , " ref:vatin " ,
" healthcare " , " dispensing " , " lawyer " , " vending " , " vending_machine " ,
" self_service " , " second_hand " , " branch " , " delivery " , " start_date " ,
" beauty " , " facebook " , " tobacco " , " bulk_purchase " ,
" drive_through " , " pastry " , " stroller " , " fax " , " trade " , " network " ,
" mobile " , " sport " , " produce " , " lottery " , " supermarket " , " information " ,
" tourism " , " government " , " brewery "
];
2025-06-05 15:09:28 +02:00
public $tags_to_remove = [
" diet: " , " contact: " , " name: " , " payment: " , " delivery: " , " type:FR: " , " ref:FR:SDIS: " ,
2025-05-27 19:13:35 +02:00
" brand: " , " fuel: " , " service: " , " description: " , " operator: " , " tickets: " , " healthcare: "
];
2025-06-05 15:09:28 +02:00
public $tags_to_convert = [
" shop " => " was:shop " ,
2025-05-27 19:13:35 +02:00
" information " => " was:information " ,
" office " => " was:office " ,
" amenity " => " was:amenity " ,
" craft " => " was:craft " ,
" operator " => " was:operator " ,
" clothes " => " was:clothes " ,
" cuisine " => " was:cuisine " ,
" official_name " => " was:official_name " ,
" short_name " => " was:short_name " ,
" alt_name " => " was:alt_name "
];
2025-06-03 12:51:20 +02:00
2025-06-03 13:04:09 +02:00
public function export ( $zone ) {
$query = $this -> get_export_query ( $zone );
try {
$response = $this -> client -> request ( 'GET' , 'https://overpass-api.de/api/interpreter' , [
'query' => [
'data' => $query
]
]);
return $response -> getContent ();
} catch ( \Exception $e ) {
return " Erreur lors de la requête Overpass : " . $e -> getMessage ();
}
}
public function get_export_query ( $zone ) {
return <<< QUERY
[ out : csv ( :: id , :: type , :: lat , :: lon , name , amenity , shop , office , healthcare , " contact:email " , email , " contact:phone " , phone , " contact:website " , website , image , url , wikidata , opening_hours , " contact:housenumber " , " addr:housenumber " , " contact:street " , " addr:street " , note , fixme , harassment_prevention , cuisine , brand , tourism , source , zip_code , " ref:FR:SIRET " )];
2025-06-03 16:19:07 +02:00
{{ geocodeArea : " { $zone } , France " }} ->. searchArea ;
{ $this -> overpass_base_places }
out skel qt ;
2025-06-03 13:04:09 +02:00
QUERY ;
}
2025-06-03 12:51:20 +02:00
public function get_query_places ( $zone ) {
2025-06-03 16:19:07 +02:00
return ' [ out : json ][ timeout : 25 ];
2025-06-05 15:09:28 +02:00
area [ " ref:INSEE " = " '. $zone .' " ] ->. searchArea ;
2025-06-03 16:19:07 +02:00
'.$this->overpass_base_places.'
out center tags ; ' ;
2025-06-03 12:51:20 +02:00
}
2025-05-26 23:51:46 +02:00
private $more_tags = [ 'image' , 'ref:FR:SIRET' ];
2025-05-26 11:55:44 +02:00
public function __construct (
2025-05-26 12:57:10 +02:00
private HttpClientInterface $client ,
private EntityManagerInterface $entityManager
2025-05-26 11:55:44 +02:00
) {
}
2025-05-27 11:18:29 +02:00
public function map_post_values ( $request_post ) {
$has_ask_angela = false ;
$remove_ask_angela = false ;
2025-06-03 12:51:20 +02:00
$has_opening_hours = false ;
2025-05-27 11:18:29 +02:00
$modified_request_post = [];
foreach ( $request_post as $key => $value ) {
if ( strpos ( $key , 'custom__ask_angela' ) === 0 ) {
if ( $value == 'ask_angela' ){
$has_ask_angela = true ;
} else {
$remove_ask_angela = true ;
}
}
if ( strpos ( $key , 'custom__opening_hours' ) === 0 && $value != '' ) {
$has_opening_hours = true ;
}
$modified_request_post [ $key ] = $value ;
}
if ( $has_ask_angela ) {
$modified_request_post [ 'commerce_tag_value__harassment_prevention' ] = 'ask_angela' ;
}
if ( $remove_ask_angela ) {
unset ( $modified_request_post [ 'commerce_tag_value__harassment_prevention' ]);
}
if ( $has_opening_hours ) {
$modified_request_post [ 'commerce_tag_value__opening_hours' ] = $request_post [ 'commerce_tag_value__opening_hours' ];
}
return $modified_request_post ;
}
2025-05-26 23:51:46 +02:00
2025-05-26 11:55:44 +02:00
public function labourer ( string $zone ) : array
{
try {
$response = $this -> client -> request ( 'POST' , $this -> overpassApiUrl , [
2025-06-05 15:09:28 +02:00
'body' => [ 'data' => $this -> get_query_places ( $zone )]
2025-05-26 11:55:44 +02:00
]);
$data = json_decode ( $response -> getContent (), true );
2025-05-26 12:57:10 +02:00
$places = [];
2025-05-26 11:55:44 +02:00
if ( isset ( $data [ 'elements' ])) {
2025-06-05 15:43:11 +02:00
$batchSize = 100 ; // Traiter par lots de 100 éléments
$totalElements = count ( $data [ 'elements' ]);
for ( $i = 0 ; $i < $totalElements ; $i += $batchSize ) {
$batch = array_slice ( $data [ 'elements' ], $i , $batchSize );
foreach ( $batch as $element ) {
if ( isset ( $element [ 'tags' ])) {
$email = " " ;
$places [] = [
'id' => $element [ 'id' ],
'type' => $element [ 'type' ],
'name' => $element [ 'tags' ][ 'name' ] ? ? '' ,
'email' => $email ,
'lat' => $element [ 'lat' ] ? ? null ,
'lon' => $element [ 'lon' ] ? ? null ,
'tags' => $element [ 'tags' ]
];
}
2025-05-26 11:55:44 +02:00
}
2025-06-05 15:43:11 +02:00
// Libérer la mémoire après chaque lot
unset ( $batch );
gc_collect_cycles ();
2025-05-26 11:55:44 +02:00
}
}
2025-05-26 12:57:10 +02:00
return $places ;
2025-05-26 11:55:44 +02:00
} catch ( \Exception $e ) {
throw new \Exception ( " Erreur lors de la requête Overpass : " . $e -> getMessage ());
}
}
2025-06-03 12:51:20 +02:00
public function get_city_osm_from_zip_code ( $zip_code ) {
// Requête Overpass pour obtenir la zone administrative de niveau 8 avec un nom
$query = " [out:json][timeout:25];
2025-06-05 15:09:28 +02:00
area [ \ " ref:INSEE \" = \" { $zip_code } \" ]->.searchArea;
2025-06-03 12:51:20 +02:00
(
relation [ \ " admin_level \" = \" 8 \" ][ \" name \" ][ \" type \" = \" boundary \" ][ \" boundary \" = \" administrative \" ](area.searchArea);
);
out body ;
> ;
out skel qt ; " ;
2025-06-05 15:09:28 +02:00
2025-06-03 12:51:20 +02:00
$response = $this -> client -> request ( 'POST' , $this -> overpassApiUrl , [
'body' => [ 'data' => $query ]
]);
$data = json_decode ( $response -> getContent (), true );
if ( isset ( $data [ 'elements' ]) && ! empty ( $data [ 'elements' ])) {
$city = $data [ 'elements' ][ 0 ][ 'tags' ][ 'name' ];
return $city ;
}
return null ;
}
2025-05-26 12:57:10 +02:00
public function get_osm_object_data ( $osm_kind = 'node' , $osm_object_id = 12855459190 )
2025-05-26 11:55:44 +02:00
{
2025-05-26 12:57:10 +02:00
$object_id = " https://www.openstreetmap.org/api/0.6/ " . $osm_kind . " / " . $osm_object_id ;
2025-06-05 15:09:28 +02:00
2025-05-26 11:55:44 +02:00
try {
$response = $this -> client -> request ( 'GET' , $object_id );
$xml = simplexml_load_string ( $response -> getContent ());
$json = json_encode ( $xml );
$osm_object_data = json_decode ( $json , true );
} catch ( \Exception $e ) {
throw new \Exception ( " Impossible de récupérer les données OSM : " . $e -> getMessage ());
}
2025-05-26 23:51:46 +02:00
// convertir les tags en clés et valeurs, remplir avec les tags de base
$osm_object_data [ 'tags_converted' ] = $this -> base_tags ;
2025-05-26 12:57:10 +02:00
// Initialiser le tableau des tags convertis
if ( isset ( $osm_object_data [ 'node' ])) {
$osm_object_data [ 'node' ][ 'tags_converted' ] = [];
} elseif ( isset ( $osm_object_data [ 'way' ])) {
$osm_object_data [ 'way' ][ 'tags_converted' ] = [];
}
2025-05-29 13:24:50 +02:00
if ( isset ( $osm_object_data [ 'node' ])){
// Vérifier si le nœud a des tags
if ( ! isset ( $osm_object_data [ 'node' ][ 'tag' ])) {
$osm_object_data [ 'node' ][ 'tags_converted' ] = [];
return $osm_object_data [ 'node' ];
}
// Si un seul tag, le convertir en tableau
if ( isset ( $osm_object_data [ 'node' ][ 'tag' ][ '@attributes' ])) {
$osm_object_data [ 'node' ][ 'tag' ] = [ $osm_object_data [ 'node' ][ 'tag' ]];
}
foreach ( $osm_object_data [ 'node' ][ 'tag' ] as $tag ) {
$osm_object_data [ 'node' ][ 'tags_converted' ][ $tag [ '@attributes' ][ 'k' ]] = $tag [ '@attributes' ][ 'v' ];
2025-05-26 12:57:10 +02:00
}
2025-05-27 19:13:35 +02:00
$osm_object_data [ 'node' ][ 'tags_converted' ] = $this -> migrate_tags ( $osm_object_data [ 'node' ][ 'tags_converted' ]);
2025-05-26 12:57:10 +02:00
return $osm_object_data [ 'node' ];
}
if ( isset ( $osm_object_data [ 'way' ])){
2025-05-29 17:49:35 +02:00
// Vérifier si le way a des tags
if ( ! isset ( $osm_object_data [ 'way' ][ 'tag' ])) {
$osm_object_data [ 'way' ][ 'tags_converted' ] = [];
return $osm_object_data [ 'way' ];
}
// Si un seul tag, le convertir en tableau
if ( isset ( $osm_object_data [ 'way' ][ 'tag' ][ '@attributes' ])) {
$osm_object_data [ 'way' ][ 'tag' ] = [ $osm_object_data [ 'way' ][ 'tag' ]];
}
2025-05-26 12:57:10 +02:00
foreach ( $osm_object_data [ 'way' ][ 'tag' ] as $attribute ) {
$osm_object_data [ 'way' ][ 'tags_converted' ][ $attribute [ '@attributes' ][ 'k' ]] = $attribute [ '@attributes' ][ 'v' ];
}
2025-05-27 19:13:35 +02:00
$osm_object_data [ 'way' ][ 'tags_converted' ] = $this -> migrate_tags ( $osm_object_data [ 'way' ][ 'tags_converted' ]);
2025-05-26 12:57:10 +02:00
return $osm_object_data [ 'way' ];
2025-05-26 11:55:44 +02:00
}
2025-05-26 12:57:10 +02:00
return $osm_object_data ;
2025-05-26 11:55:44 +02:00
}
2025-05-26 12:57:10 +02:00
2025-05-26 11:55:44 +02:00
2025-06-03 18:07:57 +02:00
public function find_main_tag ( $tags ) {
if ( isset ( $tags [ 'amenity' ]) && $tags [ 'amenity' ] != '' ) {
return $tags [ 'amenity' ];
}
if ( isset ( $tags [ 'shop' ]) && $tags [ 'shop' ] != '' ) {
return $tags [ 'shop' ];
}
if ( isset ( $tags [ 'tourism' ]) && $tags [ 'tourism' ] != '' ) {
return $tags [ 'tourism' ];
}
if ( isset ( $tags [ 'healthcare' ]) && $tags [ 'healthcare' ] != '' ) {
return $tags [ 'healthcare' ];
}
if ( isset ( $tags [ 'office' ]) && $tags [ 'office' ] != '' ) {
return $tags [ 'office' ];
}
return null ;
}
2025-06-05 15:09:28 +02:00
2025-05-27 19:13:35 +02:00
public function migrate_tags ( $osm_object_data ) {
// migrer email vers contact:email
if ( isset ( $osm_object_data [ 'email' ]) && ! isset ( $osm_object_data [ 'contact:email' ])){
$osm_object_data [ 'contact:email' ] = $osm_object_data [ 'email' ];
unset ( $osm_object_data [ 'email' ]);
}
// migrer phone vers contact:phone
if ( isset ( $osm_object_data [ 'phone' ]) && ! isset ( $osm_object_data [ 'contact:phone' ])){
$osm_object_data [ 'contact:phone' ] = $osm_object_data [ 'phone' ];
unset ( $osm_object_data [ 'phone' ]);
}
// migrer website vers contact:website
if ( isset ( $osm_object_data [ 'website' ]) && ! isset ( $osm_object_data [ 'contact:website' ])){
$osm_object_data [ 'contact:website' ] = $osm_object_data [ 'website' ];
unset ( $osm_object_data [ 'website' ]);
}
2025-06-03 11:37:27 +02:00
// migrer addr:housenumber vers contact:housenumber
if ( isset ( $osm_object_data [ 'addr:housenumber' ]) && ! isset ( $osm_object_data [ 'contact:housenumber' ])){
$osm_object_data [ 'contact:housenumber' ] = $osm_object_data [ 'addr:housenumber' ];
unset ( $osm_object_data [ 'addr:housenumber' ]);
}
// migrer addr:street vers contact:street
if ( isset ( $osm_object_data [ 'addr:street' ]) && ! isset ( $osm_object_data [ 'contact:street' ])){
$osm_object_data [ 'contact:street' ] = $osm_object_data [ 'addr:street' ];
unset ( $osm_object_data [ 'addr:street' ]);
}
2025-05-27 19:13:35 +02:00
return $osm_object_data ;
}
2025-05-29 16:50:25 +02:00
public static function uuid_create_static () {
return $this -> uuid_create ();
}
2025-05-26 12:57:10 +02:00
public function uuid_create () {
return sprintf ( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x' ,
// 32 bits for "time_low"
mt_rand ( 0 , 0xffff ), mt_rand ( 0 , 0xffff ),
2025-05-26 11:55:44 +02:00
2025-05-26 12:57:10 +02:00
// 16 bits for "time_mid"
mt_rand ( 0 , 0xffff ),
// 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
mt_rand ( 0 , 0x0fff ) | 0x4000 ,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
mt_rand ( 0 , 0x3fff ) | 0x8000 ,
// 48 bits for "node"
mt_rand ( 0 , 0xffff ), mt_rand ( 0 , 0xffff ), mt_rand ( 0 , 0xffff )
);
}
2025-05-26 16:22:01 +02:00
public function formatOsmDataForSubmit ( array $data ) : array
{
// Garder uniquement les tags essentiels
$essentialTags = [
'name' ,
'opening_hours' ,
2025-06-05 15:09:28 +02:00
// 'phone',
2025-05-26 16:22:01 +02:00
'contact:email' ,
'contact:phone' ,
'website' ,
'contact:website' ,
'wheelchair' ,
'addr:housenumber' ,
'addr:street' ,
'addr:city' ,
'addr:postcode' ,
'amenity' ,
'shop' ,
'tourism' ,
'source' ,
'ref:FR:SIRET'
];
$formattedData = [
'node' => [
'@attributes' => [
'id' => $data [ '@attributes' ][ 'id' ],
'version' => $data [ '@attributes' ][ 'version' ],
'changeset' => $data [ '@attributes' ][ 'changeset' ],
'lat' => $data [ '@attributes' ][ 'lat' ],
'lon' => $data [ '@attributes' ][ 'lon' ]
],
'tag' => []
]
];
// Filtrer et ajouter uniquement les tags essentiels
if ( isset ( $data [ 'tag' ])) {
foreach ( $data [ 'tag' ] as $tag ) {
if ( in_array ( $tag [ '@attributes' ][ 'k' ], $essentialTags )) {
$formattedData [ 'node' ][ 'tag' ][] = $tag ;
}
}
}
return $formattedData ;
}
private function arrayToXml ( array $data ) : string
{
$xml = new \SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><osm></osm>' );
if ( isset ( $data [ 'node' ])) {
$node = $xml -> addChild ( 'node' );
foreach ( $data [ 'node' ][ '@attributes' ] as $key => $value ) {
$node -> addAttribute ( $key , $value );
}
if ( isset ( $data [ 'node' ][ 'tag' ])) {
foreach ( $data [ 'node' ][ 'tag' ] as $tag ) {
$tagElement = $node -> addChild ( 'tag' );
$tagElement -> addAttribute ( 'k' , $tag [ '@attributes' ][ 'k' ]);
$tagElement -> addAttribute ( 'v' , $tag [ '@attributes' ][ 'v' ]);
}
}
}
return $xml -> asXML ();
}
public function submitOsmData ( array $data ) : void
{
$formattedData = $this -> formatOsmDataForSubmit ( $data );
$xmlData = $this -> arrayToXml ( $formattedData );
try {
$response = $this -> client -> request ( 'PUT' ,
" { $this -> osmApiUrl } /node/ { $data [ '@attributes' ][ 'id' ] } " ,
[
'body' => $xmlData ,
'headers' => [
'Content-Type' => 'application/xml; charset=utf-8'
]
]
);
if ( $response -> getStatusCode () !== 200 ) {
throw new \Exception ( " Erreur lors de la soumission des données : " . $response -> getContent ());
}
} catch ( \Exception $e ) {
throw new \Exception ( " Erreur lors de la communication avec l'API OSM : " . $e -> getMessage ());
}
}
2025-05-26 23:51:46 +02:00
public function calculateStats ( array $places ) : array
{
$counters = [
'avec_horaires' => 0 ,
'avec_adresse' => 0 ,
'avec_site' => 0 ,
'avec_accessibilite' => 0 ,
'avec_note' => 0
];
foreach ( $places as $place ) {
if ( $place -> hasOpeningHours ()) {
$counters [ 'avec_horaires' ] ++ ;
}
if ( $place -> hasAddress ()) {
$counters [ 'avec_adresse' ] ++ ;
}
if ( $place -> hasWebsite ()) {
$counters [ 'avec_site' ] ++ ;
}
if ( $place -> hasWheelchair ()) {
$counters [ 'avec_accessibilite' ] ++ ;
}
if ( $place -> hasNote ()) {
$counters [ 'avec_note' ] ++ ;
}
}
$totalPlaces = count ( $places );
$completionPercent = 0 ;
if ( $totalPlaces > 0 ) {
$totalCriteria = 5 ; // nombre total de critères
$totalCompleted = array_sum ( $counters );
$completionPercent = round (( $totalCompleted / ( $totalPlaces * $totalCriteria )) * 100 );
}
return [
'places_count' => $totalPlaces ,
'completion_percent' => $completionPercent ,
'counters' => $counters
];
}
2025-05-26 11:55:44 +02:00
}