如果你在很久以前用个Google Earth,应该会对地图上密密麻麻的图片小图标有印象。 前阵子在外面旅游的时候忽然想到,要是能把旅游的照片也像这样显示在地图上,也是一件挺好玩的事情。 而且去了哪些地方也一目了然。所以便想着实现这个想法了。 整个想法的实现也比较简单,因为利用手机拍摄的照片一般都会随相片保存有地理位置信息(这项功能没关闭且定位正常的情况下)。 所以可以通过读取相片中的地理位置信息并结合地图SDK(如百度地图),便可以将图片显示在地图上,从而可以实现类似刚刚说到的Google Earth上的功能。 所以实现主要分两个步骤,一是读取相片EXIF中的位置信息,二是将位置显示在地图上。 当然会有很多细节问题,这在下面会提到。
1.获取照片EXIF信息
在Android中获取EXIF信息非常简单,因为系统SDK中已经集成了相关API,我们要做的就是调用就好。读取EXIF信息的部分代码如下。
public double cvtLocTag(String locTag, String locRef) {
// 直接获取到的位置信息需要解析一下才能使用
// 33/1,26/1,465/10000
// 度分秒以逗号隔开,且以除号表示,这样做的好处是以整形存储了浮点型数据
String loc_deg = locTag.split(",")[0];
String loc_min = locTag.split(",")[1];
String loc_sec = locTag.split(",")[2];
double loc_Deg = Double.parseDouble(loc_deg.split("/")[0]) / 1.0;
double loc_Min = Double.parseDouble(loc_min.split("/")[0]) / 1.0;
double loc_Sec = Double.parseDouble(loc_sec.split("/")[0]) / 10000.0;
double loc = loc_Deg + loc_Min / 60.0 + loc_Sec / 3600.0;
// 在计算中南纬和西经都为负数,所以增加符号
if (locRef.contains("S") || locRef.contains("W")) {
loc = -1 * loc;
}
return loc;
}
public LatLng getLocInfo(String img_path) {
try {
// Step1 新建ExifInterface对象用于获取EXIF信息,传入的参数是图片路径
ExifInterface exifInterface = new ExifInterface(img_path);
// Step2 调用获取属性函数获取属性值
// TAG_GPS_LATITUDE:纬度 TAG_GPS_LONGITUDE:经度
// TAG_GPS_LATITUDE_REF:南或北半球(S or N) TAG_GPS_LONGITUDE_REF:东或西半球(E or W)
String latitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
String longitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
String latitudeRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
String longitudeRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
// 有些照片没有位置信息,因此需要做个判断
if (latitude == null || longitude == null) {
return null;
} else {
// 对于有位置信息的照片,调用函数解析经纬度并返回
double lat = cvtLocTag(latitude, latitudeRef);
double lon = cvtLocTag(longitude, longitudeRef);
LatLng point = new LatLng(lat, lon);
return point;
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
EXIF信息有自己的格式规范,所以照着格式读取即可。
这里LatLng
是百度地图SDK里的坐标点类,存放的就是经纬度信息。如果只是读取信息不与百度地图交互的话,可以不用这个类。
除此之外,也利用Python实现了EXIF的读取,代码如下。
# coding=utf-8
from PIL import Image
from PIL.ExifTags import TAGS
import os
def findAllFiles(root_dir, filter):
"""
遍历搜索文件
:param root_dir:搜索目录
:param filter: 搜索文件类型
:return: 路径、文件名、路径+文件名
"""
print("Finding files ends with \'" + filter + "\' ...")
separator = os.path.sep
paths = []
names = []
files = []
# 遍历
for parent, dirname, filenames in os.walk(root_dir):
for filename in filenames:
if filename.endswith(filter):
paths.append(parent + separator)
names.append(filename)
for i in range(paths.__len__()):
files.append(paths[i] + names[i])
print (names.__len__().__str__() + " files have been found.")
paths.sort()
names.sort()
files.sort()
return paths, names, files
def get_exif_data(fname):
"""
获取EXIF信息
:param fname: 影像文件路径
:return: 字典类型的EXIF信息
"""
ret = {}
try:
img = Image.open(fname)
if hasattr(img, '_getexif'):
exifinfo = img._getexif()
if exifinfo != None:
for tag, value in exifinfo.items():
decoded = TAGS.get(tag, tag)
ret[decoded] = value
except IOError:
print 'IOERROR ' + fname
return ret
def decodeGPS(gps_info):
"""
从结构数据中解析经纬度信息
:param gps_info: 结构化数据
:return: 经纬度数据
"""
latFlag = gps_info[0][1]
lat = gps_info[1][1]
lat_deg = lat[0][0]
lat_min = lat[1][0]
lat_sec = lat[2][0] / 10000.0
lonFlag = gps_info[2][1]
lon = gps_info[3][1]
lon_deg = lon[0][0]
lon_min = lon[1][0]
lon_sec = lon[2][0] / 10000.0
lat_decimal = lat_deg + lat_min / 60.0 + lat_sec / 3600.0
lon_decimal = lon_deg + lon_min / 60.0 + lon_sec / 2600.0
lat_info = (latFlag, lat_decimal)
lon_info = (lonFlag, lon_decimal)
return lat_info, lon_info
def decode2Str(loc_info):
"""
将位置信息转成字符串
:param loc_info: 结构化位置信息
:return: 字符串位置信息,如32.12345N,117.12345E
"""
lat = loc_info[0][1].__str__() + loc_info[0][0]
lon = loc_info[1][1].__str__() + loc_info[1][0]
str = lat + "," + lon
return str
def getGPSInfo(filename):
"""
获取EXIF中的地理位置信息
:param filename: 影像路径
:return: 字符串类型的地理位置信息
"""
exif = get_exif_data(filename)
if exif.has_key('GPSInfo'):
info = exif.get('GPSInfo')
gps_info = info.items()
if len(gps_info) < 4:
return "No GPSInfo."
else:
loc = decodeGPS(gps_info)
return decode2Str(loc)
else:
return "No GPSInfo."
def getGPSInfoBatch(filenames):
"""
批量获取文件的位置信息
:param filenames: list类型的文件列表
:return: list类型的位置列表
"""
GPSInfos = []
for filename in filenames:
res = getGPSInfo(filename)
if res.__contains__('No GPSInfo'):
continue
else:
GPSInfos.append(res)
return GPSInfos
paths, names, files = findAllFiles('E:\\Camera', 'jpg')
GPS = getGPSInfoBatch(files)
for item in GPS:
print item
其实最开始我是先写的Python版本,因为写起来比较快,来试试可行性的。 在PIL库中也自带了获取EXIF信息的接口,所以写起来很方便。 后来发现有可行性,感觉还不错,才写了JAVA版的函数,继续进行了下去。
2.调用百度地图SDK
在获取到了影像的位置信息后,下面就是利用百度地图SDK在地图上进行显示了。 关于如何使用百度地图SDK,在前几篇博客中已经说了很多了,如果不会的话可以看看。 主要涉及到的就是Marker的显示。获取到各个影像的位置后,利用for循环即可。 限于篇幅,添加并绘制Marker的部分代码如下。完整项目代码见文末。
private Marker addMarker(BaiduMap baiduMap, LatLng loc, @DrawableRes int ID) {
BitmapDescriptor bd = BitmapDescriptorFactory.fromResource(ID);
MarkerOptions markerOptions = new MarkerOptions().position(loc).icon(bd).zIndex(9);
Marker marker = (Marker) baiduMap.addOverlay(markerOptions);
return marker;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
String filepath = data.getData().getPath();
if (filepath.contains("external_files")) {
filepath = Environment.getExternalStorageDirectory().toString() + filepath.substring(15);
}
// 由于这里我们需要的并不是某一个文件,而是文件夹
// 所以可以让用户选择某个文件,我们识别出当前文件夹,并遍历图像
String dirPath = filepath.substring(0, filepath.lastIndexOf('/'));
File file = new File(dirPath);
File[] subFile = file.listFiles();
for (File temp : subFile) {
LatLng point = getLocInfo(temp.getPath());
if (point != null) {
img_paths.add(temp.getPath());
img_locs.add(point);
} else {
Log.e("error", "no location info");
}
}
for (LatLng item : img_locs) {
markerList.add(addMarker(baiduMap, item, R.drawable.marker_01));
}
}
这样,整个APP的大体功能就被实现了。演示效果如下。
总体而言效果还是不错的,但有时因为GPS本身定位不准等等问题,可以看到Marker显示的位置并不一定精确。 但这就不是我们这个博客所讨论的问题了。
3.其它细节问题
(1)应用权限问题
在调试过程中发现,可能会出现无法打开图片,打开图片就闪退的情况。遇到这种情况请先检查是否给了APP读写存储的权限。 如果没有,在给了权限以后应该就好了。高版本Android和以前不一样了,虽然在清单中申明了权限,但有时可能也不太好使。 新版本Android引入了动态权限管理机制,在APP需要权限时如果没有,则会弹出对话框让用户选择是否给予权限。 如果有兴趣可以搜索相关资料。这里为了代码的简洁就没有使用过多复杂的东西。
(2)Marker的InfoWindow
在之前介绍Marker的这篇博客中,没有说如何使用InfoWindow,但也比较简单。 直接新建一个InfoWindow对象,并在地图中show出来即可,示例代码如下。
// 新建InfoWindow对象
private InfoWindow infoWindow;
baiduMap.setOnMarkerClickListener(new BaiduMap.OnMarkerClickListener() {
@Override
public boolean onMarkerClick(final Marker marker) {
// 创建InfoWindow的内容
Button button = new Button(getApplicationContext());
button.setBackgroundResource(R.drawable.popup);
// 新建InfoWindow点击事件监听器
InfoWindow.OnInfoWindowClickListener listener = null;
// 具体指定点击InfoWindow后触发的操作
listener = new InfoWindow.OnInfoWindowClickListener() {
@Override
public void onInfoWindowClick() {
passIMG2Sys(img_paths.get(index));
baiduMap.hideInfoWindow();
}
};
// 配置InfoWindow
infoWindow = new InfoWindow(BitmapDescriptorFactory.fromView(button), ll, -47, listener);
// 显示InfoWindow
baiduMap.showInfoWindow(infoWindow);
return false;
}
});
InfoWindow的点击事件不是必须的,如果你只是用它来显示一些信息的话。
最后别忘了showInfoWindow()
这句,有时候你点Marker就是不显示InfoWindow,又没报错,很有可能就是漏了这句。
(3)Android遍历获取文件
Android遍历获取文件完全可以采用JAVA的办法,只是需要注意一些权限的问题。在JAVA中遍历获取文件的代码如下。
List<String> files = new ArrayList<>();
String dirPath = "dir";
File file = new File(dirPath);
File[] subFile = file.listFiles();
for (File temp : subFile) {
files.add(temp.getPath());
File
对象不仅仅可以针对具体文件,也可以针对一个目录,这点需要注意一下。
(4)传递文件路径问题
在高版本的Android系统中,对于URI的控制更为严格了。在以前直接Uri.fromFile(String filepath)
即可。现在则直接会报错。
具体解决办法可以参考这篇博客,说的挺详细的。
唯一需要补充一点的就是provider
标签要放在application
标签内部,否则无法识别。
对于高版本的Android系统,传出图片文件路径的代码如下。
public void passIMG2Sys(String filepath) {
File img = new File(filepath);
Intent intent = new Intent(Intent.ACTION_VIEW);
//判断是否是AndroidN以及更高的版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(
getApplicationContext(),
BuildConfig.APPLICATION_ID + ".fileProvider", img);
intent.setDataAndType(contentUri, "image/*");
} else {
intent.setDataAndType(Uri.fromFile(img), "image/*");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivity(intent);
}
上述代码实现了传出一个图片文件路径,并开启一个Intent,用户可以选择打开这个图片文件的不同方式。
(5)标题栏按钮
在标题栏右边添加按钮,可以参考这个网页,写的简单易懂。
4.更多功能
由于时间有限,只是把想法做了最基本的实现。如果有时间还可以继续完善。 如可以将读取的照片以ListView方式展示,用户点击某个Item,地图就可以自动跳转到相应地方。 以及增加不同Marker间的距离统计功能,统计出这一路走来走过的距离等等很多有趣好玩的功能。 而且这些功能实现起来并不困难,基本都有对应的接口和函数。
最后,将项目的所有代码以及apk文件上传到了Github,点击查看。
本文作者原创,未经许可不得转载,谢谢配合