Project

General

Profile

« Previous | Next » 

Revision 2cd9a3d6

Added by Andreas Kohlbecker over 14 years ago

image gallery

View differences:

modules/cdm_dataportal/cdm_dataportal.theme.php
15 15
  * see INSTALL.txt
16 16
  *
17 17
  */
18
  drupal_add_js(drupal_get_path('module', 'cdm_dataportal').'/js/jquery.imagetool.min.js');
19
  drupal_add_js(drupal_get_path('module', 'cdm_dataportal').'/js/thickbox.js');
20
  drupal_add_css(drupal_get_path('module', 'cdm_dataportal').'/js/cdm_thickbox.css');
18
  //drupal_add_js(drupal_get_path('module', 'cdm_dataportal').'/js/jquery.imagetool.min.js');
19
  drupal_add_js(drupal_get_path('module', 'cdm_dataportal').'/js/thickbox/thickbox.js');
20
  drupal_add_css(drupal_get_path('module', 'cdm_dataportal').'/js/thickbox/cdm_thickbox.css');
21 21
}
22 22

  
23
function _add_js_lightbox($galleryID){
24
  $lightBoxBasePath = drupal_get_path('module', 'cdm_dataportal') .'/js/jquery-lightbox-0.5';
25
  drupal_add_js($lightBoxBasePath.'/js/jquery.lightbox-0.5.js');
26
  drupal_add_css($lightBoxBasePath.'/css/jquery.lightbox-0.5.css');
27
  drupal_add_js ('$(document).ready(function() {
28
      $(\'#'.$galleryID.' a.lightbox\').lightBox({
29
        fixedNavigation:  true,
30
        imageLoading:     \''.$lightBoxBasePath.'/images/lightbox-ico-loading.gif\', 
31
        imageBtnPrev:     \''.$lightBoxBasePath.'/images/lightbox-btn-prev.gif\',    
32
        imageBtnNext:     \''.$lightBoxBasePath.'/images/lightbox-btn-next.gif\',   
33
        imageBtnClose:    \''.$lightBoxBasePath.'/images/lightbox-btn-close.gif\',  
34
        imageBlank:       \''.$lightBoxBasePath.'/images/lightbox-blank.gif\'
35
      });
36
    });', 'inline');
37
}
38

  
39

  
23 40
function _add_js_cluetip(){
24 41
  
25 42
  //TODO replace by http://www.socialembedded.com/labs/jQuery-Tooltip-Plugin/jQuery-Tooltip-Plugin.html
......
257 274
  return $out;
258 275
}
259 276

  
260
function theme_cdm_media_gallerie($mediaList, $maxExtend, $cols = 4, $maxRows = 1, $mediaLinks = null, $moreLink = null ){
277

  
278
function theme_cdm_media_caption($media, $elements = array('title', 'description', 'file', 'filename'), $fileUri = null){
279
  $out = '<div class="media_caption">';
280
  if(isset($elements['title']) && $media->title_L10n){
281
    $out .= '<span class="title">'.$media->title_L10n.'</span>';
282
  }
283
  if(isset($elements['description']) && $media->description_L10n){
284
    $out .= '<span class="description">'.$media->description_L10n.'</span>';
285
  }
286
  if(isset($elements['file'])){
287
    $out .= '<span class="file">'.$media->title_L10n.'</span>';
288
  }
289
  if(isset($elements['filename']) && $fileUri){
290
    $filename = substr($fileUri, strrpos($fileUri, "/")+1);
291
    $out .= '<span class="filename">'.$filename.'</span>';
292
  }
293
  $out .= '</div>';
294
  return $out;
295
}
296

  
297
/**
298
 * @param $mediaList an array of Media entities
299
 * @param $maxExtend
300
 * @param $cols
301
 * @param $maxRows
302
 * @param $mediaLinkType valid values: 
303
 *      "NONE": do not link the images, 
304
 *      "LIGHTBOX": open the link in a light box,
305
 *      "IMAGEPAGE": link to the image page
306
 * @param $alternativeMediaUri an array of alternative URIs to link the images wich will overwrite the URIs of the media parts. 
307
 *     The order of URI in this array must correspond with the order of images in $mediaList
308
 * @param $moreLink an URI to link the the hint on more images to; if null no link is created
309
 * @return unknown_type
310
 */
311
function theme_cdm_media_gallerie($mediaList, $galleryName, $maxExtend = 150, $cols = 4, $maxRows = false, $captionElements = array('title'),
312
    $mediaLinkType = 'LIGHTBOX', $alternativeMediaUri = null, $moreLinkUri = null ){
313
      
314
  //TODO correctly handle multiple media representation parts
261 315
  
316
  // prevent from errors
262 317
  if(!isset($mediaList[0])){
263 318
    return;
264 319
  }
265
  $out = '<table class="media_gallerie">';
266
  for($r = 0; $r < $maxRows && count($mediaList) > 0; $r++){
320
  
321
  $galleryID = "media_gallery_".$galleryName;
322
  
323
  // prepare media links
324
  $doLink = false;
325
  $linkAttributes = null;
326
  if($mediaLinkType = 'LIGHTBOX'){
327
    $doLink = true;
328
    //_add_js_thickbox();
329
    //$linkAttributes = array("class"=>"thickbox", "rel"=>"media_gallerie".$galleryName);
330
    _add_js_lightbox($galleryID);
331
    $linkAttributes = array("class"=>"lightbox");
332
  }
333
  
334
  // render the media gallery grid 
335
  $out = '<table id="'.$galleryID.'" class="media_gallery">';
267 336
    $out .= '<colgroup>';
268
    for($r = 0; $r < $cols; $r++){
337
    for($c = 0; $c < $cols; $c++){
269 338
      $out .= '<col width="'.(100 / $cols).'%">';
270 339
    }
271 340
    $out .= '</colgroup>';
272
  }
273
  for($r = 0; $r < $maxRows && count($mediaList) > 0; $r++){
341
    
342
  $mediaIndex = 0;
343
  for($r = 0; ($r < $maxRows || !$maxRows) && count($mediaList) > 0; $r++){
344
    $captionParts = array();
274 345
    $out .= '<tr>';  
275
    for($r = 0; $r < $cols; $r++){
346
    for($c = 0; $c < $cols; $c++){
276 347
      $media = array_shift($mediaList);
277 348
      if(isset($media->representations[0]->parts[0])){
278 349
        $contentTypeDirectory = substr($media->representations[0]->mimeType, 0, stripos($media->representations[0]->mimeType, '/'));
279
        $mediaPartHtml = theme('cdm_media_gallerie_'.$contentTypeDirectory, $media->representations[0], $maxExtend);
280
//        if($mediaPartHtml){
281
//          '<img src="" width="'.$maxExtend.'" height="'.$maxExtend.'" />';
282
//        }
350
        $mediaIndex++;
351
        $mediaPartHtml = theme('cdm_media_gallerie_'.$contentTypeDirectory, $media->representations[0]->parts[0], $maxExtend, TRUE);
352
        
353
        // --- compose Media Link
354
        $mediaLinkUri = false;
355
        if($alternativeMediaUri){
356
          if(isset($alternativeMediaUri[$mediaIndex])){
357
            $mediaLinkUri = $alternativeMediaUri[$mediaIndex];
358
          }
359
          if(is_string($alternativeMediaUri)){
360
            $mediaLinkUri = $alternativeMediaUri;
361
          }
362
        } else {
363
          $mediaLinkUri = $media->representations[0]->parts[0]->uri;
364
        }
365
        $linkAttributes['title'] = ($media->title_L10n ? $media->title_L10n : '')
366
            .($media->title_L10n && $media->description_L10n ? ' - ' : '')
367
            .($media->description_L10n ? $media->description_L10n : '');
368
        
369
        // --- assemble captions
370
        if(isset($media->representations[0]->parts[0]->uri)){
371
          $fileUri = $media->representations[0]->parts[0]->uri;
372
        }
373
        $captionPartHtml = theme('cdm_media_caption', $media, $captionElements, $fileUri);
374
        if(isset($captionElements['#uri'])){
375
          $captionPartHtml .= l($captionElements['#uri'], path_to_media($media->uuid), null, null, null, FALSE, TRUE);
376
        }
377
        $captionParts[] = $captionPartHtml;
378
        
379
        // --- surround imagePart with link
380
        if($doLink){
381
          $mediaPartHtml = l($mediaPartHtml, $mediaLinkUri, $linkAttributes, null, null, FALSE, TRUE);
382
        }
383
        
283 384
      } else {
284 385
        $mediaPartHtml = '';
386
        $captionParts[] = '';
285 387
      }
286
      $out .= '<td>'.$mediaPartHtml.'</td>';    
388
      $out .= '<td>'.$mediaPartHtml.'</td>';
389
    }
390
    $out .= '</tr>';
391
    if(isset($captionElements[0])){
392
      $out .= '<tr>';
393
      // add caption row
394
      foreach($captionParts as $captionPartHtml){
395
        $out .= '<td>'.$captionPartHtml.'</td>';
396
      }
397
      $out .= '</tr>';  
287 398
    }
288
    $out .= '</tr>';  
289 399
  }
290 400
  if(count($mediaList) > 0){
291
     $out .= '<tr><td colspan="'.$cols.'">'.count($mediaList).' '.t('more ...').'</td></tr>';
401
     $moreHtml = count($mediaList).' '.t('more ...');
402
     if($moreLinkUri){
403
       $moreHtml = l($moreHtml, $moreLinkUri);
404
     }
405
     $out .= '<tr><td colspan="'.$cols.'">'.$moreHtml.'</td></tr>';
292 406
  }
293 407
  $out .= '</table>';
294 408
  return $out;
295 409
}
296 410

  
297
function theme_cdm_media_gallerie_image($mediaRepresentation, $maxExtend){
298
  if(isset($mediaRepresentation->parts[0])){
299
    return  '<img src="'.$mediaRepresentation->parts[0]->uri.'" width="'.$maxExtend.'" height="'.$maxExtend.'" />';
411
function theme_cdm_media_gallerie_image($mediaRepresentationPart, $maxExtend, $addPassePartout = FALSE, $attributes = null){
412
  //TODO merge with theme_cdm_media_mime_image?
413
  
414
  if(isset($mediaRepresentationPart)){
415
    $h = $mediaRepresentationPart->height;
416
    $w = $mediaRepresentationPart->width;
417
    $margins = '0 0 0 0';
418
    $ratio = $w / $h;
419
    if($ratio > 1){
420
      $displayHeight = round($maxExtend / $ratio);
421
      $displayWidth = $maxExtend;
422
      $m = round(($maxExtend - $displayHeight) / 2);
423
      $margins = 'margin:'.$m.'px 0 '.$m.'px 0;';
424
    } else {
425
      $displayHeight = $maxExtend;
426
      $displayWidth = round($maxExtend * $ratio);
427
      $m = round(($maxExtend - $displayWidth) / 2);
428
      $margins = 'margin:0 '.$m.'px 0 '.$m.'px;';
429
    }
430
    
431
    // turn attributes array into string
432
    $attrStr = ' ';
433
    //$attributes['title'] = 'h:'.$h.', w:'.$w.',ratio:'.$ratio;
434
    if(is_array($attributes)){
435
      foreach($attributes as $name=>$value){
436
        $attrStr .= $name.'="'.$value.'" ';
437
      }
438
    }
439
    
440
    //return  '<img src="'."http://wp5.e-taxonomy.eu/dataportal/cichorieae/media/photos/Lapsana_communis_A_01.jpg".'" width="'.$maxExtend.'" height="'.$maxExtend.'" />';
441
    if($addPassePartout){
442
      $out .= '<div class="image-passe-partout" style="width:'.$maxExtend.'px; height:'.$maxExtend.'px;">';
443
    } else {
444
      // do not add margins if no pass partout is shown
445
      $margins = '';
446
    }
447
    $out .= '<img src="'.$mediaRepresentationPart->uri.'" width="'.$displayWidth.'" height="'.$displayHeight.'" style="'.$margins.'"'.$attrStr.' /></div>';
448
    return $out;
300 449
  }
301 450

  
302 451
}
303 452

  
453
function theme_cdm_openlayers_image($mediaRepresentationPart, $maxExtend){
454
  
455
  // see http://trac.openlayers.org/wiki/UsingCustomTiles#UsingTilesWithoutaProjection
456
  // and http://trac.openlayers.org/wiki/SettingZoomLevels
457
  
458
  $w = $mediaRepresentationPart->width;
459
  $h = $mediaRepresentationPart->height;
460
  
461
  // calculate  maxResolution (default is 360 deg / 256 px) and the bounds
462
  if($w > $h){
463
    $lat = 90;
464
    $lon = 90 * ($h / $w);
465
    $maxRes = $w / $maxExtend;
466
  } else {
467
    $lat = 90 * ($w / $h);
468
    $lon = 90;
469
    $maxRes =  $h / $maxExtend ;
470
  }
471
  
472
  $maxRes *= 1;
473
  drupal_add_js('
474
 var map;
475

  
476
 var imageLayerOptions={
477
     maxResolution: '.$maxRes.',
478
     maxExtent: new OpenLayers.Bounds(0, 0, '.$w.', '.$h.')
479
  };
480
  var mapOptions={
481
     restrictedExtent:  new OpenLayers.Bounds(0, 0, '.$w.', '.$h.')
482
  };
483
 
484
 var graphic = new OpenLayers.Layer.Image(
485
          \'Image Title\',
486
          \''.$mediaRepresentationPart->uri.'\',
487
          new OpenLayers.Bounds(0, 0, '.$w.', '.$h.'),
488
          new OpenLayers.Size('.$w.', '.$h.'),
489
          imageLayerOptions
490
          );
491
  
492
 function init() {
493
   map = new OpenLayers.Map(\'openlayers_image\', mapOptions);
494
   map.addLayers([graphic]);
495
   map.setCenter(new OpenLayers.LonLat(0, 0), 1);
496
   map.zoomToMaxExtent();
497
 }
498
 
499
$(document).ready(function(){
500
  init();
501

  
502
});'
503
      , 'inline');
504
      $out = '<div id="openlayers_image" class="image_viewer" style="width: '.$maxExtend.'px; height:'.($maxExtend).'px"></div>';
505
      return $out;
506
  
507
}
508

  
509
function theme_cdm_media_page($media){
510
  $out = '';
511

  
512
  $title = $media->title_L10n;
513
  
514
  $imageMaxExtend = variable_get('image-page-maxextend', 400);
515
  
516
  if(!$title){
517
    $title = 'Media '.$media->uuid.'';
518
  }
519

  
520
  //choose representation
521
  $representation = $media->representations[0];
522
  
523
  drupal_set_title($title);
524
  //TODO show image in openlayers viewer
525

  
526
  $out .= '<div class="media">';
527
  
528
  $out .= '<div class="viewer">';
529
  //$out .= theme('cdm_media_gallerie_image', $representation->parts[0], $imageMaxExtend);
530
  $out .= theme('cdm_openlayers_image', $representation->parts[0], $imageMaxExtend);
531
  $out .= '</div>';
532
  
533
  
534
  // general media metadata
535
  $out .= '<h4 class="title">'.$media->title_L10n.'</h4>';
536
  $out .= '<div class="description">'.$media->description_L10n.'</div>';
537
  $out .= '<div class="artist">'.$media->artist->titleCache.'</div>';
538
  $out .= '<ul class="rights">';
539
  foreach($media->rights as $right){  
540
    $out .= '<li>'.theme('cdm_right', $right).'</li>';
541
  }
542
  $out .= '</div>';
543
  
544
  // representation(-part) specific metadata
545
  $thumbnailMaxExtend = 100;
546
  $out .= '<table>';
547
  $out .= '<tr><th colspan="3">'.t('MimeType').': '.$representation->mimeType.'</th></tr>';
548
  $i = 1;
549
  foreach($representation->parts as $part){
550
    $out .= '<tr><th>'.t('Part').' '.$i++.'</th><td>';
551
    switch($part->class){
552
      case 'ImageFile': $out .= $part->width.' x '.$part->height.' - '.$part->size.'k'; break;
553
      case 'AudioFile': 
554
      case 'MovieFile': $out .= t('Duration').': '.$part->duration.'s - '.$part->size.'k'; break;
555
      default: $out .= $part->size.'k';
556
    }   
557
    $out .= '</td><td>'.theme('cdm_media_gallerie_image', $part, $thumbnailMaxExtend, true);'</td><tr>';
558
  }
559
  $out .= '</table>';
560
  $out .= '</div>';
561
  
562
  return $out;
563
}
564

  
565
function theme_cdm_right($right){
566
  $out = '<div class="right">';
567
  $out .= '<span class="type">'.$right->type->representation_L10n.' </span>';
568
  if($right->uri){
569
    $out .= '<a href="'.$right->uri.'>'.$right->abbreviatedText.'</a>';
570
  } else {
571
    $out .= $right->abbreviatedText;
572
  }
573
  $out .= '<span class="agent"> '.$right->abbreviatedText.'</span>';
574
  $out .= '</div>';
575
  return $out;
576
  //$right->type->representation_L10n
577
}
578

  
304 579
/**
305 580
 * TODO
306 581
 * Quick-and-dirty solution to show distribution service to exemplar groups
......
827 1102
    $table_of_accepted[$synUuid] = cdm_ws_get(CDM_WS_TAXON_ACCEPTED, $synUuid);
828 1103
  }
829 1104
  // .. well, for sure not as performant as before, but better than nothing.
830

  
1105
  $captionElements = array('title', '#uri'=>t('open Image'));
1106
  $gallery_name = $taxon->uuid;
831 1107
  foreach($records as $taxon){
1108
    // its a Taxon
832 1109
    if($taxon->class == "Taxon"){
833 1110
      $taxonUri = url(path_to_taxon($taxon->uuid));
834 1111
      if(isset($taxon->name->nomenclaturalReference)){
......
836 1113
      }
837 1114
      $out .= '<li class="Taxon">'.theme('cdm_taxonName', $taxon->name, $taxonUri, $referenceUri, $renderPath);
838 1115
      if($showMedia_taxa){
1116
          $moreLinkUri = path_to_taxon($taxon->uuid).'/images';
839 1117
          $mediaList = cdm_ws_get(CDM_WS_TAXON_MEDIA, array($taxon->uuid, $prefMimeTypeRegex, $prefMediaQuality));
840
          $out .= theme('cdm_media_gallerie', $mediaList, $maxExtend, $cols, $maxRows);
1118
          $out .= theme('cdm_media_gallerie', $mediaList, $gallery_name ,$maxExtend, $cols, $maxRows, $captionElements
1119
                , 'LIGHTBOX', null, $moreLinkUri);
841 1120
      }
842 1121
      $out .= '</li>';
843 1122
    } else {
1123
      // its a synonym
844 1124
      $uuid = $taxon->uuid;
845 1125
      $acceptedTaxa = $table_of_accepted[$uuid];
846 1126
      if(count($acceptedTaxa) == 1){
......
852 1132
        $out .= '<li class="Synonym">'.theme('cdm_taxonName', $taxon->name, $taxonUri, $referenceUri, $renderPath);
853 1133
        if($showMedia_synonyms){
854 1134
          $mediaList = cdm_ws_get(CDM_WS_TAXON_MEDIA, array($acceptedTaxon->uuid, $prefMimeTypeRegex, $prefMediaQuality));
855
          $out .= theme('cdm_media_gallerie', $mediaList, $maxExtend,$cols, $maxRows);
1135
          $out .= theme('cdm_media_gallerie', $mediaList, $gallery_name, $maxExtend,$cols, $maxRows, $gallery_captions);
856 1136
        }
857 1137
      $out .= '</li>';
858 1138
      } else {
......
1162 1442
 * 
1163 1443
 */
1164 1444
function theme_cdm_taxon_page_images($taxon, $media){
1165

  
1166
  $flashLink = isset($media[0]);
1167
  
1168
  if($flashLink){
1169
    
1170
    $taggedName = $taxon->name->taggedName;
1171
    
1172
    $nameArray = array();
1173
    foreach($taggedName as $taggedText){
1174
      if($taggedText->type == 'name'){
1175
        $nameArray[] = $taggedText->text;
1176
      }
1177
    }
1178
    
1179
    $query = join("%5F", $nameArray) . '%20AND%20jpg';
1180
    
1181
  $out = '
1182
  
1183
  <script type="text/javascript" src="http://media.bgbm.org/erez/js/fsiwriter.js"></script>
1184

  
1185
<script type="text/javascript">
1186
<!--
1187
	writeFlashCode( "http://media.bgbm.org/erez/fsi/fsi.swf?&cfg=showcase_presets/showcase_info.fsi&effects=%26quality%3D95&showcase_query='.$query.'&skin=silver&showcase_labeltextheight=50&textbox_textfrom=IPTC_WP6&textbox_height=50&param_backgroundcolor=454343&publishwmode=opaque&showcase_hscroll=true&showcase_basecolor=454343&plugins=PrintSave,textbox",
1188
		"http://media.bgbm.org/erez/erez?src=erez-private/flashrequired.svg&tmp=Large&quality=97&width=470&height=400",
1189
		"width=470;height=400;bgcolor=454343;wmode=opaque");
1190
// -->
1191
</script>
1192
<noscript>
1193
	<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,65,0" width="470" height="400">
1194
		<param name="movie" value="http://media.bgbm.org/erez/fsi/fsi.swf?&cfg=showcase_presets/showcase_info.fsi&effects=%26quality%3D95&showcase_query='.$query.'&skin=silver&showcase_labeltextheight=50&textbox_textfrom=IPTC_WP6&textbox_height=50&param_backgroundcolor=454343&publishwmode=opaque&showcase_hscroll=true&showcase_basecolor=454343plugins=PrintSave,textbox"/>
1195
		<param name="bgcolor" value="454343" />
1196
		<param name="wmode" value="opaque" />
1197
		<param name="allowscriptaccess" value="always" />
1198
		<param name="allowfullscreen" value="true" />
1199
		<param name="quality" value="high" />
1200
		<embed src="http://media.bgbm.org/erez/fsi/fsi.swf?&cfg=showcase_presets/showcase_info.fsi&effects=%26quality%3D95&showcase_query='.$query.'&skin=silver&showcase_labeltextheight=50&textbox_textfrom=IPTC_WP6&textbox_height=50&param_backgroundcolor=454343&publishwmode=opaque&showcase_hscroll=true&showcase_basecolor=454343plugins=PrintSave,textbox"
1201
			width="470"
1202
			height="400"
1203
			bgcolor="454343"
1204
			wmode="opaque"
1205
			allowscriptaccess="always"
1206
			allowfullscreen="true"
1207
			quality="high"
1208
			type="application/x-shockwave-flash"
1209
			pluginspage="http://www.adobe.com/go/getflashplayer">
1210
		</embed>
1211
	</object>
1212

  
1213
</noscript>';
1214 1445
  
1446
  $hasImages = isset($media[0]);
1447
  
1448
  if($hasImages){
1449
    //
1450
    $maxExtend = 150;
1451
    $cols = 3; 
1452
    $maxRows = false;
1453
    $alternativeMediaUri = null;
1454
    $captionElements = array('title', '#uri'=>t('open Image'));
1455
    $gallery_name = $taxon->uuid;
1456
    $out = '<div class="image-gallerie">';
1457
    $out .= theme('cdm_media_gallerie', $media, $gallery_name, $maxExtend, $cols, $maxRows, $captionElements, null, null);
1458
    $out .= '</div>';
1215 1459
  }else{
1216 1460
    $out = 'No images available.';
1217 1461
  
......
1309 1553

  
1310 1554
}
1311 1555

  
1312
function theme_cdm_preferredImage($media, $defaultImage, $imageWidth, $imageHeight, $parameters = ''){
1556
function theme_cdm_preferredImage($media, $defaultImage, $imageMaxExtend, $parameters = ''){
1313 1557

  
1314 1558
  if(isset($media[0])){
1315 1559
    $preferredMedia = $media[0];
......
1318 1562
  //$widthAndHeight = ($imageWidth ? ' width="'.$imageWidth : '').($imageHeight ? '" height="'.$imageHeight : '');
1319 1563
  $imageUri = $preferredMedia ? $preferredMedia->representations[0]->parts[0]->uri . $parameters : $defaultImage;
1320 1564
  $altText = $preferredMedia ? $preferredMedia->representations[0]->parts[0]->uri : "no image available";
1321
  $out = '<img class="left" '.$widthAndHeight.' " src="'.$imageUri.'" alt="'.$altText.'" />';
1565
  $out = theme('cdm_media_gallerie_image', $preferredMedia->representations[0]->part[0], $imageMaxExtend, false);
1566
  // $out = '<img class="left" '.$widthAndHeight.' " src="'.$imageUri.'" alt="'.$altText.'" />';
1322 1567
  return $out;
1323 1568
}
1324 1569

  

Also available in: Unified diff