indoor.js 初学者指南

引用自http://indoor.io/indoor.js/1.1.0/apidocs/#guide-libs

常用术语

地图

地图技术上是一些 MBTile 文件(每个楼层一个文件:包含地图标题和 UTFGrid 数据)和一个 GeoJSON 文件。在默认的情况下。MBTile 文件和 GeoJSON 文件都托管在 indoor.io 服务器上。indoor.io tools 生产这些文件。 UTFGrid 数据基于 GeoJSON 文件。但是 GeoJSON 文件对于关键字搜索仍然是需要的。

地图特性

GeoJSON 数据(由 indoor.io 生产)包含 区,线,和点。在本文中它们被称为地图特性。所有房间和开放空间都是区特性。墙,门,公共通道,和其他诸如此类的都是线特性。具有定义属性的特定点是点特性。

 

地图标识符

一个由 indoor.io mapper tools 产生的标识符。这个标识符指定多楼层的一个目的地。

 

项目标识符

一个由 indoor.io mapper tools 在导出的过程中产生的标识符。indoor.io tools 提供导出一个 web 地图格式的 3D 地图。这个标识符指定一个结果的导出过程。

实际上,地图和项目标识符一起表明“渲染中”的地图。它是一个或多个 MBTile 文件和一个附加的 GeoJSON 文件。

 

包含的库

  indoor.js 依赖于 jQuery. 请确认你的页面包含 jQuery。

为了使用 indoor.js。你必须包含两个库:mapbox.js 和 indoor.js。这可以通过增加下面的有<head>HTML元素的代码行来完成:

// jquery; please use jquery 1.9.1 or above
<script src='http://indoor.io/indoor.js/1.1.0/js/jquery-1.9.1.min.js'></script>
// map libraries
<link href='http://indoor.io/indoor.js/1.1.0/js/mapbox/mapbox.css' rel='stylesheet' />
<script src='http://indoor.io/indoor.js/1.1.0/js/mapbox/mapbox.js'></script>
<script src='http://indoor.io/indoor.js/1.1.0/js/indoor.js'></script>

mapbox.js 是一个由 mapbox.com 维护的开源库,你可以从这里阅读他的许可证。

 

地图初始化

在这个示例中。我们使用一个预定义的地图标识符,一个预定义的项目标识符,和两个回调函数(用于点击和悬停)创建一个新的地图。

第三个参数是未定义的。它可以是一个可能传入 L.mapbox.map 构造器的对象。

第四个参数是一个回调函数,它在地图加载完成时被调用。在我们的案例中,我们渐入地图并高亮特定的商店。

function go() {
  // let's create a map
  map = L.indoor.map('map', { 
      map: map_id, 
      project: project_id, 
      click: indoorMapClicked, 
      hover: indoorMapHovered
    }, 
    null, 
    function() {
      // if we wanted to show initially a specific level, we could do it here!
      // var levels = map.getLevels();
      map.setLevel('1');

      // instead, let's highlight some stores
      highlightSomeFeatures();

      // enable marker dragging; let's come back to this later
      enableMarkerDragging();
    });
}

go();

 

地图交互

所有基础的地图交互都是使用 Leaflet API functions 完成。这些函数包含地图的标记,拖动,平移,缩放,边界,页面偏移到 LatLng(经纬度)的转换等等,同样,你可以使用它加入附加的瓦片层,或自定义画布覆盖。请查看 Leaflet API 文档详细了解。

标记的位置是 L.indoor.latLng 类型(比如:它们有一个 level 属性),当 level 改变时会自动显示或隐藏。

在构造器函数中,提供了两个回调函数:indoorMapClicked 用于点击地图,indoorHovered 用于鼠标移动。

回调函数(提供给构造器函数)有一个具有下面两个属性的参数:

  1、event.feature: 一个 L.indoor.feature 对象

  2、event.latLng: 一个 L.indoor.latLng 对象

 

一个点击回调示例

在下面的示例函数中,点击商店弹出一个商店信息的链接和一个路线按钮。点击商店外部只弹出一个路线按钮。

var storeHighlight = null;
var storePopup = null;

function indoorMapClicked(event) {
  // uncomment this row to see some logging of the feature properties
  // if(event.feature) console.log(JSON.stringify(event.feature.geometry));
  // only do this if a known feature (that has a store id that we know) 
  if(event.feature && event.feature.properties.store && stores[event.feature.properties.store]) {
    if(storeHighlight) {
      // clear earlier highlights
      map.clearHighlight(storeHighlight);
      storeHighlight = null;
    }

    // if there is an existing click popup, remove it
    if(storePopup && map.hasLayer(storePopup)) map.removeLayer(storePopup);

    storeHighlight = map.highlightFeatures(event.feature, {stroke: true, color: '#00ff00'});
    
    // let's create a popup (using Leaflet API)
    storePopup = L.popup()
      .setLatLng(event.latLng)
      .setContent(
        // add a title
        '<h4>'+stores[event.feature.properties.store].name+'</h4>'+
        // add a store info button
        '<center>'+
        '<button class="btn btn-primary" '+
           'onclick="showRoute(\''+
             event.feature.properties.store+'\', '+
             event.latLng.lat+', '+
             event.latLng.lng+');">'+
           'Find route'+
        '</button>'+
        '</center>')
      .addTo(map);
  } else {
    var storePopup = L.popup()
        .setLatLng(event.latLng)
        .setContent(
          '<center>'+
          '<button class="btn btn-primary" '+
            'onclick="showRoute(undefined, '+
              event.latLng.lat+', '+
              event.latLng.lng+');">Find route</button></center>')
        .addTo(map);
  }
return;
}

 

一个悬停的回调示例

var hoverHighlight = null;
var hoveredFeature = null;

function indoorMapHovered(event) {
  // uncomment this row to see some logging of the feature properties
  // if(event.feature) console.log(JSON.stringify(event.feature.properties));
  if(event.feature) {
    if((hoveredFeature && hoveredFeature.properties.featureIdentifier != event.feature.properties.featureIdentifier) || !hoveredFeature) {
      if(hoverHighlight) map.clearHighlight(hoverHighlight);
      hoverHighlight = map.highlightFeatures(event.feature, {clickable: false, stroke: true, color: '#ffffff', opacity: '0.3'});
      hoveredFeature = event.feature;
    } else {
    
    }
    if(event.feature.properties.name) $("#legend_hover").empty().append(event.feature.properties.name);
    else $("#legend_hover").empty().append('an unnamed area');
    $("#legend_hover_wrapper").fadeIn();
  }
}

 

特性高亮

通过调用 map.highlightFeatures 函数高亮特性,单个特性和特性数组它都可以接受。特性高亮可以是任何 L.Path 样式。高亮通过在地图上增加一个 L.Polygon 图层完成。

L.indoor.feature 对象在这里有两种来源 :

  1、map.getFeatures 函数

  2、map 对象构造器中的地图鼠标事件

 

使用正则表达式匹配特性

在这个示例用,getFeature 带有一个商店属性对象调用。商店属性在 indoor.io mapping tools 中是设定的特定属性。

这个正则表达式匹配所有以 1 开头的三位数值:

function highlightFeaturesByRegexp() {
  // let's search with a reg exp that matches all features with a store value 140-149
  // but let's exclude those whose store info we know in an external array (stores)
  // and highlight the results with a blue color

  map.getFeatures({store: /14[0-9]{1}/}, function(error, features) {
    map.highlightFeatures(features, {
      clickable: false,
      stroke: true,
      fillColor: '#0000ff',
      fill: true,
      color: '#0000ff'
    });
  });
}

使用一个属性值匹配特性

在本例中,我们使用一个简单属性值匹配并高亮特性。

function highlightFeaturesByPropertyValue() {
  // search and highlight those features that are on level 2
  map.getFeatures({level: '2', store: /.+/}, function(error, features) {
    map.highlightFeatures(features, {
      clickable: false,
      stroke: true,
      fillColor: '#0000ff',
      fill: true,
      color: '#00ff00'
    });
  // if we wanted to zoom to specified feature 
  // map.fitFeatures(results[0]);
  });
}


增加一个附加特性

在本例中,我们从一个虚拟的数据源总获得了一个 WGS84 位置。

因为 indoor 地图可能在一个虚拟坐标系统中显示,在使用普通 WGS84 坐标增加图层或者标记之前必须转换成正确的坐标系统:

function addAnExternalFeature() {
  // let's add a marker to a WGS84 location. we have to convert the location first
  // because we are working in a virtual coordinate system
  map.convertLatLng(new L.LatLng(60.210388,25.080092), function(error, latLng) {
    // we can use plain Leaflet API now
    var icon = new L.Icon({
      iconUrl: 'marker-icon.png',
      iconSize: [25, 41],
      iconAnchor: [12, 41],
      shadowUrl: 'marker-shadow.png'});
    map.addLayer(new L.Marker(latLng, {icon: icon}));
  });
  map.convertLatLng(new L.LatLng(51.53793627891727, -0.616342926025401), function(error, latLng) {
    // we can use plain Leaflet API now
    var icon = new L.Icon({
      iconUrl: 'marker-icon.png',
      iconSize: [25, 41],
      iconAnchor: [12, 41],
      shadowUrl: 'marker-shadow.png'});
    map.addLayer(new L.Marker(latLng, {icon: icon}));
  });
 
}

最后,当地图加载后让我们调用所有这些函数:

function highlightSomeFeatures() {
  highlightFeaturesByRegexp();
  highlightFeaturesByPropertyValue();
  
  map.getFeatures({store: '152'}, function(error, stores) {
    for(var i in stores) {
      var icon = new L.DivIcon({html: 'Anttila'});
      var marker = new L.Marker(stores[i].properties.markerLocation, {className: 'textIcon', icon: icon});
      map.addLayer(marker);
    }
  });
  map.getFeatures({text: /.*/}, function(error, texts) {

  for(var i in texts) {
    var icon = new L.DivIcon({html: texts[i].properties.text});
    var marker = new L.Marker(texts[i].properties.markerLocation, {className: 'textIcon', icon: icon});
    map.addLayer(marker);
  }
  });

  if(false) map.getFeatures({icon: /.*/}, function(error, icons) {
    for(var i in icons) {
      var icon = new L.DivIcon({html: icons[i].properties.icon});
      var marker = new L.Marker(icons[i].properties.markerLocation, {className: 'textIcon', icon: icon});
      map.addLayer(marker);
    }
  });
  addAnExternalFeature();
}

 

路线

在本例中,使用者拖动一个标记图标到地图上。这个标记定义了起点。终点则根据用户点击地图确定。

这个路线计算是通过网络异步进行的。所以最好显示一个进度直到我们拿到路线:

function showRoute(id, lat, lng) {
  if(storePopup && map.hasLayer(storePopup)) map.removeLayer(storePopup);
  if(myLocation!= null) {
    // let's add a spinner for the route calculation time
    var spinner = new L.Marker(
      new L.LatLng(lat, lng), {
        icon: new L.Icon({
        iconUrl: 'images/spinner.gif', 
        iconAnchor: [20, 20], 
        className: 'L_indoor_spinner'})})
      .addTo(map);
    // we like white circles with a shadow
    $('.L_indoor_spinner')
      .css('box-shadow', '0px 0px 10px white')
      .css('padding-right', '2px')
      .css('background', 'white')
      .css('border-radius', '20px');

    // let's define our destination 
    var latLng = new L.indoor.latLng(lat, lng, map.getLevel());
    map.getRoute(myLocation, latLng, function(error, route) {
      if(error || route.length == 0) {
        map.removeLayer(spinner);
        alert('Route calculation was unsuccessful');
      } else {
        map.removeLayer(spinner);
        if(lastLine) map.hideRoute(lastLine);
        lastLine = map.showRoute(route, {
  startIcon: null, 
  endIcon: new L.Icon.Default(), 
  animate: false, 
  animateIcon: new L.Icon.Default(), 
  lineOptions: {color: '#000033', opacity: 1, dashArray: '5,1', weight: 10}
});
animateRoute({
     data: route, 
  options: {
    animateIcon: new L.Icon.Default()
  }
});
      }
    });
  } else {
    alert('Please define your position first by dragging the blue marker on the map.');
  }
  prevLatLng = event.latLng;
}

这段代码是一个动态路线的示例:

function animateRoute(route) {
  if(route.data.length == 0) return;
  if(!route.options.animateIcon) route.options.animateIcon = new L.divIcon({html: 'X'});
  if(!route.status) route.status = {};
  if(!route.headMarker && route.data.length > 0) {
    route.headMarker = new L.marker(new L.LatLng(route.data[0].lat, route.data[0].lng), {icon: route.options.animateIcon});
  }

  if(route.status.animationProgress == undefined) route.status.animationProgress = 0;

  // IF the route steps have not been calculated yet
  if(!route.totalLength) {
    // THEN calculate the route steps, only once for each route
    route.totalLength = 0;

    var lats = [];
    var lons = [];

    for(var p=0;p<route.data.length-1;p++) {
      route.data[p].distanceToNext = Math.sqrt(Math.pow(route.data[p].lat-route.data[p+1].lat, 2)+Math.pow(route.data[p].lng-route.data[p+1].lng, 2));
      route.data[p].totalDistanceToThis = route.totalLength;
      route.totalLength += route.data[p].distanceToNext;
      lats.push(route.data[p].lat);
      lons.push(route.data[p].lng);
    }
    lats.push(route.data[route.data.length-1].lat);
    lons.push(route.data[route.data.length-1].lng);

    var bbox = {
      minlon: Math.min.apply(null, lons), 
      minlat: Math.min.apply(null, lats),
      maxlon: Math.max.apply(null, lons), 
      maxlat: Math.max.apply(null, lats)};
    map.fitBounds([[bbox.minlat, bbox.minlon], [bbox.maxlat, bbox.maxlon]], new L.Point(80,70));

    route.data[route.data.length-1].totalDistanceToThis = route.totalLength;
    route.data[route.data.length-1].distanceToNext = -1;

    var refDistance = 0.0005640361071184976;
    var refCount = 60;
    route.status.animationMax = route.totalLength/refDistance*refCount;

    route.animationPoints = [];
    var animationUnit = route.totalLength/(route.status.animationMax-1);

    for(var i=0;i<route.status.animationMax;i++) {
      var ometer = i*animationUnit; 
      var previousPoint = route.data[0];
      var nextPoint = route.data[0];
      for(var p=0;p<route.data.length-1 && route.data[p].totalDistanceToThis<ometer;p++) {
previousPoint = nextPoint;
nextPoint = route.data[p+1];
      }
 
      if(previousPoint == nextPoint) 
route.animationPoints.push(previousPoint);
      else {
var distanceLeft = ometer-previousPoint.totalDistanceToThis;
var relDistance = distanceLeft/previousPoint.distanceToNext;
route.animationPoints.push({lat: previousPoint.lat+relDistance*(nextPoint.lat-previousPoint.lat), lng: previousPoint.lng+relDistance*(nextPoint.lng-previousPoint.lng), level: previousPoint.level});
      }
    }
  }

  var levelChangeDelay = 1000;
  var levelChangeFactor = 0;
  if(route.status.animationProgress >= route.status.animationMax) {
    route.animationComplete = true;
    route.status.animationProgress = undefined;
    if(map.hasLayer(route.headMarker)) 
      map.removeLayer(route.headMarker);
  } else {
    // animation here
    if(!map.hasLayer(route.headMarker)) map.addLayer(route.headMarker);
    var latLng = new L.LatLng(route.animationPoints[route.status.animationProgress].lat, route.animationPoints[route.status.animationProgress].lng);
    route.headMarker.setLatLng(latLng);
    if(map.getLevel() != route.animationPoints[route.status.animationProgress].level) {
      map.setLevel(route.animationPoints[route.status.animationProgress].level);
      levelChangeFactor = 1;
    }
  }
  
  if(!route.animationComplete) {
    route.status.animationProgress = Math.min(route.status.animationProgress+1, route.status.animationMax);
    setTimeout(function() { animateRoute(route); }, 1000/15+levelChangeDelay*levelChangeFactor);
  } 
}

 

在地图中拖入标记

在这个示例中, jQuery UI 用作创建一个可拖动到侧板的图标。我们实现这个可拖动对象的两个事件回调函数(开始和结束)。

在开始函数中,我们保存图标位移。在结束函数中,我们计算落点位置的像素坐标。然后我们调用 L.Map 的containerPointToLatLng 函数:

  请注意这个示例依赖于 jQuery UI。

// posMarker contains the latest position of the location marker
var posMarker = null;
// iconOffset stores the offset in the source icon
var iconOffset = null;


function enableMarkerDragging() {
  // this is a jQuery UI function
  $("#drag_icon").draggable({
    helper: function(event) {
      return '<img id="drag_helper" style="z-index: 99999;" src="marker-icon.png"/>';
    }, 
    start: function(event, element) {
      iconOffset = {left: $(event.target).width()/2, top: $(event.target).height()};
    },
    stop: function(event, element) {
      var offset = element.offset;
      var mapOffset = $("#map").offset();
      offset.left -= mapOffset.left;
      offset.top -= mapOffset.top;      

      if(offset.left < $("#map").width() && offset.top < $("#map").height()) {
        if(posMarker && map.hasLayer(posMarker)) map.removeLayer(posMarker);
        var latLng = map.containerPointToLatLng(
          new L.Point(
            offset.left+iconOffset.left, 
            offset.top+iconOffset.top)); 
        myLocation = latLng;
        myLocation.level = map.getLevel();25,41
        var icon = new L.Icon({iconUrl: 'marker-icon.png',
          iconSize: [25, 41],
          iconAnchor: [12, 41],
          shadowUrl: 'marker-shadow.png'});
        posMarker = new L.Marker(latLng, {icon: icon}).addTo(map);
      }
    }
  });
}

 

创建一个静态地图视图

在本例中,我们创建一个静态地图视图。它显示一个 'store' 属性是 '148' 的特性。

这是通过简单金庸所有控件和行动来达成的: level 控件,缩放空间,拖动和不同的缩放。

function createThumbnail() {
  $("#thumbnailButton").replaceWith(
    '<img src="images/spinner.gif" '+
       'id="static_spinner" '+
       'style="position: absolute; border-radius: 20px; box-shadow: 0px 0px 10px white;"/>');
  // let's create a new map
  var static_map = L.indoor.map('static_map', {
    map: map_id,
    project: project_id,
    levelControl: false // hides the level control
  }, {
    dragging: false, // disables mouse dragging
    touchZoom: false, // disables touch zooming
    boxZoom: false, // let's disable every zoom
    scrollWheelZoom: false,
    doubleClickZoom: false,
    zoomControl: false
  }, function() {
    $("#static_spinner").remove();

    // now the map has been loaded; look for a feature and highlight it.
    var features = static_map.getFeatures({store: '148'});
    static_map.fitFeatures(features);
    static_map.highlightFeatures(features, {clickable: false, fill: true, fillColor: '#ff0000'});
  });
}

 

API 文档

indoor.js API 结构

indoor.js API 封装了 Mapbox.js API(文档),Mapbox.js 又封装了 Leaflet API(文档)。

indoor.js API 定义了一些新的数据结构:

  1、L.indoor.map,一个新的地图基础类

  2、L.indoor.feature,一个数据结构用作标记地图特性(store, areas 等等)

      3、L.indoor.latLng,一个数据结构用作增加了 L.LatLng 的 level 属性的位置。

indoor.js 一直使用 2.5D。上面的地图就是一个 2.5D 的展示。它使用一个视角来显示要展现的建筑视图。

 

L.indoor.map

L.indoor.map 是主要的地图类。使用纯 Leaflet API。你可以通过调用 'new L.Map(args)' 创建一个地图对象。使用 Mapbox.js

API, 你调用 'L.mapbox.map(args)'。使用indoor.js, 你调用 'L.indoor.map(args)'。

构造器

通过调用 L.indoor.map 穿件一个新的 L.indoor.map 实例(不要使用 'new' 关键字)

var map = L.indoor.map('map', {map: '098234...', project: 'acd3234...', click: clicked, hover: hovered}, null, initView);        

 

 

参数描述
<String> elementId 地图 DOM 元素的的 Id
<Object> indoorIoOptions 一个具有下列属性的对象:
  • <String> map: 一个从 indoor.io mapper tools 提供的地图标识符
  • <String> project: 一个从 indoor.io mapper tools 提供的地图导出标识符
  • <function(event)> click: 一个回调函数,在鼠标点击时调用
  • <function(event)> hover: 一个回调函数,在发生鼠标移动事件时调用
<Object> mapboxJsOptions 一个可选对象,它将传入到 L.mapbox.map 构造器
<function()> callback 当 indoor 地图成功加载并初始化后这个函数将被调用

 

 回调函数

这个回调函数(构造函数给予的)有一个带有下列属性的参数:

  1、event.feature: 一个 L.indoor.feature 对象

  2、event.latLng:一个 L.indoor.latLng 对象

层级

 

方法返回值描述
getLevels() String[] 返回有效地图的层级名称数组
getLevel() String 返回当前层级的名称
setLevel( level) this 改变地图的当前层级,隐藏不等于新的有效层级的地点标记

 

路线

 

方法返回值描述
getRoute( <L.indoor.latLng> a,
<L.indoor.latLng> b,
<function(error, <L.indoor.latLng[]> route)> callback)
this  向 indoor.io 服务器创建一个的路线查询. 这个回调函数的 error 参数在路线计算成功的情况下是空值. 路线可能仍然是一个空数组, 它意味着在地图给定的几何点间没有路线.
showRoute( <L.indoor.latLng[]> route,
<Object> options)
<String> routeIdentifier  在地图上显示路线,并动态显示. 可选 object 支持的属性有
  • <L.Icon> startIcon: 路线显示的起点图标
  • <L.Icon> endIcon: 路线显示的终点图标
  • <L.Icon> animateIcon: 动态路线中显示的图标
  • <boolean> animate: 允许/禁止动态路线
  • <L.Path options> lineOptions: 定义路线的线条样式
 图标可以以 L.IconL.DivIcon构造。动态图标 (使用 GIF 图像) 将以 L.DivIcons 实现
hideRoute( <String> routeIdentifier) this 隐藏路线包括他的终点, 起点, 和动态图标和路线的路径

 

地图特性

map.getFeatures 是一个用于查询特性的函数。它接受一个最大的两个参数:一个字符串,一个正则表达式或者一个对象。还有一个附加函数:

  1、string: 如果给定一个字符串,每个带有给定值得属性的特性将被加入到结果特性列表中。

  2、RegExp: 如果给定一个正则表达式,每个匹配正则表达式属性的特性将被加入到结果特性列表中。

  3、object: 如果给定一个对象,每个匹配属性的特性将被加入到结果特性列表中。参数对象的属性值可能是正则表达式。

  4、function(feature): 如果给定一个函数,它将被每个结果特性调用。如果函数返回 false。这个特性将从结果中排除。这个函数也可以在特性上一个接一个的使用。

 

方法返回值描述
getFeatures( <string | object | RegExp> 查询参数, <function(<L.indoor.feature> feature)> 迭代函数) <L.indoor.feature[]> 特性数组 返回数据属性匹配给定的参数的特性
highlightFeatures( <L.indoor.feature | L.indoor.feature[]> 特性, <L.Path options> 样式) <String> 高亮 Id  通过使用给定样式画一个多边形来高亮给定特性
clearHighlight( <String> 高亮 Id) this 清除一个高亮 (单一特性或一个高亮的参数).
clearHighlights( ) this 清除所有特性的高亮.

 

L.indoor.feature

L.indoor.feature 是一个由 indoor.js 创建的有下列属性的常用 JavaScript 对象:

 

属性描述
geometry 定义特性的几何结构成一个路径数组 (每个路径是一个 L.indoor.latLng 对象数组). 所以这是一个 L.indoor.latLng 对象数组的数组。大部分特性将总是有一个唯一的路径,  所以开发者通常用 for(var c=0;c<geometry[0].length;c++) 来穿过这个多边形.

 这个属性可以用作自定义几何结构, 比如在原多边形中绘制自定义多边形,或者剪裁它.
properties 特性的这个数据属性,来自于 mapper tool 中定义的 area 特性. 除了这些, 还有一直存在的特性:
  • <L.indoor.latLng> markerLocation: 特性的一个默认的位置点. 它对于增加商店标记或者寻找到商店的路线很有用.
level 特性的层级(作为一个字符串)

 

L.indoor.latLng

L.indoor.latLng 是一个带有额外层级属性的普通 L.indoor.latLng 对象

 

浏览器支持

indoor.js 支持 Leaflet API 所支持的浏览器

桌面浏览器

  1、Chrome

  2、Firefox

  3、Safari 5+

  4、Opera 11.11+

  5、IE 7-10

  6、IE 6(部分支持)

移动浏览器

  1、Safari for iOS 3/4/5/6+

  2、Android browser 2.2+,3.1+,4+

  3、Chrome for Android 4+ and iOS

  4、Firefox for Android

  5、Other webkit-based browser(webOS,Blackberry 7+)

  6、IE10 for Window 8 -based devices

 

posted on 2015-10-20 17:21  Sky.Y.Chen  阅读(973)  评论(0)    收藏  举报