001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.imagery; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.StringReader; 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.EnumMap; 012import java.util.List; 013import java.util.Locale; 014import java.util.Map; 015import java.util.Objects; 016import java.util.Optional; 017import java.util.concurrent.TimeUnit; 018import java.util.regex.Matcher; 019import java.util.regex.Pattern; 020import java.util.stream.Collectors; 021 022import javax.json.Json; 023import javax.json.JsonObject; 024import javax.json.JsonReader; 025import javax.swing.ImageIcon; 026 027import org.openstreetmap.josm.data.StructUtils.StructEntry; 028import org.openstreetmap.josm.data.sources.ISourceCategory; 029import org.openstreetmap.josm.data.sources.ISourceType; 030import org.openstreetmap.josm.data.sources.SourceBounds; 031import org.openstreetmap.josm.data.sources.SourceInfo; 032import org.openstreetmap.josm.data.sources.SourcePreferenceEntry; 033import org.openstreetmap.josm.tools.CheckParameterUtil; 034import org.openstreetmap.josm.tools.ImageProvider; 035import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 036import org.openstreetmap.josm.tools.Logging; 037import org.openstreetmap.josm.tools.MultiMap; 038import org.openstreetmap.josm.tools.PlatformManager; 039import org.openstreetmap.josm.tools.StreamUtils; 040import org.openstreetmap.josm.tools.Utils; 041 042/** 043 * Class that stores info about an image background layer. 044 * 045 * @author Frederik Ramm 046 */ 047public class ImageryInfo extends 048 SourceInfo<ImageryInfo.ImageryCategory, ImageryInfo.ImageryType, ImageryInfo.ImageryBounds, ImageryInfo.ImageryPreferenceEntry> { 049 050 /** 051 * Type of imagery entry. 052 */ 053 public enum ImageryType implements ISourceType<ImageryType> { 054 /** A WMS (Web Map Service) entry. **/ 055 WMS("wms"), 056 /** A TMS (Tile Map Service) entry. **/ 057 TMS("tms"), 058 /** TMS entry for Microsoft Bing. */ 059 BING("bing"), 060 /** TMS entry for Russian company <a href="https://wiki.openstreetmap.org/wiki/WikiProject_Russia/kosmosnimki">ScanEx</a>. **/ 061 SCANEX("scanex"), 062 /** A WMS endpoint entry only stores the WMS server info, without layer, which are chosen later by the user. **/ 063 WMS_ENDPOINT("wms_endpoint"), 064 /** WMTS stores GetCapabilities URL. Does not store any information about the layer **/ 065 WMTS("wmts"), 066 /** Mapbox Vector Tiles entry*/ 067 MVT("mvt"); 068 069 private final String typeString; 070 071 ImageryType(String typeString) { 072 this.typeString = typeString; 073 } 074 075 /** 076 * Returns the unique string identifying this type. 077 * @return the unique string identifying this type 078 * @since 6690 079 */ 080 @Override 081 public final String getTypeString() { 082 return typeString; 083 } 084 085 /** 086 * Returns the imagery type from the given type string. 087 * @param s The type string 088 * @return the imagery type matching the given type string 089 */ 090 public static ImageryType fromString(String s) { 091 return Arrays.stream(ImageryType.values()) 092 .filter(type -> type.getTypeString().equals(s)) 093 .findFirst().orElse(null); 094 } 095 096 @Override 097 public ImageryType getFromString(String s) { 098 return fromString(s); 099 } 100 101 @Override 102 public ImageryType getDefault() { 103 return WMS; 104 } 105 } 106 107 /** 108 * Category of imagery entry. 109 * @since 13792 110 */ 111 public enum ImageryCategory implements ISourceCategory<ImageryCategory> { 112 /** A aerial or satellite photo. **/ 113 PHOTO(/* ICON(data/imagery/) */ "photo", tr("Aerial or satellite photo")), 114 /** A map of digital terrain model, digital surface model or contour lines. **/ 115 ELEVATION(/* ICON(data/imagery/) */ "elevation", tr("Elevation map")), 116 /** A map. **/ 117 MAP(/* ICON(data/imagery/) */ "map", tr("Map")), 118 /** A historic or otherwise outdated map. */ 119 HISTORICMAP(/* ICON(data/imagery/) */ "historicmap", tr("Historic or otherwise outdated map")), 120 /** A map based on OSM data. **/ 121 OSMBASEDMAP(/* ICON(data/imagery/) */ "osmbasedmap", tr("Map based on OSM data")), 122 /** A historic or otherwise outdated aerial or satellite photo. **/ 123 HISTORICPHOTO(/* ICON(data/imagery/) */ "historicphoto", tr("Historic or otherwise outdated aerial or satellite photo")), 124 /** A map for quality assurance **/ 125 QUALITY_ASSURANCE(/* ICON(data/imagery/) */ "qa", tr("Map for quality assurance")), 126 /** Any other type of imagery **/ 127 OTHER(/* ICON(data/imagery/) */ "other", tr("Imagery not matching any other category")); 128 129 private final String category; 130 private final String description; 131 private static final Map<ImageSizes, Map<ImageryCategory, ImageIcon>> iconCache = 132 Collections.synchronizedMap(new EnumMap<>(ImageSizes.class)); 133 134 ImageryCategory(String category, String description) { 135 this.category = category; 136 this.description = description; 137 } 138 139 /** 140 * Returns the unique string identifying this category. 141 * @return the unique string identifying this category 142 */ 143 @Override 144 public final String getCategoryString() { 145 return category; 146 } 147 148 /** 149 * Returns the description of this category. 150 * @return the description of this category 151 */ 152 @Override 153 public final String getDescription() { 154 return description; 155 } 156 157 /** 158 * Returns the category icon at the given size. 159 * @param size icon wanted size 160 * @return the category icon at the given size 161 * @since 15049 162 */ 163 @Override 164 public final ImageIcon getIcon(ImageSizes size) { 165 return iconCache 166 .computeIfAbsent(size, x -> Collections.synchronizedMap(new EnumMap<>(ImageryCategory.class))) 167 .computeIfAbsent(this, x -> ImageProvider.get("data/imagery", x.category, size)); 168 } 169 170 /** 171 * Returns the imagery category from the given category string. 172 * @param s The category string 173 * @return the imagery category matching the given category string 174 */ 175 public static ImageryCategory fromString(String s) { 176 return Arrays.stream(ImageryCategory.values()) 177 .filter(category -> category.getCategoryString().equals(s)) 178 .findFirst().orElse(null); 179 } 180 181 @Override 182 public ImageryCategory getDefault() { 183 return OTHER; 184 } 185 186 @Override 187 public ImageryCategory getFromString(String s) { 188 return fromString(s); 189 } 190 } 191 192 /** 193 * Multi-polygon bounds for imagery backgrounds. 194 * Used to display imagery coverage in preferences and to determine relevant imagery entries based on edit location. 195 */ 196 public static class ImageryBounds extends SourceBounds { 197 198 /** 199 * Constructs a new {@code ImageryBounds} from string. 200 * @param asString The string containing the list of shapes defining this bounds 201 * @param separator The shape separator in the given string, usually a comma 202 */ 203 public ImageryBounds(String asString, String separator) { 204 super(asString, separator); 205 } 206 } 207 208 private double pixelPerDegree; 209 /** maximum zoom level for TMS imagery */ 210 private int defaultMaxZoom; 211 /** minimum zoom level for TMS imagery */ 212 private int defaultMinZoom; 213 /** projections supported by WMS servers */ 214 private List<String> serverProjections = Collections.emptyList(); 215 /** 216 * marked as best in other editors 217 * @since 11575 218 */ 219 private boolean bestMarked; 220 /** 221 * marked as overlay 222 * @since 13536 223 */ 224 private boolean overlay; 225 226 /** mirrors of different type for this entry */ 227 protected List<ImageryInfo> mirrors; 228 /** 229 * Auxiliary class to save an {@link ImageryInfo} object in the preferences. 230 */ 231 /** is the geo reference correct - don't offer offset handling */ 232 private boolean isGeoreferenceValid; 233 /** Should this map be transparent **/ 234 private boolean transparent = true; 235 private int minimumTileExpire = (int) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get()); 236 237 /** 238 * The ImageryPreferenceEntry class for storing data in JOSM preferences. 239 * 240 * @author Frederik Ramm, modified by Taylor Smock 241 */ 242 public static class ImageryPreferenceEntry extends SourcePreferenceEntry<ImageryInfo> { 243 @StructEntry String d; 244 @StructEntry double pixel_per_eastnorth; 245 @StructEntry int max_zoom; 246 @StructEntry int min_zoom; 247 @StructEntry String projections; 248 @StructEntry MultiMap<String, String> noTileHeaders; 249 @StructEntry MultiMap<String, String> noTileChecksums; 250 @StructEntry int tileSize = -1; 251 @StructEntry Map<String, String> metadataHeaders; 252 @StructEntry boolean valid_georeference; 253 @StructEntry boolean bestMarked; 254 @StructEntry boolean modTileFeatures; 255 @StructEntry boolean overlay; 256 @StructEntry boolean transparent; 257 @StructEntry int minimumTileExpire; 258 259 /** 260 * Constructs a new empty WMS {@code ImageryPreferenceEntry}. 261 */ 262 public ImageryPreferenceEntry() { 263 super(); 264 } 265 266 /** 267 * Constructs a new {@code ImageryPreferenceEntry} from a given {@code ImageryInfo}. 268 * @param i The corresponding imagery info 269 */ 270 public ImageryPreferenceEntry(ImageryInfo i) { 271 super(i); 272 pixel_per_eastnorth = i.pixelPerDegree; 273 bestMarked = i.bestMarked; 274 overlay = i.overlay; 275 max_zoom = i.defaultMaxZoom; 276 min_zoom = i.defaultMinZoom; 277 if (!i.serverProjections.isEmpty()) { 278 projections = String.join(",", i.serverProjections); 279 } 280 if (!Utils.isEmpty(i.noTileHeaders)) { 281 noTileHeaders = new MultiMap<>(i.noTileHeaders); 282 } 283 284 if (!Utils.isEmpty(i.noTileChecksums)) { 285 noTileChecksums = new MultiMap<>(i.noTileChecksums); 286 } 287 288 if (!Utils.isEmpty(i.metadataHeaders)) { 289 metadataHeaders = i.metadataHeaders; 290 } 291 292 tileSize = i.getTileSize(); 293 294 valid_georeference = i.isGeoreferenceValid(); 295 modTileFeatures = i.isModTileFeatures(); 296 transparent = i.isTransparent(); 297 minimumTileExpire = i.minimumTileExpire; 298 } 299 300 @Override 301 public String toString() { 302 StringBuilder s = new StringBuilder("ImageryPreferenceEntry [name=").append(name); 303 if (id != null) { 304 s.append(" id=").append(id); 305 } 306 s.append(']'); 307 return s.toString(); 308 } 309 } 310 311 /** 312 * Constructs a new WMS {@code ImageryInfo}. 313 */ 314 public ImageryInfo() { 315 super(); 316 } 317 318 /** 319 * Constructs a new WMS {@code ImageryInfo} with a given name. 320 * @param name The entry name 321 */ 322 public ImageryInfo(String name) { 323 super(name); 324 } 325 326 /** 327 * Constructs a new WMS {@code ImageryInfo} with given name and extended URL. 328 * @param name The entry name 329 * @param url The entry extended URL 330 */ 331 public ImageryInfo(String name, String url) { 332 this(name); 333 setExtendedUrl(url); 334 } 335 336 /** 337 * Constructs a new WMS {@code ImageryInfo} with given name, extended and EULA URLs. 338 * @param name The entry name 339 * @param url The entry URL 340 * @param eulaAcceptanceRequired The EULA URL 341 */ 342 public ImageryInfo(String name, String url, String eulaAcceptanceRequired) { 343 this(name); 344 setExtendedUrl(url); 345 this.eulaAcceptanceRequired = eulaAcceptanceRequired; 346 } 347 348 /** 349 * Constructs a new {@code ImageryInfo} with given name, url, extended and EULA URLs. 350 * @param name The entry name 351 * @param url The entry URL 352 * @param type The entry imagery type. If null, WMS will be used as default 353 * @param eulaAcceptanceRequired The EULA URL 354 * @param cookies The data part of HTTP cookies header in case the service requires cookies to work 355 * @throws IllegalArgumentException if type refers to an unknown imagery type 356 */ 357 public ImageryInfo(String name, String url, String type, String eulaAcceptanceRequired, String cookies) { 358 this(name); 359 setExtendedUrl(url); 360 ImageryType t = ImageryType.fromString(type); 361 this.cookies = cookies; 362 this.eulaAcceptanceRequired = eulaAcceptanceRequired; 363 if (t != null) { 364 this.sourceType = t; 365 } else if (!Utils.isEmpty(type)) { 366 throw new IllegalArgumentException("unknown type: "+type); 367 } 368 } 369 370 /** 371 * Constructs a new {@code ImageryInfo} with given name, url, id, extended and EULA URLs. 372 * @param name The entry name 373 * @param url The entry URL 374 * @param type The entry imagery type. If null, WMS will be used as default 375 * @param eulaAcceptanceRequired The EULA URL 376 * @param cookies The data part of HTTP cookies header in case the service requires cookies to work 377 * @param id tile id 378 * @throws IllegalArgumentException if type refers to an unknown imagery type 379 */ 380 public ImageryInfo(String name, String url, String type, String eulaAcceptanceRequired, String cookies, String id) { 381 this(name, url, type, eulaAcceptanceRequired, cookies); 382 setId(id); 383 } 384 385 /** 386 * Constructs a new {@code ImageryInfo} from an imagery preference entry. 387 * @param e The imagery preference entry 388 */ 389 public ImageryInfo(ImageryPreferenceEntry e) { 390 super(e.name, e.url, e.id); 391 CheckParameterUtil.ensureParameterNotNull(e.name, "name"); 392 CheckParameterUtil.ensureParameterNotNull(e.url, "url"); 393 description = e.description; 394 cookies = e.cookies; 395 eulaAcceptanceRequired = e.eula; 396 sourceType = ImageryType.fromString(e.type); 397 if (sourceType == null) throw new IllegalArgumentException("unknown type"); 398 pixelPerDegree = e.pixel_per_eastnorth; 399 defaultMaxZoom = e.max_zoom; 400 defaultMinZoom = e.min_zoom; 401 if (e.bounds != null) { 402 bounds = new ImageryBounds(e.bounds, ","); 403 if (e.shapes != null) { 404 try { 405 for (String s : e.shapes.split(";", -1)) { 406 bounds.addShape(new Shape(s, ",")); 407 } 408 } catch (IllegalArgumentException ex) { 409 Logging.warn(ex); 410 } 411 } 412 } 413 if (!Utils.isEmpty(e.projections)) { 414 // split generates null element on empty string which gives one element Array[null] 415 setServerProjections(Arrays.asList(e.projections.split(",", -1))); 416 } 417 attributionText = Utils.intern(e.attribution_text); 418 attributionLinkURL = e.attribution_url; 419 permissionReferenceURL = e.permission_reference_url; 420 attributionImage = e.logo_image; 421 attributionImageURL = e.logo_url; 422 date = e.date; 423 bestMarked = e.bestMarked; 424 overlay = e.overlay; 425 termsOfUseText = e.terms_of_use_text; 426 termsOfUseURL = e.terms_of_use_url; 427 countryCode = Utils.intern(e.country_code); 428 icon = Utils.intern(e.icon); 429 if (e.noTileHeaders != null) { 430 noTileHeaders = e.noTileHeaders.toMap(); 431 } 432 if (e.noTileChecksums != null) { 433 noTileChecksums = e.noTileChecksums.toMap(); 434 } 435 setTileSize(e.tileSize); 436 metadataHeaders = e.metadataHeaders; 437 isGeoreferenceValid = e.valid_georeference; 438 modTileFeatures = e.modTileFeatures; 439 if (e.default_layers != null) { 440 try (JsonReader jsonReader = Json.createReader(new StringReader(e.default_layers))) { 441 defaultLayers = jsonReader. 442 readArray(). 443 stream(). 444 map(x -> DefaultLayer.fromJson((JsonObject) x, sourceType)). 445 collect(Collectors.toList()); 446 } 447 } 448 setCustomHttpHeaders(e.customHttpHeaders); 449 transparent = e.transparent; 450 minimumTileExpire = e.minimumTileExpire; 451 category = ImageryCategory.fromString(e.category); 452 } 453 454 /** 455 * Constructs a new {@code ImageryInfo} from an existing one. 456 * @param i The other imagery info 457 */ 458 public ImageryInfo(ImageryInfo i) { 459 super(i.name, i.url, i.id); 460 this.noTileHeaders = i.noTileHeaders; 461 this.noTileChecksums = i.noTileChecksums; 462 this.minZoom = i.minZoom; 463 this.maxZoom = i.maxZoom; 464 this.cookies = i.cookies; 465 this.tileSize = i.tileSize; 466 this.metadataHeaders = i.metadataHeaders; 467 this.modTileFeatures = i.modTileFeatures; 468 469 this.origName = i.origName; 470 this.langName = i.langName; 471 this.defaultEntry = i.defaultEntry; 472 this.eulaAcceptanceRequired = null; 473 this.sourceType = i.sourceType; 474 this.pixelPerDegree = i.pixelPerDegree; 475 this.defaultMaxZoom = i.defaultMaxZoom; 476 this.defaultMinZoom = i.defaultMinZoom; 477 this.bounds = i.bounds; 478 this.serverProjections = i.serverProjections; 479 this.description = i.description; 480 this.langDescription = i.langDescription; 481 this.attributionText = i.attributionText; 482 this.privacyPolicyURL = i.privacyPolicyURL; 483 this.permissionReferenceURL = i.permissionReferenceURL; 484 this.attributionLinkURL = i.attributionLinkURL; 485 this.attributionImage = i.attributionImage; 486 this.attributionImageURL = i.attributionImageURL; 487 this.termsOfUseText = i.termsOfUseText; 488 this.termsOfUseURL = i.termsOfUseURL; 489 this.countryCode = i.countryCode; 490 this.date = i.date; 491 this.bestMarked = i.bestMarked; 492 this.overlay = i.overlay; 493 // do not copy field {@code mirrors} 494 this.icon = Utils.intern(i.icon); 495 this.isGeoreferenceValid = i.isGeoreferenceValid; 496 setDefaultLayers(i.defaultLayers); 497 setCustomHttpHeaders(i.customHttpHeaders); 498 this.transparent = i.transparent; 499 this.minimumTileExpire = i.minimumTileExpire; 500 this.categoryOriginalString = Utils.intern(i.categoryOriginalString); 501 this.category = i.category; 502 } 503 504 /** 505 * Adds a mirror entry. Mirror entries are completed with the data from the master entry 506 * and only describe another method to access identical data. 507 * 508 * @param entry the mirror to be added 509 * @since 9658 510 */ 511 public void addMirror(ImageryInfo entry) { 512 if (mirrors == null) { 513 mirrors = new ArrayList<>(); 514 } 515 mirrors.add(entry); 516 } 517 518 /** 519 * Returns the mirror entries. Entries are completed with master entry data. 520 * 521 * @return the list of mirrors 522 * @since 9658 523 */ 524 public List<ImageryInfo> getMirrors() { 525 List<ImageryInfo> l = new ArrayList<>(); 526 if (mirrors != null) { 527 int num = 1; 528 for (ImageryInfo i : mirrors) { 529 ImageryInfo n = new ImageryInfo(this); 530 if (i.defaultMaxZoom != 0) { 531 n.defaultMaxZoom = i.defaultMaxZoom; 532 } 533 if (i.defaultMinZoom != 0) { 534 n.defaultMinZoom = i.defaultMinZoom; 535 } 536 n.setServerProjections(i.getServerProjections()); 537 n.url = i.url; 538 n.sourceType = i.sourceType; 539 if (i.getTileSize() != 0) { 540 n.setTileSize(i.getTileSize()); 541 } 542 if (i.getPrivacyPolicyURL() != null) { 543 n.setPrivacyPolicyURL(i.getPrivacyPolicyURL()); 544 } 545 if (n.id != null) { 546 n.id = n.id + "_mirror" + num; 547 } 548 if (num > 1) { 549 n.name = tr("{0} mirror server {1}", n.name, num); 550 if (n.origName != null) { 551 n.origName += " mirror server " + num; 552 } 553 } else { 554 n.name = tr("{0} mirror server", n.name); 555 if (n.origName != null) { 556 n.origName += " mirror server"; 557 } 558 } 559 l.add(n); 560 ++num; 561 } 562 } 563 return l; 564 } 565 566 /** 567 * Check if this object equals another ImageryInfo with respect to the properties 568 * that get written to the preference file. 569 * 570 * The field {@link #pixelPerDegree} is ignored. 571 * 572 * @param other the ImageryInfo object to compare to 573 * @return true if they are equal 574 */ 575 @Override 576 public boolean equalsPref(SourceInfo<ImageryInfo.ImageryCategory, ImageryInfo.ImageryType, 577 ImageryInfo.ImageryBounds, ImageryInfo.ImageryPreferenceEntry> other) { 578 if (!(other instanceof ImageryInfo)) { 579 return false; 580 } 581 ImageryInfo realOther = (ImageryInfo) other; 582 583 // CHECKSTYLE.OFF: BooleanExpressionComplexity 584 return super.equalsPref(realOther) && 585 this.bestMarked == realOther.bestMarked && 586 this.overlay == realOther.overlay && 587 this.isGeoreferenceValid == realOther.isGeoreferenceValid && 588 this.defaultMaxZoom == realOther.defaultMaxZoom && 589 this.defaultMinZoom == realOther.defaultMinZoom && 590 Objects.equals(this.serverProjections, realOther.serverProjections) && 591 this.transparent == realOther.transparent && 592 this.minimumTileExpire == realOther.minimumTileExpire; 593 // CHECKSTYLE.ON: BooleanExpressionComplexity 594 } 595 596 @Override 597 public int compareTo(SourceInfo<ImageryInfo.ImageryCategory, ImageryInfo.ImageryType, 598 ImageryInfo.ImageryBounds, ImageryInfo.ImageryPreferenceEntry> other) { 599 int i = super.compareTo(other); 600 if (other instanceof ImageryInfo) { 601 ImageryInfo in = (ImageryInfo) other; 602 if (i == 0) { 603 i = Double.compare(pixelPerDegree, in.pixelPerDegree); 604 } 605 } 606 return i; 607 } 608 609 /** 610 * Sets the pixel per degree value. 611 * @param ppd The ppd value 612 * @see #getPixelPerDegree() 613 */ 614 public void setPixelPerDegree(double ppd) { 615 this.pixelPerDegree = ppd; 616 } 617 618 /** 619 * Sets the maximum zoom level. 620 * @param defaultMaxZoom The maximum zoom level 621 */ 622 public void setDefaultMaxZoom(int defaultMaxZoom) { 623 this.defaultMaxZoom = defaultMaxZoom; 624 } 625 626 /** 627 * Sets the minimum zoom level. 628 * @param defaultMinZoom The minimum zoom level 629 */ 630 public void setDefaultMinZoom(int defaultMinZoom) { 631 this.defaultMinZoom = defaultMinZoom; 632 } 633 634 @Override 635 public void setBounds(ImageryBounds b) { 636 // for binary compatibility 637 this.bounds = b; 638 } 639 640 @Override 641 public ImageryBounds getBounds() { 642 // for binary compatibility 643 return super.getBounds(); 644 } 645 646 /** 647 * Sets the extended URL of this entry. 648 * @param url Entry extended URL containing in addition of service URL, its type and min/max zoom info 649 */ 650 public void setExtendedUrl(String url) { 651 CheckParameterUtil.ensureParameterNotNull(url); 652 653 // Default imagery type is WMS 654 this.url = url; 655 this.sourceType = ImageryType.WMS; 656 657 defaultMaxZoom = 0; 658 defaultMinZoom = 0; 659 for (ImageryType type : ImageryType.values()) { 660 Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+)[,-])?(\\d+)])?:(.*)").matcher(url); 661 if (m.matches()) { 662 this.url = m.group(3); 663 this.sourceType = type; 664 if (m.group(2) != null) { 665 defaultMaxZoom = Integer.parseInt(m.group(2)); 666 } 667 if (m.group(1) != null) { 668 defaultMinZoom = Integer.parseInt(m.group(1)); 669 } 670 break; 671 } 672 } 673 674 if (serverProjections.isEmpty()) { 675 Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)}.*").matcher(url.toUpperCase(Locale.ENGLISH)); 676 if (m.matches()) { 677 setServerProjections(Arrays.asList(m.group(1).split(",", -1))); 678 } 679 } 680 } 681 682 /** 683 * Gets the pixel per degree value 684 * @return The ppd value. 685 */ 686 public double getPixelPerDegree() { 687 return this.pixelPerDegree; 688 } 689 690 /** 691 * Returns the maximum zoom level. 692 * @return The maximum zoom level 693 */ 694 @Override 695 public int getMaxZoom() { 696 return this.defaultMaxZoom; 697 } 698 699 /** 700 * Returns the minimum zoom level. 701 * @return The minimum zoom level 702 */ 703 @Override 704 public int getMinZoom() { 705 return this.defaultMinZoom; 706 } 707 708 /** 709 * Returns a tool tip text for display. 710 * @return The text 711 * @since 8065 712 */ 713 @Override 714 public String getToolTipText() { 715 boolean htmlSupported = PlatformManager.getPlatform().isHtmlSupportedInMenuTooltips(); 716 StringBuilder res = new StringBuilder(getName()); 717 boolean html = false; 718 String dateStr = getDate(); 719 if (!Utils.isEmpty(dateStr)) { 720 html = addNewLineInTooltip(res, tr("Date of imagery: {0}", dateStr), htmlSupported); 721 } 722 if (category != null && category.getDescription() != null) { 723 html = addNewLineInTooltip(res, tr("Imagery category: {0}", category.getDescription()), htmlSupported); 724 } 725 if (bestMarked) { 726 html = addNewLineInTooltip(res, tr("This imagery is marked as best in this region in other editors."), htmlSupported); 727 } 728 if (overlay) { 729 html = addNewLineInTooltip(res, tr("This imagery is an overlay."), htmlSupported); 730 } 731 String desc = getDescription(); 732 if (!Utils.isEmpty(desc)) { 733 html = addNewLineInTooltip(res, desc, htmlSupported); 734 } 735 if (html) { 736 res.insert(0, "<html>").append("</html>"); 737 } 738 return res.toString(); 739 } 740 741 private static boolean addNewLineInTooltip(StringBuilder res, String line, boolean htmlSupported) { 742 if (htmlSupported) { 743 res.append("<br>").append(Utils.escapeReservedCharactersHTML(line)); 744 } else { 745 res.append('\n').append(line); 746 } 747 return htmlSupported; 748 } 749 750 /** 751 * Get the projections supported by the server. Only relevant for 752 * WMS-type ImageryInfo at the moment. 753 * @return null, if no projections have been specified; the list 754 * of supported projections otherwise. 755 */ 756 public List<String> getServerProjections() { 757 return Collections.unmodifiableList(serverProjections); 758 } 759 760 /** 761 * Sets the list of collections the server supports 762 * @param serverProjections The list of supported projections 763 */ 764 public void setServerProjections(Collection<String> serverProjections) { 765 CheckParameterUtil.ensureParameterNotNull(serverProjections, "serverProjections"); 766 this.serverProjections = serverProjections.stream() 767 .map(String::intern) 768 .collect(StreamUtils.toUnmodifiableList()); 769 } 770 771 /** 772 * Returns the extended URL, containing in addition of service URL, its type and min/max zoom info. 773 * @return The extended URL 774 */ 775 public String getExtendedUrl() { 776 return sourceType.getTypeString() + (defaultMaxZoom != 0 777 ? ('['+(defaultMinZoom != 0 ? (Integer.toString(defaultMinZoom) + ',') : "")+defaultMaxZoom+']') : "") + ':' + url; 778 } 779 780 /** 781 * Gets a unique toolbar key to store this layer as toolbar item 782 * @return The key. 783 */ 784 public String getToolbarName() { 785 String res = name; 786 if (pixelPerDegree != 0) { 787 res += "#PPD="+pixelPerDegree; 788 } 789 return res; 790 } 791 792 /** 793 * Gets the name that should be displayed in the menu to add this imagery layer. 794 * @return The text. 795 */ 796 public String getMenuName() { 797 String res = name; 798 if (pixelPerDegree != 0) { 799 res += " ("+pixelPerDegree+')'; 800 } 801 return res; 802 } 803 804 /** 805 * Returns the imagery type. 806 * @return The imagery type 807 * @see SourceInfo#getSourceType 808 */ 809 public ImageryType getImageryType() { 810 return super.getSourceType() != null ? super.getSourceType() : ImageryType.WMS.getDefault(); 811 } 812 813 /** 814 * Sets the imagery type. 815 * @param imageryType The imagery type 816 * @see SourceInfo#setSourceType 817 */ 818 public void setImageryType(ImageryType imageryType) { 819 super.setSourceType(imageryType); 820 } 821 822 /** 823 * Returns the imagery category. 824 * @return The imagery category 825 * @see SourceInfo#getSourceCategory 826 * @since 13792 827 */ 828 public ImageryCategory getImageryCategory() { 829 return super.getSourceCategory(); 830 } 831 832 /** 833 * Sets the imagery category. 834 * @param category The imagery category 835 * @see SourceInfo#setSourceCategory 836 * @since 13792 837 */ 838 public void setImageryCategory(ImageryCategory category) { 839 super.setSourceCategory(category); 840 } 841 842 /** 843 * Returns the imagery category original string (don't use except for error checks). 844 * @return The imagery category original string 845 * @see SourceInfo#getSourceCategoryOriginalString 846 * @since 13792 847 */ 848 public String getImageryCategoryOriginalString() { 849 return super.getSourceCategoryOriginalString(); 850 } 851 852 /** 853 * Sets the imagery category original string (don't use except for error checks). 854 * @param categoryOriginalString The imagery category original string 855 * @see SourceInfo#setSourceCategoryOriginalString 856 * @since 13792 857 */ 858 public void setImageryCategoryOriginalString(String categoryOriginalString) { 859 super.setSourceCategoryOriginalString(categoryOriginalString); 860 } 861 862 /** 863 * Gets the flag if the georeference is valid. 864 * @return <code>true</code> if it is valid. 865 */ 866 public boolean isGeoreferenceValid() { 867 return isGeoreferenceValid; 868 } 869 870 /** 871 * Sets an indicator that the georeference is valid 872 * @param isGeoreferenceValid <code>true</code> if it is marked as valid. 873 */ 874 public void setGeoreferenceValid(boolean isGeoreferenceValid) { 875 this.isGeoreferenceValid = isGeoreferenceValid; 876 } 877 878 /** 879 * Returns the status of "best" marked status in other editors. 880 * @return <code>true</code> if it is marked as best. 881 * @since 11575 882 */ 883 public boolean isBestMarked() { 884 return bestMarked; 885 } 886 887 /** 888 * Returns the overlay indication. 889 * @return <code>true</code> if it is an overlay. 890 * @since 13536 891 */ 892 public boolean isOverlay() { 893 return overlay; 894 } 895 896 /** 897 * Sets an indicator that in other editors it is marked as best imagery 898 * @param bestMarked <code>true</code> if it is marked as best in other editors. 899 * @since 11575 900 */ 901 public void setBestMarked(boolean bestMarked) { 902 this.bestMarked = bestMarked; 903 } 904 905 /** 906 * Sets overlay indication 907 * @param overlay <code>true</code> if it is an overlay. 908 * @since 13536 909 */ 910 public void setOverlay(boolean overlay) { 911 this.overlay = overlay; 912 } 913 914 /** 915 * Determines if this imagery should be transparent. 916 * @return should this imagery be transparent 917 */ 918 public boolean isTransparent() { 919 return transparent; 920 } 921 922 /** 923 * Sets whether imagery should be transparent. 924 * @param transparent set to true if imagery should be transparent 925 */ 926 public void setTransparent(boolean transparent) { 927 this.transparent = transparent; 928 } 929 930 /** 931 * Returns minimum tile expiration in seconds. 932 * @return minimum tile expiration in seconds 933 */ 934 public int getMinimumTileExpire() { 935 return minimumTileExpire; 936 } 937 938 /** 939 * Sets minimum tile expiration in seconds. 940 * @param minimumTileExpire minimum tile expiration in seconds 941 */ 942 public void setMinimumTileExpire(int minimumTileExpire) { 943 this.minimumTileExpire = minimumTileExpire; 944 } 945 946 /** 947 * Get a string representation of this imagery info suitable for the {@code source} changeset tag. 948 * @return English name, if known 949 * @since 13890 950 */ 951 public String getSourceName() { 952 if (ImageryType.BING == getImageryType()) { 953 return "Bing"; 954 } else { 955 if (id != null) { 956 // Retrieve english name, unfortunately not saved in preferences 957 Optional<ImageryInfo> infoEn = ImageryLayerInfo.allDefaultLayers.stream().filter(x -> id.equals(x.getId())).findAny(); 958 if (infoEn.isPresent()) { 959 return infoEn.get().getOriginalName(); 960 } 961 } 962 return getOriginalName(); 963 } 964 } 965 966 /** 967 * Return the sorted list of activated source IDs. 968 * @return sorted list of activated source IDs 969 * @since 13536 970 */ 971 public static Collection<String> getActiveIds() { 972 return getActiveIds(ImageryInfo.class); 973 } 974}