基于高德或者Google Maps让你的相册遍布全世界

基于高德或者Google Maps让你的相册遍布全世界

效果

https://l0vo.com/

代码

Java

maven

	<!--exif元数据提取器-->
	<dependency>
		<groupId>com.drewnoakes</groupId>
		<artifactId>metadata-extractor</artifactId>
		<version>2.13.0</version>
	</dependency>
	<!--创建缩略图-->
	<dependency>
		<groupId>net.coobird</groupId>
		<artifactId>thumbnailator</artifactId>
		<version>0.4.11</version>
	</dependency>
	<!--json-->
	<dependency>
		<groupId>org.glassfish</groupId>
		<artifactId>jakarta.json</artifactId>
		<version>2.0.0-RC2</version>
	</dependency>
	<!--更改exif元数据-->
	<dependency>
		<groupId>org.apache.commons</groupId>
		<artifactId>commons-imaging</artifactId>
		<version>1.0-alpha1</version>
	</dependency>
	<dependency>
		<groupId>commons-io</groupId>
		<artifactId>commons-io</artifactId>
		<version>2.4</version>
	</dependency>

WirteGps.java

public class WirteGps {
    private String[] lonAndlans = {
            "2.557053,27.918568",
            "-3.386999,38.610584",
            "1.344556,46.693275",
            "4.585355,50.828274",
            "-2.438676,54.490766",
            "-7.883988,53.370002",
            "19.028382,52.64909",
            "27.103071,53.049358",
            "30.17312,50.330044",
            "24.861475,46.156955",
            "22.137526,39.374672",
            "19.465893,52.242634",
            "8.320581,60.531888",
            "-18.162903,65.050422",
            "35.903303,39.618995",
            "8.13747,46.866072",
            "16.172202,45.603013",
            "12.149518,43.106665"
    };// 随机取的几个定位 https://tool.lu/coordinate/

    public void changeExifMetadatas(final File jpegImageFile, final File dst)
            throws IOException, ImageWriteException {
        OutputStream os = null;
        try {
            TiffOutputSet outputSet = new TiffOutputSet();
            {
                final TiffOutputDirectory exifDirectory = outputSet
                        .getOrCreateExifDirectory();
                exifDirectory
                        .removeField(ExifTagConstants.EXIF_TAG_APERTURE_VALUE);
                exifDirectory.add(ExifTagConstants.EXIF_TAG_APERTURE_VALUE,
                        new RationalNumber(3, 10));
            }
            {
                final double longitude = Double.parseDouble(RandomStr(lonAndlans).split(",")[0]);
                final double latitude = Double.parseDouble(RandomStr(lonAndlans).split(",")[1]);
                outputSet.setGPSInDegrees(longitude, latitude);
            }
            final TiffOutputDirectory exifDirectory = outputSet
                    .getOrCreateRootDirectory();
            exifDirectory
                    .removeField(ExifTagConstants.EXIF_TAG_SOFTWARE);
            exifDirectory.add(ExifTagConstants.EXIF_TAG_SOFTWARE,
                    "随便给个名字");
            os = new FileOutputStream(dst);
            os = new BufferedOutputStream(os);
            new ExifRewriter().updateExifMetadataLossless(jpegImageFile, os,
                    outputSet);
        } catch (ImageReadException e) {
            System.out.println(jpegImageFile.getName());
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(os);
        }
    }

    private static String RandomStr(String[] strs) {
        int random_index = (int) (Math.random() * strs.length);
        return strs[random_index];
    }

    public static void main(String[] args) throws IOException, ImageWriteException {
        File file = new File("原来图片的文件夹");
        File[] fs = file.listFiles();
        for (File f : fs) {
            if (!f.isDirectory()&&f.exists()) {
                if 	(f.getName().endsWith(".jpeg")&&f.length()>0){
                    new WirteGps().changeExifMetadatas(f,new File("修改exif后的文件夹/"+f.getName()));
                }
            }
        }
    }
}

Extract.java

public class Extract {

    public static void main(String[] args) throws Exception {
        // 准备好图片
        Path photoDir = Paths.get("原来的图片路径");
        Path thumbnailDir = Paths.get("生成的略缩图路径");
        Files.createDirectories(thumbnailDir);
        Path photosJsonFile = Paths.get("需要生成的json文件路径");
        try (Writer writer = Files.newBufferedWriter(photosJsonFile);
             JsonGenerator jg = Json.createGenerator(writer)) {
            jg.writeStartArray();
            extractMetadata(jg, photoDir, thumbnailDir);
            jg.writeEnd();
        }

    }

    private static void extractMetadata(JsonGenerator jg, Path photoDir,
                                        Path thumbnailDir) throws IOException {
        Files.list(photoDir).forEach(photo -> {
            try (InputStream is = Files.newInputStream(photo)) {
                // 元数据
                Metadata metadata = ImageMetadataReader.readMetadata(is);
                // 经纬度
                GpsDirectory gpsDirectory = metadata.getFirstDirectoryOfType(GpsDirectory.class);
                // exif
                ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
                Date date = directory.getDate(ExifIFD0Directory.TAG_DATETIME);
                if (gpsDirectory != null) {
                    GeoLocation geoLocation = gpsDirectory.getGeoLocation();
                    if (geoLocation != null && !geoLocation.isZero()) {
                        if (!Files.exists(thumbnailDir.resolve(photo.getFileName()))) {
                            // 略缩图
                            Thumbnails.of(photo.toFile()).size(36, 36)
                                    .toFiles(thumbnailDir.toFile(), Rename.NO_CHANGE);
                        }
                        jg.writeStartObject();
                        // 纬度
                        jg.write("lat", geoLocation.getLatitude());
                        // 经度
                        jg.write("lng", geoLocation.getLongitude());
                        // 图片名
                        jg.write("img", photo.getFileName().toString());
                        // 日期
                        if (date != null) {
                            jg.write("ts", (int) (date.getTime() / 1000));
                        }
                        jg.writeEnd();
                        jg.flush();
                    }
                }
            } catch (IOException | ImageProcessingException e) {
                e.printStackTrace();
            }

        });

    }

}

应用

mkdir app
cd app
npm init

修改package.json

{
  "name": "geophotos",
  "version": "1.0.0",
  "main": "index.html",
  "scripts": {
    "prestart": "rimraf dist/*.*",
    "start": "parcel --port 12345 src/index.html",
    "prebuild": "rimraf dist/*.*",
    "build": "parcel build src/index.html --no-source-maps --public-url ./",
    "postbuild": "bread-compressor dist !*.jpg !*.jpeg",
    "serve-dist": "ws --hostname localhost -d dist -p 12345 -o --log.format stats"
  },
  "devDependencies": {
    "@babel/core": "7.9.6",
    "@babel/plugin-transform-runtime": "7.9.6",
    "bread-compressor-cli": "1.1.0",
    "local-web-server": "4.0.0",
    "parcel-bundler": "1.12.4",
    "rimraf": "3.0.2"
  },
  "dependencies": {
    "@google/markerclustererplus": "5.0.3",
    "date-fns": "2.13.0",
    "lg-autoplay.js": "1.0.0",
    "lg-fullscreen.js": "1.1.0",
    "lg-zoom.js": "1.0.1",
    "lightgallery.js": "1.1.3"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]
}


在项目的根目录中创建.babelrc

{
  "plugins": ["@babel/plugin-transform-runtime"]
}
  • 创建srcdistdist/assets文件夹
  • photos.json文件复制到文件dist/assets
  • 将缩略图复制到 dist/assets/thumbnails
  • 将照片复制到 dist/assets/photos
  • node_modules/@google/markerclustererplus/images下找到
    m1.png~m5.png图片复制到到dist/assets下重命名为1.png~5.png
npm install

mkdir src
mkdir dist
mkdir dist/assets
cd src
touch index.html
touch main.css
touch main.js

main.css

@import '../node_modules/lightgallery.js/dist/css/lightgallery.css';

#map {
  position: absolute;
  top: 10px;
  bottom: 10px;
  left: 10px;
  right: 10px;
}

/* Outside white border */
.asset-map-image-marker {
	background-color: gold;
	border-radius: 5px;
	cursor: pointer !important;
	height: 40px;
	margin-left: -20px; /* margin-left = -width/2 */
	margin-top: -50px; /* margin-top = -height + arrow */
	padding: 0px;
	position: absolute;
	width: 40px;
}

/* Arrow on bottom of container */
.asset-map-image-marker:after {
	border-color: #ffffff transparent;
	border-style: solid;
	border-width: 10px 10px 0;
	bottom: -10px;
	content: '';
	display: block;
	left: 10px;
	position: absolute;
	width: 0;
}

/* Inner image container */
.asset-map-image-marker div.image {
	background-position: center center;
	background-size: cover;
	border-radius: 5px;
	height: 36px;
	margin: 2px;
	width: 36px;
}


HTMLMapMarker.js 缩略图显示为标记图标

/*
 * https://blackatlascreative.com/blog/custom-clickable-google-map-markers-with-images/
 * https://levelup.gitconnected.com/how-to-create-custom-html-markers-on-google-maps-9ff21be90e4b
 * HTMLMapMarker Javascript class
 * Extends the Google Maps OverlayView class
 * Set up to accept our latlng, html for the div, and the map to attach it to as arguments
 */
export class HTMLMapMarker extends google.maps.OverlayView {
  // Constructor accepting args
  constructor(args) {
    super();
    this.latlng = args.latlng;
    this.html = args.html;
    this.photo = args.photo;
    this.setMap(args.map);
  }

  // Create the div with content and add a listener for click events
  createDiv() {
    this.div = document.createElement('div');
    this.div.style.position = 'absolute';
    if (this.html) {
      this.div.innerHTML = this.html;
    }
    google.maps.event.addDomListener(this.div, 'click', event => {
      google.maps.event.trigger(this, 'click');
    });
  }

  // Append to the overlay layer
  // Appending to both overlayLayer and overlayMouseTarget which should allow this to be clickable
  appendDivToOverlay() {
    const panes = this.getPanes();
    panes.overlayLayer.appendChild(this.div);
    panes.overlayMouseTarget.appendChild(this.div);
  }

  // Position the div according to the coordinates
  positionDiv() {
    const point = this.getProjection().fromLatLngToDivPixel(this.latlng);
    if (point) {
      this.div.style.left = `${point.x}px`;
      this.div.style.top = `${point.y}px`;
    }
  }

  // Create the div and append to map
  draw() {
    if (!this.div) {
      this.createDiv();
      this.appendDivToOverlay();
    }
    this.positionDiv();
  }

  // Remove this from map
  remove() {
    if (this.div) {
      this.div.parentNode.removeChild(this.div);
      this.div = null;
    }
  }

  // Return lat and long object
  getPosition() {
    return this.latlng;
  }

  // Return whether this is draggable
  getDraggable() {
    return false;
  }
}


main.js

import { HTMLMapMarker } from './HTMLMapMarker.js';
import MarkerClusterer from '@google/markerclustererplus';
import format from 'date-fns/format'
import 'lightgallery.js';
import 'lg-fullscreen.js';
import 'lg-autoplay.js';
import 'lg-zoom.js';

let map;
const markers = [];

loadMap();
loadPhotos();

function loadMap() {
  const latLng = new google.maps.LatLng(30.942088,121.378448);

  const mapOptions = {
    center: latLng,
    zoom: 10,
    mapTypeId: google.maps.MapTypeId.HYBRID
  }

  map = new google.maps.Map(document.getElementById('map'), mapOptions);
}

async function loadPhotos() {
  const response = await fetch('assets/photos.json');
  const photos = await response.json();
  for (const photo of photos) {
    drawMarker(photo);
  }
  new MarkerClusterer(map, markers, { imagePath: 'assets/', minimumClusterSize: 20 });
}

function drawMarker(photo) {
  const marker = new HTMLMapMarker({
    photo: photo.img,
    latlng: new google.maps.LatLng(photo.lat, photo.lng),
    html: `<div class="asset-map-image-marker"><div title="${photo.ts ? format(new Date(photo.ts * 1000), 'yyyy-MM-dd HH:mm') : ''}" class="image" style="background-image: url(assets/thumbnails/${photo.img})"></div></div>`
  });

  marker.addListener('click', () => {
    const el = document.getElementById('lightgallery');
    const lg = window.lgData[el.getAttribute('lg-uid')];
    if (lg) {
      lg.destroy(true);
    }
    lightGallery(el, {
      dynamic: true,
      dynamicEl: visiblePhotos(photo)
    });
  });

  markers.push(marker);
}

function visiblePhotos(photo) {
  const bounds = map.getBounds();
  const result = [{ src: `assets/photos/${photo.img}` }];
  for (const marker of markers) {
    if (bounds.contains(marker.getPosition())) {
      if (photo.img !== marker.photo) {
        result.push({ src: `assets/photos/${marker.photo}` });
      }
    }
  }
  return result;
}

获取api

https://developers.google.com/maps/documentation/javascript/get-api-key

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <title>GeoPhoto</title>
  <link rel="stylesheet" type="text/css" href="main.css">
  <link rel='shortcut icon' type='image/x-icon' href='favicon.ico' />
</head>

<body>
  <div id="map"></div>
  <div id="lightgallery"></div>
  <script src="https://maps.googleapis.com/maps/api/js?key=你的api"></script>
  <script src="main.js"></script>
</body>
</html>

高德地图版本

main.js

import format from 'date-fns/format'
import 'lightgallery.js';
import 'lg-fullscreen.js';
import 'lg-autoplay.js';
import 'lg-zoom.js';

let map; // 地图
const markers = []; // 标记

loadMap();
loadPhotos();

// 初始化
function loadMap() {
  map = new AMap.Map('map', {
    center:[113.87782499999999,22.792013888888892],
    zoom:10
  });
}

// 数据初始化
async function loadPhotos() {
  const response = await fetch('assets/photos.json');
  const photos = await response.json();
  for (const photo of photos) {
    drawMarker(photo);
  }
  map.add(markers);
}
// 标记与事件
function drawMarker(photo) {
  var marker = new AMap.Marker({
    content: `<div class="asset-map-image-marker"><div title="${photo.ts ? format(new Date(photo.ts * 1000), 'yyyy-MM-dd HH:mm') : ''}" class="image" style="background-image: url(assets/thumbnails/${photo.img})"></div></div>`,
    position: new AMap.LngLat(photo.lng,photo.lat),
    offset: new AMap.Pixel(-10, -10)
  });
  marker.photo=photo.img;
  marker.on('click', () => {
    const el = document.getElementById('lightgallery');
    const lg = window.lgData[el.getAttribute('lg-uid')];
    if (lg) {
      lg.destroy(true);
    }
    lightGallery(el, {
      dynamic: true,
      dynamicEl: visiblePhotos(photo)
    });
  });
  markers.push(marker);
}
// 当前视图范围的所有图片
function visiblePhotos(photo) {
  let bounds = map.getBounds();
  let result = [{ src: `assets/photos/${photo.img}` }];
  for (const marker of markers) {
    let position= marker.getPosition();
    if (bounds.contains(position)) {
      if (photo.img !== marker.photo) {
        result.push({ src: `assets/photos/${marker.photo}` });
      }
    }
  }
  return result;
}




index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <title>GeoPhoto</title>
  <link rel="stylesheet" type="text/css" href="main.css">
  <link rel='shortcut icon' type='image/x-icon' href='favicon.ico' />
</head>

<body>
  <div id="map"></div>
  <div id="lightgallery"></div>
  <script src="https://webapi.amap.com/maps?v=1.4.15&key=你的key"></script>
  <script src="main.js"></script>
</body>
</html>

运行测试

npm run start

打包部署

npm run build
直接将dist文件夹整个复制到服务器即可
例如nginx配置

    location /photos{
    	alias /l0vo.com/dist;
   	index index.html;
    }