Removes user confusing geocoding multichoice.
[platal.git] / classes / address.php
1 <?php
2 /***************************************************************************
3 * Copyright (C) 2003-2011 Polytechnique.org *
4 * http://opensource.polytechnique.org/ *
5 * *
6 * This program is free software; you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation; either version 2 of the License, or *
9 * (at your option) any later version. *
10 * *
11 * This program is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU General Public License *
17 * along with this program; if not, write to the Free Software *
18 * Foundation, Inc., *
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20 ***************************************************************************/
21
22 /** Class Address is meant to perform most of the access to the table profile_addresses.
23 *
24 * profile_addresses describes an Address, which can be related to either a
25 * Profile, a Job or a Company:
26 * - for a Profile:
27 * - `type` is set to 'home'
28 * - `pid` is set to the related profile pid (in profiles)
29 * - `id` is the id of the address in the list of those related to that profile
30 * - `jobid` is set to 0
31 *
32 * - for a Company:
33 * - `type` is set to 'hq'
34 * - `pid` is set to 0
35 * - `jobid` is set to the id of the company (in profile_job_enum)
36 * - `id` is set to 0 (only one address per Company)
37 *
38 * - for a Job:
39 * - `type` is set to 'job'
40 * - `pid` is set to the pid of the Profile of the related Job (in both profiles and profile_job)
41 * - `id` is the id of the job to which we refer (in profile_job)
42 * - `jobid` is set to 0
43 *
44 * - for a Group:
45 * - `type` is set to 'group'
46 * - `pid` is set to 0
47 * - `jobid` is set to 0
48 * - `groupid` is set to the group id
49 *
50 * Thus an Address can be linked to a Company, a Profile, or a Job.
51 */
52 class Address
53 {
54 const LINK_JOB = 'job';
55 const LINK_COMPANY = 'hq';
56 const LINK_PROFILE = 'home';
57 const LINK_GROUP = 'group';
58
59 // List of all available postal formattings.
60 private static $formattings = array('FRANCE' => 'FR');
61
62 // Abbreviations to be used to format French postal addresses.
63 private static $streetAbbreviations = array(
64 'ALLEE' => 'ALL',
65 'AVENUE' => 'AV',
66 'BOULEVARD' => 'BD',
67 'CENTRE' => 'CTRE',
68 'CENTRE COMMERCIAL' => 'CCAL',
69 'IMMEUBLE' => 'IMM',
70 'IMMEUBLES' => 'IMM',
71 'IMPASSE' => 'IMP',
72 'LIEU-DIT' => 'LD',
73 'LOTISSEMENT' => 'LOT',
74 'PASSAGE' => 'PAS',
75 'PLACE' => 'PL',
76 'RESIDENCE' => 'RES',
77 'ROND-POINT' => 'RPT',
78 'ROUTE' => 'RTE',
79 'SQUARE' => 'SQ',
80 'VILLAGE' => 'VLGE',
81 'ZONE D\'ACTIVITE' => 'ZA',
82 'ZONE D\'AMENAGEMENT CONCERTE' => 'ZAC',
83 'ZONE D\'AMENAGEMENT DIFFERE' => 'ZAD',
84 'ZONE INDUSTRIELLE' => 'ZI'
85 );
86 private static $otherAbbreviations = array(
87 'ADJUDANT' => 'ADJ',
88 'AERODROME' => 'AERD',
89 'AEROGARE' => 'AERG',
90 'AERONAUTIQUE' => 'AERN',
91 'AEROPORT' => 'AERP',
92 'AGENCE' => 'AGCE',
93 'AGRICOLE' => 'AGRIC',
94 'ANCIEN' => 'ANC',
95 'ANCIENNEMENT' => 'ANC',
96 'APPARTEMENT' => 'APP',
97 'APPARTEMENTS' => 'APP',
98 'ARMEMENT' => 'ARMT',
99 'ARRONDISSEMENT' => 'ARR',
100 'ASPIRANT' => 'ASP',
101 'ASSOCIATION' => 'ASSOC',
102 'ASSURANCE' => 'ASSUR',
103 'ATELIER' => 'AT',
104 'BARAQUEMENT' => 'BRQ',
105 'BAS' => 'BAS',
106 'BASSE' => 'BAS',
107 'BASSES' => 'BAS',
108 'BATAILLON' => 'BTN',
109 'BATAILLONS' => 'BTN',
110 'BATIMENT' => 'BAT',
111 'BATIMENTS' => 'BAT',
112 'BIS' => 'B',
113 'BOITE POSTALE' => 'BP',
114 'CABINET' => 'CAB',
115 'CANTON' => 'CANT',
116 'CARDINAL' => 'CDL',
117 'CASE POSTALE' => 'CP',
118 'CHAMBRE' => 'CHBR',
119 'CITADELLE' => 'CTD',
120 'COLLEGE' => 'COLL',
121 'COLONEL' => 'CNL',
122 'COLONIE' => 'COLO',
123 'COMITE' => 'CTE',
124 'COMMANDANT' => 'CDT',
125 'COMMERCIAL' => 'CIAL',
126 'COMMUNE' => 'COM',
127 'COMMUNAL' => 'COM',
128 'COMMUNAUX' => 'COM',
129 'COMPAGNIE' => 'CIE',
130 'COMPAGNON' => 'COMP',
131 'COMPAGNONS' => 'COMP',
132 'COOPERATIVE' => 'COOP',
133 'COURSE SPECIALE' => 'CS',
134 'CROIX' => 'CRX',
135 'DELEGATION' => 'DELEG',
136 'DEPARTEMENTAL' => 'DEP',
137 'DEPARTEMENTAUX' => 'DEP',
138 'DIRECTEUR' => 'DIR',
139 'DIRECTECTION' => 'DIR',
140 'DIVISION' => 'DIV',
141 'DOCTEUR' => 'DR',
142 'ECONOMIE' => 'ECO',
143 'ECONOMIQUE' => 'ECO',
144 'ECRIVAIN' => 'ECRIV',
145 'ECRIVAINS' => 'ECRIV',
146 'ENSEIGNEMENT' => 'ENST',
147 'ENSEMBLE' => 'ENS',
148 'ENTREE' => 'ENT',
149 'ENTREES' => 'ENT',
150 'ENTREPRISE' => 'ENTR',
151 'EPOUX' => 'EP',
152 'EPOUSE' => 'EP',
153 'ETABLISSEMENT' => 'ETS',
154 'ETAGE' => 'ETG',
155 'ETAT MAJOR' => 'EM',
156 'EVEQUE' => 'EVQ',
157 'FACULTE' => 'FAC',
158 'FORET' => 'FOR',
159 'FORESTIER' => 'FOR',
160 'FRANCAIS' => 'FR',
161 'FRANCAISE' => 'FR',
162 'FUSILIER' => 'FUS',
163 'GENDARMERIE' => 'GEND',
164 'GENERAL' => 'GAL',
165 'GOUVERNEMENTAL' => 'GOUV',
166 'GOUVERNEUR' => 'GOU',
167 'GRAND' => 'GD',
168 'GRANDE' => 'GDE',
169 'GRANDES' => 'GDES',
170 'GRANDS' => 'GDS',
171 'HAUT' => 'HT',
172 'HAUTE' => 'HTE',
173 'HAUTES' => 'HTES',
174 'HAUTS' => 'HTS',
175 'HOPITAL' => 'HOP',
176 'HOPITAUX' => 'HOP',
177 'HOSPICE' => 'HOSP',
178 'HOSPITALIER' => 'HOSP',
179 'HOTEL' => 'HOT',
180 'INFANTERIE' => 'INFANT',
181 'INFERIEUR' => 'INF',
182 'INFERIEUR' => 'INF',
183 'INGENIEUR' => 'ING',
184 'INSPECTEUR' => 'INSP',
185 'INSTITUT' => 'INST',
186 'INTERNATIONAL' => 'INTERN',
187 'INTERNATIONALE' => 'INTERN',
188 'LABORATOIRE' => 'LABO',
189 'LIEUTENANT' => 'LT',
190 'LIEUTENANT DE VAISSEAU' => 'LTDV',
191 'MADAME' => 'MME',
192 'MADEMOISELLE' => 'MLLE',
193 'MAGASIN' => 'MAG',
194 'MAISON' => 'MAIS',
195 'MAITRE' => 'ME',
196 'MARECHAL' => 'MAL',
197 'MARITIME' => 'MAR',
198 'MEDECIN' => 'MED',
199 'MEDICAL' => 'MED',
200 'MESDAMES' => 'MMES',
201 'MESDEMOISELLES' => 'MLLES',
202 'MESSIEURS' => 'MM',
203 'MILITAIRE' => 'MIL',
204 'MINISTERE' => 'MIN',
205 'MONSEIGNEUR' => 'MGR',
206 'MONSIEUR' => 'M',
207 'MUNICIPAL' => 'MUN',
208 'MUTUEL' => 'MUT',
209 'NATIONAL' => 'NAL',
210 'NOTRE DAME' => 'ND',
211 'NOUVEAU' => 'NOUV',
212 'NOUVEL' => 'NOUV',
213 'NOUVELLE' => 'NOUV',
214 'OBSERVATOIRE' => 'OBS',
215 'PASTEUR' => 'PAST',
216 'PETIT' => 'PT',
217 'PETITE' => 'PTE',
218 'PETITES' => 'PTES',
219 'PETITS' => 'PTS',
220 'POLICE' => 'POL',
221 'PREFET' => 'PREF',
222 'PREFECTURE' => 'PREF',
223 'PRESIDENT' => 'PDT',
224 'PROFESSEUR' => 'PR',
225 'PROFESSIONNEL' => 'PROF',
226 'PROFESSIONNELE' => 'PROF',
227 'PROLONGE' => 'PROL',
228 'PROLONGEE' => 'PROL',
229 'PROPRIETE' => 'PROP',
230 'QUATER' => 'Q',
231 'QUINQUIES' => 'C',
232 'RECTEUR' => 'RECT',
233 'REGIMENT' => 'RGT',
234 'REGION' => 'REG',
235 'REGIONAL' => 'REG',
236 'REGIONALE' => 'REG',
237 'REPUBLIQUE' => 'REP',
238 'RESTAURANT' => 'REST',
239 'SAINT' => 'ST',
240 'SAINTE' => 'STE',
241 'SAINTES' => 'STES',
242 'SAINTS' => 'STS',
243 'SANATORIUM' => 'SANA',
244 'SERGENT' => 'SGT',
245 'SERVICE' => 'SCE',
246 'SOCIETE' => 'SOC',
247 'SOUS COUVERT' => 'SC',
248 'SOUS-PREFET' => 'SPREF',
249 'SUPERIEUR' => 'SUP',
250 'SUPERIEURE' => 'SUP',
251 'SYNDICAT' => 'SYND',
252 'TECHNICIEN' => 'TECH',
253 'TECHNICIENNE' => 'TECH',
254 'TECHNICIQUE' => 'TECH',
255 'TER' => 'T',
256 'TRI SERVICE ARRIVEE' => 'TSA',
257 'TUNNEL' => 'TUN',
258 'UNIVERSITAIRE' => 'UNVT',
259 'UNIVERSITE' => 'UNIV',
260 'VELODROME' => 'VELOD',
261 'VEUVE' => 'VVE',
262 'VIEILLE' => 'VIEL',
263 'VIEILLES' => 'VIEL',
264 'VIEUX' => 'VX'
265 );
266 private static $entrepriseAbbreviations = array(
267 'COOPERATIVE D\'UTILISATION DE MATERIEL AGRICOLE EN COMMUN' => 'CUMA',
268 'ETABLISSEMENT PUBLIC A CARACTERE INDUSTRIEL ET COMMERCIAL' => 'EPIC',
269 'ETABLISSEMENT PUBLIC ADMINISTRATIF' => 'EPA',
270 'GROUPEMENT AGRICOLE D\'EXPLOITATION EN COMMUN' => 'GAEC',
271 'GROUPEMENT D\'INTERET ECONOMIQUE' => 'GIE',
272 'GROUPEMENT D\'INTERET PUBLIC' => 'GIP',
273 'GROUPEMENT EUROPEEN D\'INTERET ECONOMIQUE' => 'GEIE',
274 'OFFICE PUBLIC D\'HABITATION A LOYER MODERE' => 'OPHLM',
275 'SOCIETE A RESPONSABILITE LIMITEE' => 'SARL',
276 'SOCIETE ANONYME' => 'SA',
277 'SOCIETE CIVILE DE PLACEMENT COLLECTIF IMMOBILIER' => 'SCPI',
278 'SOCIETE CIVILE PROFESSIONNELLE' => 'SCP',
279 'SOCIETE COOPERATIVE OUVRIERE DE PRODUCTION ET DE CREDIT' => 'SCOP',
280 'SOCIETE D\'AMENAGEMENT FONCIER ET D\'EQUIPEMENT RURAL' => 'SAFER',
281 'SOCIETE D\'ECONOMIE MIXTE' => 'SEM',
282 'SOCIETE D\'INTERET COLLECTIF AGRICOLE' => 'SICA',
283 'SOCIETE D\'INVESTISSEMENT A CAPITAL VARIABLE' => 'SICAV',
284 'SOCIETE EN NOM COLLECTIF' => 'SNC',
285 'SOCIETE IMMOBILIERE POUR LE COMMERCE ET L\'INDUSTRIE' => 'SICOMI',
286 'SOCIETE MIXTE D\'INTERET AGRICOLE' => 'SMIA',
287 'SYNDICAT INTERCOMMUNAL A VOCATION MULTIPLE' => 'SIVOM',
288 'SYNDICAT INTERCOMMUNAL A VOCATION UNIQUE' => 'SIVU'
289 );
290
291 // Primary key fields: the quadruplet ($pid, $jobid, $type, $id) defines a unique address.
292 public $pid = 0;
293 public $jobid = 0;
294 public $groupid = 0;
295 public $type = Address::LINK_PROFILE;
296 public $id = 0;
297
298 // Geocoding fields.
299 public $accuracy = 0;
300 public $text = '';
301 public $postalText = '';
302 public $postalCode = null;
303 public $localityId = null;
304 public $subAdministrativeAreaId = null;
305 public $administrativeAreaId = null;
306 public $localityName = null;
307 public $subAdministrativeAreaName = null;
308 public $administrativeAreaName = null;
309 public $localityNameLocal = null;
310 public $subAdministrativeAreaNameLocal = null;
311 public $administrativeAreaNameLocal = null;
312 public $countryId = null;
313 public $latitude = null;
314 public $longitude = null;
315 public $north = null;
316 public $south = null;
317 public $east = null;
318 public $west = null;
319
320 // Database's field required for both 'home' and 'job' addresses.
321 public $pub = 'ax';
322
323 // Database's fields required for 'home' addresses.
324 public $flags = null; // 'current', 'temporary', 'secondary', 'mail', 'cedex', 'deliveryIssue'
325 public $comment = null;
326 public $current = null;
327 public $temporary = null;
328 public $secondary = null;
329 public $mail = null;
330 public $deliveryIssue = null;
331
332 // Remaining fields that do not belong to profile_addresses.
333 public $phones = array();
334 public $error = false;
335 public $changed = 0;
336 public $removed = 0;
337
338 public function __construct(array $data = array())
339 {
340 if (count($data) > 0) {
341 foreach ($data as $key => $val) {
342 $this->$key = $val;
343 }
344 }
345
346 if (!is_null($this->flags)) {
347 $this->flags = new PlFlagSet($this->flags);
348 } else {
349 static $flags = array('current', 'temporary', 'secondary', 'mail', 'deliveryIssue');
350
351 $this->flags = new PlFlagSet();
352 foreach ($flags as $flag) {
353 if (!is_null($this->$flag) && ($this->$flag == 1 || $this->$flag == 'on')) {
354 $this->flags->addFlag($flag, 1);
355 $this->$flag = null;
356 }
357 $this->flags->addFlag('cedex', (strpos(strtoupper(preg_replace(array("/[0-9,\"'#~:;_\- ]/", "/\r\n/"),
358 array('', "\n"), $this->text)), 'CEDEX')) !== false);
359 }
360 }
361 }
362
363 public function setId($id)
364 {
365 $this->id = $id;
366 }
367
368 public function phones()
369 {
370 return $this->phones;
371 }
372
373 public function addPhone(Phone $phone)
374 {
375 if ($phone->link_type == Phone::LINK_ADDRESS && $phone->pid == $this->pid) {
376 $this->phones[$phone->uniqueId()] = $phone;
377 }
378 }
379
380 public function hasFlag($flag)
381 {
382 return ($this->flags != null && $this->flags->hasFlag($flag));
383 }
384
385 public function addFlag($flag)
386 {
387 $this->flags->addFlag($flag);
388 }
389
390 /** Auxilary function for formatting postal addresses.
391 * If the needle is found in the haystack, it notifies the substitution's
392 * success, modifies the length accordingly and returns either the matching
393 * substitution or the needle.
394 */
395 private function substitute($needle, $haystack, &$length, &$success, $trim = false)
396 {
397 if (array_key_exists($needle, $haystack)) {
398 $success = true;
399 $length -= (strlen($needle) - strlen($haystack[$needle]));
400 return $haystack[$needle];
401 } elseif ($trim) {
402 $success = true;
403 if (strlen($needle) > 4) {
404 $length -= (strlen($needle) - 4);
405 $needle = $needle{4};
406 }
407 }
408 return $needle;
409 }
410
411 /** Checks if the line corresponds to a French street line.
412 * A line is considered a French street line if it starts by between 1 and 4 numbers.
413 */
414 private function isStreetFR($line)
415 {
416 return preg_match('/^\d{1,4}\D/', $line);
417 }
418
419 /** Retrieves a French street number and slit the rest of the line into an array.
420 * @param $words: array containing the rest of the line (a word per cell).
421 * @param $line: line to consider.
422 * Returns the street number.
423 */
424 private function getStreetNumberFR(&$line)
425 {
426 // First we define numbers and separators.
427 $numberReq = '(\d{1,4})\s*(BIS|TER|QUATER|[A-Z])?';
428 $separatorReq = '\s*(?:\\|-|&|A|ET)?\s*';
429
430 // Then we retrieve the number(s) and the rest of the line.
431 // $matches contains:
432 // -0: the full patern, here the given line,
433 // -1: the number,
434 // -2: its optionnal quantifier,
435 // -3: an optionnal second number,
436 // -4: the second number's optionnal quantifier,
437 // -5: the rest of the line.
438 preg_match('/^' . $numberReq . '(?:' . $separatorReq . $numberReq . ')?\s+(.*)/', $line, $matches);
439 $number = $matches[1];
440 $line = $matches[5];
441
442 // If there is a precision on the address, we concatenate it to the number.
443 if ($matches[2] != '') {
444 $number .= $matches[2]{0};
445 } elseif ($matches[4] != '') {
446 $number .= $matches[4]{0};
447 }
448
449 return $number;
450 }
451
452 /** Checks if the line corresponds to a French locality line.
453 * A line is considered a French locality line if it starts by exactly a
454 * postal code of exactly 5 numbers.
455 */
456 private function isLocalityFR($line)
457 {
458 return preg_match('/^\d{5}\D/', $line);
459 }
460
461 /** Retrieves a French postal code and slit the rest of the line into an array.
462 * @param $words: array containing the rest of the line (a word per cell).
463 * @param $line: line to consider.
464 * Returns the postal code, and cuts it out from the line.
465 */
466 private function getPostalCodeFR(&$line)
467 {
468 $number = substr($line, 0, 5);
469 $line = trim(substr($line, 5));
470 return $number;
471 }
472
473 /** Returns the address formated for French postal use (cf AFNOR XPZ 10-011).
474 * A postal addresse containts at most 6 lines of at most 38 characters each:
475 * - addressee's identification ("MONSIEUR JEAN DURAND", "DURAND SA"…),
476 * - delivery point identification ("CHEZ TOTO APPARTEMENT 2", "SERVICE ACHAT"…),
477 * - building localisation complement ("ENTREE A BATIMENT DES JONQUILLES", "ZONE INDUSTRIELLE OUEST"…),
478 * - N° and street name ("25 RUE DES FLEURS", "LES VIGNES"…),
479 * - delivery service, street localisation complement ("BP 40122", "BP 40112 AREYRES"…),
480 * - postal code and locality or cedex code and cedex ("33500 LIBOURNE", "33506 LIBOURNE CEDEX"…).
481 * Punctuation must be removed, all leters must be uppercased.
482 * Both locality and street name must not take more than 32 characters.
483 *
484 * @param $arrayText: array containing the address to be formated, one
485 * address line per array line.
486 * @param $count: array size.
487 */
488 private function formatPostalAddressFR($arrayText)
489 {
490 // First removes country if any.
491 $count = count($arrayText);
492 if ($arrayText[$count - 1] == 'FRANCE') {
493 unset($arrayText[$count - 1]);
494 --$count;
495 }
496
497 // All the lines must have less than 38 characters but street and
498 // locality lines whose limit is 32 characters.
499 foreach ($arrayText as $lineNumber => $line) {
500 if ($isStreetLine = $this->isStreetFR($line)) {
501 $formattedLine = $this->getStreetNumberFR($line) . ' ';
502 $limit = 32;
503 } elseif ($this->isLocalityFR($line)) {
504 $formattedLine = $this->getPostalCodeFR($line) . ' ';
505 $limit = 32;
506 } else {
507 $formattedLine = '';
508 $limit = 38;
509 }
510
511 $words = explode(' ', $line);
512 $count = count($words);
513 $length = $count - 1;
514 foreach ($words as $word) {
515 $length += strlen($word);
516 }
517
518 // Checks is length is ok. Otherwise, we try to shorten words and
519 // update the length of the current line accordingly.
520 for ($i = 0; $i < $count && $length > $limit; ++$i) {
521 $success = false;
522 if ($isStreetLine) {
523 $sub = $this->substitute($words[$i], Address::$streetAbbreviations, $length, $success, ($i == 0));
524 }
525 // Entreprises' substitution are only suitable for the first two lines.
526 if ($lineNumber <= 2 && !$success) {
527 $sub = $this->substitute($words[$i], Address::$entrepriseAbbreviations, $length, $success);
528 }
529 if (!$success) {
530 $sub = $this->substitute($words[$i], Address::$otherAbbreviations, $length, $success);
531 }
532
533 $formattedLine .= $sub . ' ';
534 }
535 for (; $i < $count; ++$i) {
536 $formattedLine .= $words[$i] . ' ';
537 }
538 $arrayText[$lineNumber] = trim($formattedLine);
539 }
540
541 return implode("\n", $arrayText);
542 }
543
544 // Formats postal addresses.
545 // First erases punctuation, accents… Then uppercase the address and finally
546 // calls the country's dedicated formatting function.
547 public function formatPostalAddress()
548 {
549 // Performs rough formatting.
550 $text = mb_strtoupper(replace_accent($this->text));
551 $text = str_replace(array(',', ';', '.', ':', '!', '?', '"', '«', '»'), '', $text);
552 $text = preg_replace('/( |\t)+/', ' ', $text);
553 $arrayText = explode("\n", $text);
554 $arrayText = array_map('trim', $arrayText);
555
556 // Formats according to country rules. Thus we first identify the
557 // country, then apply corresponding formatting or translate country
558 // into default language.
559 $count = count($arrayText);
560 if (in_array(strtoupper($this->countryId), Address::$formattings)) {
561 $text = call_user_func(array($this, 'formatPostalAddress' . strtoupper($this->countryId)), $arrayText);
562 } else {
563 list($countryId, $country) = XDB::fetchOneRow('SELECT gc.iso_3166_1_a2, gc.country
564 FROM geoloc_countries AS gc
565 INNER JOIN geoloc_languages AS gl ON (gc.iso_3166_1_a2 = gl.iso_3166_1_a2)
566 WHERE gc.iso_3166_1_a2 = {?} OR gl.countryPlain = {?} OR gc.countryPlain = {?}',
567 $this->countryId, $arrayText[$count - 1], $arrayText[$count - 1]);
568 if (is_null($countryId)) {
569 $text = $this->formatPostalAddressFR($arrayText);
570 } elseif (in_array(strtoupper($countryId), Address::$formattings)) {
571 $text = call_user_func(array($this, 'formatPostalAddress' . strtoupper($countryId)), $arrayText);
572 } else {
573 $arrayText[$count - 1] = mb_strtoupper(replace_accent($country));
574 $text = implode("\n", $arrayText);
575 }
576 }
577
578 $this->postalText = $text;
579 }
580
581 public function format()
582 {
583 $this->text = trim($this->text);
584 $this->phones = Phone::formatFormArray($this->phones, $this->error, new ProfileVisibility($this->pub));
585 if ($this->removed == 1) {
586 if (!S::user()->checkPerms('directory_private') && Phone::hasPrivate($this->phones)) {
587 Platal::page()->trigWarning("L'adresse ne peut être supprimée car elle contient des informations pour lesquelles vous n'avez le droit d'édition.");
588 } else {
589 $this->text = '';
590 return true;
591 }
592 }
593
594 $this->formatPostalAddress();
595 if ($this->changed == 1) {
596 $gmapsGeocoder = new GMapsGeocoder();
597 $gmapsGeocoder->getGeocodedAddress($this);
598 }
599 foreach (array('administrativeArea', 'subAdministrativeArea', 'locality') as $area) {
600 Geocoder::getAreaId($this, $area);
601 }
602 if ($this->countryId == '') {
603 $this->countryId = null;
604 }
605
606 return true;
607 }
608
609 public function toFormArray()
610 {
611 $address = array(
612 'accuracy' => $this->accuracy,
613 'text' => $this->text,
614 'postalText' => $this->postalText,
615 'postalCode' => $this->postalCode,
616 'localityId' => $this->localityId,
617 'subAdministrativeAreaId' => $this->subAdministrativeAreaId,
618 'administrativeAreaId' => $this->administrativeAreaId,
619 'countryId' => $this->countryId,
620 'localityName' => $this->localityName,
621 'subAdministrativeAreaName' => $this->subAdministrativeAreaName,
622 'administrativeAreaName' => $this->administrativeAreaName,
623 'localityNameLocal' => $this->localityNameLocal,
624 'subAdministrativeAreaNameLocal' => $this->subAdministrativeAreaNameLocal,
625 'administrativeAreaNameLocal' => $this->administrativeAreaNameLocal,
626 'latitude' => $this->latitude,
627 'longitude' => $this->longitude,
628 'north' => $this->north,
629 'south' => $this->south,
630 'east' => $this->east,
631 'west' => $this->west,
632 'error' => $this->error,
633 'changed' => $this->changed,
634 'removed' => $this->removed,
635 );
636
637 if ($this->type == self::LINK_PROFILE || $this->type == self::LINK_JOB) {
638 $address['pub'] = $this->pub;
639 }
640 if ($this->type == self::LINK_PROFILE) {
641 static $flags = array('current', 'temporary', 'secondary', 'mail', 'cedex', 'deliveryIssue');
642
643 foreach ($flags as $flag) {
644 $address[$flag] = $this->flags->hasFlag($flag);
645 }
646 $address['comment'] = $this->comment;
647 $address['phones'] = Phone::formatFormArray($this->phones);
648 }
649
650 return $address;
651 }
652
653 private function toString()
654 {
655 $address = $this->text;
656 if ($this->type == self::LINK_PROFILE || $this->type == self::LINK_JOB) {
657 static $pubs = array('public' => 'publique', 'ax' => 'annuaire AX', 'private' => 'privé');
658 $address .= ' (affichage ' . $pubs[$this->pub];
659 }
660 if ($this->type == self::LINK_PROFILE) {
661 static $flags = array(
662 'current' => 'actuelle',
663 'temporary' => 'temporaire',
664 'secondary' => 'secondaire',
665 'mail' => 'conctactable par courier',
666 'deliveryIssue' => 'n\'habite pas à l\'adresse indiquée',
667 'cedex' => 'type cédex',
668 );
669
670 if (!$this->flags->hasFlag('temporary')) {
671 $address .= ', permanente';
672 }
673 if (!$this->flags->hasFlag('secondary')) {
674 $address .= ', principale';
675 }
676 foreach ($flags as $flag => $flagName) {
677 if ($this->flags->hasFlag($flag)) {
678 $address .= ', ' . $flagName;
679 }
680 }
681 if ($this->comment) {
682 $address .= ', commentaire : ' . $this->comment;
683 }
684 if ($phones = Phone::formArrayToString($this->phones)) {
685 $address .= ', ' . $phones;
686 }
687 } elseif ($this->type == self::LINK_JOB) {
688 $address .= ')';
689 }
690 return $address;
691 }
692
693 private function isEmpty()
694 {
695 return (!$this->text || $this->text == '');
696 }
697
698 public function save()
699 {
700 if (!$this->isEmpty()) {
701 XDB::execute('INSERT IGNORE INTO profile_addresses (pid, jobid, groupid, type, id, flags, accuracy,
702 text, postalText, postalCode, localityId,
703 subAdministrativeAreaId, administrativeAreaId,
704 countryId, latitude, longitude, pub, comment,
705 north, south, east, west)
706 VALUES ({?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?},
707 {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?})',
708 $this->pid, $this->jobid, $this->groupid, $this->type, $this->id, $this->flags, $this->accuracy,
709 $this->text, $this->postalText, $this->postalCode, $this->localityId,
710 $this->subAdministrativeAreaId, $this->administrativeAreaId,
711 $this->countryId, $this->latitude, $this->longitude,
712 $this->pub, $this->comment,
713 $this->north, $this->south, $this->east, $this->west);
714
715 if ($this->type == self::LINK_PROFILE) {
716 Phone::savePhones($this->phones, $this->pid, Phone::LINK_ADDRESS, $this->id);
717 }
718 }
719 }
720
721 public function delete()
722 {
723 XDB::execute('DELETE FROM profile_addresses
724 WHERE pid = {?} AND jobid = {?} AND groupid = {?} AND type = {?} AND id = {?}',
725 $this->pid, $this->jobid, $this->groupid, $this->type, $this->id);
726 }
727
728 static public function deleteAddresses($pid, $type, $jobid = null, $groupid = null, $deletePrivate = true)
729 {
730 $where = '';
731 if (!is_null($pid)) {
732 $where = XDB::format(' AND pid = {?}', $pid);
733 }
734 if (!is_null($jobid)) {
735 $where = XDB::format(' AND jobid = {?}', $jobid);
736 }
737 if (!is_null($groupid)) {
738 $where = XDB::format(' AND groupid = {?}', $groupid);
739 }
740 XDB::execute('DELETE FROM profile_addresses
741 WHERE type = {?}' . $where . (($deletePrivate) ? '' : ' AND pub IN (\'public\', \'ax\')'),
742 $type);
743 if ($type == self::LINK_PROFILE) {
744 Phone::deletePhones($pid, Phone::LINK_ADDRESS, null, $deletePrivate);
745 }
746 }
747
748 /** Saves addresses into the database.
749 * @param $data: an array of form formatted addresses.
750 * @param $pid, $type, $linkid: pid, type and id concerned by the update.
751 */
752 static public function saveFromArray(array $data, $pid, $type = self::LINK_PROFILE, $linkid = null, $savePrivate = true)
753 {
754 foreach ($data as $id => $value) {
755 if ($value['pub'] != 'private' || $savePrivate) {
756 if (!is_null($linkid)) {
757 $value['id'] = $linkid;
758 } else {
759 $value['id'] = $id;
760 }
761 if (!is_null($pid)) {
762 $value['pid'] = $pid;
763 }
764 if (!is_null($type)) {
765 $value['type'] = $type;
766 }
767 $address = new Address($value);
768 $address->save();
769 }
770 }
771 }
772
773 static private function formArrayWalk(array $data, $function, &$success = true, $requiresEmptyAddress = false)
774 {
775 $addresses = array();
776 foreach ($data as $item) {
777 $address = new Address($item);
778 $success = ($address->format() && $success);
779 if (!$address->isEmpty()) {
780 $addresses[] = call_user_func(array($address, $function));
781 }
782 }
783 if (count($address) == 0 && $requiresEmptyAddress) {
784 $address = new Address();
785 $addresses[] = call_user_func(array($address, $function));
786 }
787 return $addresses;
788 }
789
790 // Compares two addresses. First sort by publicity, then place primary
791 // addresses before secondary addresses.
792 static private function compare(array $a, array $b)
793 {
794 $value = ProfileVisibility::comparePublicity($a, $b);
795 if ($value == 0) {
796 if ($a['secondary'] != $b['secondary']) {
797 $value = $a['secondary'] ? 1 : -1;
798 }
799 }
800 return $value;
801 }
802
803 // Formats an array of form addresses into an array of form formatted addresses.
804 static public function formatFormArray(array $data, &$success = true)
805 {
806 $addresses = self::formArrayWalk($data, 'toFormArray', $success, true);
807
808 // Only a single address can be the profile's current address and she must have one.
809 $hasCurrent = false;
810 foreach ($addresses as $key => &$address) {
811 if (isset($address['current']) && $address['current']) {
812 if ($hasCurrent) {
813 $address['current'] = false;
814 } else {
815 $hasCurrent = true;
816 }
817 }
818 }
819 if (!$hasCurrent && count($value) > 0) {
820 foreach ($value as &$address) {
821 $address['current'] = true;
822 break;
823 }
824 }
825
826 usort($addresses, 'Address::compare');
827 return $addresses;
828 }
829
830 static public function formArrayToString(array $data)
831 {
832 return implode(', ', self::formArrayWalk($data, 'toString'));
833 }
834
835 static public function hasPrivate(array $addresses)
836 {
837 foreach ($addresses as $address) {
838 if ($address['pub'] == 'private') {
839 return true;
840 }
841 }
842 return false;
843 }
844
845 static public function iterate(array $pids = array(), array $types = array(),
846 array $jobids = array(), array $pubs = array())
847 {
848 return new AddressIterator($pids, $types, $jobids, $pubs);
849 }
850 }
851
852 /** Iterator over a set of Phones
853 *
854 * @param $pid, $type, $jobid, $pub
855 *
856 * The iterator contains the phones that correspond to the value stored in the
857 * parameters' arrays.
858 */
859 class AddressIterator implements PlIterator
860 {
861 private $dbiter;
862
863 public function __construct(array $pids, array $types, array $jobids, array $pubs)
864 {
865 $where = array();
866 if (count($pids) != 0) {
867 $where[] = XDB::format('(pa.pid IN {?})', $pids);
868 }
869 if (count($types) != 0) {
870 $where[] = XDB::format('(pa.type IN {?})', $types);
871 }
872 if (count($jobids) != 0) {
873 $where[] = XDB::format('(pa.jobid IN {?})', $jobids);
874 }
875 if (count($pubs) != 0) {
876 $where[] = XDB::format('(pa.pub IN {?})', $pubs);
877 }
878 $sql = 'SELECT pa.pid, pa.jobid, pa.type, pa.id, pa.flags,
879 pa.accuracy, pa.text, pa.postalText, pa.postalCode,
880 pa.localityId, pa.subAdministrativeAreaId,
881 pa.administrativeAreaId, pa.countryId,
882 pa.latitude, pa.longitude, pa.north, pa.south, pa.east, pa.west,
883 pa.pub, pa.comment,
884 gl.name AS locality, gl.nameLocal AS localityLocal,
885 gs.name AS subAdministrativeArea, gs.nameLocal AS subAdministrativeAreaLocal,
886 ga.name AS administrativeArea, ga.nameLocal AS administrativeAreaLocal,
887 gc.country
888 FROM profile_addresses AS pa
889 LEFT JOIN geoloc_localities AS gl ON (gl.id = pa.localityId)
890 LEFT JOIN geoloc_administrativeareas AS ga ON (ga.id = pa.administrativeAreaId)
891 LEFT JOIN geoloc_subadministrativeareas AS gs ON (gs.id = pa.subAdministrativeAreaId)
892 LEFT JOIN geoloc_countries AS gc ON (gc.iso_3166_1_a2 = pa.countryId)
893 ' . ((count($where) > 0) ? 'WHERE ' . implode(' AND ', $where) : '') . '
894 ORDER BY pa.pid, pa.jobid, pa.id';
895 $this->dbiter = XDB::iterator($sql);
896 }
897
898 public function next()
899 {
900 if (is_null($this->dbiter)) {
901 return null;
902 }
903 $data = $this->dbiter->next();
904 if (is_null($data)) {
905 return null;
906 }
907 // Adds phones to addresses.
908 $it = Phone::iterate(array($data['pid']), array(Phone::LINK_ADDRESS), array($data['id']));
909 while ($phone = $it->next()) {
910 $data['phones'][$phone->id] = $phone->toFormArray();
911 }
912 return new Address($data);
913 }
914
915 public function total()
916 {
917 return $this->dbiter->total();
918 }
919
920 public function first()
921 {
922 return $this->dbiter->first();
923 }
924
925 public function last()
926 {
927 return $this->dbiter->last();
928 }
929
930 public function value()
931 {
932 return $this->dbiter;
933 }
934 }
935
936 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
937 ?>