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