Fixes erroneous address formatting.
[platal.git] / classes / address.php
1 <?php
2 /***************************************************************************
3 * Copyright (C) 2003-2010 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 // Primary key fields: the quadruplet ($pid, $jobid, $type, $id) defines a unique address.
53 public $pid = 0;
54 public $jobid = 0;
55 public $type = Address::LINK_PROFILE;
56 public $id = 0;
57
58 // Geocoding fields.
59 public $accuracy = 0;
60 public $text = '';
61 public $postalText = '';
62 public $postalCode = null;
63 public $localityId = null;
64 public $subAdministrativeAreaId = null;
65 public $administrativeAreaId = null;
66 public $localityName = null;
67 public $subAdministrativeAreaName = null;
68 public $administrativeAreaName = null;
69 public $countryId = null;
70 public $latitude = null;
71 public $longitude = null;
72 public $north = null;
73 public $south = null;
74 public $east = null;
75 public $west = null;
76 public $geocodedText = null;
77 public $geocodeChosen = null;
78
79 // Database's field required for both 'home' and 'job' addresses.
80 public $pub = 'ax';
81
82 // Database's fields required for 'home' addresses.
83 public $flags = null; // 'current', 'temporary', 'secondary', 'mail', 'cedex', 'deliveryIssue'
84 public $comment = null;
85 public $current = null;
86 public $temporary = null;
87 public $secondary = null;
88 public $mail = null;
89 public $deliveryIssue = null;
90
91 // Remaining fields that do not belong to profile_addresses.
92 public $phones = array();
93 public $error = false;
94 public $changed = 0;
95 public $removed = 0;
96
97 public function __construct(array $data = array())
98 {
99 if (count($data) > 0) {
100 foreach ($data as $key => $val) {
101 $this->$key = $val;
102 }
103 }
104
105 if (!is_null($this->flags)) {
106 $this->flags = new PlFlagSet($this->flags);
107 } else {
108 static $flags = array('current', 'temporary', 'secondary', 'mail', 'deliveryIssue');
109
110 $this->flags = new PlFlagSet();
111 foreach ($flags as $flag) {
112 if (!is_null($this->$flag) && ($this->$flag == 1 || $this->$flag == 'on')) {
113 $this->flags->addFlag($flag, 1);
114 $this->$flag = null;
115 }
116 $this->flags->addFlag('cedex', (strpos(strtoupper(preg_replace(array("/[0-9,\"'#~:;_\- ]/", "/\r\n/"),
117 array('', "\n"), $this->text)), 'CEDEX')) !== false);
118 }
119 }
120 }
121
122 public function setId($id)
123 {
124 $this->id = $id;
125 }
126
127 public function phones()
128 {
129 return $this->phones;
130 }
131
132 public function addPhone(Phone &$phone)
133 {
134 if ($phone->linkType() == Phone::LINK_ADDRESS && $phone->pid() == $this->pid) {
135 $this->phones[$phone->uniqueId()] = $phone;
136 }
137 }
138
139 public function hasFlag($flag)
140 {
141 return ($this->flags != null && $this->flags->hasFlag($flag));
142 }
143
144 // Returns the address formated for postal use.
145 // The main rules are (cf AFNOR XPZ 10-011):
146 // -everything in upper case;
147 // -if there are more then than 38 characters in a line, split it;
148 // -if there are more then than 32 characters in the description of the "street", use abbreviations.
149 public function formatPostalAddress() {
150 static $abbreviations = array(
151 'IMPASSE' => 'IMP',
152 'RUE' => 'R',
153 'AVENUE' => 'AV',
154 'BOULEVARD' => 'BVD',
155 'ROUTE' => 'R',
156 'STREET' => 'ST',
157 'ROAD' => 'RD',
158 );
159
160 $text = strtoupper($text);
161 $arrayText = explode("\n", $text);
162 $postalText = '';
163
164 foreach ($arrayText as $i => $line) {
165 $postalText .= (($i == 0) ? '' : "\n");
166 if (($length = strlen($line)) > 32) {
167 $words = explode(' ', $line);
168 $count = 0;
169 foreach ($words as $word) {
170 if (isset($abbreviations[$word])) {
171 $word = $abbreviations[$word];
172 }
173 if ($count + ($wordLength = strlen($word)) <= 38) {
174 $postalText .= (($count == 0) ? '' : ' ') . $word;
175 $count += (($count == 0) ? 0 : 1) + $wordLength;
176 } else {
177 $postalText .= "\n" . $word;
178 $count = strlen($word);
179 }
180 }
181 } else {
182 $postalText .= $line;
183 }
184 }
185 $this->postalText = $postalText;
186 }
187
188 public function format(array $format = array())
189 {
190 if (empty($format)) {
191 $format['requireGeocoding'] = false;
192 $format['stripGeocoding'] = false;
193 $format['postalText'] = false;
194 }
195 $this->text = trim($this->text);
196 if ($this->removed == 1) {
197 $this->text = '';
198 return true;
199 }
200
201 if ($format['requireGeocoding'] || $this->changed == 1) {
202 $gmapsGeocoder = new GMapsGeocoder();
203 $gmapsGeocoder->getGeocodedAddress($this);
204 $this->changed = 0;
205 $this->error = !empty($this->geocodedText);
206 }
207 if ($format['stripGeocoding'] || ($this->type == self::LINK_COMPANY && $this->error) || $this->geocodeChosen === '0') {
208 $gmapsGeocoder = new GMapsGeocoder();
209 $gmapsGeocoder->stripGeocodingFromAddress($this);
210 if ($this->geocodeChosen === '0') {
211 $mailer = new PlMailer('profile/geocoding.mail.tpl');
212 $mailer->assign('text', $this->text);
213 $mailer->assign('geoloc', $this->geocodedText);
214 $mailer->send();
215 }
216 }
217 if ($format['postalText']) {
218 $this->formatPostalAddress();
219 }
220 if ($this->countryId == '') {
221 $this->countryId = null;
222 }
223 $this->geocodeChosen = null;
224 $this->phones = Phone::formatFormArray($this->phones, $this->error);
225 return !$this->error;
226 }
227
228 public function toFormArray()
229 {
230 $address = array(
231 'accuracy' => $this->accuracy,
232 'text' => $this->text,
233 'postalText' => $this->postalText,
234 'postalCode' => $this->postalCode,
235 'localityId' => $this->localityId,
236 'subAdministrativeAreaId' => $this->subAdministrativeAreaId,
237 'administrativeAreaId' => $this->administrativeAreaId,
238 'countryId' => $this->countryId,
239 'localityName' => $this->localityName,
240 'subAdministrativeAreaName' => $this->subAdministrativeAreaName,
241 'administrativeAreaName' => $this->administrativeAreaName,
242 'latitude' => $this->latitude,
243 'longitude' => $this->longitude,
244 'north' => $this->north,
245 'south' => $this->south,
246 'east' => $this->east,
247 'west' => $this->west,
248 'error' => $this->error,
249 'changed' => $this->changed,
250 'removed' => $this->removed,
251 );
252 if (!is_null($this->geocodedText)) {
253 $address['geocodedText'] = $this->geocodedText;
254 $address['geocodeChosen'] = $this->geocodeChosen;
255 }
256
257 if ($this->type == self::LINK_PROFILE || $this->type == self::LINK_JOB) {
258 $address['pub'] = $this->pub;
259 }
260 if ($this->type == self::LINK_PROFILE) {
261 static $flags = array('current', 'temporary', 'secondary', 'mail', 'cedex', 'deliveryIssue');
262
263 foreach ($flags as $flag) {
264 $address[$flag] = $this->flags->hasFlag($flag);
265 }
266 $address['comment'] = $this->comment;
267 $address['phones'] = Phone::formatFormArray($this->phones);
268 }
269
270 return $address;
271 }
272
273 private function toString()
274 {
275 $address = 'Adresse : ' . $this->text;
276 if ($this->type == self::LINK_PROFILE || $this->type == self::LINK_JOB) {
277 $address .= ', affichage : ' . $this->pub;
278 }
279 if ($this->type == self::LINK_PROFILE) {
280 static $flags = array(
281 'current' => 'actuelle',
282 'temporary' => 'temporaire',
283 'secondary' => 'secondaire',
284 'mail' => 'conctactable par courier',
285 'deliveryIssue' => 'n\'habite pas à l\'adresse indiquée',
286 'cedex' => 'type cédex',
287 );
288
289 $address .= ', commentaire : ' . $this->comment;
290 foreach ($flags as $flag => $flagName) {
291 if ($this->flags->hasFlag($flag)) {
292 $address .= ', ' . $flagName;
293 }
294 }
295 if ($phones = Phone::formArrayToString($this->phones)) {
296 $address .= ', ' . $phones;
297 }
298 }
299 return $address;
300 }
301
302 private function isEmpty()
303 {
304 return (!$this->text || $this->text == '');
305 }
306
307 public function save()
308 {
309 static $areas = array('administrativeArea', 'subAdministrativeArea', 'locality');
310
311 $this->format(array('postalText' => true));
312 if (!$this->isEmpty()) {
313 foreach ($areas as $area) {
314 Geocoder::getAreaId($this, $area);
315 }
316
317 XDB::execute('INSERT INTO profile_addresses (pid, jobid, type, id, flags, accuracy,
318 text, postalText, postalCode, localityId,
319 subAdministrativeAreaId, administrativeAreaId,
320 countryId, latitude, longitude, pub, comment,
321 north, south, east, west)
322 VALUES ({?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?},
323 {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?})',
324 $this->pid, $this->jobid, $this->type, $this->id, $this->flags, $this->accuracy,
325 $this->text, $this->postalText, $this->postalCode, $this->localityId,
326 $this->subAdministrativeAreaId, $this->administrativeAreaId,
327 $this->countryId, $this->latitude, $this->longitude,
328 $this->pub, $this->comment,
329 $this->north, $this->south, $this->east, $this->west);
330
331 if ($this->type == self::LINK_PROFILE) {
332 Phone::savePhones($this->phones, $this->pid, Phone::LINK_ADDRESS, $this->id);
333 }
334 }
335 }
336
337 public function delete()
338 {
339 XDB::execute('DELETE FROM profile_addresses
340 WHERE pid = {?} AND jobid = {?} AND type = {?} AND id = {?}',
341 $this->pid, $this->jobid, $this->type, $this->id);
342 }
343
344 static public function deleteAddresses($pid, $type, $jobid = null)
345 {
346 $where = '';
347 if (!is_null($pid)) {
348 $where = XDB::format(' AND pid = {?}', $pid);
349 }
350 if (!is_null($jobid)) {
351 $where = XDB::format(' AND jobid = {?}', $jobid);
352 }
353 XDB::execute('DELETE FROM profile_addresses
354 WHERE type = {?}' . $where,
355 $type);
356 if ($type == self::LINK_PROFILE) {
357 Phone::deletePhones($pid, Phone::LINK_ADDRESS);
358 }
359 }
360
361 /** Saves addresses into the database.
362 * @param $data: an array of form formatted addresses.
363 * @param $pid, $type, $linkid: pid, type and id concerned by the update.
364 */
365 static public function saveFromArray(array $data, $pid, $type = self::LINK_PROFILE, $linkid = null)
366 {
367 foreach ($data as $id => $value) {
368 if (!is_null($linkid)) {
369 $value['id'] = $linkid;
370 } else {
371 $value['id'] = $id;
372 }
373 if (!is_null($pid)) {
374 $value['pid'] = $pid;
375 }
376 if (!is_null($type)) {
377 $value['type'] = $type;
378 }
379 $address = new Address($value);
380 $address->save();
381 }
382 }
383
384 static private function formArrayWalk(array $data, $function, &$success = true, $requiresEmptyAddress = false)
385 {
386 $addresses = array();
387 foreach ($data as $item) {
388 $address = new Address($item);
389 $success = ($address->format() && $success);
390 if (!$address->isEmpty()) {
391 $addresses[] = call_user_func(array($address, $function));
392 }
393 }
394 if (count($address) == 0 && $requiresEmptyAddress) {
395 $address = new Address();
396 $addresses[] = call_user_func(array($address, $function));
397 }
398 return $addresses;
399 }
400
401 // Formats an array of form addresses into an array of form formatted addresses.
402 static public function formatFormArray(array $data, &$success = true)
403 {
404 // Only a single address can be the profile's current address and she must have one.
405 $hasCurrent = false;
406 foreach ($data as $key => &$address) {
407 if (isset($address['current']) && $address['current']) {
408 if ($hasCurrent) {
409 $address['current'] = false;
410 } else {
411 $hasCurrent = true;
412 }
413 }
414 }
415 if (!$hasCurrent && count($value) > 0) {
416 foreach ($value as &$address) {
417 $address['current'] = true;
418 break;
419 }
420 }
421
422 return self::formArrayWalk($data, 'toFormArray', $success, true);
423 }
424
425 static public function formArrayToString(array $data)
426 {
427 return implode(' ; ', self::formArrayWalk($data, 'toString'));
428 }
429
430 static public function iterate(array $pids = array(), array $types = array(),
431 array $jobids = array(), array $pubs = array())
432 {
433 return new AddressIterator($pids, $types, $jobids, $pubs);
434 }
435 }
436
437 /** Iterator over a set of Phones
438 *
439 * @param $pid, $type, $jobid, $pub
440 *
441 * The iterator contains the phones that correspond to the value stored in the
442 * parameters' arrays.
443 */
444 class AddressIterator implements PlIterator
445 {
446 private $dbiter;
447
448 public function __construct(array $pids, array $types, array $jobids, array $pubs)
449 {
450 $where = array();
451 if (count($pids) != 0) {
452 $where[] = XDB::format('(pa.pid IN {?})', $pids);
453 }
454 if (count($types) != 0) {
455 $where[] = XDB::format('(pa.type IN {?})', $types);
456 }
457 if (count($jobids) != 0) {
458 $where[] = XDB::format('(pa.jobid IN {?})', $jobids);
459 }
460 if (count($pubs) != 0) {
461 $where[] = XDB::format('(pa.pub IN {?})', $pubs);
462 }
463 $sql = 'SELECT pa.pid, pa.jobid, pa.type, pa.id, pa.flags,
464 pa.accuracy, pa.text, pa.postalText, pa.postalCode,
465 pa.localityId, pa.subAdministrativeAreaId,
466 pa.administrativeAreaId, pa.countryId,
467 pa.latitude, pa.longitude, pa.north, pa.south, pa.east, pa.west,
468 pa.pub, pa.comment,
469 gl.name AS locality, gs.name AS subAdministrativeArea,
470 ga.name AS administrativeArea, gc.countryFR AS country
471 FROM profile_addresses AS pa
472 LEFT JOIN geoloc_localities AS gl ON (gl.id = pa.localityId)
473 LEFT JOIN geoloc_administrativeareas AS ga ON (ga.id = pa.administrativeAreaId)
474 LEFT JOIN geoloc_subadministrativeareas AS gs ON (gs.id = pa.subAdministrativeAreaId)
475 LEFT JOIN geoloc_countries AS gc ON (gc.iso_3166_1_a2 = pa.countryId)
476 ' . ((count($where) > 0) ? 'WHERE ' . implode(' AND ', $where) : '') . '
477 ORDER BY pa.pid, pa.jobid, pa.id';
478 $this->dbiter = XDB::iterator($sql);
479 }
480
481 public function next()
482 {
483 if (is_null($this->dbiter)) {
484 return null;
485 }
486 $data = $this->dbiter->next();
487 if (is_null($data)) {
488 return null;
489 }
490 // Adds phones to addresses.
491 $it = Phone::iterate(array($data['pid']), array(Phone::LINK_ADDRESS), array($data['id']));
492 while ($phone = $it->next()) {
493 $data['phones'][$phone->id()] = $phone->toFormArray();
494 }
495 return new Address($data);
496 }
497
498 public function total()
499 {
500 return $this->dbiter->total();
501 }
502
503 public function first()
504 {
505 return $this->dbiter->first();
506 }
507
508 public function last()
509 {
510 return $this->dbiter->last();
511 }
512
513 public function value()
514 {
515 return $this->dbiter;
516 }
517 }
518
519 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
520 ?>