diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e65fe25dc..408e6736ef 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,7 @@ option(OSGEARTH_BUILD_DOCS "Include the documentation folder" ON) option(OSGEARTH_ENABLE_PROFILING "Enable profiling with Tracy" OFF) option(OSGEARTH_ENABLE_GEOCODER "Enable the geocoder (GDAL/OGR must be built with geocoder support)" OFF) option(OSGEARTH_ASSUME_SINGLE_GL_CONTEXT "Assume the use of a single GL context for all GL objects (advanced)" OFF) +option(OSGEARTH_ASSUME_SINGLE_THREADED_OSG "Assume OSG will always be configured to run in SingleThreaded mode (advanced)" OFF) option(OSGEARTH_INSTALL_SHADERS "Whether to deploy GLSL shaders when installing (OFF=inlined shaders)" OFF) mark_as_advanced(OSGEARTH_BUILD_TESTS) @@ -204,6 +205,10 @@ if(OSGEARTH_ASSUME_SINGLE_GL_CONTEXT) add_definitions(-DOSGEARTH_SINGLE_GL_CONTEXT) endif() +if(OSGEARTH_ASSUME_SINGLE_THREADED_OSG) + add_definitions(-DOSGEARTH_SINGLE_THREADED_OSG) +endif() + # Protobuf enables the MVT (Mapnik Vector Tiles) format set(PROTOBUF_USE_DLLS FALSE CACHE BOOL "Set this to true if Protobuf is compiled as dll") diff --git a/src/applications/osgearth_terrainprofile/osgearth_terrainprofile.cpp b/src/applications/osgearth_terrainprofile/osgearth_terrainprofile.cpp index 1a4f70b608..8d030008ef 100644 --- a/src/applications/osgearth_terrainprofile/osgearth_terrainprofile.cpp +++ b/src/applications/osgearth_terrainprofile/osgearth_terrainprofile.cpp @@ -108,24 +108,28 @@ class TerrainProfileGraph : public osg::Group _distanceMinLabel->setFont( font.get() ); _distanceMinLabel->setAlignment(osgText::TextBase::LEFT_BOTTOM); _distanceMinLabel->setColor(textColor); + _distanceMinLabel->setDataVariance(osg::Object::DYNAMIC); _distanceMaxLabel = new osgText::Text(); _distanceMaxLabel->setCharacterSize( textSize ); _distanceMaxLabel->setFont( font.get() ); _distanceMaxLabel->setAlignment(osgText::TextBase::RIGHT_BOTTOM); _distanceMaxLabel->setColor(textColor); + _distanceMaxLabel->setDataVariance(osg::Object::DYNAMIC); _elevationMinLabel = new osgText::Text(); _elevationMinLabel->setCharacterSize( textSize ); _elevationMinLabel->setFont( font.get() ); _elevationMinLabel->setAlignment(osgText::TextBase::RIGHT_BOTTOM); _elevationMinLabel->setColor(textColor); + _elevationMinLabel->setDataVariance(osg::Object::DYNAMIC); _elevationMaxLabel = new osgText::Text(); _elevationMaxLabel->setCharacterSize( textSize ); _elevationMaxLabel->setFont( font.get() ); _elevationMaxLabel->setAlignment(osgText::TextBase::RIGHT_TOP); _elevationMaxLabel->setColor(textColor); + _elevationMaxLabel->setDataVariance(osg::Object::DYNAMIC); } ~TerrainProfileGraph() diff --git a/src/osgEarth/BuildGeometryFilter.cpp b/src/osgEarth/BuildGeometryFilter.cpp index e2f9c193c9..05945cdc64 100644 --- a/src/osgEarth/BuildGeometryFilter.cpp +++ b/src/osgEarth/BuildGeometryFilter.cpp @@ -47,6 +47,7 @@ #include #include #include +#include "weemesh.h" #define LC "[BuildGeometryFilter] " @@ -973,6 +974,43 @@ namespace #ifdef USE_GNOMONIC_TESSELLATION +namespace +{ + // Transforms a range of points from geographic (long lat) to gnomonic coordinates + // around a centroid with an optional scale. + template + void geo_to_gnomonic(T& p, const T& centroid, double scale) + { + double lon0 = deg2rad(centroid[0]); + double lat0 = deg2rad(centroid[1]); + + double lon = deg2rad(p[0]); + double lat = deg2rad(p[1]); + double d = sin(lat0) * sin(lat) + cos(lat0) * cos(lat) * cos(lon - lon0); + p[0] = scale * (cos(lat) * sin(lon - lon0)) / d; + p[1] = scale * (cos(lat0) * sin(lat) - sin(lat0) * cos(lat) * cos(lon - lon0)) / d; + } + + // Transforms a range of points from gnomonic coordinates around a centroid with a + // given scale to geographic (long lat) coordinates. + template + void gnomonic_to_geo(T1& p, const T2& centroid, double scale) + { + double lon0 = deg2rad(centroid[0]); + double lat0 = deg2rad(centroid[1]); + + double x = p[0] / scale, y = p[1] / scale; + double rho = sqrt(x * x + y * y); + double c = atan(rho); + + double lat = asin(cos(c) * sin(lat0) + (y * sin(c) * cos(lat0) / rho)); + double lon = lon0 + atan((x * sin(c)) / (rho * cos(lat0) * cos(c) - y * sin(lat0) * sin(c))); + + p[0] = rad2deg(lon); + p[1] = rad2deg(lat); + } +} + void BuildGeometryFilter::tileAndBuildPolygon( Geometry* input, @@ -992,120 +1030,265 @@ BuildGeometryFilter::tileAndBuildPolygon( // hard copy so we can project the values osg::ref_ptr proj = input->clone(); - Tessellator::Plane plane = Tessellator::PLANE_XY; + auto render = _style.get(); - if (outputSRS) + // weemesh path ONLY happens if maxTessAngle is set for now. + // We will keep it this way until testing is complete -gw + if (outputSRS && outputSRS->isGeographic() && render && render->maxTessAngle().isSet()) { - // for geographic data we need to project into 2D before tessellating: - if (outputSRS->isGeographic()) + // weemesh triangulation approach (from Rocky) + + // scales our local gnomonic coordinates so they are the same order of magnitude as + // weemesh's default epsilon values: + const double gnomonic_scale = 1000.0; + + // Meshed triangles will be at a maximum this many degrees across in size, + // to help follow the curvature of the earth. + const double resolution_degrees = render ? render->maxTessAngle()->as(Units::DEGREES): 1.0; + + // some conversions we will need: + auto feature_geo = inputSRS; + + // centroid for use with the gnomonic projection: + osg::Vec3d centroid = input->getBounds().center(); + inputSRS->transform(centroid, outputSRS, centroid); // to geographic + + // transform to gnomonic. We are not using SRS/PROJ for the gnomonic projection + // because it would require creating a new SRS for each and every feature (because + // of the centroid) and that is way too slow. + auto local_geom = proj; // working copy + Bounds local_ex; + double z = -DBL_MAX; + GeometryIterator iter(local_geom.get()); + while (iter.hasMore()) + { + auto part = iter.next(); + inputSRS->transform(part->asVector(), outputSRS); // to geographic + for (auto& p : *part) + { + geo_to_gnomonic(p, centroid, gnomonic_scale); + local_ex.expandBy(p); + z = std::max(z, p.z()); + } + } + + // start with a weemesh covering the feature extent. + weemesh::mesh_t m; + const int marker = 0; + double xspan = gnomonic_scale * resolution_degrees * 3.14159 / 180.0; + double yspan = gnomonic_scale * resolution_degrees * 3.14159 / 180.0; + double width = (local_ex.xMax() - local_ex.xMin()); + double height = (local_ex.yMax() - local_ex.yMin()); + int cols = std::max(2, (int)(width / xspan)); + int rows = std::max(2, (int)(height / yspan)); + for (int row = 0; row < rows; ++row) + { + double v = (double)row / (double)(rows - 1); + double y = local_ex.yMin() + v * height; + + for (int col = 0; col < cols; ++col) + { + double u = (double)col / (double)(cols - 1); + double x = local_ex.xMin() + u * width; + m.get_or_create_vertex_from_vec3(weemesh::vert_t{ x, y, z }, marker | m._has_elevation_marker); + } + } + + for (int row = 0; row < rows - 1; ++row) + { + for (int col = 0; col < cols - 1; ++col) + { + int k = row * cols + col; + m.add_triangle(k, k + 1, k + cols); + m.add_triangle(k + 1, k + cols + 1, k + cols); + } + } + + // next, apply the segments of the polygon to slice the mesh into triangles. + ConstGeometryIterator segment_iter(local_geom.get()); + while (segment_iter.hasMore()) + { + auto part = segment_iter.next(); + for (unsigned i = 0; i < part->size(); ++i) + { + unsigned j = (i == part->size() - 1) ? 0 : i + 1; + weemesh::vert_t a((*part)[i].x(), (*part)[i].y(), (*part)[i].z()); + weemesh::vert_t b((*part)[j].x(), (*part)[j].y(), (*part)[j].z()); + m.insert(weemesh::segment_t{ a, b }, marker); + } + } + + // next we need to remove all the exterior triangles. + std::unordered_set insiders; + std::unordered_set outsiders; + ConstGeometryIterator remove_iter(local_geom.get(), false); + while (remove_iter.hasMore()) + { + auto part = remove_iter.next(); + + for (auto& tri_iter : m.triangles) + { + weemesh::triangle_t& tri = tri_iter.second; + auto c = (tri.p0 + tri.p1 + tri.p2) * (1.0 / 3.0); // centroid + bool inside = part->contains2D(c.x, c.y); + if (inside) + insiders.insert(&tri); + else + outsiders.insert(&tri); + } + } + for (auto tri : outsiders) + { + if (insiders.count(tri) == 0) + { + m.remove_triangle(*tri); + } + } + + // Finally we convert from gnomonic back to localized tile coordinates. + osg::ref_ptr new_verts = new osg::Vec3Array(); + new_verts->reserve(m.verts.size()); + osg::Vec3d temp; + for (auto& v : m.verts) + { + gnomonic_to_geo(v, centroid, gnomonic_scale); // to geographic + outputSRS->transformToWorld(osg::Vec3d(v.x, v.y, v.z), temp); // to ECEF + new_verts->push_back(temp * world2local); // localized to tile + } + + // Assemble the final geometry. + auto de = new osg::DrawElementsUInt(GL_TRIANGLES); + de->reserve(m.triangles.size() * 3); + for (auto& tri : m.triangles) { - osg::Vec3d temp; - osg::BoundingBoxd ecef_bb; + de->addElement(tri.second.i0); + de->addElement(tri.second.i1); + de->addElement(tri.second.i2); + } - bool allOnEquator = true; - GeometryIterator xform_iter(proj.get(), true); - while (xform_iter.hasMore()) + osgGeom->setVertexArray(new_verts.get()); + osgGeom->removePrimitiveSet(0, osgGeom->getNumPrimitiveSets()); + osgGeom->addPrimitiveSet(de); + } + + else + { + // original tesselation approach + Tessellator::Plane plane = Tessellator::PLANE_XY; + + if (outputSRS) + { + // for geographic data we need to project into 2D before tessellating: + if (outputSRS->isGeographic()) { - Geometry* part = xform_iter.next(); - part->open(); - for (osg::Vec3d& p : *part) + osg::Vec3d temp; + osg::BoundingBoxd ecef_bb; + + bool allOnEquator = true; + GeometryIterator xform_iter(proj.get(), true); + while (xform_iter.hasMore()) { - inputSRS->transform(p, outputSRS, temp); - if (temp.y() != 0.0) + Geometry* part = xform_iter.next(); + part->open(); + for (osg::Vec3d& p : *part) { - allOnEquator = false; + inputSRS->transform(p, outputSRS, temp); + if (temp.y() != 0.0) + { + allOnEquator = false; + } + outputSRS->transformToWorld(temp, p); + ecef_bb.expandBy(p); } - outputSRS->transformToWorld(temp, p); - ecef_bb.expandBy(p); } - } - const osg::Vec3d& center = ecef_bb.center(); + const osg::Vec3d& center = ecef_bb.center(); - GeometryIterator proj_iter(proj.get(), true); - while (proj_iter.hasMore()) - { - Geometry* part = proj_iter.next(); - for (osg::Vec3d& p : *part) + GeometryIterator proj_iter(proj.get(), true); + while (proj_iter.hasMore()) { - // The gnomonic equation won't provide any variation in y values if all of the coordinates are on the equator, so - // adjust the point slightly up from the equator if all points lie on the equator. - if (allOnEquator) + Geometry* part = proj_iter.next(); + for (osg::Vec3d& p : *part) { - p.z() += 0.0000001; + // The gnomonic equation won't provide any variation in y values if all of the coordinates are on the equator, so + // adjust the point slightly up from the equator if all points lie on the equator. + if (allOnEquator) + { + p.z() += 0.0000001; + } + ecef_to_gnomonic(p, center, outputSRS->getEllipsoid()); } - ecef_to_gnomonic(p, center, outputSRS->getEllipsoid()); } } - } - else - { - GeometryIterator xform_iter(proj.get(), true); - while (xform_iter.hasMore()) + else { - Geometry* part = xform_iter.next(); - part->open(); - inputSRS->transform(part->asVector(), outputSRS); + GeometryIterator xform_iter(proj.get(), true); + while (xform_iter.hasMore()) + { + Geometry* part = xform_iter.next(); + part->open(); + inputSRS->transform(part->asVector(), outputSRS); + } } } - } - else - { - // with no SRS, we need to automatically figure out what - // is the closest plane for tessellation - plane = Tessellator::PLANE_AUTO; - } + else + { + // with no SRS, we need to automatically figure out what + // is the closest plane for tessellation + plane = Tessellator::PLANE_AUTO; + } - // tessellate - Tessellator tess; + // tessellate + Tessellator tess; - std::vector indices; - if (tess.tessellate2D(proj.get(), indices, plane) == false) - return; + std::vector indices; + if (tess.tessellate2D(proj.get(), indices, plane) == false) + return; - if (indices.empty()) - return; + if (indices.empty()) + return; - int offset = verts->size(); + int offset = verts->size(); - osg::Vec3d temp, vert; + osg::Vec3d temp, vert; - if (outputSRS && outputSRS->isGeographic()) - { - ConstGeometryIterator verts_iter(input, true); - while (verts_iter.hasMore()) + if (outputSRS && outputSRS->isGeographic()) { - const Geometry* part = verts_iter.next(); - for (const auto& p : *part) + ConstGeometryIterator verts_iter(input, true); + while (verts_iter.hasMore()) { - inputSRS->transform(p, outputSRS, temp); - outputSRS->transformToWorld(temp, vert); - vert = vert * world2local; - verts->push_back(vert); + const Geometry* part = verts_iter.next(); + for (const auto& p : *part) + { + inputSRS->transform(p, outputSRS, temp); + outputSRS->transformToWorld(temp, vert); + vert = vert * world2local; + verts->push_back(vert); + } } } - } - else - { - ConstGeometryIterator verts_iter(proj.get(), true); - while (verts_iter.hasMore()) + else { - const Geometry* part = verts_iter.next(); - for (const auto& p : *part) + ConstGeometryIterator verts_iter(proj.get(), true); + while (verts_iter.hasMore()) { - verts->push_back(p * world2local); + const Geometry* part = verts_iter.next(); + for (const auto& p : *part) + { + verts->push_back(p * world2local); + } } } - } - osg::DrawElements* de = new osg::DrawElementsUInt( - GL_TRIANGLES, - indices.size(), - &indices[0]); + osg::DrawElements* de = new osg::DrawElementsUInt( + GL_TRIANGLES, + indices.size(), + &indices[0]); - osgGeom->setVertexArray(verts.get()); - osgGeom->addPrimitiveSet(de); + osgGeom->setVertexArray(verts.get()); + osgGeom->addPrimitiveSet(de); + } } #else diff --git a/src/osgEarth/Cache b/src/osgEarth/Cache index 3db88aa82c..735c8b8f63 100644 --- a/src/osgEarth/Cache +++ b/src/osgEarth/Cache @@ -156,7 +156,7 @@ OSGEARTH_SPECIALIZE_CONFIG(osgEarth::CacheOptions); namespace osgEarth { - typedef PerObjectRefMap ThreadSafeCacheBinMap; + using ThreadSafeCacheBinMap = PerObjectRefMap; /** * Cache is a container for local storage of keyed data elements. diff --git a/src/osgEarth/Config b/src/osgEarth/Config index d1475bf07b..0655c4f6c9 100644 --- a/src/osgEarth/Config +++ b/src/osgEarth/Config @@ -666,7 +666,9 @@ namespace osgEarth MYCLASS (const ConfigOptions& opt) : SUPERCLASS(opt) { fromConfig(_conf); } //! optional property macro -#define OE_OPTION(TYPE, NAME) \ +#define OE_OPTION_0_ARGS() +#define OE_OPTION_1_ARGS() +#define OE_OPTION_2_ARGS(TYPE, NAME) \ private: \ osgEarth::optional< TYPE > _ ## NAME ; \ mutable std::vector> _ ## NAME ## _listeners; \ @@ -679,6 +681,27 @@ namespace osgEarth _ ## NAME = value; \ for(auto& notify : _ ## NAME ## _listeners) notify(value); } +#define OE_OPTION_3_ARGS(TYPE, NAME, DEFAULT_VALUE) \ + private: \ + osgEarth::optional< TYPE > _ ## NAME {DEFAULT_VALUE}; \ + mutable std::vector> _ ## NAME ## _listeners; \ + public: \ + osgEarth::optional< TYPE >& NAME () { return _ ## NAME; } \ + const osgEarth::optional< TYPE >& NAME () const { return _ ## NAME; } \ + void NAME ## Changed (std::function f) const { \ + _ ## NAME ## _listeners.push_back(f); } \ + void set_ ## NAME (const TYPE& value) { \ + _ ## NAME = value; \ + for(auto& notify : _ ## NAME ## _listeners) notify(value); } + +// https://stackoverflow.com/a/28074198 +#define OE_OPTION_FUNC_CHOOSER(_f1, _f2, _f3, _f4, ...) _f4 +#define OE_OPTION_FUNC_RECOMPOSER(ARGS) OE_OPTION_FUNC_CHOOSER ARGS +#define OE_OPTION_CHOOSE_FROM_ARG_COUNT(...) OE_OPTION_FUNC_RECOMPOSER((__VA_ARGS__, OE_OPTION_3_ARGS, OE_OPTION_2_ARGS, OE_OPTION_1_ARGS, )) +#define OE_OPTION_NO_ARG_EXPANDER() ,,OE_OPTION_0_ARGS +#define OE_OPTION_MACRO_CHOOSER(...) OE_OPTION_CHOOSE_FROM_ARG_COUNT(OE_OPTION_NO_ARG_EXPANDER __VA_ARGS__ ()) +#define OE_OPTION(...) OE_OPTION_MACRO_CHOOSER(__VA_ARGS__)(__VA_ARGS__) + //! ref_ptr property macro #define OE_OPTION_REFPTR(TYPE, NAME) \ private: \ diff --git a/src/osgEarth/Containers b/src/osgEarth/Containers index 1d5b8b4814..368745dcb2 100644 --- a/src/osgEarth/Containers +++ b/src/osgEarth/Containers @@ -598,6 +598,7 @@ namespace osgEarth { namespace Util { PerObjectMap() { } PerObjectMap(const std::string& name) : _mutex(name) { } + bool threadsafe = true; DATA& get(KEY k) { @@ -634,6 +635,7 @@ namespace osgEarth { namespace Util { PerObjectFastMap() { } PerObjectFastMap(const std::string& name) : _mutex(name) { } + bool threadsafe = true; struct Functor { virtual void operator()(DATA& data) =0; @@ -645,7 +647,7 @@ namespace osgEarth { namespace Util DATA& get(KEY k) { - osgEarth::Threading::ScopedMutexLock lock(_mutex); + osgEarth::Threading::ScopedLockIf lock(_mutex, threadsafe); typename std::unordered_map::iterator i = _data.find(k); if ( i != _data.end() ) return i->second; @@ -655,47 +657,47 @@ namespace osgEarth { namespace Util void remove(KEY k) { - osgEarth::Threading::ScopedMutexLock lock(_mutex); + osgEarth::Threading::ScopedLockIf lock(_mutex, threadsafe); _data.erase( k ); } void forEach(Functor& functor) { - osgEarth::Threading::ScopedMutexLock lock(_mutex); + osgEarth::Threading::ScopedLockIf lock(_mutex, threadsafe); for (typename std::unordered_map::iterator i = _data.begin(); i != _data.end(); ++i) functor.operator()(i->second); } void forEach(std::function functor) { - osgEarth::Threading::ScopedMutexLock lock(_mutex); + osgEarth::Threading::ScopedLockIf lock(_mutex, threadsafe); for (auto& entry : _data) functor(entry.second); } void forEach(ConstFunctor& functor) const { - osgEarth::Threading::ScopedMutexLock lock(_mutex); + osgEarth::Threading::ScopedLockIf lock(_mutex, threadsafe); for (typename std::unordered_map::const_iterator i = _data.begin(); i != _data.end(); ++i) functor.operator()(i->second); } void forEach(std::function functor) const { - osgEarth::Threading::ScopedMutexLock lock(_mutex); + osgEarth::Threading::ScopedLockIf lock(_mutex, threadsafe); for (auto& entry : _data) functor(entry.second); } unsigned size() const { - osgEarth::Threading::ScopedMutexLock lock(_mutex); + osgEarth::Threading::ScopedLockIf lock(_mutex, threadsafe); return _data.size(); } void clear() { - osgEarth::Threading::ScopedMutexLock lock(_mutex); + osgEarth::Threading::ScopedLockIf lock(_mutex, threadsafe); _data.clear(); } diff --git a/src/osgEarth/Coverage b/src/osgEarth/Coverage index 72d7844979..4d578e2003 100644 --- a/src/osgEarth/Coverage +++ b/src/osgEarth/Coverage @@ -211,6 +211,73 @@ namespace osgEarth return _pixels[ptr] > 0; } + Config getConfig() const + { + Config root("coverage"); + root.set("width", _width); + root.set("height", _height); + + Config values("values"); + for (auto& entry : _lut) + { + Config value_conf = entry.first.getConfig(); + value_conf.set("__coverage_index", entry.second); + values.add(value_conf); + } + root.add(values); + + std::ostringstream pixels_string; + int count = 0; + int last_value = -1; + for(auto& pixel : _pixels) + { + if (last_value >= 0 && pixel != last_value) { + pixels_string << (int)count << ' ' << (int)last_value << ' '; + count = 0; + } + last_value = pixel; + ++count; + } + if (count > 0) { + pixels_string << (int)count << ' ' << (int)last_value; + } + root.add(Config("pixels", pixels_string.str())); + + return root; + } + + void setConfig(const Config& root) + { + unsigned w = 256, h = 256; + root.get("width", w); + root.get("height", h); + allocate(w, h); + + Config values = root.child("values"); + for (auto& value_conf : values.children()) + { + T value(value_conf); + int index; + value_conf.get("__coverage_index", index); + _lut[value] = index; + _rlut[index] = value; + } + + std::string pixels_string = root.value("pixels"); + std::istringstream pixels_stream(pixels_string); + int count, pixel; + int ptr = 0; + while (!pixels_stream.eof() && ptr < (int)(w*h)) + { + pixels_stream >> count >> pixel; + for (int i = 0; i < count; ++i) { + _pixels[ptr++] = pixel; + if (pixel > 0) + ++_valid_count; + } + } + } + private: Coverage() { @@ -299,21 +366,52 @@ namespace osgEarth bool readAtCoords(T& value, double x, double y) const { - unsigned xs = (unsigned)((double)(s()-1) * (x - _extent.xMin()) / _extent.width()); - unsigned yt = (unsigned)((double)(t()-1) * (y - _extent.yMin()) / _extent.height()); - if (xs >= 0 && xs < s() && yt >= 0 && yt < t()) + if (_extent.contains(x, y)) { - read(value, xs, yt); - return true; + unsigned xs = (unsigned)((double)(s() - 1) * (x - _extent.xMin()) / _extent.width()); + unsigned yt = (unsigned)((double)(t() - 1) * (y - _extent.yMin()) / _extent.height()); + if (xs >= 0 && xs < s() && yt >= 0 && yt < t()) + { + read(value, xs, yt); + return true; + } } return false; } + + const T* readAtCoords(double x, double y) const + { + if (_extent.contains(x, y)) + { + unsigned xs = (unsigned)((double)(s() - 1) * (x - _extent.xMin()) / _extent.width()); + unsigned yt = (unsigned)((double)(t() - 1) * (y - _extent.yMin()) / _extent.height()); + if (xs >= 0 && xs < s() && yt >= 0 && yt < t()) + { + return read(xs, yt); + } + } + return nullptr; + } const GeoExtent& getExtent() const { return _extent; } + Config getConfig() const + { + Config root("geocoverage"); + root.set("extent", _extent.getConfig()); + root.add(_coverage->getConfig()); + return root; + } + + void setConfig(const Config& in) + { + _extent.fromConfig(in.child("extent")); + _coverage->setConfig(in.child("coverage")); + } + private: GeoExtent _extent; bool _valid; diff --git a/src/osgEarth/CoverageLayer b/src/osgEarth/CoverageLayer index 3cd0f0a59d..711cd47fd4 100644 --- a/src/osgEarth/CoverageLayer +++ b/src/osgEarth/CoverageLayer @@ -26,6 +26,7 @@ #include #include #include +#include namespace osgEarth { @@ -129,10 +130,105 @@ namespace osgEarth OE_PROFILING_ZONE_TEXT(_layer->getName() + " " + key.str()); GeoCoverage result; - populate(result, key, progress); + + if (!readFromCache(key, result, progress)) + { + populate(result, key, progress); + writeToCache(key, result, progress); + } + return result; } + inline bool readFromCache(const TileKey& key, GeoCoverage& result, ProgressCallback* p) + { + if (_layer->_memCache.valid()) + { + char memCacheKey[64]; + sprintf(memCacheKey, "%d/%s/%s", _layer->getRevision(), key.str().c_str(), key.getProfile()->getHorizSignature().c_str()); + CacheBin* bin = _layer->_memCache->getOrCreateDefaultBin(); + ReadResult r = bin->readObject(memCacheKey, nullptr); + if (r.succeeded()) + { + result = GeoCoverage(Coverage::create(), key.getExtent()); + Config conf; + conf.fromJSON(r.getString()); + result.setConfig(conf); + OE_DEBUG << _layer->getName() << " " << key.str() << " read from mem cache" << std::endl; + return true; + } + } + + CacheBin* cacheBin = _layer->getCacheBin(key.getProfile()); + const CachePolicy& policy = _layer->getCacheSettings()->cachePolicy().get(); + if (!policy.isCacheOnly() && !_layer->getProfile()) + { + _layer->disable("Could not establish a valid profile"); + return false; + } + + // First, attempt to read from the cache. Since the cached data is stored in the + // map profile, we can try this first. + if (cacheBin && policy.isCacheReadable()) + { + // the cache key combines the Key and the horizontal profile. + std::string cacheKey = Cache::makeCacheKey(key.str() + "-" + key.getProfile()->getHorizSignature(), "coverage"); + ReadResult r = cacheBin->readString(cacheKey, nullptr); + if (r.succeeded() && !policy.isExpired(r.lastModifiedTime())) + { + result = GeoCoverage(Coverage::create(), key.getExtent()); + Config conf; + conf.fromJSON(r.getString()); + result.setConfig(conf); + OE_DEBUG << _layer->getName() << " " << key.str() << " read from disk cache" << std::endl; + return true; + } + } + + // The data was not in the cache. If we are cache-only, fail sliently + if (policy.isCacheOnly()) + { + // If it's cache only and we have an expired but cached image, just return it. + return !result.empty(); + } + + return result.valid(); + } + + inline void writeToCache(const TileKey& key, const GeoCoverage& result, ProgressCallback* p) + { + if (result.empty()) + return; + + osg::ref_ptr so; + + if (_layer->_memCache.valid()) + { + char memCacheKey[64]; + sprintf(memCacheKey, "%d/%s/%s", _layer->getRevision(), key.str().c_str(), key.getProfile()->getHorizSignature().c_str()); + CacheBin* bin = _layer->_memCache->getOrCreateDefaultBin(); + Config conf = result.getConfig(); + so = new StringObject(conf.toJSON(false)); + bin->write(memCacheKey, so.get(), nullptr); + OE_DEBUG << _layer->getName() << " " << key.str() << " write to mem cache" << std::endl; + } + + // If we got a result, the cache is valid and we are caching in the map profile, + // write to the map cache. + CacheBin* cacheBin = _layer->getCacheBin(key.getProfile()); + const CachePolicy& policy = _layer->getCacheSettings()->cachePolicy().get(); + if (cacheBin && policy.isCacheWriteable()) + { + std::string cacheKey = Cache::makeCacheKey(key.str()+"-"+key.getProfile()->getHorizSignature(), "coverage"); + if (!so.valid()) { + Config conf = result.getConfig(); + so = new StringObject(conf.toJSON(false)); + } + cacheBin->write(cacheKey, so.get(), nullptr); + OE_DEBUG << _layer->getName() << " " << key.str() << " write to disk cache" << std::endl; + } + } + private: //! Create a GeoCoverage for a given tile key. @@ -399,7 +495,11 @@ namespace osgEarth std::string preset_name; if (child.get("value", value)) { - if (child.get("preset", preset_name)) + if (value == 0u) + { + OE_WARN << "[CoverageLayer] " << getName() << " : Illegal value 0 in mapping (zero is reserved for 'no data'); skipping." << std::endl; + } + else if (child.get("preset", preset_name)) { const Config& p = presets.get(preset_name); Config temp = child; diff --git a/src/osgEarth/DecalLayer b/src/osgEarth/DecalLayer index 0957c92626..cc732058f0 100644 --- a/src/osgEarth/DecalLayer +++ b/src/osgEarth/DecalLayer @@ -64,17 +64,19 @@ namespace osgEarth { namespace Contrib //! GL_ONE, GL_ZERO, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, //! GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA void setBlendFuncs( - GLenum sourceFactor, - GLenum destFactor, - GLenum alphaSourceFactor, - GLenum alphaDestFactor); + GLenum R_source, GLenum R_dest, + GLenum G_source, GLenum G_dest, + GLenum B_source, GLenum B_dest, + GLenum A_source, GLenum A_dest); //! Set blend equation //! Supported values are //! GL_FUNC_ADD, GL_FUNC_MIN, GL_FUNC_MAX void setBlendEquations( - GLenum rgbEquation, - GLenum alphaEquation); + GLenum R_equation, + GLenum G_equation, + GLenum B_equation, + GLenum A_equation); public: // ImageLayer @@ -105,7 +107,9 @@ namespace osgEarth { namespace Contrib std::list _decalList; using DecalIndex = std::unordered_map::iterator>; DecalIndex _decalIndex; - GLenum _srcRGB, _dstRGB, _srcAlpha, _dstAlpha, _rgbEquation, _alphaEquation; + GLenum _src[4]; + GLenum _dst[4]; + GLenum _equation[4]; }; /** diff --git a/src/osgEarth/DecalLayer.cpp b/src/osgEarth/DecalLayer.cpp index 9b8e8ce276..c083edae34 100644 --- a/src/osgEarth/DecalLayer.cpp +++ b/src/osgEarth/DecalLayer.cpp @@ -60,34 +60,44 @@ DecalImageLayer::init() layerHints().cachePolicy() = CachePolicy::NO_CACHE; // blending defaults - _srcRGB = GL_SRC_ALPHA; - _dstRGB = GL_ONE_MINUS_SRC_ALPHA; - _srcAlpha = GL_ONE; - _dstAlpha = GL_ZERO; - _rgbEquation = GL_FUNC_ADD; - _alphaEquation = GL_FUNC_ADD; + _src[0] = GL_SRC_ALPHA; + _dst[0] = GL_ONE_MINUS_SRC_ALPHA; + _src[1] = GL_SRC_ALPHA; + _dst[1] = GL_ONE_MINUS_SRC_ALPHA; + _src[2] = GL_SRC_ALPHA; + _dst[2] = GL_ONE_MINUS_SRC_ALPHA; + _src[3] = GL_ONE; + _dst[3] = GL_ZERO; + _equation[0] = GL_FUNC_ADD; + _equation[1] = GL_FUNC_ADD; + _equation[2] = GL_FUNC_ADD; + _equation[3] = GL_FUNC_ADD; } void DecalImageLayer::setBlendFuncs( - GLenum srcRGB, - GLenum dstRGB, - GLenum srcAlpha, - GLenum dstAlpha) + GLenum R_source, GLenum R_dest, + GLenum G_source, GLenum G_dest, + GLenum B_source, GLenum B_dest, + GLenum A_source, GLenum A_dest) { - _srcRGB = srcRGB; - _dstRGB = dstRGB; - _srcAlpha = srcAlpha; - _dstAlpha = dstAlpha; + _src[0] = R_source; _dst[0] = R_dest; + _src[1] = G_source; _dst[1] = G_dest; + _src[2] = B_source; _dst[2] = B_dest; + _src[3] = A_source; _dst[3] = A_dest; } void DecalImageLayer::setBlendEquations( - GLenum rgbEquation, - GLenum alphaEquation) + GLenum R_equation, + GLenum G_equation, + GLenum B_equation, + GLenum A_equation) { - _rgbEquation = rgbEquation; - _alphaEquation = alphaEquation; + _equation[0] = R_equation; + _equation[1] = G_equation; + _equation[2] = B_equation; + _equation[3] = A_equation; } namespace @@ -146,7 +156,6 @@ DecalImageLayer::createImageImplementation( ImageUtils::PixelReader readOutput(output.get()); osg::Vec4 src, dst, out; - float srcRGB, dstRGB, srcAlpha, dstAlpha; // Start by copying the canvas to the output. Use a scale/bias // since the canvas might be larger (lower resolution) than the @@ -217,49 +226,26 @@ DecalImageLayer::createImageImplementation( readInput(src, in_u, in_v); // figure out how to blend them: - srcRGB = get_blend(_srcRGB, src, dst); - dstRGB = get_blend(_dstRGB, src, dst); - srcAlpha = get_blend(_srcAlpha, src, dst); - dstAlpha = get_blend(_dstAlpha, src, dst); - - // perform the blending based on the blend equation - if (_rgbEquation == GL_FUNC_ADD) - { - out.r() = src.r()*srcRGB + dst.r()*dstRGB; - out.g() = src.g()*srcRGB + dst.g()*dstRGB; - out.b() = src.b()*srcRGB + dst.b()*dstRGB; - } - else if (_rgbEquation == GL_MAX) - { - out.r() = std::max(src.r()*srcRGB, dst.r()*dstRGB); - out.g() = std::max(src.g()*srcRGB, dst.g()*dstRGB); - out.b() = std::max(src.b()*srcRGB, dst.b()*dstRGB); - } - else if (_rgbEquation == GL_MIN) + for (int n = 0; n < 4; ++n) { - out.r() = std::min(src.r()*srcRGB, dst.r()*dstRGB); - out.g() = std::min(src.g()*srcRGB, dst.g()*dstRGB); - out.b() = std::min(src.b()*srcRGB, dst.b()*dstRGB); - } + float sf = get_blend(_src[n], src, dst); + float df = get_blend(_dst[n], src, dst); - if (_alphaEquation == GL_FUNC_ADD) - { - out.a() = src.a()*srcAlpha + dst.a()*dstAlpha; - } - else if (_alphaEquation == GL_MAX) - { - out.a() = std::max(src.a()*srcAlpha, dst.a()*dstAlpha); - } - else if (_alphaEquation == GL_MIN) - { - out.a() = std::min(src.a()*srcAlpha, dst.a()*dstAlpha); - } + if (_equation[n] == GL_FUNC_ADD) + { + out[n] = src[n] * sf + dst[n] * df; + } + else if (_equation[n] == GL_MAX) + { + out[n] = std::max(src[n] * sf, dst[n] * df); + } + else if (_equation[n] == GL_MIN) + { + out[n] = std::min(src[n] * sf, dst[n] * df); + } - // done, clamp and write. - out.r() = clamp(out.r(), 0.0f, 1.0f); - out.g() = clamp(out.g(), 0.0f, 1.0f); - out.b() = clamp(out.b(), 0.0f, 1.0f); - out.a() = clamp(out.a(), 0.0f, 1.0f); + out[n] = clamp(out[n], 0.0f, 1.0f); + } writeOutput(out, s, t); } diff --git a/src/osgEarth/DrapingCullSet.cpp b/src/osgEarth/DrapingCullSet.cpp index 6a3da1f9b5..689b32c351 100644 --- a/src/osgEarth/DrapingCullSet.cpp +++ b/src/osgEarth/DrapingCullSet.cpp @@ -32,7 +32,9 @@ DrapingManager::DrapingManager() : _sets(OE_MUTEX_NAME), _renderBinNum(1) { - //nop +#ifdef OSGEARTH_SINGLE_THREADED_OSG + _sets.threadsafe = false; +#endif } DrapingCullSet& diff --git a/src/osgEarth/ElevationLayer b/src/osgEarth/ElevationLayer index 5311e94059..2ecf95b4ac 100644 --- a/src/osgEarth/ElevationLayer +++ b/src/osgEarth/ElevationLayer @@ -35,8 +35,8 @@ namespace osgEarth public: META_LayerOptions(osgEarth, Options, TileLayer::Options); OE_OPTION(std::string, verticalDatum); - OE_OPTION(bool, offset); - OE_OPTION(ElevationNoDataPolicy, noDataPolicy); + OE_OPTION(bool, offset, false); + OE_OPTION(ElevationNoDataPolicy, noDataPolicy, NODATA_INTERPOLATE); virtual Config getConfig() const; private: void fromConfig( const Config& conf ); diff --git a/src/osgEarth/ElevationLayer.cpp b/src/osgEarth/ElevationLayer.cpp index 7ff51ecdbb..2889cf5c95 100644 --- a/src/osgEarth/ElevationLayer.cpp +++ b/src/osgEarth/ElevationLayer.cpp @@ -47,9 +47,6 @@ ElevationLayer::Options::getConfig() const void ElevationLayer::Options::fromConfig( const Config& conf ) { - _offset.init( false ); - _noDataPolicy.init( NODATA_INTERPOLATE ); - conf.get("vdatum", verticalDatum() ); conf.get("vsrs", verticalDatum() ); // back compat conf.get("offset", offset() ); diff --git a/src/osgEarth/ElevationPool b/src/osgEarth/ElevationPool index 5344b3378c..3ba0047ed7 100644 --- a/src/osgEarth/ElevationPool +++ b/src/osgEarth/ElevationPool @@ -257,7 +257,7 @@ namespace osgEarth ElevationLayerVector _elevationLayers; - size_t getElevationHash() const; + size_t getElevationHash(WorkingSet*) const; void sync(const Map*, WorkingSet*); @@ -300,7 +300,7 @@ namespace osgEarth //! @param threads Number of threads the sampler should use AsyncElevationSampler( const Map* map, - unsigned threads =1u); + unsigned threads =0u); //! Destructor virtual ~AsyncElevationSampler() { } diff --git a/src/osgEarth/ElevationPool.cpp b/src/osgEarth/ElevationPool.cpp index 9bf3e7adc1..aec99a6cae 100644 --- a/src/osgEarth/ElevationPool.cpp +++ b/src/osgEarth/ElevationPool.cpp @@ -95,13 +95,18 @@ ElevationPool::setMap(const Map* map) } size_t -ElevationPool::getElevationHash() const +ElevationPool::getElevationHash(WorkingSet* ws) const { // yes, must do this every time because individual // layers can "bump" their revisions (dynamic layers) size_t hash = hash_value_unsigned(_mapRevision); - for (auto& layer : _elevationLayers) + // using the working set or the baseline? + auto& layers = + (ws && ws->_elevationLayers.size() > 0) ? ws->_elevationLayers : + this->_elevationLayers; + + for (auto& layer : layers) if (layer->isOpen()) hash = hash_value_unsigned(hash, layer->getRevision()); else @@ -137,7 +142,7 @@ ElevationPool::refresh(const Map* map) delete static_cast(_index); _mapRevision = _map->getOpenLayers(_elevationLayers); - _elevationHash = getElevationHash(); + _elevationHash = getElevationHash(nullptr); MaxLevelIndex* index = new MaxLevelIndex(); _index = index; @@ -207,7 +212,7 @@ ElevationPool::needsRefresh() } // Check to see if any of the elevation layers in our list have changed. - return getElevationHash() != _elevationHash; + return getElevationHash(nullptr) != _elevationHash; } ElevationPool::WorkingSet::WorkingSet(unsigned size) : @@ -449,7 +454,7 @@ ElevationPool::prepareEnvelope( sync(env._map.get(), ws); - env._key._revision = getElevationHash(); + env._key._revision = getElevationHash(ws); env._raster = nullptr; env._cache.clear(); @@ -611,7 +616,7 @@ ElevationPool::sampleMapCoords( ScopedReadLock lk(_mutex); Internal::RevElevationKey key; - key._revision = getElevationHash(); + key._revision = getElevationHash(ws); osg::ref_ptr raster; osg::Vec4 elev; @@ -752,7 +757,7 @@ ElevationPool::sampleMapCoords( ScopedReadLock lk(_mutex); Internal::RevElevationKey key; - key._revision = getElevationHash(); + key._revision = getElevationHash(ws); osg::ref_ptr raster; osg::Vec4 elev; @@ -894,7 +899,7 @@ ElevationPool::getSample( if (lod >= 0) { key._tilekey = map->getProfile()->createTileKey(p.x(), p.y(), lod); - key._revision = getElevationHash(); + key._revision = getElevationHash(ws); osg::ref_ptr raster = getOrCreateRaster( key, // key to query @@ -991,7 +996,7 @@ ElevationPool::getTile( Internal::RevElevationKey key; key._tilekey = tilekey; - key._revision = getElevationHash(); + key._revision = getElevationHash(ws); out_tex = getOrCreateRaster( key, @@ -1020,7 +1025,7 @@ namespace osgEarth { namespace Internal void operator()(osg::Object*) { - if (!_promise.isAbandoned()) + if (!_promise.empty()) { osg::ref_ptr map; if (_map.lock(map)) @@ -1044,9 +1049,7 @@ AsyncElevationSampler::AsyncElevationSampler( _arena(nullptr) { _arena = JobArena::get("oe.asyncelevation"); - - unsigned c = _arena->getConcurrency(); - _arena->setConcurrency(std::max(c, numThreads)); + _arena->setConcurrency(numThreads > 0 ? numThreads : _arena->getConcurrency()); } Future diff --git a/src/osgEarth/ExampleResources.cpp b/src/osgEarth/ExampleResources.cpp index ec2c54d1ca..999793e4f8 100644 --- a/src/osgEarth/ExampleResources.cpp +++ b/src/osgEarth/ExampleResources.cpp @@ -28,7 +28,7 @@ #include #include #include -#include +#include #include #include @@ -36,8 +36,6 @@ #include #include -#include - #include #include #include @@ -383,6 +381,14 @@ MapNodeHelper::loadWithoutControls( return node; } + // move any mapnode siblings under the mapnode. + auto siblings = findSiblings(mapNode.get()); + for (auto& sibling : siblings) + { + mapNode->addChild(sibling); + sibling->getParent(0)->removeChild(sibling); + } + // GPU tessellation? if (args.read("--tessellation") || args.read("--tess")) { @@ -630,10 +636,6 @@ MapNodeHelper::parse( bool showActivity = useControls && args.read("--activity"); bool useLogDepth2 = args.read("--logdepth2"); bool useLogDepth = !args.read("--nologdepth") && !useLogDepth2; //args.read("--logdepth"); - bool kmlUI = useControls && args.read("--kmlui"); - - std::string kmlFile; - args.read( "--kml", kmlFile ); // animation path: std::string animpath; @@ -676,47 +678,6 @@ MapNodeHelper::parse( view->addEventHandler(new ToggleCanvasEventHandler(canvas, 'y')); } - // Loading KML from the command line: - if ( !kmlFile.empty() && mapNode ) - { - KML::KMLOptions kml_options; - kml_options.declutter() = true; - - // set up a default icon for point placemarks: - IconSymbol* defaultIcon = new IconSymbol(); - defaultIcon->url()->setLiteral(KML_PUSHPIN_URL); - kml_options.defaultIconSymbol() = defaultIcon; - - TextSymbol* defaultText = new TextSymbol(); - defaultText->halo() = Stroke(0.3,0.3,0.3,1.0); - kml_options.defaultTextSymbol() = defaultText; - - URI kmlFileURI( kmlFile ); - osg::Node* kml = KML::load(kmlFileURI, mapNode, kml_options ); - if ( kml ) - { - if (kmlUI) - { - Control* c = AnnotationGraphControlFactory().create(kml, view); - if ( c ) - { - c->setVertAlign( Control::ALIGN_TOP ); - mainContainer->addControl( c ); - } - } - - auto kmllayer = new AnnotationLayer(); - kmllayer->setName(kmlFileURI.base()); - kmllayer->addChild(kml); - - mapNode->getMap()->addLayer(kmllayer); - } - else - { - OE_NOTICE << "Failed to load " << kmlFile << std::endl; - } - } - // Configure the mouse coordinate readout: if ( useCoords && mapNode ) { @@ -963,9 +924,6 @@ MapNodeHelper::usage() const { return Stringify() << " --sky : add a sky model\n" - << " --kml : load a KML or KMZ file\n" - << " --kmlui : display a UI for toggling nodes loaded with --kml\n" - << " --coords : display map coords under mouse\n" << " --ortho : use an orthographic camera\n" << " --logdepth : activates the logarithmic depth buffer\n" << " --logdepth2 : activates logarithmic depth buffer with per-fragment interpolation\n" diff --git a/src/osgEarth/FilteredFeatureSource.cpp b/src/osgEarth/FilteredFeatureSource.cpp index 9261d3a7a9..ce480d321b 100644 --- a/src/osgEarth/FilteredFeatureSource.cpp +++ b/src/osgEarth/FilteredFeatureSource.cpp @@ -53,11 +53,21 @@ Status FilteredFeatureSource::openImplementation() void FilteredFeatureSource::addedToMap(const Map* map) { options().featureSource().addedToMap(map); - if (getFeatureSource()) + + if (!getFeatureSource()) { - setFeatureProfile(getFeatureSource()->getFeatureProfile()); + setStatus(Status(Status::ResourceUnavailable, "Cannot find feature source")); + return; } + if (!getFeatureSource()->getFeatureProfile()) + { + setStatus(Status(Status::ConfigurationError, "Feature source does not report a valid profile")); + return; + } + + setFeatureProfile(getFeatureSource()->getFeatureProfile()); + FeatureSource::addedToMap(map); } @@ -93,7 +103,7 @@ FeatureCursor* FilteredFeatureSource::createFeatureCursorImplementation( const Query& query, ProgressCallback* progress) { - if (getFeatureSource()) + if (isOpen() && getFeatureSource()) { osg::ref_ptr< FeatureCursor > cursor = getFeatureSource()->createFeatureCursor(query, progress); if (cursor.valid()) diff --git a/src/osgEarth/GDAL b/src/osgEarth/GDAL index 12038b2f10..b3c53660a9 100644 --- a/src/osgEarth/GDAL +++ b/src/osgEarth/GDAL @@ -67,12 +67,12 @@ namespace osgEarth { Options(const ConfigOptions& input); OE_OPTION(URI, url); OE_OPTION(std::string, connection); - OE_OPTION(unsigned, subDataSet); - OE_OPTION(RasterInterpolation, interpolation); + OE_OPTION(unsigned, subDataSet, 0u); + OE_OPTION(RasterInterpolation, interpolation, INTERP_AVERAGE); OE_OPTION(ProfileOptions, warpProfile); - OE_OPTION(bool, useVRT); - OE_OPTION(bool, coverageUsesPaletteIndex); - OE_OPTION(bool, singleThreaded); + OE_OPTION(bool, useVRT, false); + OE_OPTION(bool, coverageUsesPaletteIndex, true); + OE_OPTION(bool, singleThreaded, false); void readFrom(const Config& conf); void writeTo(Config& conf) const; diff --git a/src/osgEarth/GDAL.cpp b/src/osgEarth/GDAL.cpp index 84fcbfb90b..97aebac0e8 100644 --- a/src/osgEarth/GDAL.cpp +++ b/src/osgEarth/GDAL.cpp @@ -1664,11 +1664,6 @@ GDAL::Options::Options(const ConfigOptions& input) void GDAL::Options::readFrom(const Config& conf) { - _interpolation.init(INTERP_AVERAGE); - _useVRT.init(false); - coverageUsesPaletteIndex().setDefault(true); - singleThreaded().setDefault(false); - conf.get("url", _url); conf.get("connection", _connection); conf.get("subdataset", _subDataSet); diff --git a/src/osgEarth/GLUtils b/src/osgEarth/GLUtils index 1b6a40d27d..f64f3deb89 100644 --- a/src/osgEarth/GLUtils +++ b/src/osgEarth/GLUtils @@ -871,7 +871,7 @@ namespace osgEarth Future dispatch(Delegate delegate) { auto operation = new DelegateOperation(delegate); - Future future = operation->_promise.getFuture(); + Future future = operation->_promise; // .getFuture(); if (_dispatcher.valid()) _dispatcher->push(operation); else diff --git a/src/osgEarth/GLUtils.cpp b/src/osgEarth/GLUtils.cpp index 1a5b53b074..830c306e72 100644 --- a/src/osgEarth/GLUtils.cpp +++ b/src/osgEarth/GLUtils.cpp @@ -1832,7 +1832,7 @@ GLObjectsCompiler::compileAsync( auto compileSet = new osgUtil::IncrementalCompileOperation::CompileSet(); compileSet->buildCompileMap(ico->getContextSet(), *state.get()); ICOCallback* callback = new ICOCallback(node, _jobsActive); - result = callback->_promise.getFuture(); + result = callback->_promise; // .getFuture(); compileSet->_compileCompletedCallback = callback; _jobsActive++; ico->add(compileSet, false); @@ -1844,7 +1844,7 @@ GLObjectsCompiler::compileAsync( { // no ICO available - just resolve the future immediately Promise> promise; - result = promise.getFuture(); + result = promise; // .getFuture(); promise.resolve(node); } } @@ -1873,7 +1873,7 @@ GLObjectsCompiler::compileAsync( auto compileSet = new osgUtil::IncrementalCompileOperation::CompileSet(); compileSet->buildCompileMap(ico->getContextSet(), *state); ICOCallback* callback = new ICOCallback(node, _jobsActive); - result = callback->_promise.getFuture(); + result = callback->_promise; // .getFuture(); compileSet->_compileCompletedCallback = callback; _jobsActive++; ico->add(compileSet, false); @@ -1885,7 +1885,7 @@ GLObjectsCompiler::compileAsync( { // no ICO available - just resolve the future immediately Promise> promise; - result = promise.getFuture(); + result = promise; // .getFuture(); promise.resolve(node); } @@ -1901,7 +1901,7 @@ GLObjectsCompiler::compileNow( { if (node) { - Future> result = compileAsync(node, host, progress); + auto result = compileAsync(node, host, progress); result.join(progress); } } diff --git a/src/osgEarth/GeoData b/src/osgEarth/GeoData index 9ab7af549f..1713567dcb 100644 --- a/src/osgEarth/GeoData +++ b/src/osgEarth/GeoData @@ -583,9 +583,14 @@ namespace osgEarth * extents. Otherwise returns false. */ bool isWholeEarth() const; + public: static GeoExtent INVALID; + public: // config + Config getConfig() const; + void fromConfig(const Config& conf); + private: double _west, _width, _south, _height; osg::ref_ptr _srs; diff --git a/src/osgEarth/GeoData.cpp b/src/osgEarth/GeoData.cpp index 64a6bc2f9b..32a2948637 100644 --- a/src/osgEarth/GeoData.cpp +++ b/src/osgEarth/GeoData.cpp @@ -787,6 +787,37 @@ _height(rhs._height) //NOP } +Config +GeoExtent::getConfig() const +{ + Config conf("geoextent"); + if (isValid()) { + conf.set("west", west()); + conf.set("south", south()); + conf.set("east", east()); + conf.set("north", north()); + conf.set("srs", getSRS()->getHorizInitString()); + conf.set("vdatum", getSRS()->getVertInitString()); + } + return conf; +} + +void +GeoExtent::fromConfig(const Config& conf) +{ + double west, south, east, north; + conf.get("west", west); + conf.get("south", south); + conf.get("east", east); + conf.get("north", north); + std::string srs; + conf.get("srs", srs); + std::string vdatum; + conf.get("vdatum", vdatum); + _srs = SpatialReference::create(srs, vdatum); + set(west, south, east, north); +} + bool GeoExtent::isGeographic() const { @@ -1814,7 +1845,7 @@ GeoImage::GeoImage(Threading::Future> fimage, const Geo { _future = fimage; - if (_future->isAbandoned()) + if (_future->empty()) { _status.set(Status::ResourceUnavailable, "Async request canceled"); } @@ -1834,14 +1865,14 @@ GeoImage::valid() const return false; return - (_future.isSet() && !_future->isAbandoned()) || + (_future.isSet() && !_future->empty()) || _myimage.valid(); } const osg::Image* GeoImage::getImage() const { - return _future.isSet() && _future->isAvailable() ? + return _future.isSet() && _future->available() ? _future->join().get() : _myimage.get(); } diff --git a/src/osgEarth/Geocoder.cpp b/src/osgEarth/Geocoder.cpp index cf62f4b47c..9e4e4fe17d 100644 --- a/src/osgEarth/Geocoder.cpp +++ b/src/osgEarth/Geocoder.cpp @@ -144,7 +144,7 @@ namespace void operator()(osg::Object*) { OE_PROFILING_ZONE_NAMED("Geocode"); - if (!_promise.isAbandoned()) + if (!_promise.empty()) { osg::ref_ptr cursor; Status status = _impl->search(_input, cursor); @@ -227,15 +227,15 @@ Geocoder::Results::Results(const Geocoder::OutputData& data) : Status Geocoder::Results::getStatus() { - return _future.isAvailable() ? - _future.get()._status : + return _future.available() ? + _future.value()._status : Status(Status::ServiceUnavailable, "Operation canceled"); } FeatureCursor* Geocoder::Results::getFeatures() { - return _future.isAvailable() ? - _future.get()._cursor.get() : + return _future.available() ? + _future.value()._cursor.get() : nullptr; } diff --git a/src/osgEarth/HorizonClipPlane.cpp b/src/osgEarth/HorizonClipPlane.cpp index e35590fccf..4a5feec517 100644 --- a/src/osgEarth/HorizonClipPlane.cpp +++ b/src/osgEarth/HorizonClipPlane.cpp @@ -61,7 +61,9 @@ HorizonClipPlane::HorizonClipPlane(const Ellipsoid& em) : _num(0u), _data(OE_MUTEX_NAME) { - //nop +#ifdef OSGEARTH_SINGLE_THREADED_OSG + _data.threadsafe = false; +#endif } void diff --git a/src/osgEarth/ImGui/AnnotationsGUI b/src/osgEarth/ImGui/AnnotationsGUI index cce8f86d64..b0b534b694 100644 --- a/src/osgEarth/ImGui/AnnotationsGUI +++ b/src/osgEarth/ImGui/AnnotationsGUI @@ -66,15 +66,25 @@ namespace { auto anode = dynamic_cast(&node); auto data = dynamic_cast(node.getUserData()); + std::string name; if (data) { ImGui::PushID((std::uintptr_t)data); - auto name = data->getName(); - auto vp = data->getViewpoint(); + name = data->getName(); + auto vp_ptr = data->getViewpoint(); auto desc = data->getDescription(); + Viewpoint vp; + if (vp_ptr) vp = *vp_ptr; + + if (!vp.valid()) + { + osgEarth::Util::ViewFitter fitter(srs, camera); + fitter.createViewpoint(&node, vp); + } + bool visible = node.getNodeMask() != 0; if (ImGui::Checkbox("", &visible)) { @@ -83,9 +93,9 @@ namespace } ImGui::SameLine(); bool is_selected = false; - if (ImGui::Selectable(name.c_str(), &is_selected) && manip && vp) + if (ImGui::Selectable(name.c_str(), &is_selected) && manip && vp.valid()) { - manip->setViewpoint(*vp); + manip->setViewpoint(vp); } ImGui::PopID(); @@ -96,7 +106,7 @@ namespace { ImGui::PushID((std::uintptr_t)data); - auto name = anode->getName(); + name = anode->getName(); if (name.empty()) name = "[" + std::string(anode->className()) + "]"; bool visible = node.getNodeMask() != 0; @@ -118,7 +128,6 @@ namespace ImGui::PopID(); ImGui::Indent(); - } traverse(node); diff --git a/src/osgEarth/ImGui/EnvironmentGUI b/src/osgEarth/ImGui/EnvironmentGUI index 306bfaf6bd..076aba3e46 100644 --- a/src/osgEarth/ImGui/EnvironmentGUI +++ b/src/osgEarth/ImGui/EnvironmentGUI @@ -219,7 +219,7 @@ namespace osgEarth auto year = mark.year(); auto hour = mark.hours(); - if (ImGuiLTable::SliderDouble("Hour", &hour, 0.0f, 24.0f)) + if (ImGuiLTable::SliderDouble("Hour (UTC)", &hour, 0.0f, 24.0f)) dirtySettings(); if (_showDetails) diff --git a/src/osgEarth/ImGui/LayersGUI b/src/osgEarth/ImGui/LayersGUI index 9d2c5c313a..d89b80b12b 100644 --- a/src/osgEarth/ImGui/LayersGUI +++ b/src/osgEarth/ImGui/LayersGUI @@ -302,7 +302,6 @@ namespace osgEarth bool _showDisabled = false; bool _sortByCat = false; LayerVector _layers; - //std::vector _layerExpanded; std::unordered_map _layerExpanded; int _mapRevision = -1; bool _first = true; @@ -447,6 +446,9 @@ namespace osgEarth ImGui::SameLine(); + ImGui::Text("(%d)", _layers.size()); + ImGui::SameLine(); + if (ImGui::Checkbox("Sort", &_sortByCat)) dirtySettings(); @@ -696,7 +698,7 @@ namespace osgEarth } #ifdef HAVE_OSGEARTHCESIUM - auto cesiumNativeLayer = dynamic_cast(layer); + auto cesiumNativeLayer = dynamic_cast(layer); if (cesiumNativeLayer) { float sse = cesiumNativeLayer->getMaximumScreenSpaceError(); @@ -793,7 +795,7 @@ namespace osgEarth if (_mouseOverImageLayer == imageLayer) { - if (_imageLayerValueUnderMouse.isAvailable()) + if (_imageLayerValueUnderMouse.available()) { if (_imageLayerValueUnderMouse->isOK()) { diff --git a/src/osgEarth/ImGui/TerrainGUI b/src/osgEarth/ImGui/TerrainGUI index e921bc61d5..53978bcf27 100644 --- a/src/osgEarth/ImGui/TerrainGUI +++ b/src/osgEarth/ImGui/TerrainGUI @@ -188,7 +188,7 @@ namespace osgEarth ImGui::Text("Distance from camera: %.1lf m", dist); ImGui::Text("Camera radius: %.1lf m", eye.length()); - if (_sample.isAvailable()) + if (_sample.available()) { if (_sample->elevation().getValue() == NO_DATA_VALUE) { @@ -197,26 +197,26 @@ namespace osgEarth } else { - Distance cartRes = mp.transformResolution(_sample.get().resolution(), Units::METERS); + Distance cartRes = mp.transformResolution(_sample.value().resolution(), Units::METERS); const VerticalDatum* egm96 = VerticalDatum::get("egm96"); if (egm96) { - double egm96z = _sample.get().elevation().getValue(); + double egm96z = _sample.value().elevation().getValue(); VerticalDatum::transform( mp.getSRS()->getVerticalDatum(), egm96, mp.y(), mp.x(), egm96z); - Distance elevEGM96(egm96z, _sample.get().elevation().getUnits()); + Distance elevEGM96(egm96z, _sample.value().elevation().getUnits()); ImGui::Text("Elevation: %s MSL / %s HAE", elevEGM96.asParseableString().c_str(), - _sample.get().elevation().asParseableString().c_str()); + _sample.value().elevation().asParseableString().c_str()); } else { ImGui::Text("Elevation: %s HAE", - _sample.get().elevation().asParseableString().c_str()); + _sample.value().elevation().asParseableString().c_str()); } ImGui::Text("Resolution: %s", @@ -301,7 +301,7 @@ namespace osgEarth .onMove([&](osg::View* v, float x, float y) { onMove(v, x, y); }, false) .onClick([&](osg::View* v, float x, float y) { onClick(v, x, y); }, false); - _sampler = std::unique_ptr(new AsyncElevationSampler(_mapNode->getMap())); + _sampler = std::unique_ptr(new AsyncElevationSampler(_mapNode->getMap(), 1u)); Style measureStyle; measureStyle.getOrCreate()->stroke()->width() = 4.0f; diff --git a/src/osgEarth/ImageLayer b/src/osgEarth/ImageLayer index 0470a5a981..cd72173069 100644 --- a/src/osgEarth/ImageLayer +++ b/src/osgEarth/ImageLayer @@ -67,18 +67,18 @@ namespace osgEarth public: META_LayerOptions(osgEarth, Options, TileLayer::Options); OE_OPTION(URI, noDataImageFilename); - OE_OPTION(osg::Vec4ub, transparentColor); + OE_OPTION(osg::Vec4ub, transparentColor, osg::Vec4ub(0, 0, 0, 0)); OE_OPTION(ColorFilterChain, colorFilters); - OE_OPTION(bool, shared); - OE_OPTION(bool, coverage); - OE_OPTION(osg::Texture::FilterMode, minFilter); - OE_OPTION(osg::Texture::FilterMode, magFilter); + OE_OPTION(osg::Texture::FilterMode, minFilter, osg::Texture::LINEAR_MIPMAP_LINEAR); + OE_OPTION(osg::Texture::FilterMode, magFilter, osg::Texture::LINEAR); OE_OPTION(std::string, textureCompression); - OE_OPTION(double, edgeBufferRatio); - OE_OPTION(unsigned, reprojectedTileSize); + OE_OPTION(double, edgeBufferRatio, 0.0); + OE_OPTION(unsigned, reprojectedTileSize, 256u); OE_OPTION(Distance, altitude); - OE_OPTION(bool, acceptDraping); - OE_OPTION(bool, async); + OE_OPTION(bool, coverage, false); + OE_OPTION(bool, acceptDraping, false); + OE_OPTION(bool, async, false); + OE_OPTION(bool, shared, false); OE_OPTION(std::string, shareTexUniformName); OE_OPTION(std::string, shareTexMatUniformName); virtual Config getConfig() const; diff --git a/src/osgEarth/ImageLayer.cpp b/src/osgEarth/ImageLayer.cpp index 182dcbe805..b925838190 100644 --- a/src/osgEarth/ImageLayer.cpp +++ b/src/osgEarth/ImageLayer.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -42,14 +43,6 @@ using namespace osgEarth; void ImageLayer::Options::fromConfig(const Config& conf) { - _transparentColor.setDefault( osg::Vec4ub(0,0,0,0) ); - _minFilter.setDefault( osg::Texture::LINEAR_MIPMAP_LINEAR ); - _magFilter.setDefault( osg::Texture::LINEAR ); - _textureCompression.setDefault(""); - _shared.setDefault( false ); - _coverage.setDefault( false ); - _reprojectedTileSize.setDefault( 256 ); - conf.get( "nodata_image", _noDataImageFilename ); conf.get( "shared", _shared ); conf.get( "coverage", _coverage ); @@ -427,10 +420,33 @@ ImageLayer::createImage( return createImageImplementation(canvas, key, progress); } +struct Hooks +{ + static void put(osgDB::Options* read_options, std::shared_ptr hooks) + { + ObjectStorage::set(read_options, hooks); + } + + static std::shared_ptr get(const osgDB::Options* read_options) + { + std::shared_ptr hooks; + ObjectStorage::get(read_options, hooks); + return hooks; + } + + ReadResult pre_read(Layer* layer, const TileKey& key, ProgressCallback* progress) + { + return ReadResult(ReadResult::RESULT_OK); + } + + ReadResult post_read(Layer* layer, const TileKey& key, const GeoImage& data, ProgressCallback* progress) + { + return ReadResult(ReadResult::RESULT_OK); + } +}; + GeoImage -ImageLayer::createImageInKeyProfile( - const TileKey& key, - ProgressCallback* progress) +ImageLayer::createImageInKeyProfile(const TileKey& key, ProgressCallback* progress) { // If the layer is disabled, bail out. if ( !isOpen() ) @@ -454,6 +470,21 @@ ImageLayer::createImageInKeyProfile( GeoImage result; +#if HOOKS + auto hooks = Hooks::get(getReadOptions()); + if (hooks) + { + auto rr = hooks->pre_read(this, key, progress); + auto image = rr.releaseImage(); + auto status = hooks->prehook_image(this, key, progress, result); + if (status.isError()) + return GeoImage(status); + else if (result.valid()) + return result; + } +#endif + +#if 1 OE_DEBUG << LC << "create image for \"" << key.str() << "\", ext= " << key.getExtent().toString() << std::endl; @@ -530,20 +561,19 @@ ImageLayer::createImageInKeyProfile( return GeoImage::INVALID; } } +#endif if (key.getProfile()->isHorizEquivalentTo(getProfile())) { bool createUpsampledImage = false; - if (getUpsample() && - getMaxDataLevel() > key.getLOD()) + if (getUpsample() == true && getMaxDataLevel() > key.getLOD()) { TileKey best = getBestAvailableTileKey(key, false); if (best.valid()) { TileKey best_upsampled = getBestAvailableTileKey(key, true); - if (best_upsampled.valid() && - best.getLOD() < best_upsampled.getLOD()) + if (best_upsampled.valid() && best.getLOD() < best_upsampled.getLOD()) { createUpsampledImage = true; } @@ -586,6 +616,16 @@ ImageLayer::createImageInKeyProfile( // invoke user callbacks invoke_onCreate(key, result); +#if HOOKS + if (hooks) + { + auto status = hooks->posthook_image(this, key, result, progress, result); + if (status.isError()) + return GeoImage(status); + } +#endif + +#if 1 if (_memCache.valid()) { CacheBin* bin = _memCache->getOrCreateDefaultBin(); @@ -615,6 +655,7 @@ ImageLayer::createImageInKeyProfile( OE_DEBUG << LC << "Using cached but expired image for " << key.str() << std::endl; result = GeoImage( cachedImage.get(), key.getExtent()); } +#endif } return result; @@ -913,18 +954,18 @@ FutureTexture2D::update() return; } - else if (_result.isCanceled()) + else if (_result.canceled()) { dispatch(); return; } - else if (_result.isAvailable() == true) + else if (_result.available()) { OE_DEBUG<< LC << "Async result available for " << getName() << std::endl; // fetch the result - GeoImage geoImage = _result.get(); + GeoImage geoImage = _result.value(); if (geoImage.getStatus().isError()) { diff --git a/src/osgEarth/Layer b/src/osgEarth/Layer index 66df1aae38..6dbb3cf642 100644 --- a/src/osgEarth/Layer +++ b/src/osgEarth/Layer @@ -186,17 +186,17 @@ namespace osgEarth public: META_LayerOptions(osgEarth, Options, ConfigOptions); OE_OPTION(std::string, name); - OE_OPTION(bool, openAutomatically); + OE_OPTION(bool, openAutomatically, true); + OE_OPTION(bool, terrainPatch, false); OE_OPTION(std::string, cacheId); OE_OPTION(CachePolicy, cachePolicy); OE_OPTION(std::string, shaderDefine); - OE_OPTION(bool, terrainPatch); OE_OPTION(std::string, attribution); OE_OPTION(ShaderOptions, shader); OE_OPTION_VECTOR(ShaderOptions, shaders); OE_OPTION(ProxySettings, proxySettings); OE_OPTION(std::string, osgOptionString); - OE_OPTION(unsigned int, l2CacheSize); + OE_OPTION(unsigned, l2CacheSize, 0u); virtual Config getConfig() const; private: void fromConfig(const Config& conf); diff --git a/src/osgEarth/Layer.cpp b/src/osgEarth/Layer.cpp index 511bbea408..158c3fb421 100644 --- a/src/osgEarth/Layer.cpp +++ b/src/osgEarth/Layer.cpp @@ -62,10 +62,6 @@ Layer::Options::getConfig() const void Layer::Options::fromConfig(const Config& conf) { - // defaults: - openAutomatically().setDefault(true); - terrainPatch().setDefault(false); - conf.get("name", name()); conf.get("open", openAutomatically()); // back compat conf.get("enabled", openAutomatically()); diff --git a/src/osgEarth/MapNode b/src/osgEarth/MapNode index 74aad22708..3f14f2707e 100644 --- a/src/osgEarth/MapNode +++ b/src/osgEarth/MapNode @@ -63,17 +63,17 @@ namespace osgEarth class OSGEARTH_EXPORT Options : public ConfigOptions { public: META_ConfigOptions(osgEarth, Options, ConfigOptions); - OE_OPTION(ProxySettings, proxySettings); - OE_OPTION(bool, enableLighting); - OE_OPTION(bool, overlayBlending); - OE_OPTION(bool, overlayBlendingSource); - OE_OPTION(unsigned, overlayTextureSize); - OE_OPTION(bool, overlayMipMapping); - OE_OPTION(float, overlayResolutionRatio); - OE_OPTION(bool, useCascadeDraping); + OE_OPTION(int, drapingRenderBinNumber, 1); + OE_OPTION(bool, enableLighting, true); + OE_OPTION(bool, overlayBlending, true); + OE_OPTION(bool, overlayMipMapping, false); + OE_OPTION(bool, useCascadeDraping, false); + OE_OPTION(float, overlayResolutionRatio, 3.0f); + OE_OPTION(float, screenSpaceError, 25.0f); + OE_OPTION(unsigned, overlayTextureSize, 4096); + OE_OPTION(std::string, overlayBlendingSource, "alpha"); OE_OPTION(TerrainOptions, terrain); - OE_OPTION(int, drapingRenderBinNumber); - OE_OPTION(float, screenSpaceError); + OE_OPTION(ProxySettings, proxySettings); virtual Config getConfig() const; private: void fromConfig(const Config& conf); diff --git a/src/osgEarth/MapNode.cpp b/src/osgEarth/MapNode.cpp index 5359672987..ce5f93ad68 100644 --- a/src/osgEarth/MapNode.cpp +++ b/src/osgEarth/MapNode.cpp @@ -44,8 +44,6 @@ using namespace osgEarth::Contrib; #define LC "[MapNode] " -//--------------------------------------------------------------------------- - namespace { /** @@ -214,18 +212,6 @@ MapNode::Options::getConfig() const void MapNode::Options::fromConfig(const Config& conf) { - proxySettings().init(ProxySettings()); - enableLighting().init(true); - overlayBlending().init(true); - overlayBlendingSource().init("alpha"); - overlayMipMapping().init(false); - overlayTextureSize().init(4096); - overlayResolutionRatio().init(3.0f); - useCascadeDraping().init(false); - terrain().init(TerrainOptions()); - drapingRenderBinNumber().init(1); - screenSpaceError().setDefault(25.0f); - conf.get( "proxy", proxySettings() ); conf.get( "lighting", enableLighting() ); conf.get( "overlay_blending", overlayBlending() ); diff --git a/src/osgEarth/NodeUtils b/src/osgEarth/NodeUtils index f4fa1315bc..f5624dce8f 100644 --- a/src/osgEarth/NodeUtils +++ b/src/osgEarth/NodeUtils @@ -256,6 +256,20 @@ namespace osgEarth { namespace Util return 0L; } + /** Finds all the siblings of a node */ + inline std::vector> findSiblings(osg::Node* node) + { + std::vector> output; + if (node && node->getNumParents() > 0) + { + auto parent = node->getParent(0); + for(unsigned i=0; igetNumChildren(); ++i) + if (parent->getChild(i) != node) + output.push_back(parent->getChild(i)); + } + return output; + } + class FindNamedNodeVisitor : public osg::NodeVisitor { public: diff --git a/src/osgEarth/PagedNode b/src/osgEarth/PagedNode index b6f8f81776..b9a599bfbd 100644 --- a/src/osgEarth/PagedNode +++ b/src/osgEarth/PagedNode @@ -244,11 +244,12 @@ namespace osgEarth { namespace Util class OSGEARTH_EXPORT PagingManager : public osg::Group { public: - PagingManager(); - inline void* use(PagedNode2* node, void* token) { - ScopedMutexLock lock(_trackerMutex); + //! Subordinates call this to inform the paging manager they are still alive. + void* use(PagedNode2* node, void* token) + { + ScopedLockIf lock(_trackerMutex, _threadsafe); return _tracker.use(node, token); } @@ -263,6 +264,7 @@ namespace osgEarth { namespace Util void traverse(osg::NodeVisitor& nv); private: + bool _threadsafe = true; Mutex _trackerMutex; SentryTracker> _tracker; std::list _trash; diff --git a/src/osgEarth/PagedNode.cpp b/src/osgEarth/PagedNode.cpp index 328463f5f5..05de3432f0 100644 --- a/src/osgEarth/PagedNode.cpp +++ b/src/osgEarth/PagedNode.cpp @@ -130,7 +130,8 @@ PagedNode2::traverse(osg::NodeVisitor& nv) if (inRange) { // load paged child if necessary - load(priority, &nv); + if (!_merged) + load(priority, &nv); // traverse children traverseChildren(nv); @@ -144,7 +145,7 @@ PagedNode2::traverse(osg::NodeVisitor& nv) for (auto& child : _children) { osg::Node* compiled = - _compiled.isAvailable() ? _compiled.get().get() : + _compiled.available() ? _compiled.value().get() : nullptr; if (child.get() != compiled) @@ -165,9 +166,9 @@ PagedNode2::traverseChildren(osg::NodeVisitor& nv) { if (_refinePolicy == REFINE_REPLACE && _merged == true && - _compiled.get().valid()) + _compiled.value().valid()) { - _compiled.get()->accept(nv); + _compiled.value()->accept(nv); } else { @@ -202,13 +203,13 @@ PagedNode2::merge(int revision) // This is called from PagingManager. // We're in the UPDATE traversal. OE_SOFT_ASSERT_AND_RETURN(_merged == false, false); - OE_SOFT_ASSERT_AND_RETURN(_compiled.isAvailable(), false); - OE_SOFT_ASSERT_AND_RETURN(_compiled.get().valid(), false); + OE_SOFT_ASSERT_AND_RETURN(_compiled.available(), false); + OE_SOFT_ASSERT_AND_RETURN(_compiled.value().valid(), false); - addChild(_compiled.get()); + addChild(_compiled.value()); if (_callbacks.valid()) - _callbacks->firePostMergeNode(_compiled.get().get()); + _callbacks->firePostMergeNode(_compiled.value().get()); _merged = true; _failed = false; @@ -230,17 +231,18 @@ PagedNode2::computeBound() const if (_loadTriggered == true && _merged == false && - _loaded.isAvailable() && - _loaded.get()._node.valid() ) + _loaded.available() && + _loaded.value()._node.valid() ) { - bs.expandBy(_loaded.get()._node->computeBound()); + bs.expandBy(_loaded.value()._node->computeBound()); } return bs; } } -void PagedNode2::load(float priority, const osg::Object* host) +void +PagedNode2::load(float priority, const osg::Object* host) { if (_loadTriggered.exchange(true) == false) { @@ -287,16 +289,17 @@ void PagedNode2::load(float priority, const osg::Object* host) { // There is no load function so go all the way to the end of the state machine. _failed = true; + _merged = false; _compileTriggered.exchange(true); _mergeTriggered.exchange(true); } } else if ( - _loaded.isAvailable() && + _loaded.available() && _compileTriggered.exchange(true) == false) { - if (_loaded.get()._node.valid()) + if (_loaded.value()._node.valid()) { dirtyBound(); @@ -307,8 +310,8 @@ void PagedNode2::load(float priority, const osg::Object* host) osg::ref_ptr p = new ObserverProgressCallback(this); _compiled = compiler.compileAsync( - _loaded.get()._node, - _loaded.get()._state.get(), + _loaded.value()._node, + _loaded.value()._state.get(), host, p.get()); } @@ -316,8 +319,8 @@ void PagedNode2::load(float priority, const osg::Object* host) { // resolve immediately Promise> promise; - _compiled = promise.getFuture(); - promise.resolve(_loaded.get()._node); + _compiled = promise; // .getFuture(); + promise.resolve(_loaded.value()._node); } } else @@ -331,7 +334,7 @@ void PagedNode2::load(float priority, const osg::Object* host) _loaded.abandon(); } else if ( - _compiled.isAvailable() && + _compiled.available() && _pagingManager != nullptr && _mergeTriggered.exchange(true) == false) { @@ -347,9 +350,9 @@ void PagedNode2::unload() //{ // _compiled.get()->releaseGLObjects(nullptr); //} - if (_compiled.isAvailable() && _compiled.get().valid()) + if (_compiled.available() && _compiled.value().valid()) { - removeChild(_compiled.get()); + removeChild(_compiled.value()); } _compiled.abandon(); _loaded.abandon(); @@ -384,22 +387,9 @@ PagingManager::PagingManager() : arena->setConcurrency(4u); _metrics = arena->metrics(); - // NOTE: this is causing multiple model layers to not appear. - // Need to debug before using. - //osg::observer_ptr pm_ptr(this); - //_updateFunc = [pm_ptr](Cancelable*) mutable - //{ - // osg::ref_ptr pm(pm_ptr); - // if (pm.valid()) - // { - // pm->update(); - // Job(JobArena::get(JobArena::UPDATE_TRAVERSAL)) - // .dispatch(pm->_updateFunc); - // } - //}; - - //Job(JobArena::get(JobArena::UPDATE_TRAVERSAL)) - // .dispatch(_updateFunc); +#ifdef OSGEARTH_SINGLE_THREADED_OSG + _threadsafe = false; +#endif } PagingManager::~PagingManager() @@ -433,7 +423,8 @@ PagingManager::traverse(osg::NodeVisitor& nv) if (nv.getVisitorType() == nv.CULL_VISITOR) { // After culling is complete, update all of the ranges for all of the node - ScopedMutexLock lock(_trackerMutex); // unnecessary? + ScopedLockIf lock(_trackerMutex, _threadsafe); + for (auto& entry : _tracker._list) { if (entry._data.valid()) @@ -448,7 +439,8 @@ PagingManager::traverse(osg::NodeVisitor& nv) void PagingManager::merge(PagedNode2* host) { - ScopedMutexLock lock(_mergeMutex); + ScopedLockIf lock(_mergeMutex, _threadsafe); + ToMerge toMerge; toMerge._node = host; toMerge._revision = host->_revision; @@ -459,7 +451,7 @@ PagingManager::merge(PagedNode2* host) void PagingManager::update() { - ScopedMutexLock lock(_trackerMutex); + ScopedLockIf lock(_trackerMutex, _threadsafe); _tracker.flush( _mergesPerFrame, diff --git a/src/osgEarth/RenderSymbol b/src/osgEarth/RenderSymbol index 3566aca2d3..8fdbf61025 100644 --- a/src/osgEarth/RenderSymbol +++ b/src/osgEarth/RenderSymbol @@ -85,6 +85,10 @@ namespace osgEarth optional& maxCreaseAngle() { return _maxCreaseAngle; } const optional& maxCreaseAngle() const { return _maxCreaseAngle; } + /** maximum angle at which to tessellate geometry when curving it to the earth's surface */ + optional& maxTessAngle() { return _maxTessAngle; } + const optional& maxTessAngle() const { return _maxTessAngle; } + /** maximum visibility altitude */ optional& maxAltitude() { return _maxAltitude; } const optional& maxAltitude() const { return _maxAltitude; } @@ -118,6 +122,7 @@ namespace osgEarth optional _transparent; optional _decal; optional _maxCreaseAngle; + optional _maxTessAngle; optional _maxAltitude; optional _geometricError; optional _sdfMinDistance; diff --git a/src/osgEarth/RenderSymbol.cpp b/src/osgEarth/RenderSymbol.cpp index 266299ba3c..a267a9b86e 100644 --- a/src/osgEarth/RenderSymbol.cpp +++ b/src/osgEarth/RenderSymbol.cpp @@ -39,7 +39,8 @@ RenderSymbol::RenderSymbol(const RenderSymbol& rhs, const osg::CopyOp& copyop) : _maxAltitude(rhs._maxAltitude), _geometricError(rhs._geometricError), _sdfMinDistance(rhs._sdfMinDistance), - _sdfMaxDistance(rhs._sdfMaxDistance) + _sdfMaxDistance(rhs._sdfMaxDistance), + _maxTessAngle(rhs._maxTessAngle) { //nop } @@ -55,6 +56,7 @@ RenderSymbol::RenderSymbol(const Config& conf) : _transparent(false), _decal(false), _maxCreaseAngle(Angle(0.0, Units::DEGREES)), + _maxTessAngle(Angle(1.0, Units::DEGREES)), _maxAltitude(Distance(FLT_MAX, Units::METERS)), _geometricError(Distance(0.0, Units::METERS)), _sdfMinDistance(0.0), @@ -79,6 +81,7 @@ RenderSymbol::getConfig() const conf.set( "transparent", _transparent ); conf.set( "decal", _decal); conf.set( "max_crease_angle", _maxCreaseAngle); + conf.set( "max_tess_angle", _maxTessAngle); conf.set( "max_altitude", _maxAltitude); conf.set( "geometric_error", _geometricError ); conf.set( "sdf_min_distance", _sdfMinDistance); @@ -100,6 +103,7 @@ RenderSymbol::mergeConfig( const Config& conf ) conf.get( "transparent", _transparent ); conf.get( "decal", _decal); conf.get( "max_crease_angle", _maxCreaseAngle); + conf.get( "max_tess_angle", _maxTessAngle); conf.get( "max_altitude", _maxAltitude); conf.get( "geometric_error", _geometricError); conf.get( "sdf_min_distance", _sdfMinDistance); @@ -170,6 +174,11 @@ RenderSymbol::parseSLD(const Config& c, Style& style) if (Units::parse(c.value(), value, units, Units::METERS)) style.getOrCreate()->maxCreaseAngle() = Angle(value, units); } + else if (match(c.key(), "render-max-tess-angle")) { + float value; Units units; + if (Units::parse(c.value(), value, units, Units::METERS)) + style.getOrCreate()->maxTessAngle() = Angle(value, units); + } else if (match(c.key(), "render-max-altitude")) { float value; Units units; if (Units::parse(c.value(), value, units, Units::METERS)) diff --git a/src/osgEarth/SimplexNoise b/src/osgEarth/SimplexNoise index b1b7d74ae5..c1722c985f 100644 --- a/src/osgEarth/SimplexNoise +++ b/src/osgEarth/SimplexNoise @@ -160,8 +160,6 @@ namespace osgEarth { namespace Util static double const G4; unsigned char permMod12[512]; - //bool permMod12Computed; - void ComputePermMod12(); // This method is a *lot* faster than using (int)Math.floor(x) inline static int FastFloor(double x); diff --git a/src/osgEarth/SimplexNoise.cpp b/src/osgEarth/SimplexNoise.cpp index 3871f48729..c5e269e97c 100644 --- a/src/osgEarth/SimplexNoise.cpp +++ b/src/osgEarth/SimplexNoise.cpp @@ -165,7 +165,7 @@ double SimplexNoise::getTiledValue(double x, double y) const { const double TwoPI = 2.0 * osg::PI; double freq = _freq; - double o = osg::maximum(1u, _octaves); + double o = std::max(1u, _octaves); double amp = 1.0; double maxamp = 0.0; double n = 0.0; @@ -198,7 +198,7 @@ double SimplexNoise::getTiledValueWithTurbulence(double x, double y, double F) c { const double TwoPI = 2.0 * osg::PI; double freq = _freq; - double o = osg::maximum(1u, _octaves); + double o = std::max(1u, _octaves); double amp = 1.0; double maxamp = 0.0; double n = 0.0; @@ -233,7 +233,7 @@ double SimplexNoise::getTiledValueWithTurbulence(double x, double y, double F) c double SimplexNoise::getValue(double xin, double yin) const { double freq = _freq; - double o = osg::maximum(1u, _octaves); + double o = std::max(1u, _octaves); double amp = 1.0; double maxamp = 0.0; double n = 0.0; @@ -683,8 +683,8 @@ SimplexNoise::createSeamlessImage(unsigned dim) const { double v = (double)t / (double)dim; value.r() = noise.getTiledValue(u, v); - minN = osg::minimum(minN, value.r()); - maxN = osg::maximum(maxN, value.r()); + minN = std::min(minN, value.r()); + maxN = std::max(maxN, value.r()); write(value, s, t); } } diff --git a/src/osgEarth/TDTiles.cpp b/src/osgEarth/TDTiles.cpp index 271caa3803..423ca35765 100644 --- a/src/osgEarth/TDTiles.cpp +++ b/src/osgEarth/TDTiles.cpp @@ -878,9 +878,9 @@ bool ThreeDTileNode::isContentReady() void ThreeDTileNode::resolveContent() { // Resolve the future - if (!_content.valid() && _requestedContent && _contentFuture.isAvailable()) + if (!_content.valid() && _requestedContent && _contentFuture.available()) { - _content = _contentFuture.get(); + _content = _contentFuture.value(); if (_content.valid()) { diff --git a/src/osgEarth/TerrainTileModel b/src/osgEarth/TerrainTileModel index 7c099b54cd..2ec6fc4438 100644 --- a/src/osgEarth/TerrainTileModel +++ b/src/osgEarth/TerrainTileModel @@ -112,7 +112,8 @@ namespace osgEarth //! that will be managed by a TextureArena void getStateToCompile( osgUtil::StateToCompile& out, - bool bindless) const; + bool bindless, + osg::Object* token) const; protected: bool _requiresUpdateTraverse; diff --git a/src/osgEarth/TerrainTileModel.cpp b/src/osgEarth/TerrainTileModel.cpp index 3b930bb4e1..164de2f836 100644 --- a/src/osgEarth/TerrainTileModel.cpp +++ b/src/osgEarth/TerrainTileModel.cpp @@ -27,15 +27,32 @@ using namespace osgEarth; namespace { // adapter that lets us compile a Texture::Ptr using the ICO - struct TextureAdapter : public osg::Texture2D + struct TextureICOAdapter : public osg::Texture2D { osgEarth::Texture::Ptr _tex; - TextureAdapter(osgEarth::Texture::Ptr value) : _tex(value) { } + osg::observer_ptr _token; + bool _hasToken = false; + + TextureICOAdapter(osgEarth::Texture::Ptr value, osg::Object* cancelation_token) : + _tex(value), _token(cancelation_token), _hasToken(cancelation_token != nullptr) { } // apply is called by the ICO for textures (not compileGLObjects) - void apply(osg::State& state) const override { + void apply(osg::State& state) const override + { + // cancelation check: + if (_hasToken && !_token.valid()) + { + //OE_WARN << "Canceled ICO for " << _tex->name() << std::endl; + return; + } + if (_tex) - _tex->compileGLObjects(state); + { + if (_tex->compileGLObjects(state)) + { + //OE_WARN << "Compiled ICO = " << _tex->name() << (std::uintptr_t)_tex.get() << std::endl; + } + } } }; } @@ -62,16 +79,14 @@ TerrainTileModel::TerrainTileModel( } void -TerrainTileModel::getStateToCompile( - osgUtil::StateToCompile& out, - bool bindless) const +TerrainTileModel::getStateToCompile(osgUtil::StateToCompile& out, bool bindless, osg::Object* token) const { for (auto& colorLayer : colorLayers()) { if (colorLayer.texture()) { out._textures.insert(bindless ? - new TextureAdapter(colorLayer.texture()) : + new TextureICOAdapter(colorLayer.texture(), token) : colorLayer.texture()->osgTexture().get()); } } @@ -79,21 +94,21 @@ TerrainTileModel::getStateToCompile( if (normalMap().texture()) { out._textures.insert(bindless ? - new TextureAdapter(normalMap().texture()) : + new TextureICOAdapter(normalMap().texture(), token) : normalMap().texture()->osgTexture().get()); } if (elevation().texture()) { out._textures.insert(bindless ? - new TextureAdapter(elevation().texture()) : + new TextureICOAdapter(elevation().texture(), token) : elevation().texture()->osgTexture().get()); } if (landCover().texture()) { out._textures.insert(bindless ? - new TextureAdapter(landCover().texture()) : + new TextureICOAdapter(landCover().texture(), token) : landCover().texture()->osgTexture().get()); } } diff --git a/src/osgEarth/TextureArena b/src/osgEarth/TextureArena index 7105f0dd0d..5bca34c9bf 100644 --- a/src/osgEarth/TextureArena +++ b/src/osgEarth/TextureArena @@ -76,7 +76,8 @@ namespace osgEarth void update(osg::NodeVisitor& nv); //! GL memory functions - void compileGLObjects(osg::State&) const; + //! Returns true if the object needed compiling and was compiled. + bool compileGLObjects(osg::State&) const; void resizeGLObjectBuffers(unsigned); void releaseGLObjects(osg::State*, bool force = false) const; @@ -103,8 +104,7 @@ namespace osgEarth struct GLObjects : public BindlessShareableGLObjects { GLTexture::Ptr _gltexture; - unsigned _imageModCount; - GLObjects() : _imageModCount(0) { } + unsigned _imageModCount = 0u; }; mutable osg::buffered_object _globjects; diff --git a/src/osgEarth/TextureArena.cpp b/src/osgEarth/TextureArena.cpp index 0eeb7fe5be..c521d7aee3 100644 --- a/src/osgEarth/TextureArena.cpp +++ b/src/osgEarth/TextureArena.cpp @@ -169,9 +169,6 @@ Texture::needsCompile(const osg::State& state) const if ((gc._gltexture == nullptr || !gc._gltexture->valid()) && hasData == true) return true; - if (hasData == false) - return false; - return (osgTexture()->getImage(0)->getModifiedCount() != gc._imageModCount); } @@ -198,15 +195,15 @@ Texture::dataLoaded() const osgTexture()->getImage(0) != nullptr; } -void +bool Texture::compileGLObjects(osg::State& state) const { if (!needsCompile(state)) - return; + return false; OE_PROFILING_ZONE; OE_PROFILING_ZONE_TEXT(name().c_str()); - OE_HARD_ASSERT(dataLoaded() == true); + OE_SOFT_ASSERT_AND_RETURN(dataLoaded() == true, false); osg::GLExtensions* ext = state.get(); auto& gc = GLObjects::get(_globjects, state); @@ -218,9 +215,8 @@ Texture::compileGLObjects(osg::State& state) const { // hmm, it's already compiled. Does it need a recompile // because of a modified image? - if (gc._imageModCount == image->getModifiedCount()) - return; // nope + return false; // nope } if (target() == GL_TEXTURE_2D) @@ -351,10 +347,7 @@ Texture::compileGLObjects(osg::State& state) const { if (target() == GL_TEXTURE_2D) { - unsigned char* dataptr = - image->getMipmapData(mipLevel); - - + unsigned char* dataptr = image->getMipmapData(mipLevel); if (compressed) { @@ -436,6 +429,8 @@ Texture::compileGLObjects(osg::State& state) const // sync the mod counts. gc._imageModCount = image->getModifiedCount(); + + return true; } void @@ -479,18 +474,20 @@ Texture::releaseGLObjects(osg::State* state, bool force) const if (_host != nullptr && force == false) return; + OE_DEVEL << "RELEASING = " << name() << std::endl; + if (state) { auto& gc = GLObjects::get(_globjects, *state); if (gc._gltexture != nullptr) { // debugging - OE_DEVEL << LC - << "Texture::releaseGLObjects '" << name() << "'" << std::endl; + //OE_DEVEL << LC + // << "Texture::releaseGLObjects '" << name() << "'" << std::endl; //<< "' name=" << gc._gltexture->name() //<< " handle=" << gc._gltexture->handle(*state) << std::endl; - // will activate the releaser + // will activate the releaser gc._gltexture->release(); // redundant? gc._gltexture = nullptr; } @@ -699,7 +696,7 @@ TextureArena::add(Texture::Ptr tex, const osgDB::Options* readOptions) { for (int i = 0; i < _textures.size(); ++i) { - if (_textures[i] == nullptr) // || _textures[i].use_count() == 1) + if (_textures[i] == nullptr) { index = i; break; @@ -716,7 +713,7 @@ TextureArena::add(Texture::Ptr tex, const osgDB::Options* readOptions) // add to all existing GCs: for (unsigned i = 0; i < _globjects.size(); ++i) { - if (_globjects[i]._inUse) + if (_globjects[i]._inUse) _globjects[i]._toCompile.push(index); } @@ -881,16 +878,15 @@ TextureArena::apply(osg::State& state) const { // If we are going to compile any textures, we need to save and restore // the OSG texture state... - const osg::StateAttribute* savedActiveOsgTexture = nullptr; if (!gc._toCompile.empty()) { + OE_PROFILING_ZONE_NAMED("_toCompile"); + // need to save any bound texture so we can reinstate it: - savedActiveOsgTexture = state.getLastAppliedTextureAttribute( + auto savedActiveOsgTexture = state.getLastAppliedTextureAttribute( state.getActiveTextureUnit(), osg::StateAttribute::TEXTURE); - } - { - OE_PROFILING_ZONE_NAMED("_toCompile"); + unsigned num_compiled = 0; while (!gc._toCompile.empty()) { @@ -899,7 +895,11 @@ TextureArena::apply(osg::State& state) const auto tex = _textures[ptr]; if (tex) { - tex->compileGLObjects(state); + if (tex->compileGLObjects(state)) + { + ++num_compiled; + OE_DEVEL << "Compiled on demand = " << tex->name() << " " << (std::uintptr_t)tex.get() << std::endl; + } } GLTexture* gltex = nullptr; @@ -911,20 +911,20 @@ TextureArena::apply(osg::State& state) const if (gc._handles[index] != handle) { gc._handles[index] = handle; + + // mark the GC to re-upload its LUT + gc._handleBufferDirty = true; } + } - // mark the GC to re-upload its LUT - gc._handleBufferDirty = true; + // reinstate the old bound texture + if (savedActiveOsgTexture) + { + state.applyTextureAttribute(state.getActiveTextureUnit(), savedActiveOsgTexture); } } gc._lastAppliedFrame = state.getFrameStamp()->getFrameNumber(); - - // reinstate the old bound texture - if (savedActiveOsgTexture) - { - state.applyTextureAttribute(state.getActiveTextureUnit(), savedActiveOsgTexture); - } } // upload to GPU if it changed: @@ -992,7 +992,7 @@ TextureArena::releaseGLObjects(osg::State* state, bool force) const { ScopedMutexLock lock(_m); - OE_DEVEL << LC << "releaseGLObjects on arena " << getName() << std::endl; + //OE_DEVEL << LC << "releaseGLObjects on arena " << getName() << std::endl; if (state) { diff --git a/src/osgEarth/Threading b/src/osgEarth/Threading index 8d8cabd821..eb718475dd 100644 --- a/src/osgEarth/Threading +++ b/src/osgEarth/Threading @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -160,6 +161,18 @@ namespace osgEarth { namespace Threading using ScopedMutexLock = std::lock_guard; using ScopedLock = ScopedMutexLock; + struct ScopedMutexLockIf { + ScopedMutexLockIf(BasicLockable& lock, bool condition) : _lock(lock), _condition(condition) { + if (_condition) _lock.lock(); + } + ~ScopedMutexLockIf() { + if (_condition) _lock.unlock(); + } + BasicLockable& _lock; + bool _condition; + }; + using ScopedLockIf = ScopedMutexLockIf; + //! Locks a recursive mutex for the duration of the scope using ScopedRecursiveMutexLock = std::lock_guard; using ScopedRecursiveLock = ScopedRecursiveMutexLock; @@ -182,6 +195,7 @@ namespace osgEarth { namespace Threading { public: virtual bool isCanceled() const = 0; + bool canceled() const { return isCanceled(); } }; /** @@ -203,29 +217,67 @@ namespace osgEarth { namespace Threading { public: //! Construct a new event - Event(); + Event() : _set(false) { } //! DTOR - ~Event(); + ~Event() { + _set = false; + for (int i = 0; i < 255; ++i) // workaround buggy broadcast + _cond.notify_all(); + } //! Block until the event is set, then return true. - bool wait(); + inline bool wait() { + while (!_set) { + std::unique_lock lock(_m); + if (!_set) + _cond.wait(lock); + } + return _set; + } //! Block until the event is set or the timout expires. //! Return true if the event has set, otherwise false. - bool wait(unsigned timeout_ms); + template + inline bool wait(T timeout) { + if (!_set) { + std::unique_lock lock(_m); + if (!_set) + _cond.wait_for(lock, timeout); + } + return _set; + } - //! Like wait(), but resets the state and always returns true. - bool waitAndReset(); + //! Block until the event is set; then reset it. + inline bool waitAndReset() { + std::unique_lock lock(_m); + if (!_set) + _cond.wait(lock); + _set = false; + return true; + } //! Set the event state, causing any waiters to unblock. - void set(); + inline void set() { + if (!_set) { + std::unique_lock lock(_m); + if (!_set) { + _set = true; + _cond.notify_all(); + } + } + } //! Reset (unset) the event state; new waiters will block until set() is called. - void reset(); + inline void reset() { + std::unique_lock lock(_m); + _set = false; + } //! Whether the event state is set (waiters will not block). - inline bool isSet() const { return _set; } + inline bool isSet() const { + return _set; + } protected: std::mutex _m; // do not use Mutex, we never want tracking @@ -234,97 +286,82 @@ namespace osgEarth { namespace Threading }; /** - * Future is the consumer-side interface to an asynchronous operation. + * Future holds the future result of an asynchronous operation. * * Usage: - * Producer (usually an asynchronous function call) creates a Promise - * and immediately returns promise.getFuture(). The Consumer then performs other - * work, and eventually (or immediately) checks isAvailable() for a result or - * isAbandoned() for cancelation. + * Producer (usually an asynchronous function call) creates a Future + * (the promise of a future result) and immediately returns it. The Consumer + * then performs other work, and eventually (or immediately) checks available() + * for a result or canceled() for cancelation. If availabile() is true, + * Consumer calls value() to fetch the valid result. + * + * As long as at least two equivalent Future object (i.e. Futures pointing to the + * same internal shared data) exist, the Future is considered valid. Once + * that count goes to one, the Future is either available (the value is ready) + * or empty (i.e., canceled or abandoned). */ template class Future : public Cancelable { - public: - using Callback = std::function; - private: - // internal structure to track referenced to the result - struct Container { - Container() { } - void set(const T& obj) { - ScopedMutexLock lock(_m); - _obj = obj; - } - void set(const T&& obj) { - ScopedMutexLock lock(_m); - _obj = std::move(obj); - } - const T& obj() const { - ScopedMutexLock lock(_m); - return _obj; - } + // internal structure to track references to the result + // One instance of this is shared among all Future instances + // created from the copy constructor. + struct Shared + { T _obj; - mutable Mutex _m; + mutable Event _ev; }; public: //! Blank CTOR Future() { - _ev = std::make_shared(); - _shared = std::make_shared(); + _shared = std::make_shared(); } - //! Copy CTOR - Future(const Future& rhs) : - _ev(rhs._ev), - _shared(rhs._shared) { } + Future(const Future& rhs) = default; - //! Assignment - Future& operator = (const Future& rhs) { - _ev = rhs._ev; - _shared = rhs._shared; - return *this; + //! True is this Future is unused and not connected to any other Future + bool empty() const { + return !available() && _shared.use_count() == 1; } //! True if the promise was resolved and a result if available. - bool isAvailable() const { - return _ev->isSet(); - } - - //! True if the Promise that generated this Future no longer exists - //! and the Promise was never resolved. - bool isAbandoned() const { - return !isAvailable() && _shared.use_count() == 1; + bool available() const { + return _shared->_ev.isSet(); } - //! True if a Promise exists, but has not yet been fulfilled. - bool isWorking() const { - return !isAvailable() && !isAbandoned(); + //! True if a promise exists, but has not yet been resolved; + //! Presumably the asynchronous task is still working. + bool working() const { + return !empty() && !available(); } - //! Synonym for isAbandoned. (Canceleble interface) + //! Synonym for empty() - Cancelable interface bool isCanceled() const override { - return isAbandoned(); + return empty(); + } + bool canceled() const { + return empty(); } - //! Deference the result object. Make sure you check isAvailable() + //! Deference the result object. Make sure you check available() //! to check that the future was actually resolved; otherwise you //! will just get the default object. - T get() const { - return _shared->obj(); + const T& value() const { + return _shared->_obj; } - //! Alias for get() + //! Dereference this object to const pointer to the result. const T* operator -> () const { - return &(_shared->obj()); + return &_shared->_obj; } - //! Same as get(), but if the result is available will reset the + //! Same as value(), but if the result is available will reset the //! future before returning the result object. T release() { - bool avail = isAvailable(); - T result = get(); + bool avail = available(); + T result = value(); if (avail) reset(); return result; @@ -334,28 +371,34 @@ namespace osgEarth { namespace Threading //! then returns the result object. T join() const { while ( - !isAbandoned() && - !_ev->wait(1u)); - return get(); + !empty() && + !_shared->_ev.wait(std::chrono::milliseconds(1))); + return value(); } //! Blocks until the result becomes available or the future is abandoned //! or a cancelation flag is set; then returns the result object. - T join(const Cancelable* cancelable) const { - while ( - !isAvailable() && - !isAbandoned() && - !(cancelable && cancelable->isCanceled())) + T join(const Cancelable& p) const { + while (working() && !p.canceled()) + { + _shared->_ev.wait(std::chrono::milliseconds(1)); + } + return value(); + } + + //! Blocks until the result becomes available or the future is abandoned + //! or a cancelation flag is set; then returns the result object. + T join(Cancelable* p) const { + while (working() && (p == nullptr || !p->canceled())) { - _ev->wait(1u); + _shared->_ev.wait(std::chrono::milliseconds(1)); } - return get(); + return value(); } //! Release reference to a promise, resetting this future to its default state void abandon() { - _shared.reset(new Container()); - _ev.reset(new Event()); + _shared.reset(new Shared()); } //! synonym for abandon. @@ -363,76 +406,37 @@ namespace osgEarth { namespace Threading abandon(); } - //! The number of objects, including this one, that - //! refernece the shared container. if this method - //! returns 1, that means this is the only object with - //! access to the data. This method will never return zero. - unsigned refs() const { - return _shared.use_count(); - } - - //! Function to execute upon a Promise resolving this Future. - void whenAvailable(const Callback& cb) { - _whenAvailable = cb; - } - - private: - std::shared_ptr _ev; - std::shared_ptr _shared; - Callback _whenAvailable; - template friend class Promise; - }; - - /** - * Promise is the producer-side interface to an asynchronous operation. - * - * Usage: The code that initiates an asychronous operation creates a Promise - * object, dispatches the asynchronous code, and immediately returns - * Promise.getFuture(). The caller can then call future.get() to block until - * the result is available. - */ - template - class Promise : public Cancelable - { - public: - Promise() { } - - //! This promise's future result. - Future getFuture() const { return _future; } - //! Resolve (fulfill) the promise with the provided result value. void resolve(const T& value) { - _future._shared->set(value); - _future._ev->set(); - if (_future._whenAvailable) - _future._whenAvailable(_future.get()); + _shared->_obj = value; + _shared->_ev.set(); } - //! Resolve (fulfill) the promise with a default result. - void resolve() { - _future._ev->set(); - if (_future._whenAvailable) - _future._whenAvailable(_future.get()); - } - - //! True if the promise is resolved and the Future holds a valid result. - bool isResolved() const { - return _future._ev->isSet(); + //! Resolve (fulfill) the promise with an rvalue + void resolve(T&& value) { + _shared->_obj = std::move(value); + _shared->_ev.set(); } - //! True is there are no Future objects waiting on this Promise. - bool isAbandoned() const { - return _future._shared.use_count() == 1; + //! Resolve (fulfill) the promise with a default result + void resolve() { + _shared->_ev.set(); } - bool isCanceled() const override { - return isAbandoned(); + //! The number of objects, including this one, that + //! reference the shared container. If this method + //! returns 1, that means this is the only object with + //! access to the data. This method will never return zero. + unsigned refs() const { + return _shared.use_count(); } private: - Future _future; + std::shared_ptr _shared; }; + template using Promise = Future; + /** * Convenience base class for representing a Result object that may be * synchronous or asynchronous, depending on which constructor you use. @@ -442,11 +446,11 @@ namespace osgEarth { namespace Threading { public: bool isReady() const { - return _future.isAvailable() || _future.isAbandoned(); + return _future.available() || _future.empty(); } bool isWorking() const { - return !_future.isAvailable() && !_future.isAbandoned(); + return !_future.available() && !_future.empty(); } protected: @@ -456,7 +460,7 @@ namespace osgEarth { namespace Threading //! Immediate synchronous resolve constructor FutureResult(const T& data) { Promise p; - _future = p.getFuture(); + _future = p; // .getFuture(); p.resolve(data); } @@ -1058,10 +1062,10 @@ namespace osgEarth { namespace Threading std::function function) const { Promise promise; - Future future = promise.getFuture(); + Future future = promise; // .getFuture(); JobArena::Delegate delegate = [function, promise]() mutable { - bool good = !promise.isAbandoned(); + bool good = !promise.empty(); if (good) promise.resolve(function(&promise)); return good; diff --git a/src/osgEarth/Threading.cpp b/src/osgEarth/Threading.cpp index 8424bc0e88..7c3987daad 100644 --- a/src/osgEarth/Threading.cpp +++ b/src/osgEarth/Threading.cpp @@ -289,6 +289,7 @@ unsigned osgEarth::Threading::getConcurrency() //................................................................... +#if 0 Event::Event() : _set(false) { @@ -352,7 +353,7 @@ void Event::reset() std::lock_guard lock(_m); _set = false; } - +#endif void osgEarth::Threading::setThreadName(const std::string& name) diff --git a/src/osgEarth/TileLayer b/src/osgEarth/TileLayer index bbcdce0a0f..0913328955 100644 --- a/src/osgEarth/TileLayer +++ b/src/osgEarth/TileLayer @@ -48,16 +48,16 @@ namespace osgEarth class OSGEARTH_EXPORT Options : public VisibleLayer::Options { public: META_LayerOptions(osgEarth, Options, VisibleLayer::Options); - OE_OPTION(unsigned, minLevel); + OE_OPTION(unsigned, minLevel, 0u); + OE_OPTION(unsigned, maxLevel, 23u); + OE_OPTION(unsigned, maxDataLevel, 99u); OE_OPTION(double, minResolution); - OE_OPTION(unsigned, maxLevel); OE_OPTION(double, maxResolution); - OE_OPTION(unsigned, maxDataLevel); - OE_OPTION(unsigned, tileSize); - OE_OPTION(float, noDataValue); - OE_OPTION(float, minValidValue); - OE_OPTION(float, maxValidValue); - OE_OPTION(bool, upsample); + OE_OPTION(unsigned, tileSize, 256u); + OE_OPTION(float, noDataValue, -32767.0f); // SHRT_MIN + OE_OPTION(float, minValidValue, -32766.0f); // -(2^15 - 2) + OE_OPTION(float, maxValidValue, 32767.0f); // 2^15 - 1 + OE_OPTION(bool, upsample, false); OE_OPTION(ProfileOptions, profile); virtual Config getConfig() const; private: diff --git a/src/osgEarth/TileLayer.cpp b/src/osgEarth/TileLayer.cpp index 0905ccdde8..9250a1a28d 100644 --- a/src/osgEarth/TileLayer.cpp +++ b/src/osgEarth/TileLayer.cpp @@ -32,7 +32,7 @@ namespace using DataExtentsIndex = RTree; } -#define LC "[" << className() << "] " << getName() << "\" " +#define LC "[" << className() << "] \"" << getName() << "\" " //------------------------------------------------------------------------ @@ -59,15 +59,6 @@ TileLayer::Options::getConfig() const void TileLayer::Options::fromConfig(const Config& conf) { - _minLevel.init( 0 ); - _maxLevel.init( 23 ); - _maxDataLevel.init( 99 ); - _tileSize.init( 256 ); - _noDataValue.init( -32767.0f ); // SHRT_MIN - _minValidValue.init( -32766.0f ); // -(2^15 - 2) - _maxValidValue.init( 32767.0f ); - upsample().setDefault(false); - conf.get( "min_level", _minLevel ); conf.get( "max_level", _maxLevel ); conf.get( "min_resolution", _minResolution ); @@ -85,20 +76,20 @@ TileLayer::Options::fromConfig(const Config& conf) //------------------------------------------------------------------------ TileLayer::CacheBinMetadata::CacheBinMetadata() : -_valid(false) + _valid(false) { //nop } TileLayer::CacheBinMetadata::CacheBinMetadata(const TileLayer::CacheBinMetadata& rhs) : -_valid ( rhs._valid ), -_cacheBinId ( rhs._cacheBinId ), -_sourceName ( rhs._sourceName ), -_sourceDriver ( rhs._sourceDriver ), -_sourceTileSize ( rhs._sourceTileSize ), -_sourceProfile ( rhs._sourceProfile ), -_cacheProfile ( rhs._cacheProfile ), -_cacheCreateTime( rhs._cacheCreateTime ) + _valid(rhs._valid), + _cacheBinId(rhs._cacheBinId), + _sourceName(rhs._sourceName), + _sourceDriver(rhs._sourceDriver), + _sourceTileSize(rhs._sourceTileSize), + _sourceProfile(rhs._sourceProfile), + _cacheProfile(rhs._cacheProfile), + _cacheCreateTime(rhs._cacheCreateTime) { //nop } diff --git a/src/osgEarth/TileRasterizer.cpp b/src/osgEarth/TileRasterizer.cpp index f4c5ff2ff4..5d4ce87e68 100644 --- a/src/osgEarth/TileRasterizer.cpp +++ b/src/osgEarth/TileRasterizer.cpp @@ -196,7 +196,7 @@ TileRasterizer::render(osg::Node* node, const GeoExtent& extent) job->_extent = extent; // retrieve the future so we can return it to the caller: - Future result = job->_promise.getFuture(); + Future result = job->_promise; // .getFuture(); // put it on the queue: _jobQ.push(job); @@ -396,7 +396,7 @@ TileRasterizer::postDraw(osg::RenderInfo& ri) // GPU task delegate: auto gpu_task = [job](osg::State& state, Promise& promise, int invocation) { - if (promise.isAbandoned()) + if (promise.empty()) { OE_DEBUG << "Job " << job << " canceled" << std::endl; return false; // done diff --git a/src/osgEarth/Viewpoint b/src/osgEarth/Viewpoint index 0128ab5a48..feca5ac899 100644 --- a/src/osgEarth/Viewpoint +++ b/src/osgEarth/Viewpoint @@ -57,6 +57,7 @@ namespace osgEarth * a valid tracked node. */ bool isValid() const; + bool valid() const { return isValid(); } /** * Gets or sets the name of the viewpoint. diff --git a/src/osgEarth/VisibleLayer b/src/osgEarth/VisibleLayer index adeb758c02..9571c82e23 100644 --- a/src/osgEarth/VisibleLayer +++ b/src/osgEarth/VisibleLayer @@ -48,14 +48,14 @@ namespace osgEarth class OSGEARTH_EXPORT Options : public Layer::Options { public: META_LayerOptions(osgEarth, Options, Layer::Options); - OE_OPTION(bool, visible); - OE_OPTION(float, opacity); - OE_OPTION(float, minVisibleRange); - OE_OPTION(float, maxVisibleRange); - OE_OPTION(float, attenuationRange); - OE_OPTION(ColorBlending, blend); - OE_OPTION(osg::Node::NodeMask, mask); - OE_OPTION(bool, debugView); + OE_OPTION(bool, visible, true); + OE_OPTION(float, opacity, 1.0f); + OE_OPTION(float, minVisibleRange, 0.0f); + OE_OPTION(float, maxVisibleRange, FLT_MAX); + OE_OPTION(float, attenuationRange, 0.0f); + OE_OPTION(ColorBlending, blend, BLEND_INTERPOLATE); + OE_OPTION(osg::Node::NodeMask, mask, 0xffffffff); + OE_OPTION(bool, debugView, false); virtual Config getConfig() const; private: void fromConfig(const Config& conf); diff --git a/src/osgEarth/VisibleLayer.cpp b/src/osgEarth/VisibleLayer.cpp index 14d69b8609..f1420e34fe 100644 --- a/src/osgEarth/VisibleLayer.cpp +++ b/src/osgEarth/VisibleLayer.cpp @@ -167,15 +167,6 @@ VisibleLayer::Options::getConfig() const void VisibleLayer::Options::fromConfig(const Config& conf) { - _visible.init( true ); - _opacity.init( 1.0f ); - _minVisibleRange.init( 0.0 ); - _maxVisibleRange.init( FLT_MAX ); - _attenuationRange.init(0.0f); - _blend.init( BLEND_INTERPOLATE ); - _mask.init(DEFAULT_LAYER_MASK); - debugView().setDefault(false); - conf.get( "visible", _visible ); conf.get( "opacity", _opacity); conf.get( "min_range", _minVisibleRange ); diff --git a/src/osgEarthDrivers/engine_rex/LoadTileData.cpp b/src/osgEarthDrivers/engine_rex/LoadTileData.cpp index 6ec0198c13..097e220b1d 100644 --- a/src/osgEarthDrivers/engine_rex/LoadTileData.cpp +++ b/src/osgEarthDrivers/engine_rex/LoadTileData.cpp @@ -123,7 +123,7 @@ LoadTileDataOperation::dispatch(bool async) else { Promise promise; - _result = promise.getFuture(); + _result = promise; // .getFuture(); promise.resolve(load(nullptr)); } @@ -153,17 +153,17 @@ LoadTileDataOperation::merge() // no data model at all - done // GW: should never happen. - if (!_result.isAvailable()) + if (!_result.available()) { OE_WARN << tilenode->getKey().str() << " bailing out of merge b/c data model is NULL" << std::endl; return false; } - OE_SOFT_ASSERT_AND_RETURN(_result.isAvailable(), false); + OE_SOFT_ASSERT_AND_RETURN(_result.available(), false); OE_PROFILING_ZONE; - const osg::ref_ptr& model = _result.get(); + const osg::ref_ptr& model = _result.value(); //.get(); // Check the map data revision and scan the manifest and see if any // revisions don't match the revisions in the original manifest. diff --git a/src/osgEarthDrivers/engine_rex/Loader.cpp b/src/osgEarthDrivers/engine_rex/Loader.cpp index c6bda6a037..1f46723103 100644 --- a/src/osgEarthDrivers/engine_rex/Loader.cpp +++ b/src/osgEarthDrivers/engine_rex/Loader.cpp @@ -17,6 +17,7 @@ * along with this program. If not, see */ #include "Loader" +#include "TileNode" #include #include @@ -89,21 +90,24 @@ Merger::merge(LoadTileDataOperationPtr data, osg::NodeVisitor& nv) osg::ref_ptr state = glcompiler.collectState(nullptr); OE_SOFT_ASSERT_AND_RETURN(state.valid(), void()); - // populate it with the tile model contents: + // populate it with the tile model contents. + // passing in the tilenode observer_ptr as a cancelation token - the ICO will skip + // the compilation if the corresponding tilenode goes nullptr. bool bindless = GLUtils::useNVGL(); - data->_result.join()->getStateToCompile(*state.get(), bindless); + data->_result.join()->getStateToCompile(*state.get(), bindless, data->_tilenode.get()); ScopedMutexLock lock(_mutex); if (!state->empty()) { - static osg::ref_ptr dummyNode = new osg::Node(); + // make a fake node for the GL compiler to track - we are passing in the state directly + osg::ref_ptr dummy = new osg::Node(); + if (data->_tilenode.valid()) + dummy->setName(data->_tilenode->getName()); ToCompile toCompile; toCompile._data = data; - toCompile._compiled = glcompiler.compileAsync( - dummyNode.get(), state.get(), &nv, nullptr); - + toCompile._compiled = glcompiler.compileAsync(dummy, state.get(), &nv, nullptr); _compileQueue.push_back(std::move(toCompile)); } else @@ -140,7 +144,7 @@ Merger::traverse(osg::NodeVisitor& nv) // these compilesets so they don't sit in this queue forever. for(auto& next : _compileQueue) { - if (next._compiled.isAvailable()) + if (next._compiled.available()) { // compile finished, put it on the merge queue _mergeQueue.emplace(std::move(next._data)); @@ -148,7 +152,7 @@ Merger::traverse(osg::NodeVisitor& nv) // note: no change the metrics since we are just moving from // one queue to another } - else if (next._compiled.isAbandoned()) + else if (next._compiled.empty()) { // compile canceled, ditch it if (_metrics) @@ -177,7 +181,7 @@ Merger::traverse(osg::NodeVisitor& nv) if (next != nullptr) { - if (next->_result.isAvailable()) + if (next->_result.available()) { next->merge(); } diff --git a/src/osgEarthDrivers/engine_rex/RexEngine.GL4.glsl b/src/osgEarthDrivers/engine_rex/RexEngine.GL4.glsl index db285a6424..b2e3152925 100644 --- a/src/osgEarthDrivers/engine_rex/RexEngine.GL4.glsl +++ b/src/osgEarthDrivers/engine_rex/RexEngine.GL4.glsl @@ -1,7 +1,7 @@ #define MAX_NUM_SHARED_SAMPLERS 16 struct oe_rex_Shared { - vec2 morphConstants[20]; // TODO: define this + vec2 morphConstants[49]; float padding[2]; }; struct oe_rex_Tile { diff --git a/src/osgEarthDrivers/engine_rex/RexEngine.SDK.GL4.glsl b/src/osgEarthDrivers/engine_rex/RexEngine.SDK.GL4.glsl index e94ca6d944..5605428607 100644 --- a/src/osgEarthDrivers/engine_rex/RexEngine.SDK.GL4.glsl +++ b/src/osgEarthDrivers/engine_rex/RexEngine.SDK.GL4.glsl @@ -95,7 +95,6 @@ vec4 oe_terrain_getNormalAndCurvature(in uint64_t handle, in vec2 uv) return vec4(normalize(n.xyz), curv); } -#ifndef VP_STAGE_FRAGMENT /** * Scales repeating texture coordinate such that they are [0..1] * at a specific reference tile LOD. @@ -137,4 +136,4 @@ vec4 oe_terrain_scaleCoordsAndTileKeyToRefLOD(in vec2 tc, in float refLOD) return vec4(result, a); } -#endif + diff --git a/src/osgEarthDrivers/engine_rex/RexEngine.SDK.glsl b/src/osgEarthDrivers/engine_rex/RexEngine.SDK.glsl index eeddec9e76..38ff387ca2 100644 --- a/src/osgEarthDrivers/engine_rex/RexEngine.SDK.glsl +++ b/src/osgEarthDrivers/engine_rex/RexEngine.SDK.glsl @@ -58,7 +58,6 @@ vec4 oe_terrain_getNormalAndCurvature() return oe_terrain_getNormalAndCurvature(uv_scaledBiased); } -#ifndef VP_STAGE_FRAGMENT /** * Scales repeating texture coordinate such that they are [0..1] * at a specific reference tile LOD. @@ -100,4 +99,4 @@ vec4 oe_terrain_scaleCoordsAndTileKeyToRefLOD(in vec2 tc, in float refLOD) return vec4(result, a); } -#endif + diff --git a/src/osgEarthDrivers/engine_rex/TileNode.cpp b/src/osgEarthDrivers/engine_rex/TileNode.cpp index 2d177a2220..05620b7f5a 100644 --- a/src/osgEarthDrivers/engine_rex/TileNode.cpp +++ b/src/osgEarthDrivers/engine_rex/TileNode.cpp @@ -712,7 +712,7 @@ TileNode::createChildren() for (int i = 0; i < 4; ++i) { - if (_createChildResults[i].isAvailable()) + if (_createChildResults[i].available()) ++numChildrenReady; } @@ -720,7 +720,7 @@ TileNode::createChildren() { for (int i = 0; i < 4; ++i) { - osg::ref_ptr child = _createChildResults[i].get(); + osg::ref_ptr child = _createChildResults[i].value(); addChild(child); child->initializeData(); child->refreshAllLayers(); @@ -1275,14 +1275,14 @@ TileNode::load(TerrainCuller* culler) { LoadTileDataOperationPtr& op = _loadQueue.front(); - if (op->_result.isAbandoned()) + if (op->_result.empty()) { // Actually this means that the task has not yet been dispatched, // so assign the priority and do it now. op->dispatch(); } - else if (op->_result.isAvailable()) + else if (op->_result.available()) { // The task completed, so submit it to the merger. // (We can't merge here in the CULL traversal) diff --git a/src/osgEarthDrivers/engine_rex/TileRenderModel b/src/osgEarthDrivers/engine_rex/TileRenderModel index c532e0c735..0ccf5af8e8 100644 --- a/src/osgEarthDrivers/engine_rex/TileRenderModel +++ b/src/osgEarthDrivers/engine_rex/TileRenderModel @@ -44,7 +44,7 @@ namespace osgEarth { namespace REX // since it's constant data struct GL4GlobalData // align to 16 bytes (std430) { - float morphConstants[19 * 2]; //TODO + float morphConstants[98]; float padding2[2]; }; diff --git a/src/osgEarthDrivers/kml/KMLOptions b/src/osgEarthDrivers/kml/KMLOptions index bfffd57676..ed39fac751 100644 --- a/src/osgEarthDrivers/kml/KMLOptions +++ b/src/osgEarthDrivers/kml/KMLOptions @@ -68,8 +68,16 @@ namespace osgEarth { namespace KML const optional& modelRotation() const { return _modelRotation; } public: - KMLOptions() : _declutter( true ), _iconBaseScale( 1.0f ), _iconMaxSize(32), _modelScale(1.0f) { } + KMLOptions() : _declutter(true), _iconBaseScale(1.0f), _iconMaxSize(32), _modelScale(1.0f) + { + _defaultTextSymbol = new TextSymbol(); + _defaultTextSymbol->size() = 18.0f; + _defaultTextSymbol->halo() = Stroke(0.3f, 0.3f, 0.3f, 1.0f); + _defaultIconSymbol = new IconSymbol(); + _defaultIconSymbol->url()->setLiteral("https://github.com/gwaldron/osgearth/blob/master/data/placemark32.png?raw=true"); + } + virtual ~KMLOptions() { } protected: diff --git a/src/osgEarthProcedural/Biome b/src/osgEarthProcedural/Biome index 833f2ac057..5eb2b0aebc 100644 --- a/src/osgEarthProcedural/Biome +++ b/src/osgEarthProcedural/Biome @@ -26,7 +26,6 @@ #include #include #include -#include namespace osgEarth { diff --git a/src/osgEarthProcedural/BiomeLayer b/src/osgEarthProcedural/BiomeLayer index 9bb5874863..260ba533a2 100644 --- a/src/osgEarthProcedural/BiomeLayer +++ b/src/osgEarthProcedural/BiomeLayer @@ -59,6 +59,19 @@ namespace osgEarth conf.get("soiltype", soiltype()); } + Config getConfig() const + { + Config conf; + conf.set("biome_id", biomeid()); + conf.set("traits", traits()); + conf.set("dense", dense()); + conf.set("lush", lush()); + conf.set("rugged", rugged()); + conf.set("material", material()); + conf.set("soiltype", soiltype()); + return conf; + } + bool valid() const { return (biomeid().isSet() && !biomeid()->empty()) || @@ -99,6 +112,14 @@ namespace osgEarth conf.get("biome_id", biomeid()); } + Config getConfig() const + + { + Config conf; + conf.set("biome_id", biomeid()); + return conf; + } + bool valid() const { return (biomeid().isSet() && !biomeid()->empty()); diff --git a/src/osgEarthProcedural/BiomeManager b/src/osgEarthProcedural/BiomeManager index 757136dcd7..33c5b29cef 100644 --- a/src/osgEarthProcedural/BiomeManager +++ b/src/osgEarthProcedural/BiomeManager @@ -21,6 +21,7 @@ #include #include +#include namespace osgEarth { diff --git a/src/osgEarthProcedural/BiomeManager.cpp b/src/osgEarthProcedural/BiomeManager.cpp index 4845f3eb53..cdd5b97b81 100644 --- a/src/osgEarthProcedural/BiomeManager.cpp +++ b/src/osgEarthProcedural/BiomeManager.cpp @@ -552,7 +552,7 @@ BiomeManager::materializeNewAssets( residentAsset->boundingBox() = cbv.getBoundingBox(); modelcache[uri]._modelAABB = residentAsset->boundingBox(); - OE_INFO << LC << "Loaded model: " << uri.base() << + OE_DEBUG << LC << "Loaded model: " << uri.base() << " with bbox " << residentAsset->boundingBox().xMin() << " " << residentAsset->boundingBox().yMin() << " " << residentAsset->boundingBox().xMax() << " " diff --git a/src/osgEarthProcedural/ImGui/LifeMapLayerGUI b/src/osgEarthProcedural/ImGui/LifeMapLayerGUI index 4cee9fee26..0b8a63e00d 100644 --- a/src/osgEarthProcedural/ImGui/LifeMapLayerGUI +++ b/src/osgEarthProcedural/ImGui/LifeMapLayerGUI @@ -169,9 +169,9 @@ namespace osgEarth ImGui::Separator(); ImGui::Checkbox("Show lifemap under mouse", &_showLifemapUnderMouse); - if (_showLifemapUnderMouse && _lifemapUnderMouse.isAvailable()) + if (_showLifemapUnderMouse && _lifemapUnderMouse.available()) { - osg::Vec4 pixel = _lifemapUnderMouse.get(); + osg::Vec4 pixel = _lifemapUnderMouse.value(); ImGui::Text("R=%.2f D=%.2f L=%.2f M=%d", pixel.r(), pixel.g(), @@ -183,9 +183,9 @@ namespace osgEarth { ImGui::Separator(); ImGui::Checkbox("Show landcover under mouse", &_showLandcoverUnderMouse); - if (_showLandcoverUnderMouse && _landcoverUnderMouse.isAvailable()) + if (_showLandcoverUnderMouse && _landcoverUnderMouse.available()) { - LandCoverSample sample = _landcoverUnderMouse.get(); + LandCoverSample sample = _landcoverUnderMouse.value(); if (sample.biomeid().isSet()) ImGui::Text("biome_id = %s", sample.biomeid().get()); if (sample.dense().isSet()) diff --git a/src/osgEarthProcedural/ImGui/TerrainEditGUI b/src/osgEarthProcedural/ImGui/TerrainEditGUI index 6d9a3f467b..cef111bdef 100644 --- a/src/osgEarthProcedural/ImGui/TerrainEditGUI +++ b/src/osgEarthProcedural/ImGui/TerrainEditGUI @@ -135,6 +135,7 @@ namespace osgEarth float lifemapMix, osg::ref_ptr& out_elevation, osg::ref_ptr& out_lifemap, + osg::ref_ptr& out_landcover, GeoExtent& out_extent) { osg::ref_ptr feature = osg::clone(in_feature, osg::CopyOp::DEEP_COPY_ALL); @@ -154,43 +155,23 @@ namespace osgEarth out_extent.expand(-diff, 0.0); - FeatureList features { feature }; + FeatureList features{ feature }; SDFGenerator sdfgen; sdfgen.setUseGPU(false); GeoImage nnf; - sdfgen.createNearestNeighborField( - features, - 256, - out_extent, - false, - nnf, - nullptr); - - GeoImage sdf = sdfgen.allocateSDF( - 256, - out_extent); - - sdfgen.createDistanceField( - nnf, - sdf, - out_extent.height(), - 0.25f * width, - 0.5f * width, - nullptr); + sdfgen.createNearestNeighborField(features, 256, out_extent, false, nnf, nullptr); + const float min_dist = 0.25f * width; + const float max_dist = 0.5f * width; + GeoImage sdf = sdfgen.allocateSDF(256, out_extent); + sdfgen.createDistanceField(nnf, sdf, out_extent.height(), min_dist, max_dist, nullptr); out_elevation = new osg::Image(); - out_elevation->allocateImage( - 256, - 256, - 1, - GL_RED, - GL_UNSIGNED_BYTE); + out_elevation->allocateImage(256, 256, 1, GL_RED, GL_UNSIGNED_BYTE); ImageUtils::PixelReader readSDF(sdf.getImage()); - ImageUtils::PixelWriter writeElevation(out_elevation.get()); ImageUtils::ImageIterator e_iter(writeElevation); osg::Vec4 value; @@ -202,12 +183,7 @@ namespace osgEarth ); out_lifemap = new osg::Image(); - out_lifemap->allocateImage( - 256, - 256, - 1, - GL_RGBA, - GL_UNSIGNED_BYTE); + out_lifemap->allocateImage(256, 256, 1, GL_RGBA, GL_UNSIGNED_BYTE); ImageUtils::PixelWriter writeLifeMap(out_lifemap.get()); ImageUtils::ImageIterator lm_iter(writeLifeMap); @@ -233,39 +209,31 @@ namespace osgEarth class TerrainEditGUI : public BaseGUI { private: - bool _installed; + bool _installed = false; + unsigned _minLevel = 10u; + bool _placingCrater = false; + bool _placingDitch = false; + float _craterRadius = 1.0f; + float _width = 5.0f; + float _height = -3.0f; + float _rugged = 0.75f; + float _dense = 0.0f; + float _lush = 0.0f; + float _lifemapMix = 1.0f; osg::observer_ptr _mapNode; osg::ref_ptr _elevDecal; osg::ref_ptr _lifemapDecal; osg::ref_ptr _landcoverDecal; std::vector _layersToRefresh; - unsigned _minLevel; std::stack _undoStack; - bool _placingCrater; - float _craterRadius; - bool _placingDitch; - float _width; - float _height; - float _rugged; - float _dense; - float _lush; - float _lifemapMix; osg::ref_ptr _craterCursor; osg::ref_ptr _ditchCursor; osg::ref_ptr _ditchFeature; public: - TerrainEditGUI() : BaseGUI("Terrain Editing"), - _installed(false), - _placingCrater(false), - _placingDitch(false), - _width(5.0f), - _height(-3.0f), - _rugged(0.75f), - _dense(0.0f), - _lush(0.0f), - _lifemapMix(1.0f), - _minLevel(10u) { } + TerrainEditGUI() : BaseGUI("Terrain Editing") + { + } void load(const Config& conf) override { @@ -282,6 +250,7 @@ namespace osgEarth if (!_installed) { + // first time through, install the event handlers and add the decal layers auto event_view = view(ri); view(ri)->getViewerBase()->addUpdateOperation(new OneTimer([this, event_view]() { @@ -292,7 +261,7 @@ namespace osgEarth return; } - ImGui::Begin(name(), visible()); + if (ImGui::Begin(name(), visible())) { ImGui::Checkbox("Place a crater", &_placingCrater); if (_placingCrater) @@ -310,34 +279,40 @@ namespace osgEarth ImGuiLTable::SliderFloat("Dense", &_dense, 0.0f, 1.0f); ImGuiLTable::SliderFloat("Lush", &_lush, 0.0f, 1.0f); ImGuiLTable::SliderFloat("Mix", &_lifemapMix, 0.0f, 1.0f); - ImGuiLTable::End(); } + ImGui::Separator(); - if (ImGui::Button("Undo") && _undoStack.size()>0) - { - GeoExtent ex = _elevDecal->getDecalExtent(_undoStack.top()); - _elevDecal->removeDecal(_undoStack.top()); - _lifemapDecal->removeDecal(_undoStack.top()); - _landcoverDecal->removeDecal(_undoStack.top()); - _undoStack.pop(); - _mapNode->getTerrainEngine()->invalidateRegion( - _layersToRefresh, ex, _minLevel, INT_MAX); - } + if (ImGui::Button("Undo") && _undoStack.size() > 0) + undo(); + ImGui::SameLine(); if (ImGui::Button("Clear All")) - { - _elevDecal->clearDecals(); - _lifemapDecal->clearDecals(); - _landcoverDecal->clearDecals(); - _mapNode->getTerrainEngine()->invalidateRegion( - _layersToRefresh, GeoExtent::INVALID, _minLevel, INT_MAX); - } + clear(); if (_placingCrater && _placingDitch) _placingCrater = false; + + ImGui::End(); } - ImGui::End(); + } + + void undo() + { + GeoExtent ex = _elevDecal->getDecalExtent(_undoStack.top()); + _elevDecal->removeDecal(_undoStack.top()); + _lifemapDecal->removeDecal(_undoStack.top()); + _landcoverDecal->removeDecal(_undoStack.top()); + _undoStack.pop(); + _mapNode->getTerrainEngine()->invalidateRegion(_layersToRefresh, ex, _minLevel, INT_MAX); + } + + void clear() + { + _elevDecal->clearDecals(); + _lifemapDecal->clearDecals(); + _landcoverDecal->clearDecals(); + _mapNode->getTerrainEngine()->invalidateRegion(_layersToRefresh, GeoExtent::INVALID, _minLevel, INT_MAX); } void addCrater(const GeoPoint& center) @@ -480,7 +455,7 @@ namespace osgEarth _lifemapMix, elevation, lifemap, - // landcover, + landcover, extent); addDecals(elevation, lifemap, landcover, extent); @@ -518,10 +493,10 @@ namespace osgEarth // lifemap data needs special blending treatment on the A channel // since this contains material override data. _lifemapDecal->setBlendFuncs( - GL_SRC_ALPHA, - GL_ONE_MINUS_SRC_ALPHA, - GL_ZERO, - GL_ONE); + GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, + GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, + GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, + GL_ZERO, GL_ONE); // If there is a LifeMapLayer, append to it as a Post, otherwise standalone decal. LifeMapLayer* lifemap = _mapNode->getMap()->getLayer(); diff --git a/src/osgEarthProcedural/ImGui/VegetationLayerGUI b/src/osgEarthProcedural/ImGui/VegetationLayerGUI index 731cbc3264..32d3bfe8b2 100644 --- a/src/osgEarthProcedural/ImGui/VegetationLayerGUI +++ b/src/osgEarthProcedural/ImGui/VegetationLayerGUI @@ -219,14 +219,14 @@ namespace osgEarth ImGui::Checkbox("Show biome under mouse", &_showBiomeUnderMouse); if (_showBiomeUnderMouse) { - if (_biomeUnderMouse.isWorking()) + if (_biomeUnderMouse.working()) { ImGui::Text("searching..."); ImGui::Text(""); } - else if (_biomeUnderMouse.isAvailable()) + else if (_biomeUnderMouse.available()) { - const Biome* biome = _biomeUnderMouse.get(); + const Biome* biome = _biomeUnderMouse.value(); if (biome) { ImGui::Text("%s", biome->name()->c_str()); diff --git a/src/osgEarthProcedural/LifeMapLayer.cpp b/src/osgEarthProcedural/LifeMapLayer.cpp index 25cb6142ea..c0ac297c46 100644 --- a/src/osgEarthProcedural/LifeMapLayer.cpp +++ b/src/osgEarthProcedural/LifeMapLayer.cpp @@ -34,7 +34,7 @@ #include -#define LC "[LifeMapLayer] " << getName() << ": " +#define LC "[" << className() << "] \"" << getName() << "\" " using namespace osgEarth; using namespace osgEarth::Procedural; @@ -97,11 +97,11 @@ namespace class CoordScaler { public: - CoordScaler(const Profile* profile, unsigned int lod, unsigned int refLOD): + CoordScaler(const Profile* profile, unsigned int lod, unsigned int refLOD) : _profile(profile), _lod(lod), _refLOD(refLOD) - { + { _profile->getNumTiles(lod, _tilesX, _tilesY); _dL = (double)(lod - refLOD); @@ -178,7 +178,7 @@ LifeMapLayer::init() Status LifeMapLayer::openImplementation() { - Status parent = ImageLayer::openImplementation(); + Status parent = super::openImplementation(); if (parent.isError()) return parent; @@ -196,7 +196,7 @@ LifeMapLayer::openImplementation() Status LifeMapLayer::closeImplementation() { - return ImageLayer::closeImplementation(); + return super::closeImplementation(); } void @@ -219,7 +219,7 @@ LifeMapLayer::checkForLayerError(Layer* layer) void LifeMapLayer::addedToMap(const Map* map) { - ImageLayer::addedToMap(map); + super::addedToMap(map); options().biomeLayer().addedToMap(map); options().maskLayer().addedToMap(map); @@ -263,7 +263,7 @@ LifeMapLayer::removedFromMap(const Map* map) options().waterLayer().removedFromMap(map); options().colorLayer().removedFromMap(map); options().landCoverLayer().removedFromMap(map); - ImageLayer::removedFromMap(map); + super::removedFromMap(map); } void @@ -424,7 +424,7 @@ LifeMapLayer::createImageImplementation( ep->getTile(key, true, elevTile, &_workingSet, progress); // ensure we have a normal map for slopes and curvatures: - if (elevTile.valid()) + if (elevTile.valid() && getTerrainWeight() > 0.0f) { elevTile->generateNormalMap(map.get(), &_workingSet, progress); } diff --git a/src/osgEarthProcedural/NoiseTextureFactory.cpp b/src/osgEarthProcedural/NoiseTextureFactory.cpp index c4691b2a41..69ee0ca6e2 100644 --- a/src/osgEarthProcedural/NoiseTextureFactory.cpp +++ b/src/osgEarthProcedural/NoiseTextureFactory.cpp @@ -75,8 +75,9 @@ NoiseTextureFactory::createImage(unsigned dim, unsigned chans) const const float L[4] = { 2.2f, 1.0f, 1.0f, 4.0f }; // seed = 0 so it is deterministic - std::default_random_engine gen(0); - std::uniform_real_distribution rand_float(0.0f, 1.0f); + Random prng(0); + //std::default_random_engine gen(0); + //std::uniform_real_distribution rand_float(0.0f, 1.0f); for(unsigned k=0; k _rasterizer; }; -} } // namespace osgEarth::Splat +} } // namespace osgEarth::Procedural OSGEARTH_SPECIALIZE_CONFIG(osgEarth::Procedural::RoadSurfaceLayer::Options); diff --git a/src/osgEarthProcedural/RoadSurfaceLayer.cpp b/src/osgEarthProcedural/RoadSurfaceLayer.cpp index fce8eb507c..6bfc3ab8d4 100644 --- a/src/osgEarthProcedural/RoadSurfaceLayer.cpp +++ b/src/osgEarthProcedural/RoadSurfaceLayer.cpp @@ -402,6 +402,22 @@ RoadSurfaceLayer::createImageImplementation(const TileKey& key, ProgressCallback if (group && group->getBound().valid()) { + // Make sure there's actually geometry to render in the output extent + // since rasterization is expensive! + osg::Polytope polytope; + outputExtent.createPolytope(polytope); + + osg::ref_ptr intersector = new osgUtil::PolytopeIntersector(polytope); + osgUtil::IntersectionVisitor visitor(intersector); + group->accept(visitor); + + if (intersector->getIntersections().empty()) + { + OE_DEBUG << LC << "RSL: skipped an EMPTY bounds without rasterizing :) for " << key.str() << std::endl; + return GeoImage::INVALID; + } + + OE_PROFILING_ZONE_NAMED("Rasterize"); group->setName(key.str()); @@ -420,11 +436,17 @@ RoadSurfaceLayer::createImageImplementation(const TileKey& key, ProgressCallback osg::ref_ptr image = result.join(local_progress.get()); // Empty image means the texture did not render anything - if (image.valid() - && image->data() != nullptr && - !ImageUtils::isEmptyImage(image.get())) + if (image.valid() && image->data() != nullptr) { - return GeoImage(image.get(), key.getExtent()); + if (!ImageUtils::isEmptyImage(image.get())) + { + return GeoImage(image.get(), key.getExtent()); + } + else + { + OE_DEBUG << LC << "RSL: skipped an EMPTY image result for " << key.str() << std::endl; + return GeoImage::INVALID; + } } else { diff --git a/src/osgEarthProcedural/TextureSplattingLayer.cpp b/src/osgEarthProcedural/TextureSplattingLayer.cpp index cabd14d24e..8db7dfb939 100644 --- a/src/osgEarthProcedural/TextureSplattingLayer.cpp +++ b/src/osgEarthProcedural/TextureSplattingLayer.cpp @@ -278,7 +278,7 @@ void TextureSplattingLayer::update(osg::NodeVisitor& nv) { // once the materials are loaded, install them and build the state set. - if (_materials == nullptr && _materialsJob.isAvailable()) + if (_materials == nullptr && _materialsJob.available()) { _materials = _materialsJob.release(); buildStateSets(); diff --git a/src/osgEarthProcedural/VegetationLayer b/src/osgEarthProcedural/VegetationLayer index fd6b78ab39..af121c6f71 100644 --- a/src/osgEarthProcedural/VegetationLayer +++ b/src/osgEarthProcedural/VegetationLayer @@ -96,6 +96,9 @@ namespace osgEarth { namespace Procedural //! OSG render bin number for vegetation render bins OE_OPTION(int, renderBinNumber); + //! Number of threads to use for background loading + OE_OPTION(unsigned, threads); + struct OSGEARTHPROCEDURAL_EXPORT Group { //! Whether to render this group at all diff --git a/src/osgEarthProcedural/VegetationLayer.cpp b/src/osgEarthProcedural/VegetationLayer.cpp index 7eddbc693c..7b10160057 100644 --- a/src/osgEarthProcedural/VegetationLayer.cpp +++ b/src/osgEarthProcedural/VegetationLayer.cpp @@ -55,6 +55,8 @@ #define LC "[VegetationLayer] " << getName() << ": " +#define JOB_ARENA_VEGETATION "oe.vegetation" + #define OE_DEVEL OE_DEBUG #ifndef GL_MULTISAMPLE @@ -94,6 +96,7 @@ VegetationLayer::Options::getConfig() const conf.set("use_impostor_pbr_maps", useImpostorPBRMaps()); conf.set("max_texture_size", maxTextureSize()); conf.set("render_bin_number", renderBinNumber()); + conf.set("threads", threads()); Config layers("layers"); for (auto group_name : { GROUP_TREES, GROUP_BUSHES, GROUP_UNDERGROWTH }) @@ -170,6 +173,7 @@ VegetationLayer::Options::fromConfig(const Config& conf) useRGCompressedNormalMaps().setDefault(true); maxTextureSize().setDefault(INT_MAX); renderBinNumber().setDefault(3); + threads().setDefault(2u); biomeLayer().get(conf, "biomes_layer"); @@ -183,6 +187,7 @@ VegetationLayer::Options::fromConfig(const Config& conf) conf.get("use_impostor_pbr_maps", useImpostorPBRMaps()); conf.get("max_texture_size", maxTextureSize()); conf.get("render_bin_number", renderBinNumber()); + conf.get("threads", threads()); // some nice default group settings groups()[GROUP_TREES].lod().setDefault(14); @@ -387,7 +392,7 @@ VegetationLayer::update(osg::NodeVisitor& nv) checkForNewAssets(); - if (_newAssets.isAvailable()) + if (_newAssets.available()) { ScopedMutexLock lock(_assets); _assets = std::move(_newAssets.release()); @@ -824,6 +829,9 @@ VegetationLayer::prepareForRendering(TerrainEngine* engine) setImpostorHighAngle(options().impostorHighAngle().get()); setLODTransitionPadding(options().lodTransitionPadding().get()); setUseImpostorNormalMaps(options().useImpostorNormalMaps().get()); + + // configure the thread pool + JobArena::setConcurrency(JOB_ARENA_VEGETATION, options().threads().get()); } namespace @@ -1115,6 +1123,7 @@ VegetationLayer::checkForNewAssets() const Job job; job.setName("VegetationLayer asset loader"); + job.setArena(JOB_ARENA_VEGETATION); _newAssets = job.dispatch(loadNewAssets); return true; @@ -1180,6 +1189,7 @@ VegetationLayer::createDrawableAsync( Job job; job.setName("Vegetation create drawable"); + job.setArena(JOB_ARENA_VEGETATION); job.setPriority(-range); // closer is sooner return job.dispatch>(function); } @@ -1267,7 +1277,7 @@ VegetationLayer::getAssetPlacements( // Prepare to deal with holes in the terrain, where we do not want // to place vegetation TerrainConstraintQuery query; - map->getLayers(query.layers, [](const TerrainConstraintLayer* layer) + map->getLayers(query.layers, [](const auto* layer) { auto clayer = static_cast(layer); return clayer->getRemoveInterior() == true; @@ -1318,8 +1328,7 @@ VegetationLayer::getAssetPlacements( using Index = RTree; Index index; - std::minstd_rand0 gen(key.hash()); - std::uniform_real_distribution rand_float_01(0.0f, 1.0f); + std::default_random_engine gen(key.hash()); Random prng(0); // approximate area of the tile in km @@ -1369,13 +1378,33 @@ VegetationLayer::getAssetPlacements( // cumulative density function based on asset weights std::vector assetCDF; + + //TEMP - DEBUGGING DETERMINISTIC BEHAVIOR. + bool debug = false; // key.is(14, 17117, 4120); + if (debug) { + OE_INFO << LC << "---" << std::endl; + OE_INFO << LC << "Attempting to place " << max_instances << std::endl; + } + + // normal distribution for lushness + std::normal_distribution normal_dist(0.0f, 1.0f / 6.0f); + // Generate random instances within the tile: for (unsigned i = 0; i < max_instances; ++i) { + // perform all random number generations first to preserve determinism + // in the even of an early loop break. + // random tile-normalized position: float u = RAND(); float v = RAND(); + float asset_index_rand = RAND(); + float rotation_rand = RAND(); + float normal_rand = RAND(); + float lush_offset = normal_dist(gen); + + // resolve the biome at this position: const Biome* biome = nullptr; if (biomemap.valid()) @@ -1386,7 +1415,10 @@ VegetationLayer::getAssetPlacements( int index = (int)biomemap_value.r(); biome = catalog->getBiomeByIndex(index); if (!biome) + { + if (debug) OE_INFO << LC << "Instance " << i << " has invalid biome index " << index << std::endl; continue; + } } if (biome == nullptr) @@ -1400,6 +1432,7 @@ VegetationLayer::getAssetPlacements( if (iter == groupAssets.end()) { empty_biomes.insert(biome); + if (debug) OE_INFO << LC << "Instance " << i << " has no assets for biome " << biome->id() << std::endl; continue; } ResidentBiomeModelAssetInstances& biome_assets = iter->second; @@ -1418,14 +1451,13 @@ VegetationLayer::getAssetPlacements( density = lifemap_value[LIFEMAP_DENSE]; lush = lifemap_value[LIFEMAP_LUSH]; } - //if (density < 0.01f) - // continue; auto& assetInstances = biome_assets.instances; // RNG with normal distribution between approx lush-1..lush+1 - std::normal_distribution normal_dist(lush, 1.0f / 6.0f); - lush = clamp(normal_dist(gen), 0.0f, 1.0f); + // Note: moved this earlier in the loop to make it deterministic + //std::normal_distribution normal_dist(lush, 1.0f / 6.0f); + lush = clamp(lush + lush_offset, 0.0f, 1.0f); assetIndices.clear(); assetCDF.clear(); @@ -1446,13 +1478,14 @@ VegetationLayer::getAssetPlacements( // if there are no assets that match the lushness criteria, move on. if (assetIndices.empty()) { + if (debug) OE_INFO << LC << "Instance " << i << " has no assets for lushness " << lush << std::endl; continue; } int assetIndex = 0; if (assetIndices.size() > 1) { - float k = RAND() * cumulativeWeight; + float k = asset_index_rand * cumulativeWeight; for (assetIndex = 0; assetIndex < assetCDF.size() - 1 && k > assetCDF[assetIndex]; ++assetIndex); @@ -1463,6 +1496,7 @@ VegetationLayer::getAssetPlacements( // if there's no geometry... bye if (asset->chonk() == nullptr) { + if (debug) OE_INFO << LC << "Instance " << i << " has no geometry" << std::endl; continue; } @@ -1478,12 +1512,16 @@ VegetationLayer::getAssetPlacements( // apply instance-specific density adjustment: density *= instance.coverage(); +#if 0 + // Removed, because this is causing the placement to go non-deterministic + // for some reason that I have not yet identified. const float edge_threshold = 0.10f; if (scaleWithDensity && density < edge_threshold) { float edginess = (density / edge_threshold); scale *= edginess; } +#endif // tile-local coordinates of the position: osg::Vec2d local( @@ -1500,12 +1538,12 @@ VegetationLayer::getAssetPlacements( pass = false; // scale the asset bounding box in preparation for collision: - const osg::BoundingBox& aabb = asset->boundingBox(); + const auto& aabb = asset->boundingBox(); double so = (1.0 - overlap); - double a_min[2] = { local.x() + aabb.xMin() * scale.x() * so, local.y() + aabb.yMin() * scale.x() * so }; - double a_max[2] = { local.x() + aabb.xMax() * scale.y() * so, local.y() + aabb.yMax() * scale.y() * so }; - + double a_min[2] = { local.x() + aabb.xMin() * scale.x() * so, local.y() + aabb.yMin() * scale.y() * so }; + double a_max[2] = { local.x() + aabb.xMax() * scale.x() * so, local.y() + aabb.yMax() * scale.y() * so }; + if (index.Search(a_min, a_max) == 0) { index.Insert(a_min, a_max, 0); @@ -1517,10 +1555,6 @@ VegetationLayer::getAssetPlacements( { osg::Vec3d map_point(e.xMin() + u * e.width(), e.yMin() + v * e.height(), 0); - // Generate a random rotation and record the position. - // Do this before the constraint check to maintain determinism! - float rotation = RAND() * 3.1415927 * 2.0; - if (!inConstrainedRegion(map_point.x(), map_point.y(), constraints)) { map_points.emplace_back(map_point); @@ -1529,13 +1563,17 @@ VegetationLayer::getAssetPlacements( p.localPoint() = local; p.uv().set(u, v); p.scale() = scale; - p.rotation() = rotation; + p.rotation() = rotation_rand * 3.1415927 * 2.0; p.asset() = asset; p.density() = density; p.biome = biome; result.emplace_back(std::move(p)); } + else + { + if (debug) OE_INFO << LC << "Instance " << i << " is in a constrained region" << std::endl; + } } } @@ -1543,21 +1581,31 @@ VegetationLayer::getAssetPlacements( // threshold. We have to do this after the fact so that // lifemap changes don't change existing assets (due to the // collision rtree). - int numResults = result.size(); - for (int i = 0; i < numResults; ++i) + if (debug) OE_INFO << LC << (max_instances - result.size()) << " instances removed due to overlap" << std::endl; + + std::vector result_culled; + result_culled.reserve(result.size()); + + std::vector map_points_culled; + map_points_culled.reserve(result.size()); + + for (int i = 0; i < result.size(); ++i) { Placement& p = result[i]; - if (RAND() > p.density()) + if (RAND() <= p.density()) { - result[i] = std::move(result[numResults - 1]); - map_points[i] = std::move(map_points[numResults - 1]); - --numResults; - --i; + result_culled.emplace_back(std::move(p)); + map_points_culled.emplace_back(std::move(map_points[i])); } } - result.resize(numResults); - map_points.resize(numResults); + + if (debug) OE_INFO << LC << (result.size()-result_culled.size()) << " instances removed due to density" << std::endl; + + std::swap(result, result_culled); + std::swap(map_points, map_points_culled); + + if (debug) OE_INFO << LC << "Final instance count = " << result.size() << std::endl; // clamp everything to the terrain map->getElevationPool()->sampleMapCoords( @@ -1949,7 +1997,7 @@ VegetationLayer::cull(const TileBatch& batch, osg::NodeVisitor& nv) const if (view._placeholder) { - auto phcd = asChonkDrawable(view._placeholder->_drawable.get()); + auto phcd = asChonkDrawable(view._placeholder->_drawable.value()); if (phcd) birthday = phcd->getBirthday(); } @@ -1974,9 +2022,9 @@ VegetationLayer::cull(const TileBatch& batch, osg::NodeVisitor& nv) const } // if the data is ready, cull it: - if (view._tile->_drawable.isAvailable()) + if (view._tile->_drawable.available()) { - auto drawable = view._tile->_drawable.get(); + auto drawable = view._tile->_drawable.value(); if (drawable.valid()) { @@ -2020,7 +2068,7 @@ VegetationLayer::cull(const TileBatch& batch, osg::NodeVisitor& nv) const // If the job exists but was canceled for some reason, // Reset this view so it will try again later. - else if (view._tile->_drawable.isAbandoned()) + else if (view._tile->_drawable.empty()) { view._tile = nullptr; } @@ -2032,7 +2080,7 @@ VegetationLayer::cull(const TileBatch& batch, osg::NodeVisitor& nv) const // push the matrix and accept the placeholder. view._matrix->set(entry->getModelViewMatrix()); cv->pushModelViewMatrix(view._matrix.get(), osg::Transform::ABSOLUTE_RF); - view._placeholder->_drawable.get()->accept(nv); + view._placeholder->_drawable.value()->accept(nv); cv->popModelViewMatrix(); } @@ -2068,7 +2116,7 @@ VegetationLayer::resizeGLObjectBuffers(unsigned maxSize) for (auto& tile : _tiles) { - auto drawable = tile.second->_drawable.get(); + auto drawable = tile.second->_drawable.value(); if (drawable.valid()) drawable->resizeGLObjectBuffers(maxSize); } diff --git a/src/osgEarthSplat/RoadSurfaceLayer b/src/osgEarthSplat/RoadSurfaceLayer index cfaec95a03..a371d5f353 100644 --- a/src/osgEarthSplat/RoadSurfaceLayer +++ b/src/osgEarthSplat/RoadSurfaceLayer @@ -41,6 +41,7 @@ namespace osgEarth { namespace Splat public: META_LayerOptions(osgEarth, Options, ImageLayer::Options); OE_OPTION_LAYER(FeatureSource, featureSource); + OE_OPTION_VECTOR(ConfigOptions, filters); OE_OPTION_LAYER(StyleSheet, styleSheet); OE_OPTION(Distance, featureBufferWidth); virtual Config getConfig() const; @@ -49,17 +50,16 @@ namespace osgEarth { namespace Splat }; public: - META_Layer(osgEarthSplat, RoadSurfaceLayer, Options, osgEarth::ImageLayer, RoadSurface); + META_Layer(osgEarth, RoadSurfaceLayer, Options, osgEarth::ImageLayer, RoadSurface); - //! Sets the map layer from which to pull feature data. Call either - //! This or setFeatureSource + //! Sets the map layer from which to pull feature data void setFeatureSource(FeatureSource* layer); FeatureSource* getFeatureSource() const; - + //! Buffer around the road vector for querying linear data (should be at least road width/2) void setFeatureBufferWidth(const Distance& value); const Distance& getFeatureBufferWidth() const; - + //! Style for rendering the road void setStyleSheet(StyleSheet* value); StyleSheet* getStyleSheet() const; @@ -98,6 +98,7 @@ namespace osgEarth { namespace Splat mutable Gate _keygate; using FeatureListCache = LRUCache; mutable std::unique_ptr _lru; + osg::ref_ptr _filterChain; void getFeatures( FeatureSource* featureSource, diff --git a/src/osgEarthSplat/RoadSurfaceLayer.cpp b/src/osgEarthSplat/RoadSurfaceLayer.cpp index 002b64262b..6173a3abae 100644 --- a/src/osgEarthSplat/RoadSurfaceLayer.cpp +++ b/src/osgEarthSplat/RoadSurfaceLayer.cpp @@ -44,6 +44,14 @@ RoadSurfaceLayer::Options::getConfig() const featureSource().set(conf, "features"); styleSheet().set(conf, "styles"); conf.set("buffer_width", featureBufferWidth()); + + if (filters().empty() == false) + { + Config temp; + for (unsigned i = 0; i < filters().size(); ++i) + temp.add(filters()[i].getConfig()); + conf.set("filters", temp); + } return conf; } @@ -53,6 +61,10 @@ RoadSurfaceLayer::Options::fromConfig(const Config& conf) featureSource().get(conf, "features"); styleSheet().get(conf, "styles"); conf.get("buffer_width", featureBufferWidth()); + + const Config& filtersConf = conf.child("filters"); + for (ConfigSet::const_iterator i = filtersConf.children().begin(); i != filtersConf.children().end(); ++i) + filters().push_back(ConfigOptions(*i)); } //........................................................................ @@ -72,6 +84,8 @@ RoadSurfaceLayer::init() { ImageLayer::init(); + _keygate.setName("RoadSurfaceLayer " + getName()); + // Generate Mercator tiles by default. setProfile(Profile::create(Profile::GLOBAL_GEODETIC)); @@ -105,6 +119,10 @@ RoadSurfaceLayer::openImplementation() getTileSize()); } + _filterChain = FeatureFilterChain::create( + options().filters(), + getReadOptions()); + return Status::NoError; } @@ -205,7 +223,7 @@ namespace } } - void sortFeaturesIntoStyleGroups(StyleSheet* styles, FeatureList& features, FilterContext &context, StyleToFeatures& map) + void sortFeaturesIntoStyleGroups(StyleSheet* styles, FeatureList& features, FilterContext& context, StyleToFeatures& map) { if (styles == nullptr) return; @@ -358,7 +376,7 @@ RoadSurfaceLayer::createImageImplementation(const TileKey& key, ProgressCallback // Set the LTP as our output SRS. // The geometry compiler will transform all our features into the // LTP so we can render using an orthographic camera (TileRasterizer) - FilterContext fc(session.get(), featureProfile, featureExtent); + FilterContext fc(session.get(), featureProfile.get(), featureExtent); fc.setOutputSRS(outputExtent.getSRS()); // compile the features into a node. @@ -383,6 +401,22 @@ RoadSurfaceLayer::createImageImplementation(const TileKey& key, ProgressCallback if (group && group->getBound().valid()) { + // Make sure there's actually geometry to render in the output extent + // since rasterization is expensive! + osg::Polytope polytope; + outputExtent.createPolytope(polytope); + + osg::ref_ptr intersector = new osgUtil::PolytopeIntersector(polytope); + osgUtil::IntersectionVisitor visitor(intersector); + group->accept(visitor); + + if (intersector->getIntersections().empty()) + { + OE_DEBUG << LC << "RSL: skipped an EMPTY bounds without rasterizing :) for " << key.str() << std::endl; + return GeoImage::INVALID; + } + + OE_PROFILING_ZONE_NAMED("Rasterize"); group->setName(key.str()); @@ -397,12 +431,26 @@ RoadSurfaceLayer::createImageImplementation(const TileKey& key, ProgressCallback ); // Immediately blocks on the result. - const osg::ref_ptr& image = result.join(local_progress); + // That is OK - we are hopefully in a loading thread. + osg::ref_ptr image = result.join(local_progress.get()); + // Empty image means the texture did not render anything if (image.valid() && image->data() != nullptr) - return GeoImage(image.get(), key.getExtent()); + { + if (!ImageUtils::isEmptyImage(image.get())) + { + return GeoImage(image.get(), key.getExtent()); + } + else + { + OE_DEBUG << LC << "RSL: skipped an EMPTY image result for " << key.str() << std::endl; + return GeoImage::INVALID; + } + } else + { return GeoImage::INVALID; + } } } @@ -450,11 +498,13 @@ RoadSurfaceLayer::getFeatures( } else { - cursor = fs->createFeatureCursor(subkey, progress); + cursor = fs->createFeatureCursor(subkey, _filterChain.get(), nullptr, progress); if (cursor.valid()) { - cursor->fill(sublist); - //TODO: run script filter(s) on output + cursor->fill( + sublist, + [](const Feature* f) { return f->getGeometry()->isLinear(); }); + _lru->insert(subkey, sublist); } } @@ -462,10 +512,7 @@ RoadSurfaceLayer::getFeatures( // Clone features onto the end of the output list. // We must always clone since osgEarth modifies the feature data - std::transform( - sublist.begin(), - sublist.end(), - std::back_inserter(output), - [](osg::ref_ptr< Feature > f) { return osg::clone(f.get(), osg::CopyOp::DEEP_COPY_ALL); }); + for (auto& f : sublist) + output.push_back(osg::clone(f.get(), osg::CopyOp::DEEP_COPY_ALL)); } } \ No newline at end of file diff --git a/src/osgEarthSplat/SplatCatalog.cpp b/src/osgEarthSplat/SplatCatalog.cpp index b421483c61..2d10264b29 100644 --- a/src/osgEarthSplat/SplatCatalog.cpp +++ b/src/osgEarthSplat/SplatCatalog.cpp @@ -203,7 +203,7 @@ namespace { // if this is the first image loaded, remember it so we can ensure that // all images are copatible. - if ( firstImage == 0L ) + if ( firstImage == nullptr) { firstImage = result.getImage(); } @@ -215,7 +215,7 @@ namespace { osg::ref_ptr conv = ImageUtils::convert(result.getImage(), firstImage->getPixelFormat(), firstImage->getDataType()); - if ( conv->s() != firstImage->s() || conv->t() != firstImage->t() ) + if (conv.valid() && (conv->s() != firstImage->s() || conv->t() != firstImage->t())) { osg::ref_ptr conv2; if ( ImageUtils::resizeImage(conv.get(), firstImage->s(), firstImage->t(), conv2) ) @@ -224,17 +224,18 @@ namespace } } - if ( ImageUtils::textureArrayCompatible(conv.get(), firstImage) ) + if (conv.valid() && ImageUtils::textureArrayCompatible(conv.get(), firstImage)) { conv->setInternalTextureFormat( firstImage->getInternalTextureFormat() ); return conv.release(); } + else { OE_WARN << LC << "Image " << uri.base() << " was found, but cannot be used because it is not compatible with " << "other splat images (same dimensions, pixel format, etc.)\n"; - return 0L; + return nullptr; } } } diff --git a/tests/feature_draped_polygons.earth b/tests/feature_draped_polygons.earth index f0a9dca593..6b2c895aa0 100644 --- a/tests/feature_draped_polygons.earth +++ b/tests/feature_draped_polygons.earth @@ -7,22 +7,18 @@ on the map using the overlay technique. It also shows the "selector" technique of using SQL queries to selectively style the feature data. --> - + false ../data/world.tif - - - - + + + + + + + + + 14045470 and POP <= 43410900 ]]> + + + + + 43410900 and POP <= 97228750 ]]> + + + + + 97228750 and POP <= 258833000 ]]> + + + + + 258833000 ]]> + + + + + + ../data/world.shp + + + + + + diff --git a/tests/osgearth_server/README.md b/tests/osgearth_server/README.md deleted file mode 100644 index 08ce966ff1..0000000000 --- a/tests/osgearth_server/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Static Viewer to test osgearth_server -====================================== - -Simple leaflet map used to test osgearth_server. Run osgearth_server with an earth file and load this webpage index.html to test it. \ No newline at end of file diff --git a/tests/osgearth_server/css/l.geosearch.css b/tests/osgearth_server/css/l.geosearch.css deleted file mode 100644 index 5e62199da2..0000000000 --- a/tests/osgearth_server/css/l.geosearch.css +++ /dev/null @@ -1,65 +0,0 @@ -.displayNone { - display: none; -} - -.leaflet-control-geosearch { - position: relative; -} - -.leaflet-control-geosearch a { - -webkit-border-radius: 4px; - border-radius: 4px; - border-bottom: none; -} - -.leaflet-control-geosearch a.glass { - background-image: url(../images/geosearch.png); - background-size: 100% 100%; -} - -.leaflet-control-geosearch a.spinner { - background-image: url(../images/spinner.gif); - background-position: 50% 50%; -} - -.leaflet-control-geosearch a.alert { - background-image: url(../images/alert.png); - background-size: 64% 64%; -} - -.leaflet-control-geosearch a:hover { - border-bottom: none; -} - -.leaflet-control-geosearch form { - position: absolute; - top: 0; - left: 22px; - box-shadow: 0 1px 7px rgba(0, 0, 0, 0.65); - -webkit-border-radius: 4px; - border-radius: 0px 4px 4px 0px; - z-index: -1; - background: #FFF; - height: 26px; - padding: 0 6px 0 6px; -} - -.leaflet-control-geosearch form input { - width: 200px; - border: none; - outline: none; - margin: 0; - padding: 0; - font-size: 12px; - margin-top: 5px; -} - -.leaflet-control-geosearch .message { - position: absolute; - top: 26px; - left: 0px; - width: 226px; - color: #FFF; - background: rgb(40, 40, 40); - padding: 4px 0 4px 8px; -} diff --git a/tests/osgearth_server/images/Thumbs.db b/tests/osgearth_server/images/Thumbs.db deleted file mode 100644 index a46c51e7c1..0000000000 Binary files a/tests/osgearth_server/images/Thumbs.db and /dev/null differ diff --git a/tests/osgearth_server/images/alert.png b/tests/osgearth_server/images/alert.png deleted file mode 100644 index d5a20ce8bf..0000000000 Binary files a/tests/osgearth_server/images/alert.png and /dev/null differ diff --git a/tests/osgearth_server/images/geosearch.png b/tests/osgearth_server/images/geosearch.png deleted file mode 100644 index d80bbdc1bc..0000000000 Binary files a/tests/osgearth_server/images/geosearch.png and /dev/null differ diff --git a/tests/osgearth_server/images/layers-2x.png b/tests/osgearth_server/images/layers-2x.png deleted file mode 100644 index a2cf7f9efe..0000000000 Binary files a/tests/osgearth_server/images/layers-2x.png and /dev/null differ diff --git a/tests/osgearth_server/images/layers.png b/tests/osgearth_server/images/layers.png deleted file mode 100644 index bca0a0e429..0000000000 Binary files a/tests/osgearth_server/images/layers.png and /dev/null differ diff --git a/tests/osgearth_server/images/marker-icon-2x.png b/tests/osgearth_server/images/marker-icon-2x.png deleted file mode 100644 index 0015b6495f..0000000000 Binary files a/tests/osgearth_server/images/marker-icon-2x.png and /dev/null differ diff --git a/tests/osgearth_server/images/marker-icon.png b/tests/osgearth_server/images/marker-icon.png deleted file mode 100644 index e2e9f757f5..0000000000 Binary files a/tests/osgearth_server/images/marker-icon.png and /dev/null differ diff --git a/tests/osgearth_server/images/marker-shadow.png b/tests/osgearth_server/images/marker-shadow.png deleted file mode 100644 index d1e773c715..0000000000 Binary files a/tests/osgearth_server/images/marker-shadow.png and /dev/null differ diff --git a/tests/osgearth_server/images/spinner.gif b/tests/osgearth_server/images/spinner.gif deleted file mode 100644 index aa70283add..0000000000 Binary files a/tests/osgearth_server/images/spinner.gif and /dev/null differ diff --git a/tests/osgearth_server/images/transparent.png b/tests/osgearth_server/images/transparent.png deleted file mode 100644 index 19b91065e8..0000000000 Binary files a/tests/osgearth_server/images/transparent.png and /dev/null differ diff --git a/tests/osgearth_server/index.html b/tests/osgearth_server/index.html deleted file mode 100644 index b4dd4c5617..0000000000 --- a/tests/osgearth_server/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - -
- - - diff --git a/tests/osgearth_server/js/l.control.geosearch.js b/tests/osgearth_server/js/l.control.geosearch.js deleted file mode 100644 index 28ca9f9b17..0000000000 --- a/tests/osgearth_server/js/l.control.geosearch.js +++ /dev/null @@ -1,204 +0,0 @@ -/* - * L.Control.GeoSearch - search for an address and zoom to its location - * https://github.com/smeijer/leaflet.control.geosearch - */ - -L.GeoSearch = {}; -L.GeoSearch.Provider = {}; - -L.GeoSearch.Result = function (x, y, label) { - this.X = x; - this.Y = y; - this.Label = label; -}; - -L.Control.GeoSearch = L.Control.extend({ - options: { - position: 'topleft' - }, - - initialize: function (options) { - this._config = {}; - L.Util.extend(this.options, options); - this.setConfig(options); - }, - - setConfig: function (options) { - this._config = { - 'provider': options.provider, - 'searchLabel': options.searchLabel || 'Enter address', - 'notFoundMessage' : options.notFoundMessage || 'Sorry, that address could not be found.', - 'zoomLevel': options.zoomLevel || 17, - 'showMarker': typeof options.showMarker !== 'undefined' ? options.showMarker : true - }; - }, - - resetLink: function(extraClass) { - var link = this._container.querySelector('a'); - link.className = 'leaflet-bar-part leaflet-bar-part-single' + ' ' + extraClass; - }, - - onAdd: function (map) { - - // create the container - this._container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-geosearch'); - - // create the link - this will contain one of the icons - var link = L.DomUtil.create('a', '', this._container); - link.href = '#'; - link.title = this._config.searchLabel; - - // set the link's icon to magnifying glass - this.resetLink('glass'); - - var displayNoneClass = 'displayNone'; - - // create the form that will contain the input - var form = L.DomUtil.create('form', displayNoneClass, this._container); - - // create the input, and set its placeholder ("Enter address") text - var input = L.DomUtil.create('input', null, form); - input.placeholder = 'Enter address'; - - // create the error message div - var message = L.DomUtil.create('div', 'leaflet-bar message displayNone', this._container); - - L.DomEvent - .on(link, 'click', L.DomEvent.stopPropagation) - .on(link, 'click', L.DomEvent.preventDefault) - .on(link, 'click', function() { - - if (L.DomUtil.hasClass(form, displayNoneClass)) { - L.DomUtil.removeClass(form, 'displayNone'); // unhide form - input.focus(); - } else { - L.DomUtil.addClass(form, 'displayNone'); // hide form - } - - }) - .on(link, 'dblclick', L.DomEvent.stopPropagation); - - L.DomEvent - .on(input, 'keypress', this.onKeyPress, this) - .on(input, 'keyup', this.onKeyUp, this) - .on(input, 'input', this.onInput, this); - - return this._container; - }, - - geosearch: function (qry) { - try { - var provider = this._config.provider; - - if(typeof provider.GetLocations == 'function') { - var results = provider.GetLocations(qry, this._map, function(err, results) { - if (err) { - return this._printError(err); - } - - this._processResults(results); - }.bind(this)); - } - else { - var url = provider.GetServiceUrl(qry); - - $.getJSON(url, function (data) { - try { - var results = provider.ParseJSON(data); - this._processResults(results); - } - catch (error) { - this._printError(error); - } - }.bind(this)); - } - } - catch (error) { - this._printError(error); - } - }, - - _processResults: function(results) { - if (results.length === 0) - throw this._config.notFoundMessage; - - this.cancelSearch(); - this._showLocation(results[0]); - }, - - _showLocation: function (location) { - if (this._config.showMarker) { - if (typeof this._positionMarker === 'undefined') - this._positionMarker = L.marker([location.Y, location.X]).addTo(this._map); - else - this._positionMarker.setLatLng([location.Y, location.X]); - } - - // this._map.setView([location.Y, location.X], this._config.zoomLevel, false); - }, - - _isShowingError: false, - - _printError: function(error) { - var message = this._container.querySelector('.message'); - message.innerHTML = error; - L.DomUtil.removeClass(message, 'displayNone'); - - // show alert icon - this.resetLink('alert'); - - this._isShowingError = true; - }, - - cancelSearch: function() { - var form = this._container.querySelector('form'); - L.DomUtil.addClass(form, 'displayNone'); // hide form - - var input = form.querySelector('input'); - input.value = ''; // clear form - - // show glass icon - this.resetLink('glass'); - - var message = this._container.querySelector('.message'); - L.DomUtil.addClass(message, 'displayNone'); // hide message - }, - - startSearch: function() { - // show spinner icon - this.resetLink('spinner'); - - var input = this._container.querySelector('input'); - this.geosearch(input.value); - }, - - onInput: function() { - if (this._isShowingError) { - // show glass icon - this.resetLink('glass'); - - var message = this._container.querySelector('.message'); - L.DomUtil.addClass(message, 'displayNone'); // hide message - - this._isShowingError = false; - } - }, - - onKeyPress: function (e) { - var enterKey = 13; - - if (e.keyCode === enterKey) { - L.DomEvent.preventDefault(e); // prevent default form submission - - this.startSearch(); - } - }, - - onKeyUp: function (e) { - var escapeKey = 27; - - if (e.keyCode === escapeKey) { - this.cancelSearch(); - } - } -}); diff --git a/tests/osgearth_server/js/l.geosearch.provider.nominatim.js b/tests/osgearth_server/js/l.geosearch.provider.nominatim.js deleted file mode 100644 index a60a9b647f..0000000000 --- a/tests/osgearth_server/js/l.geosearch.provider.nominatim.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * L.Control.GeoSearch - search for an address and zoom to it's location - * L.GeoSearch.Provider.OpenStreetMap uses openstreetmap geocoding service - * https://github.com/smeijer/leaflet.control.geosearch - */ - -L.GeoSearch.Provider.Nominatim = L.Class.extend({ - options: { - - }, - - initialize: function(options) { - options = L.Util.setOptions(this, options); - }, - - GetLocations: function(query, map, callback) { - callback = callback || function() {}; - - var url = this.GetServiceUrl(query); - - $.getJSON(url, function (data) { - var results; - - try { - results = this.ParseJSON(data); - } catch (err) { - return callback(err); - } - - if (data.length > 0) { - var bbox = data[0].boundingbox, - viewport = [ - [bbox[0], bbox[2]], - [bbox[1], bbox[3]] - ]; - - map.fitBounds(viewport, { - maxZoom: 15 - }); - } - - return callback(null, results); - }.bind(this)); - }, - - GetServiceUrl: function (qry) { - var parameters = L.Util.extend({ - q: qry, - format: 'json' - }, this.options); - - return 'http://nominatim.openstreetmap.org/search' - + L.Util.getParamString(parameters); - }, - - ParseJSON: function (data) { - if (data.length == 0) - return []; - - var results = []; - for (var i = 0; i < data.length; i++) - results.push(new L.GeoSearch.Result( - data[i].lon, - data[i].lat, - data[i].display_name - )); - - return results; - } -}); diff --git a/tests/osgearth_server/leaflet.css b/tests/osgearth_server/leaflet.css deleted file mode 100644 index dea175f0f0..0000000000 --- a/tests/osgearth_server/leaflet.css +++ /dev/null @@ -1,479 +0,0 @@ -/* required styles */ - -.leaflet-map-pane, -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow, -.leaflet-tile-pane, -.leaflet-tile-container, -.leaflet-overlay-pane, -.leaflet-shadow-pane, -.leaflet-marker-pane, -.leaflet-popup-pane, -.leaflet-overlay-pane svg, -.leaflet-zoom-box, -.leaflet-image-layer, -.leaflet-layer { - position: absolute; - left: 0; - top: 0; - } -.leaflet-container { - overflow: hidden; - -ms-touch-action: none; - touch-action: none; - } -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - -webkit-user-drag: none; - } -.leaflet-marker-icon, -.leaflet-marker-shadow { - display: block; - } -/* map is broken in FF if you have max-width: 100% on tiles */ -.leaflet-container img { - max-width: none !important; - } -/* stupid Android 2 doesn't understand "max-width: none" properly */ -.leaflet-container img.leaflet-image-layer { - max-width: 15000px !important; - } -.leaflet-tile { - filter: inherit; - visibility: hidden; - } -.leaflet-tile-loaded { - visibility: inherit; - } -.leaflet-zoom-box { - width: 0; - height: 0; - } -/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ -.leaflet-overlay-pane svg { - -moz-user-select: none; - } - -.leaflet-tile-pane { z-index: 2; } -.leaflet-objects-pane { z-index: 3; } -.leaflet-overlay-pane { z-index: 4; } -.leaflet-shadow-pane { z-index: 5; } -.leaflet-marker-pane { z-index: 6; } -.leaflet-popup-pane { z-index: 7; } - -.leaflet-vml-shape { - width: 1px; - height: 1px; - } -.lvml { - behavior: url(#default#VML); - display: inline-block; - position: absolute; - } - - -/* control positioning */ - -.leaflet-control { - position: relative; - z-index: 7; - pointer-events: auto; - } -.leaflet-top, -.leaflet-bottom { - position: absolute; - z-index: 1000; - pointer-events: none; - } -.leaflet-top { - top: 0; - } -.leaflet-right { - right: 0; - } -.leaflet-bottom { - bottom: 0; - } -.leaflet-left { - left: 0; - } -.leaflet-control { - float: left; - clear: both; - } -.leaflet-right .leaflet-control { - float: right; - } -.leaflet-top .leaflet-control { - margin-top: 10px; - } -.leaflet-bottom .leaflet-control { - margin-bottom: 10px; - } -.leaflet-left .leaflet-control { - margin-left: 10px; - } -.leaflet-right .leaflet-control { - margin-right: 10px; - } - - -/* zoom and fade animations */ - -.leaflet-fade-anim .leaflet-tile, -.leaflet-fade-anim .leaflet-popup { - opacity: 0; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; - } -.leaflet-fade-anim .leaflet-tile-loaded, -.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { - opacity: 1; - } - -.leaflet-zoom-anim .leaflet-zoom-animated { - -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); - -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); - -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1); - transition: transform 0.25s cubic-bezier(0,0,0.25,1); - } -.leaflet-zoom-anim .leaflet-tile, -.leaflet-pan-anim .leaflet-tile, -.leaflet-touching .leaflet-zoom-animated { - -webkit-transition: none; - -moz-transition: none; - -o-transition: none; - transition: none; - } - -.leaflet-zoom-anim .leaflet-zoom-hide { - visibility: hidden; - } - - -/* cursors */ - -.leaflet-clickable { - cursor: pointer; - } -.leaflet-container { - cursor: -webkit-grab; - cursor: -moz-grab; - } -.leaflet-popup-pane, -.leaflet-control { - cursor: auto; - } -.leaflet-dragging .leaflet-container, -.leaflet-dragging .leaflet-clickable { - cursor: move; - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - } - - -/* visual tweaks */ - -.leaflet-container { - background: #ddd; - outline: 0; - } -.leaflet-container a { - color: #0078A8; - } -.leaflet-container a.leaflet-active { - outline: 2px solid orange; - } -.leaflet-zoom-box { - border: 2px dotted #38f; - background: rgba(255,255,255,0.5); - } - - -/* general typography */ -.leaflet-container { - font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; - } - - -/* general toolbar styles */ - -.leaflet-bar { - box-shadow: 0 1px 5px rgba(0,0,0,0.65); - border-radius: 4px; - } -.leaflet-bar a, -.leaflet-bar a:hover { - background-color: #fff; - border-bottom: 1px solid #ccc; - width: 26px; - height: 26px; - line-height: 26px; - display: block; - text-align: center; - text-decoration: none; - color: black; - } -.leaflet-bar a, -.leaflet-control-layers-toggle { - background-position: 50% 50%; - background-repeat: no-repeat; - display: block; - } -.leaflet-bar a:hover { - background-color: #f4f4f4; - } -.leaflet-bar a:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; - } -.leaflet-bar a:last-child { - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom: none; - } -.leaflet-bar a.leaflet-disabled { - cursor: default; - background-color: #f4f4f4; - color: #bbb; - } - -.leaflet-touch .leaflet-bar a { - width: 30px; - height: 30px; - line-height: 30px; - } - - -/* zoom control */ - -.leaflet-control-zoom-in, -.leaflet-control-zoom-out { - font: bold 18px 'Lucida Console', Monaco, monospace; - text-indent: 1px; - } -.leaflet-control-zoom-out { - font-size: 20px; - } - -.leaflet-touch .leaflet-control-zoom-in { - font-size: 22px; - } -.leaflet-touch .leaflet-control-zoom-out { - font-size: 24px; - } - - -/* layers control */ - -.leaflet-control-layers { - box-shadow: 0 1px 5px rgba(0,0,0,0.4); - background: #fff; - border-radius: 5px; - } -.leaflet-control-layers-toggle { - background-image: url(images/layers.png); - width: 36px; - height: 36px; - } -.leaflet-retina .leaflet-control-layers-toggle { - background-image: url(images/layers-2x.png); - background-size: 26px 26px; - } -.leaflet-touch .leaflet-control-layers-toggle { - width: 44px; - height: 44px; - } -.leaflet-control-layers .leaflet-control-layers-list, -.leaflet-control-layers-expanded .leaflet-control-layers-toggle { - display: none; - } -.leaflet-control-layers-expanded .leaflet-control-layers-list { - display: block; - position: relative; - } -.leaflet-control-layers-expanded { - padding: 6px 10px 6px 6px; - color: #333; - background: #fff; - } -.leaflet-control-layers-selector { - margin-top: 2px; - position: relative; - top: 1px; - } -.leaflet-control-layers label { - display: block; - } -.leaflet-control-layers-separator { - height: 0; - border-top: 1px solid #ddd; - margin: 5px -10px 5px -6px; - } - - -/* attribution and scale controls */ - -.leaflet-container .leaflet-control-attribution { - background: #fff; - background: rgba(255, 255, 255, 0.7); - margin: 0; - } -.leaflet-control-attribution, -.leaflet-control-scale-line { - padding: 0 5px; - color: #333; - } -.leaflet-control-attribution a { - text-decoration: none; - } -.leaflet-control-attribution a:hover { - text-decoration: underline; - } -.leaflet-container .leaflet-control-attribution, -.leaflet-container .leaflet-control-scale { - font-size: 11px; - } -.leaflet-left .leaflet-control-scale { - margin-left: 5px; - } -.leaflet-bottom .leaflet-control-scale { - margin-bottom: 5px; - } -.leaflet-control-scale-line { - border: 2px solid #777; - border-top: none; - line-height: 1.1; - padding: 2px 5px 1px; - font-size: 11px; - white-space: nowrap; - overflow: hidden; - -moz-box-sizing: content-box; - box-sizing: content-box; - - background: #fff; - background: rgba(255, 255, 255, 0.5); - } -.leaflet-control-scale-line:not(:first-child) { - border-top: 2px solid #777; - border-bottom: none; - margin-top: -2px; - } -.leaflet-control-scale-line:not(:first-child):not(:last-child) { - border-bottom: 2px solid #777; - } - -.leaflet-touch .leaflet-control-attribution, -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - box-shadow: none; - } -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - border: 2px solid rgba(0,0,0,0.2); - background-clip: padding-box; - } - - -/* popup */ - -.leaflet-popup { - position: absolute; - text-align: center; - } -.leaflet-popup-content-wrapper { - padding: 1px; - text-align: left; - border-radius: 12px; - } -.leaflet-popup-content { - margin: 13px 19px; - line-height: 1.4; - } -.leaflet-popup-content p { - margin: 18px 0; - } -.leaflet-popup-tip-container { - margin: 0 auto; - width: 40px; - height: 20px; - position: relative; - overflow: hidden; - } -.leaflet-popup-tip { - width: 17px; - height: 17px; - padding: 1px; - - margin: -10px auto 0; - - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - transform: rotate(45deg); - } -.leaflet-popup-content-wrapper, -.leaflet-popup-tip { - background: white; - - box-shadow: 0 3px 14px rgba(0,0,0,0.4); - } -.leaflet-container a.leaflet-popup-close-button { - position: absolute; - top: 0; - right: 0; - padding: 4px 4px 0 0; - text-align: center; - width: 18px; - height: 14px; - font: 16px/14px Tahoma, Verdana, sans-serif; - color: #c3c3c3; - text-decoration: none; - font-weight: bold; - background: transparent; - } -.leaflet-container a.leaflet-popup-close-button:hover { - color: #999; - } -.leaflet-popup-scrolled { - overflow: auto; - border-bottom: 1px solid #ddd; - border-top: 1px solid #ddd; - } - -.leaflet-oldie .leaflet-popup-content-wrapper { - zoom: 1; - } -.leaflet-oldie .leaflet-popup-tip { - width: 24px; - margin: 0 auto; - - -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; - filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); - } -.leaflet-oldie .leaflet-popup-tip-container { - margin-top: -1px; - } - -.leaflet-oldie .leaflet-control-zoom, -.leaflet-oldie .leaflet-control-layers, -.leaflet-oldie .leaflet-popup-content-wrapper, -.leaflet-oldie .leaflet-popup-tip { - border: 1px solid #999; - } - - -/* div icon */ - -.leaflet-div-icon { - background: #fff; - border: 1px solid #666; - } diff --git a/tests/osgearth_server/leaflet.js b/tests/osgearth_server/leaflet.js deleted file mode 100644 index d3d5635741..0000000000 --- a/tests/osgearth_server/leaflet.js +++ /dev/null @@ -1,9168 +0,0 @@ -/* - Leaflet, a JavaScript library for mobile-friendly interactive maps. http://leafletjs.com - (c) 2010-2013, Vladimir Agafonkin - (c) 2010-2011, CloudMade -*/ -(function (window, document, undefined) { -var oldL = window.L, - L = {}; - -L.version = '0.7.7'; - -// define Leaflet for Node module pattern loaders, including Browserify -if (typeof module === 'object' && typeof module.exports === 'object') { - module.exports = L; - -// define Leaflet as an AMD module -} else if (typeof define === 'function' && define.amd) { - define(L); -} - -// define Leaflet as a global L variable, saving the original L to restore later if needed - -L.noConflict = function () { - window.L = oldL; - return this; -}; - -window.L = L; - - -/* - * L.Util contains various utility functions used throughout Leaflet code. - */ - -L.Util = { - extend: function (dest) { // (Object[, Object, ...]) -> - var sources = Array.prototype.slice.call(arguments, 1), - i, j, len, src; - - for (j = 0, len = sources.length; j < len; j++) { - src = sources[j] || {}; - for (i in src) { - if (src.hasOwnProperty(i)) { - dest[i] = src[i]; - } - } - } - return dest; - }, - - bind: function (fn, obj) { // (Function, Object) -> Function - var args = arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null; - return function () { - return fn.apply(obj, args || arguments); - }; - }, - - stamp: (function () { - var lastId = 0, - key = '_leaflet_id'; - return function (obj) { - obj[key] = obj[key] || ++lastId; - return obj[key]; - }; - }()), - - invokeEach: function (obj, method, context) { - var i, args; - - if (typeof obj === 'object') { - args = Array.prototype.slice.call(arguments, 3); - - for (i in obj) { - method.apply(context, [i, obj[i]].concat(args)); - } - return true; - } - - return false; - }, - - limitExecByInterval: function (fn, time, context) { - var lock, execOnUnlock; - - return function wrapperFn() { - var args = arguments; - - if (lock) { - execOnUnlock = true; - return; - } - - lock = true; - - setTimeout(function () { - lock = false; - - if (execOnUnlock) { - wrapperFn.apply(context, args); - execOnUnlock = false; - } - }, time); - - fn.apply(context, args); - }; - }, - - falseFn: function () { - return false; - }, - - formatNum: function (num, digits) { - var pow = Math.pow(10, digits || 5); - return Math.round(num * pow) / pow; - }, - - trim: function (str) { - return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); - }, - - splitWords: function (str) { - return L.Util.trim(str).split(/\s+/); - }, - - setOptions: function (obj, options) { - obj.options = L.extend({}, obj.options, options); - return obj.options; - }, - - getParamString: function (obj, existingUrl, uppercase) { - var params = []; - for (var i in obj) { - params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i])); - } - return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&'); - }, - template: function (str, data) { - return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) { - var value = data[key]; - if (value === undefined) { - throw new Error('No value provided for variable ' + str); - } else if (typeof value === 'function') { - value = value(data); - } - return value; - }); - }, - - isArray: Array.isArray || function (obj) { - return (Object.prototype.toString.call(obj) === '[object Array]'); - }, - - emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' -}; - -(function () { - - // inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/ - - function getPrefixed(name) { - var i, fn, - prefixes = ['webkit', 'moz', 'o', 'ms']; - - for (i = 0; i < prefixes.length && !fn; i++) { - fn = window[prefixes[i] + name]; - } - - return fn; - } - - var lastTime = 0; - - function timeoutDefer(fn) { - var time = +new Date(), - timeToCall = Math.max(0, 16 - (time - lastTime)); - - lastTime = time + timeToCall; - return window.setTimeout(fn, timeToCall); - } - - var requestFn = window.requestAnimationFrame || - getPrefixed('RequestAnimationFrame') || timeoutDefer; - - var cancelFn = window.cancelAnimationFrame || - getPrefixed('CancelAnimationFrame') || - getPrefixed('CancelRequestAnimationFrame') || - function (id) { window.clearTimeout(id); }; - - - L.Util.requestAnimFrame = function (fn, context, immediate, element) { - fn = L.bind(fn, context); - - if (immediate && requestFn === timeoutDefer) { - fn(); - } else { - return requestFn.call(window, fn, element); - } - }; - - L.Util.cancelAnimFrame = function (id) { - if (id) { - cancelFn.call(window, id); - } - }; - -}()); - -// shortcuts for most used utility functions -L.extend = L.Util.extend; -L.bind = L.Util.bind; -L.stamp = L.Util.stamp; -L.setOptions = L.Util.setOptions; - - -/* - * L.Class powers the OOP facilities of the library. - * Thanks to John Resig and Dean Edwards for inspiration! - */ - -L.Class = function () {}; - -L.Class.extend = function (props) { - - // extended class with the new prototype - var NewClass = function () { - - // call the constructor - if (this.initialize) { - this.initialize.apply(this, arguments); - } - - // call all constructor hooks - if (this._initHooks) { - this.callInitHooks(); - } - }; - - // instantiate class without calling constructor - var F = function () {}; - F.prototype = this.prototype; - - var proto = new F(); - proto.constructor = NewClass; - - NewClass.prototype = proto; - - //inherit parent's statics - for (var i in this) { - if (this.hasOwnProperty(i) && i !== 'prototype') { - NewClass[i] = this[i]; - } - } - - // mix static properties into the class - if (props.statics) { - L.extend(NewClass, props.statics); - delete props.statics; - } - - // mix includes into the prototype - if (props.includes) { - L.Util.extend.apply(null, [proto].concat(props.includes)); - delete props.includes; - } - - // merge options - if (props.options && proto.options) { - props.options = L.extend({}, proto.options, props.options); - } - - // mix given properties into the prototype - L.extend(proto, props); - - proto._initHooks = []; - - var parent = this; - // jshint camelcase: false - NewClass.__super__ = parent.prototype; - - // add method for calling all hooks - proto.callInitHooks = function () { - - if (this._initHooksCalled) { return; } - - if (parent.prototype.callInitHooks) { - parent.prototype.callInitHooks.call(this); - } - - this._initHooksCalled = true; - - for (var i = 0, len = proto._initHooks.length; i < len; i++) { - proto._initHooks[i].call(this); - } - }; - - return NewClass; -}; - - -// method for adding properties to prototype -L.Class.include = function (props) { - L.extend(this.prototype, props); -}; - -// merge new default options to the Class -L.Class.mergeOptions = function (options) { - L.extend(this.prototype.options, options); -}; - -// add a constructor hook -L.Class.addInitHook = function (fn) { // (Function) || (String, args...) - var args = Array.prototype.slice.call(arguments, 1); - - var init = typeof fn === 'function' ? fn : function () { - this[fn].apply(this, args); - }; - - this.prototype._initHooks = this.prototype._initHooks || []; - this.prototype._initHooks.push(init); -}; - - -/* - * L.Mixin.Events is used to add custom events functionality to Leaflet classes. - */ - -var eventsKey = '_leaflet_events'; - -L.Mixin = {}; - -L.Mixin.Events = { - - addEventListener: function (types, fn, context) { // (String, Function[, Object]) or (Object[, Object]) - - // types can be a map of types/handlers - if (L.Util.invokeEach(types, this.addEventListener, this, fn, context)) { return this; } - - var events = this[eventsKey] = this[eventsKey] || {}, - contextId = context && context !== this && L.stamp(context), - i, len, event, type, indexKey, indexLenKey, typeIndex; - - // types can be a string of space-separated words - types = L.Util.splitWords(types); - - for (i = 0, len = types.length; i < len; i++) { - event = { - action: fn, - context: context || this - }; - type = types[i]; - - if (contextId) { - // store listeners of a particular context in a separate hash (if it has an id) - // gives a major performance boost when removing thousands of map layers - - indexKey = type + '_idx'; - indexLenKey = indexKey + '_len'; - - typeIndex = events[indexKey] = events[indexKey] || {}; - - if (!typeIndex[contextId]) { - typeIndex[contextId] = []; - - // keep track of the number of keys in the index to quickly check if it's empty - events[indexLenKey] = (events[indexLenKey] || 0) + 1; - } - - typeIndex[contextId].push(event); - - - } else { - events[type] = events[type] || []; - events[type].push(event); - } - } - - return this; - }, - - hasEventListeners: function (type) { // (String) -> Boolean - var events = this[eventsKey]; - return !!events && ((type in events && events[type].length > 0) || - (type + '_idx' in events && events[type + '_idx_len'] > 0)); - }, - - removeEventListener: function (types, fn, context) { // ([String, Function, Object]) or (Object[, Object]) - - if (!this[eventsKey]) { - return this; - } - - if (!types) { - return this.clearAllEventListeners(); - } - - if (L.Util.invokeEach(types, this.removeEventListener, this, fn, context)) { return this; } - - var events = this[eventsKey], - contextId = context && context !== this && L.stamp(context), - i, len, type, listeners, j, indexKey, indexLenKey, typeIndex, removed; - - types = L.Util.splitWords(types); - - for (i = 0, len = types.length; i < len; i++) { - type = types[i]; - indexKey = type + '_idx'; - indexLenKey = indexKey + '_len'; - - typeIndex = events[indexKey]; - - if (!fn) { - // clear all listeners for a type if function isn't specified - delete events[type]; - delete events[indexKey]; - delete events[indexLenKey]; - - } else { - listeners = contextId && typeIndex ? typeIndex[contextId] : events[type]; - - if (listeners) { - for (j = listeners.length - 1; j >= 0; j--) { - if ((listeners[j].action === fn) && (!context || (listeners[j].context === context))) { - removed = listeners.splice(j, 1); - // set the old action to a no-op, because it is possible - // that the listener is being iterated over as part of a dispatch - removed[0].action = L.Util.falseFn; - } - } - - if (context && typeIndex && (listeners.length === 0)) { - delete typeIndex[contextId]; - events[indexLenKey]--; - } - } - } - } - - return this; - }, - - clearAllEventListeners: function () { - delete this[eventsKey]; - return this; - }, - - fireEvent: function (type, data) { // (String[, Object]) - if (!this.hasEventListeners(type)) { - return this; - } - - var event = L.Util.extend({}, data, { type: type, target: this }); - - var events = this[eventsKey], - listeners, i, len, typeIndex, contextId; - - if (events[type]) { - // make sure adding/removing listeners inside other listeners won't cause infinite loop - listeners = events[type].slice(); - - for (i = 0, len = listeners.length; i < len; i++) { - listeners[i].action.call(listeners[i].context, event); - } - } - - // fire event for the context-indexed listeners as well - typeIndex = events[type + '_idx']; - - for (contextId in typeIndex) { - listeners = typeIndex[contextId].slice(); - - if (listeners) { - for (i = 0, len = listeners.length; i < len; i++) { - listeners[i].action.call(listeners[i].context, event); - } - } - } - - return this; - }, - - addOneTimeEventListener: function (types, fn, context) { - - if (L.Util.invokeEach(types, this.addOneTimeEventListener, this, fn, context)) { return this; } - - var handler = L.bind(function () { - this - .removeEventListener(types, fn, context) - .removeEventListener(types, handler, context); - }, this); - - return this - .addEventListener(types, fn, context) - .addEventListener(types, handler, context); - } -}; - -L.Mixin.Events.on = L.Mixin.Events.addEventListener; -L.Mixin.Events.off = L.Mixin.Events.removeEventListener; -L.Mixin.Events.once = L.Mixin.Events.addOneTimeEventListener; -L.Mixin.Events.fire = L.Mixin.Events.fireEvent; - - -/* - * L.Browser handles different browser and feature detections for internal Leaflet use. - */ - -(function () { - - var ie = 'ActiveXObject' in window, - ielt9 = ie && !document.addEventListener, - - // terrible browser detection to work around Safari / iOS / Android browser bugs - ua = navigator.userAgent.toLowerCase(), - webkit = ua.indexOf('webkit') !== -1, - chrome = ua.indexOf('chrome') !== -1, - phantomjs = ua.indexOf('phantom') !== -1, - android = ua.indexOf('android') !== -1, - android23 = ua.search('android [23]') !== -1, - gecko = ua.indexOf('gecko') !== -1, - - mobile = typeof orientation !== undefined + '', - msPointer = !window.PointerEvent && window.MSPointerEvent, - pointer = (window.PointerEvent && window.navigator.pointerEnabled) || - msPointer, - retina = ('devicePixelRatio' in window && window.devicePixelRatio > 1) || - ('matchMedia' in window && window.matchMedia('(min-resolution:144dpi)') && - window.matchMedia('(min-resolution:144dpi)').matches), - - doc = document.documentElement, - ie3d = ie && ('transition' in doc.style), - webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23, - gecko3d = 'MozPerspective' in doc.style, - opera3d = 'OTransition' in doc.style, - any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs; - - var touch = !window.L_NO_TOUCH && !phantomjs && (pointer || 'ontouchstart' in window || - (window.DocumentTouch && document instanceof window.DocumentTouch)); - - L.Browser = { - ie: ie, - ielt9: ielt9, - webkit: webkit, - gecko: gecko && !webkit && !window.opera && !ie, - - android: android, - android23: android23, - - chrome: chrome, - - ie3d: ie3d, - webkit3d: webkit3d, - gecko3d: gecko3d, - opera3d: opera3d, - any3d: any3d, - - mobile: mobile, - mobileWebkit: mobile && webkit, - mobileWebkit3d: mobile && webkit3d, - mobileOpera: mobile && window.opera, - - touch: touch, - msPointer: msPointer, - pointer: pointer, - - retina: retina - }; - -}()); - - -/* - * L.Point represents a point with x and y coordinates. - */ - -L.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) { - this.x = (round ? Math.round(x) : x); - this.y = (round ? Math.round(y) : y); -}; - -L.Point.prototype = { - - clone: function () { - return new L.Point(this.x, this.y); - }, - - // non-destructive, returns a new point - add: function (point) { - return this.clone()._add(L.point(point)); - }, - - // destructive, used directly for performance in situations where it's safe to modify existing point - _add: function (point) { - this.x += point.x; - this.y += point.y; - return this; - }, - - subtract: function (point) { - return this.clone()._subtract(L.point(point)); - }, - - _subtract: function (point) { - this.x -= point.x; - this.y -= point.y; - return this; - }, - - divideBy: function (num) { - return this.clone()._divideBy(num); - }, - - _divideBy: function (num) { - this.x /= num; - this.y /= num; - return this; - }, - - multiplyBy: function (num) { - return this.clone()._multiplyBy(num); - }, - - _multiplyBy: function (num) { - this.x *= num; - this.y *= num; - return this; - }, - - round: function () { - return this.clone()._round(); - }, - - _round: function () { - this.x = Math.round(this.x); - this.y = Math.round(this.y); - return this; - }, - - floor: function () { - return this.clone()._floor(); - }, - - _floor: function () { - this.x = Math.floor(this.x); - this.y = Math.floor(this.y); - return this; - }, - - distanceTo: function (point) { - point = L.point(point); - - var x = point.x - this.x, - y = point.y - this.y; - - return Math.sqrt(x * x + y * y); - }, - - equals: function (point) { - point = L.point(point); - - return point.x === this.x && - point.y === this.y; - }, - - contains: function (point) { - point = L.point(point); - - return Math.abs(point.x) <= Math.abs(this.x) && - Math.abs(point.y) <= Math.abs(this.y); - }, - - toString: function () { - return 'Point(' + - L.Util.formatNum(this.x) + ', ' + - L.Util.formatNum(this.y) + ')'; - } -}; - -L.point = function (x, y, round) { - if (x instanceof L.Point) { - return x; - } - if (L.Util.isArray(x)) { - return new L.Point(x[0], x[1]); - } - if (x === undefined || x === null) { - return x; - } - return new L.Point(x, y, round); -}; - - -/* - * L.Bounds represents a rectangular area on the screen in pixel coordinates. - */ - -L.Bounds = function (a, b) { //(Point, Point) or Point[] - if (!a) { return; } - - var points = b ? [a, b] : a; - - for (var i = 0, len = points.length; i < len; i++) { - this.extend(points[i]); - } -}; - -L.Bounds.prototype = { - // extend the bounds to contain the given point - extend: function (point) { // (Point) - point = L.point(point); - - if (!this.min && !this.max) { - this.min = point.clone(); - this.max = point.clone(); - } else { - this.min.x = Math.min(point.x, this.min.x); - this.max.x = Math.max(point.x, this.max.x); - this.min.y = Math.min(point.y, this.min.y); - this.max.y = Math.max(point.y, this.max.y); - } - return this; - }, - - getCenter: function (round) { // (Boolean) -> Point - return new L.Point( - (this.min.x + this.max.x) / 2, - (this.min.y + this.max.y) / 2, round); - }, - - getBottomLeft: function () { // -> Point - return new L.Point(this.min.x, this.max.y); - }, - - getTopRight: function () { // -> Point - return new L.Point(this.max.x, this.min.y); - }, - - getSize: function () { - return this.max.subtract(this.min); - }, - - contains: function (obj) { // (Bounds) or (Point) -> Boolean - var min, max; - - if (typeof obj[0] === 'number' || obj instanceof L.Point) { - obj = L.point(obj); - } else { - obj = L.bounds(obj); - } - - if (obj instanceof L.Bounds) { - min = obj.min; - max = obj.max; - } else { - min = max = obj; - } - - return (min.x >= this.min.x) && - (max.x <= this.max.x) && - (min.y >= this.min.y) && - (max.y <= this.max.y); - }, - - intersects: function (bounds) { // (Bounds) -> Boolean - bounds = L.bounds(bounds); - - var min = this.min, - max = this.max, - min2 = bounds.min, - max2 = bounds.max, - xIntersects = (max2.x >= min.x) && (min2.x <= max.x), - yIntersects = (max2.y >= min.y) && (min2.y <= max.y); - - return xIntersects && yIntersects; - }, - - isValid: function () { - return !!(this.min && this.max); - } -}; - -L.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[]) - if (!a || a instanceof L.Bounds) { - return a; - } - return new L.Bounds(a, b); -}; - - -/* - * L.Transformation is an utility class to perform simple point transformations through a 2d-matrix. - */ - -L.Transformation = function (a, b, c, d) { - this._a = a; - this._b = b; - this._c = c; - this._d = d; -}; - -L.Transformation.prototype = { - transform: function (point, scale) { // (Point, Number) -> Point - return this._transform(point.clone(), scale); - }, - - // destructive transform (faster) - _transform: function (point, scale) { - scale = scale || 1; - point.x = scale * (this._a * point.x + this._b); - point.y = scale * (this._c * point.y + this._d); - return point; - }, - - untransform: function (point, scale) { - scale = scale || 1; - return new L.Point( - (point.x / scale - this._b) / this._a, - (point.y / scale - this._d) / this._c); - } -}; - - -/* - * L.DomUtil contains various utility functions for working with DOM. - */ - -L.DomUtil = { - get: function (id) { - return (typeof id === 'string' ? document.getElementById(id) : id); - }, - - getStyle: function (el, style) { - - var value = el.style[style]; - - if (!value && el.currentStyle) { - value = el.currentStyle[style]; - } - - if ((!value || value === 'auto') && document.defaultView) { - var css = document.defaultView.getComputedStyle(el, null); - value = css ? css[style] : null; - } - - return value === 'auto' ? null : value; - }, - - getViewportOffset: function (element) { - - var top = 0, - left = 0, - el = element, - docBody = document.body, - docEl = document.documentElement, - pos; - - do { - top += el.offsetTop || 0; - left += el.offsetLeft || 0; - - //add borders - top += parseInt(L.DomUtil.getStyle(el, 'borderTopWidth'), 10) || 0; - left += parseInt(L.DomUtil.getStyle(el, 'borderLeftWidth'), 10) || 0; - - pos = L.DomUtil.getStyle(el, 'position'); - - if (el.offsetParent === docBody && pos === 'absolute') { break; } - - if (pos === 'fixed') { - top += docBody.scrollTop || docEl.scrollTop || 0; - left += docBody.scrollLeft || docEl.scrollLeft || 0; - break; - } - - if (pos === 'relative' && !el.offsetLeft) { - var width = L.DomUtil.getStyle(el, 'width'), - maxWidth = L.DomUtil.getStyle(el, 'max-width'), - r = el.getBoundingClientRect(); - - if (width !== 'none' || maxWidth !== 'none') { - left += r.left + el.clientLeft; - } - - //calculate full y offset since we're breaking out of the loop - top += r.top + (docBody.scrollTop || docEl.scrollTop || 0); - - break; - } - - el = el.offsetParent; - - } while (el); - - el = element; - - do { - if (el === docBody) { break; } - - top -= el.scrollTop || 0; - left -= el.scrollLeft || 0; - - el = el.parentNode; - } while (el); - - return new L.Point(left, top); - }, - - documentIsLtr: function () { - if (!L.DomUtil._docIsLtrCached) { - L.DomUtil._docIsLtrCached = true; - L.DomUtil._docIsLtr = L.DomUtil.getStyle(document.body, 'direction') === 'ltr'; - } - return L.DomUtil._docIsLtr; - }, - - create: function (tagName, className, container) { - - var el = document.createElement(tagName); - el.className = className; - - if (container) { - container.appendChild(el); - } - - return el; - }, - - hasClass: function (el, name) { - if (el.classList !== undefined) { - return el.classList.contains(name); - } - var className = L.DomUtil._getClass(el); - return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className); - }, - - addClass: function (el, name) { - if (el.classList !== undefined) { - var classes = L.Util.splitWords(name); - for (var i = 0, len = classes.length; i < len; i++) { - el.classList.add(classes[i]); - } - } else if (!L.DomUtil.hasClass(el, name)) { - var className = L.DomUtil._getClass(el); - L.DomUtil._setClass(el, (className ? className + ' ' : '') + name); - } - }, - - removeClass: function (el, name) { - if (el.classList !== undefined) { - el.classList.remove(name); - } else { - L.DomUtil._setClass(el, L.Util.trim((' ' + L.DomUtil._getClass(el) + ' ').replace(' ' + name + ' ', ' '))); - } - }, - - _setClass: function (el, name) { - if (el.className.baseVal === undefined) { - el.className = name; - } else { - // in case of SVG element - el.className.baseVal = name; - } - }, - - _getClass: function (el) { - return el.className.baseVal === undefined ? el.className : el.className.baseVal; - }, - - setOpacity: function (el, value) { - - if ('opacity' in el.style) { - el.style.opacity = value; - - } else if ('filter' in el.style) { - - var filter = false, - filterName = 'DXImageTransform.Microsoft.Alpha'; - - // filters collection throws an error if we try to retrieve a filter that doesn't exist - try { - filter = el.filters.item(filterName); - } catch (e) { - // don't set opacity to 1 if we haven't already set an opacity, - // it isn't needed and breaks transparent pngs. - if (value === 1) { return; } - } - - value = Math.round(value * 100); - - if (filter) { - filter.Enabled = (value !== 100); - filter.Opacity = value; - } else { - el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')'; - } - } - }, - - testProp: function (props) { - - var style = document.documentElement.style; - - for (var i = 0; i < props.length; i++) { - if (props[i] in style) { - return props[i]; - } - } - return false; - }, - - getTranslateString: function (point) { - // on WebKit browsers (Chrome/Safari/iOS Safari/Android) using translate3d instead of translate - // makes animation smoother as it ensures HW accel is used. Firefox 13 doesn't care - // (same speed either way), Opera 12 doesn't support translate3d - - var is3d = L.Browser.webkit3d, - open = 'translate' + (is3d ? '3d' : '') + '(', - close = (is3d ? ',0' : '') + ')'; - - return open + point.x + 'px,' + point.y + 'px' + close; - }, - - getScaleString: function (scale, origin) { - - var preTranslateStr = L.DomUtil.getTranslateString(origin.add(origin.multiplyBy(-1 * scale))), - scaleStr = ' scale(' + scale + ') '; - - return preTranslateStr + scaleStr; - }, - - setPosition: function (el, point, disable3D) { // (HTMLElement, Point[, Boolean]) - - // jshint camelcase: false - el._leaflet_pos = point; - - if (!disable3D && L.Browser.any3d) { - el.style[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString(point); - } else { - el.style.left = point.x + 'px'; - el.style.top = point.y + 'px'; - } - }, - - getPosition: function (el) { - // this method is only used for elements previously positioned using setPosition, - // so it's safe to cache the position for performance - - // jshint camelcase: false - return el._leaflet_pos; - } -}; - - -// prefix style property names - -L.DomUtil.TRANSFORM = L.DomUtil.testProp( - ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']); - -// webkitTransition comes first because some browser versions that drop vendor prefix don't do -// the same for the transitionend event, in particular the Android 4.1 stock browser - -L.DomUtil.TRANSITION = L.DomUtil.testProp( - ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']); - -L.DomUtil.TRANSITION_END = - L.DomUtil.TRANSITION === 'webkitTransition' || L.DomUtil.TRANSITION === 'OTransition' ? - L.DomUtil.TRANSITION + 'End' : 'transitionend'; - -(function () { - if ('onselectstart' in document) { - L.extend(L.DomUtil, { - disableTextSelection: function () { - L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault); - }, - - enableTextSelection: function () { - L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault); - } - }); - } else { - var userSelectProperty = L.DomUtil.testProp( - ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); - - L.extend(L.DomUtil, { - disableTextSelection: function () { - if (userSelectProperty) { - var style = document.documentElement.style; - this._userSelect = style[userSelectProperty]; - style[userSelectProperty] = 'none'; - } - }, - - enableTextSelection: function () { - if (userSelectProperty) { - document.documentElement.style[userSelectProperty] = this._userSelect; - delete this._userSelect; - } - } - }); - } - - L.extend(L.DomUtil, { - disableImageDrag: function () { - L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault); - }, - - enableImageDrag: function () { - L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault); - } - }); -})(); - - -/* - * L.LatLng represents a geographical point with latitude and longitude coordinates. - */ - -L.LatLng = function (lat, lng, alt) { // (Number, Number, Number) - lat = parseFloat(lat); - lng = parseFloat(lng); - - if (isNaN(lat) || isNaN(lng)) { - throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); - } - - this.lat = lat; - this.lng = lng; - - if (alt !== undefined) { - this.alt = parseFloat(alt); - } -}; - -L.extend(L.LatLng, { - DEG_TO_RAD: Math.PI / 180, - RAD_TO_DEG: 180 / Math.PI, - MAX_MARGIN: 1.0E-9 // max margin of error for the "equals" check -}); - -L.LatLng.prototype = { - equals: function (obj) { // (LatLng) -> Boolean - if (!obj) { return false; } - - obj = L.latLng(obj); - - var margin = Math.max( - Math.abs(this.lat - obj.lat), - Math.abs(this.lng - obj.lng)); - - return margin <= L.LatLng.MAX_MARGIN; - }, - - toString: function (precision) { // (Number) -> String - return 'LatLng(' + - L.Util.formatNum(this.lat, precision) + ', ' + - L.Util.formatNum(this.lng, precision) + ')'; - }, - - // Haversine distance formula, see http://en.wikipedia.org/wiki/Haversine_formula - // TODO move to projection code, LatLng shouldn't know about Earth - distanceTo: function (other) { // (LatLng) -> Number - other = L.latLng(other); - - var R = 6378137, // earth radius in meters - d2r = L.LatLng.DEG_TO_RAD, - dLat = (other.lat - this.lat) * d2r, - dLon = (other.lng - this.lng) * d2r, - lat1 = this.lat * d2r, - lat2 = other.lat * d2r, - sin1 = Math.sin(dLat / 2), - sin2 = Math.sin(dLon / 2); - - var a = sin1 * sin1 + sin2 * sin2 * Math.cos(lat1) * Math.cos(lat2); - - return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - }, - - wrap: function (a, b) { // (Number, Number) -> LatLng - var lng = this.lng; - - a = a || -180; - b = b || 180; - - lng = (lng + b) % (b - a) + (lng < a || lng === b ? b : a); - - return new L.LatLng(this.lat, lng); - } -}; - -L.latLng = function (a, b) { // (LatLng) or ([Number, Number]) or (Number, Number) - if (a instanceof L.LatLng) { - return a; - } - if (L.Util.isArray(a)) { - if (typeof a[0] === 'number' || typeof a[0] === 'string') { - return new L.LatLng(a[0], a[1], a[2]); - } else { - return null; - } - } - if (a === undefined || a === null) { - return a; - } - if (typeof a === 'object' && 'lat' in a) { - return new L.LatLng(a.lat, 'lng' in a ? a.lng : a.lon); - } - if (b === undefined) { - return null; - } - return new L.LatLng(a, b); -}; - - - -/* - * L.LatLngBounds represents a rectangular area on the map in geographical coordinates. - */ - -L.LatLngBounds = function (southWest, northEast) { // (LatLng, LatLng) or (LatLng[]) - if (!southWest) { return; } - - var latlngs = northEast ? [southWest, northEast] : southWest; - - for (var i = 0, len = latlngs.length; i < len; i++) { - this.extend(latlngs[i]); - } -}; - -L.LatLngBounds.prototype = { - // extend the bounds to contain the given point or bounds - extend: function (obj) { // (LatLng) or (LatLngBounds) - if (!obj) { return this; } - - var latLng = L.latLng(obj); - if (latLng !== null) { - obj = latLng; - } else { - obj = L.latLngBounds(obj); - } - - if (obj instanceof L.LatLng) { - if (!this._southWest && !this._northEast) { - this._southWest = new L.LatLng(obj.lat, obj.lng); - this._northEast = new L.LatLng(obj.lat, obj.lng); - } else { - this._southWest.lat = Math.min(obj.lat, this._southWest.lat); - this._southWest.lng = Math.min(obj.lng, this._southWest.lng); - - this._northEast.lat = Math.max(obj.lat, this._northEast.lat); - this._northEast.lng = Math.max(obj.lng, this._northEast.lng); - } - } else if (obj instanceof L.LatLngBounds) { - this.extend(obj._southWest); - this.extend(obj._northEast); - } - return this; - }, - - // extend the bounds by a percentage - pad: function (bufferRatio) { // (Number) -> LatLngBounds - var sw = this._southWest, - ne = this._northEast, - heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio, - widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio; - - return new L.LatLngBounds( - new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer), - new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer)); - }, - - getCenter: function () { // -> LatLng - return new L.LatLng( - (this._southWest.lat + this._northEast.lat) / 2, - (this._southWest.lng + this._northEast.lng) / 2); - }, - - getSouthWest: function () { - return this._southWest; - }, - - getNorthEast: function () { - return this._northEast; - }, - - getNorthWest: function () { - return new L.LatLng(this.getNorth(), this.getWest()); - }, - - getSouthEast: function () { - return new L.LatLng(this.getSouth(), this.getEast()); - }, - - getWest: function () { - return this._southWest.lng; - }, - - getSouth: function () { - return this._southWest.lat; - }, - - getEast: function () { - return this._northEast.lng; - }, - - getNorth: function () { - return this._northEast.lat; - }, - - contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean - if (typeof obj[0] === 'number' || obj instanceof L.LatLng) { - obj = L.latLng(obj); - } else { - obj = L.latLngBounds(obj); - } - - var sw = this._southWest, - ne = this._northEast, - sw2, ne2; - - if (obj instanceof L.LatLngBounds) { - sw2 = obj.getSouthWest(); - ne2 = obj.getNorthEast(); - } else { - sw2 = ne2 = obj; - } - - return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) && - (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng); - }, - - intersects: function (bounds) { // (LatLngBounds) - bounds = L.latLngBounds(bounds); - - var sw = this._southWest, - ne = this._northEast, - sw2 = bounds.getSouthWest(), - ne2 = bounds.getNorthEast(), - - latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat), - lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng); - - return latIntersects && lngIntersects; - }, - - toBBoxString: function () { - return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(','); - }, - - equals: function (bounds) { // (LatLngBounds) - if (!bounds) { return false; } - - bounds = L.latLngBounds(bounds); - - return this._southWest.equals(bounds.getSouthWest()) && - this._northEast.equals(bounds.getNorthEast()); - }, - - isValid: function () { - return !!(this._southWest && this._northEast); - } -}; - -//TODO International date line? - -L.latLngBounds = function (a, b) { // (LatLngBounds) or (LatLng, LatLng) - if (!a || a instanceof L.LatLngBounds) { - return a; - } - return new L.LatLngBounds(a, b); -}; - - -/* - * L.Projection contains various geographical projections used by CRS classes. - */ - -L.Projection = {}; - - -/* - * Spherical Mercator is the most popular map projection, used by EPSG:3857 CRS used by default. - */ - -L.Projection.SphericalMercator = { - MAX_LATITUDE: 85.0511287798, - - project: function (latlng) { // (LatLng) -> Point - var d = L.LatLng.DEG_TO_RAD, - max = this.MAX_LATITUDE, - lat = Math.max(Math.min(max, latlng.lat), -max), - x = latlng.lng * d, - y = lat * d; - - y = Math.log(Math.tan((Math.PI / 4) + (y / 2))); - - return new L.Point(x, y); - }, - - unproject: function (point) { // (Point, Boolean) -> LatLng - var d = L.LatLng.RAD_TO_DEG, - lng = point.x * d, - lat = (2 * Math.atan(Math.exp(point.y)) - (Math.PI / 2)) * d; - - return new L.LatLng(lat, lng); - } -}; - - -/* - * Simple equirectangular (Plate Carree) projection, used by CRS like EPSG:4326 and Simple. - */ - -L.Projection.LonLat = { - project: function (latlng) { - return new L.Point(latlng.lng, latlng.lat); - }, - - unproject: function (point) { - return new L.LatLng(point.y, point.x); - } -}; - - -/* - * L.CRS is a base object for all defined CRS (Coordinate Reference Systems) in Leaflet. - */ - -L.CRS = { - latLngToPoint: function (latlng, zoom) { // (LatLng, Number) -> Point - var projectedPoint = this.projection.project(latlng), - scale = this.scale(zoom); - - return this.transformation._transform(projectedPoint, scale); - }, - - pointToLatLng: function (point, zoom) { // (Point, Number[, Boolean]) -> LatLng - var scale = this.scale(zoom), - untransformedPoint = this.transformation.untransform(point, scale); - - return this.projection.unproject(untransformedPoint); - }, - - project: function (latlng) { - return this.projection.project(latlng); - }, - - scale: function (zoom) { - return 256 * Math.pow(2, zoom); - }, - - getSize: function (zoom) { - var s = this.scale(zoom); - return L.point(s, s); - } -}; - - -/* - * A simple CRS that can be used for flat non-Earth maps like panoramas or game maps. - */ - -L.CRS.Simple = L.extend({}, L.CRS, { - projection: L.Projection.LonLat, - transformation: new L.Transformation(1, 0, -1, 0), - - scale: function (zoom) { - return Math.pow(2, zoom); - } -}); - - -/* - * L.CRS.EPSG3857 (Spherical Mercator) is the most common CRS for web mapping - * and is used by Leaflet by default. - */ - -L.CRS.EPSG3857 = L.extend({}, L.CRS, { - code: 'EPSG:3857', - - projection: L.Projection.SphericalMercator, - transformation: new L.Transformation(0.5 / Math.PI, 0.5, -0.5 / Math.PI, 0.5), - - project: function (latlng) { // (LatLng) -> Point - var projectedPoint = this.projection.project(latlng), - earthRadius = 6378137; - return projectedPoint.multiplyBy(earthRadius); - } -}); - -L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, { - code: 'EPSG:900913' -}); - - -/* - * L.CRS.EPSG4326 is a CRS popular among advanced GIS specialists. - */ - -L.CRS.EPSG4326 = L.extend({}, L.CRS, { - code: 'EPSG:4326', - - projection: L.Projection.LonLat, - transformation: new L.Transformation(1 / 360, 0.5, -1 / 360, 0.5) -}); - - -/* - * L.Map is the central class of the API - it is used to create a map. - */ - -L.Map = L.Class.extend({ - - includes: L.Mixin.Events, - - options: { - crs: L.CRS.EPSG3857, - - /* - center: LatLng, - zoom: Number, - layers: Array, - */ - - fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android23, - trackResize: true, - markerZoomAnimation: L.DomUtil.TRANSITION && L.Browser.any3d - }, - - initialize: function (id, options) { // (HTMLElement or String, Object) - options = L.setOptions(this, options); - - - this._initContainer(id); - this._initLayout(); - - // hack for https://github.com/Leaflet/Leaflet/issues/1980 - this._onResize = L.bind(this._onResize, this); - - this._initEvents(); - - if (options.maxBounds) { - this.setMaxBounds(options.maxBounds); - } - - if (options.center && options.zoom !== undefined) { - this.setView(L.latLng(options.center), options.zoom, {reset: true}); - } - - this._handlers = []; - - this._layers = {}; - this._zoomBoundLayers = {}; - this._tileLayersNum = 0; - - this.callInitHooks(); - - this._addLayers(options.layers); - }, - - - // public methods that modify map state - - // replaced by animation-powered implementation in Map.PanAnimation.js - setView: function (center, zoom) { - zoom = zoom === undefined ? this.getZoom() : zoom; - this._resetView(L.latLng(center), this._limitZoom(zoom)); - return this; - }, - - setZoom: function (zoom, options) { - if (!this._loaded) { - this._zoom = this._limitZoom(zoom); - return this; - } - return this.setView(this.getCenter(), zoom, {zoom: options}); - }, - - zoomIn: function (delta, options) { - return this.setZoom(this._zoom + (delta || 1), options); - }, - - zoomOut: function (delta, options) { - return this.setZoom(this._zoom - (delta || 1), options); - }, - - setZoomAround: function (latlng, zoom, options) { - var scale = this.getZoomScale(zoom), - viewHalf = this.getSize().divideBy(2), - containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng), - - centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale), - newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset)); - - return this.setView(newCenter, zoom, {zoom: options}); - }, - - fitBounds: function (bounds, options) { - - options = options || {}; - bounds = bounds.getBounds ? bounds.getBounds() : L.latLngBounds(bounds); - - var paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]), - paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]), - - zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)); - - zoom = (options.maxZoom) ? Math.min(options.maxZoom, zoom) : zoom; - - var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2), - - swPoint = this.project(bounds.getSouthWest(), zoom), - nePoint = this.project(bounds.getNorthEast(), zoom), - center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom); - - return this.setView(center, zoom, options); - }, - - fitWorld: function (options) { - return this.fitBounds([[-90, -180], [90, 180]], options); - }, - - panTo: function (center, options) { // (LatLng) - return this.setView(center, this._zoom, {pan: options}); - }, - - panBy: function (offset) { // (Point) - // replaced with animated panBy in Map.PanAnimation.js - this.fire('movestart'); - - this._rawPanBy(L.point(offset)); - - this.fire('move'); - return this.fire('moveend'); - }, - - setMaxBounds: function (bounds) { - bounds = L.latLngBounds(bounds); - - this.options.maxBounds = bounds; - - if (!bounds) { - return this.off('moveend', this._panInsideMaxBounds, this); - } - - if (this._loaded) { - this._panInsideMaxBounds(); - } - - return this.on('moveend', this._panInsideMaxBounds, this); - }, - - panInsideBounds: function (bounds, options) { - var center = this.getCenter(), - newCenter = this._limitCenter(center, this._zoom, bounds); - - if (center.equals(newCenter)) { return this; } - - return this.panTo(newCenter, options); - }, - - addLayer: function (layer) { - // TODO method is too big, refactor - - var id = L.stamp(layer); - - if (this._layers[id]) { return this; } - - this._layers[id] = layer; - - // TODO getMaxZoom, getMinZoom in ILayer (instead of options) - if (layer.options && (!isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom))) { - this._zoomBoundLayers[id] = layer; - this._updateZoomLevels(); - } - - // TODO looks ugly, refactor!!! - if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) { - this._tileLayersNum++; - this._tileLayersToLoad++; - layer.on('load', this._onTileLayerLoad, this); - } - - if (this._loaded) { - this._layerAdd(layer); - } - - return this; - }, - - removeLayer: function (layer) { - var id = L.stamp(layer); - - if (!this._layers[id]) { return this; } - - if (this._loaded) { - layer.onRemove(this); - } - - delete this._layers[id]; - - if (this._loaded) { - this.fire('layerremove', {layer: layer}); - } - - if (this._zoomBoundLayers[id]) { - delete this._zoomBoundLayers[id]; - this._updateZoomLevels(); - } - - // TODO looks ugly, refactor - if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) { - this._tileLayersNum--; - this._tileLayersToLoad--; - layer.off('load', this._onTileLayerLoad, this); - } - - return this; - }, - - hasLayer: function (layer) { - if (!layer) { return false; } - - return (L.stamp(layer) in this._layers); - }, - - eachLayer: function (method, context) { - for (var i in this._layers) { - method.call(context, this._layers[i]); - } - return this; - }, - - invalidateSize: function (options) { - if (!this._loaded) { return this; } - - options = L.extend({ - animate: false, - pan: true - }, options === true ? {animate: true} : options); - - var oldSize = this.getSize(); - this._sizeChanged = true; - this._initialCenter = null; - - var newSize = this.getSize(), - oldCenter = oldSize.divideBy(2).round(), - newCenter = newSize.divideBy(2).round(), - offset = oldCenter.subtract(newCenter); - - if (!offset.x && !offset.y) { return this; } - - if (options.animate && options.pan) { - this.panBy(offset); - - } else { - if (options.pan) { - this._rawPanBy(offset); - } - - this.fire('move'); - - if (options.debounceMoveend) { - clearTimeout(this._sizeTimer); - this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200); - } else { - this.fire('moveend'); - } - } - - return this.fire('resize', { - oldSize: oldSize, - newSize: newSize - }); - }, - - // TODO handler.addTo - addHandler: function (name, HandlerClass) { - if (!HandlerClass) { return this; } - - var handler = this[name] = new HandlerClass(this); - - this._handlers.push(handler); - - if (this.options[name]) { - handler.enable(); - } - - return this; - }, - - remove: function () { - if (this._loaded) { - this.fire('unload'); - } - - this._initEvents('off'); - - try { - // throws error in IE6-8 - delete this._container._leaflet; - } catch (e) { - this._container._leaflet = undefined; - } - - this._clearPanes(); - if (this._clearControlPos) { - this._clearControlPos(); - } - - this._clearHandlers(); - - return this; - }, - - - // public methods for getting map state - - getCenter: function () { // (Boolean) -> LatLng - this._checkIfLoaded(); - - if (this._initialCenter && !this._moved()) { - return this._initialCenter; - } - return this.layerPointToLatLng(this._getCenterLayerPoint()); - }, - - getZoom: function () { - return this._zoom; - }, - - getBounds: function () { - var bounds = this.getPixelBounds(), - sw = this.unproject(bounds.getBottomLeft()), - ne = this.unproject(bounds.getTopRight()); - - return new L.LatLngBounds(sw, ne); - }, - - getMinZoom: function () { - return this.options.minZoom === undefined ? - (this._layersMinZoom === undefined ? 0 : this._layersMinZoom) : - this.options.minZoom; - }, - - getMaxZoom: function () { - return this.options.maxZoom === undefined ? - (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) : - this.options.maxZoom; - }, - - getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number - bounds = L.latLngBounds(bounds); - - var zoom = this.getMinZoom() - (inside ? 1 : 0), - maxZoom = this.getMaxZoom(), - size = this.getSize(), - - nw = bounds.getNorthWest(), - se = bounds.getSouthEast(), - - zoomNotFound = true, - boundsSize; - - padding = L.point(padding || [0, 0]); - - do { - zoom++; - boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding); - zoomNotFound = !inside ? size.contains(boundsSize) : boundsSize.x < size.x || boundsSize.y < size.y; - - } while (zoomNotFound && zoom <= maxZoom); - - if (zoomNotFound && inside) { - return null; - } - - return inside ? zoom : zoom - 1; - }, - - getSize: function () { - if (!this._size || this._sizeChanged) { - this._size = new L.Point( - this._container.clientWidth, - this._container.clientHeight); - - this._sizeChanged = false; - } - return this._size.clone(); - }, - - getPixelBounds: function () { - var topLeftPoint = this._getTopLeftPoint(); - return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); - }, - - getPixelOrigin: function () { - this._checkIfLoaded(); - return this._initialTopLeftPoint; - }, - - getPanes: function () { - return this._panes; - }, - - getContainer: function () { - return this._container; - }, - - - // TODO replace with universal implementation after refactoring projections - - getZoomScale: function (toZoom) { - var crs = this.options.crs; - return crs.scale(toZoom) / crs.scale(this._zoom); - }, - - getScaleZoom: function (scale) { - return this._zoom + (Math.log(scale) / Math.LN2); - }, - - - // conversion methods - - project: function (latlng, zoom) { // (LatLng[, Number]) -> Point - zoom = zoom === undefined ? this._zoom : zoom; - return this.options.crs.latLngToPoint(L.latLng(latlng), zoom); - }, - - unproject: function (point, zoom) { // (Point[, Number]) -> LatLng - zoom = zoom === undefined ? this._zoom : zoom; - return this.options.crs.pointToLatLng(L.point(point), zoom); - }, - - layerPointToLatLng: function (point) { // (Point) - var projectedPoint = L.point(point).add(this.getPixelOrigin()); - return this.unproject(projectedPoint); - }, - - latLngToLayerPoint: function (latlng) { // (LatLng) - var projectedPoint = this.project(L.latLng(latlng))._round(); - return projectedPoint._subtract(this.getPixelOrigin()); - }, - - containerPointToLayerPoint: function (point) { // (Point) - return L.point(point).subtract(this._getMapPanePos()); - }, - - layerPointToContainerPoint: function (point) { // (Point) - return L.point(point).add(this._getMapPanePos()); - }, - - containerPointToLatLng: function (point) { - var layerPoint = this.containerPointToLayerPoint(L.point(point)); - return this.layerPointToLatLng(layerPoint); - }, - - latLngToContainerPoint: function (latlng) { - return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng))); - }, - - mouseEventToContainerPoint: function (e) { // (MouseEvent) - return L.DomEvent.getMousePosition(e, this._container); - }, - - mouseEventToLayerPoint: function (e) { // (MouseEvent) - return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e)); - }, - - mouseEventToLatLng: function (e) { // (MouseEvent) - return this.layerPointToLatLng(this.mouseEventToLayerPoint(e)); - }, - - - // map initialization methods - - _initContainer: function (id) { - var container = this._container = L.DomUtil.get(id); - - if (!container) { - throw new Error('Map container not found.'); - } else if (container._leaflet) { - throw new Error('Map container is already initialized.'); - } - - container._leaflet = true; - }, - - _initLayout: function () { - var container = this._container; - - L.DomUtil.addClass(container, 'leaflet-container' + - (L.Browser.touch ? ' leaflet-touch' : '') + - (L.Browser.retina ? ' leaflet-retina' : '') + - (L.Browser.ielt9 ? ' leaflet-oldie' : '') + - (this.options.fadeAnimation ? ' leaflet-fade-anim' : '')); - - var position = L.DomUtil.getStyle(container, 'position'); - - if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') { - container.style.position = 'relative'; - } - - this._initPanes(); - - if (this._initControlPos) { - this._initControlPos(); - } - }, - - _initPanes: function () { - var panes = this._panes = {}; - - this._mapPane = panes.mapPane = this._createPane('leaflet-map-pane', this._container); - - this._tilePane = panes.tilePane = this._createPane('leaflet-tile-pane', this._mapPane); - panes.objectsPane = this._createPane('leaflet-objects-pane', this._mapPane); - panes.shadowPane = this._createPane('leaflet-shadow-pane'); - panes.overlayPane = this._createPane('leaflet-overlay-pane'); - panes.markerPane = this._createPane('leaflet-marker-pane'); - panes.popupPane = this._createPane('leaflet-popup-pane'); - - var zoomHide = ' leaflet-zoom-hide'; - - if (!this.options.markerZoomAnimation) { - L.DomUtil.addClass(panes.markerPane, zoomHide); - L.DomUtil.addClass(panes.shadowPane, zoomHide); - L.DomUtil.addClass(panes.popupPane, zoomHide); - } - }, - - _createPane: function (className, container) { - return L.DomUtil.create('div', className, container || this._panes.objectsPane); - }, - - _clearPanes: function () { - this._container.removeChild(this._mapPane); - }, - - _addLayers: function (layers) { - layers = layers ? (L.Util.isArray(layers) ? layers : [layers]) : []; - - for (var i = 0, len = layers.length; i < len; i++) { - this.addLayer(layers[i]); - } - }, - - - // private methods that modify map state - - _resetView: function (center, zoom, preserveMapOffset, afterZoomAnim) { - - var zoomChanged = (this._zoom !== zoom); - - if (!afterZoomAnim) { - this.fire('movestart'); - - if (zoomChanged) { - this.fire('zoomstart'); - } - } - - this._zoom = zoom; - this._initialCenter = center; - - this._initialTopLeftPoint = this._getNewTopLeftPoint(center); - - if (!preserveMapOffset) { - L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); - } else { - this._initialTopLeftPoint._add(this._getMapPanePos()); - } - - this._tileLayersToLoad = this._tileLayersNum; - - var loading = !this._loaded; - this._loaded = true; - - this.fire('viewreset', {hard: !preserveMapOffset}); - - if (loading) { - this.fire('load'); - this.eachLayer(this._layerAdd, this); - } - - this.fire('move'); - - if (zoomChanged || afterZoomAnim) { - this.fire('zoomend'); - } - - this.fire('moveend', {hard: !preserveMapOffset}); - }, - - _rawPanBy: function (offset) { - L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); - }, - - _getZoomSpan: function () { - return this.getMaxZoom() - this.getMinZoom(); - }, - - _updateZoomLevels: function () { - var i, - minZoom = Infinity, - maxZoom = -Infinity, - oldZoomSpan = this._getZoomSpan(); - - for (i in this._zoomBoundLayers) { - var layer = this._zoomBoundLayers[i]; - if (!isNaN(layer.options.minZoom)) { - minZoom = Math.min(minZoom, layer.options.minZoom); - } - if (!isNaN(layer.options.maxZoom)) { - maxZoom = Math.max(maxZoom, layer.options.maxZoom); - } - } - - if (i === undefined) { // we have no tilelayers - this._layersMaxZoom = this._layersMinZoom = undefined; - } else { - this._layersMaxZoom = maxZoom; - this._layersMinZoom = minZoom; - } - - if (oldZoomSpan !== this._getZoomSpan()) { - this.fire('zoomlevelschange'); - } - }, - - _panInsideMaxBounds: function () { - this.panInsideBounds(this.options.maxBounds); - }, - - _checkIfLoaded: function () { - if (!this._loaded) { - throw new Error('Set map center and zoom first.'); - } - }, - - // map events - - _initEvents: function (onOff) { - if (!L.DomEvent) { return; } - - onOff = onOff || 'on'; - - L.DomEvent[onOff](this._container, 'click', this._onMouseClick, this); - - var events = ['dblclick', 'mousedown', 'mouseup', 'mouseenter', - 'mouseleave', 'mousemove', 'contextmenu'], - i, len; - - for (i = 0, len = events.length; i < len; i++) { - L.DomEvent[onOff](this._container, events[i], this._fireMouseEvent, this); - } - - if (this.options.trackResize) { - L.DomEvent[onOff](window, 'resize', this._onResize, this); - } - }, - - _onResize: function () { - L.Util.cancelAnimFrame(this._resizeRequest); - this._resizeRequest = L.Util.requestAnimFrame( - function () { this.invalidateSize({debounceMoveend: true}); }, this, false, this._container); - }, - - _onMouseClick: function (e) { - if (!this._loaded || (!e._simulated && - ((this.dragging && this.dragging.moved()) || - (this.boxZoom && this.boxZoom.moved()))) || - L.DomEvent._skipped(e)) { return; } - - this.fire('preclick'); - this._fireMouseEvent(e); - }, - - _fireMouseEvent: function (e) { - if (!this._loaded || L.DomEvent._skipped(e)) { return; } - - var type = e.type; - - type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type)); - - if (!this.hasEventListeners(type)) { return; } - - if (type === 'contextmenu') { - L.DomEvent.preventDefault(e); - } - - var containerPoint = this.mouseEventToContainerPoint(e), - layerPoint = this.containerPointToLayerPoint(containerPoint), - latlng = this.layerPointToLatLng(layerPoint); - - this.fire(type, { - latlng: latlng, - layerPoint: layerPoint, - containerPoint: containerPoint, - originalEvent: e - }); - }, - - _onTileLayerLoad: function () { - this._tileLayersToLoad--; - if (this._tileLayersNum && !this._tileLayersToLoad) { - this.fire('tilelayersload'); - } - }, - - _clearHandlers: function () { - for (var i = 0, len = this._handlers.length; i < len; i++) { - this._handlers[i].disable(); - } - }, - - whenReady: function (callback, context) { - if (this._loaded) { - callback.call(context || this, this); - } else { - this.on('load', callback, context); - } - return this; - }, - - _layerAdd: function (layer) { - layer.onAdd(this); - this.fire('layeradd', {layer: layer}); - }, - - - // private methods for getting map state - - _getMapPanePos: function () { - return L.DomUtil.getPosition(this._mapPane); - }, - - _moved: function () { - var pos = this._getMapPanePos(); - return pos && !pos.equals([0, 0]); - }, - - _getTopLeftPoint: function () { - return this.getPixelOrigin().subtract(this._getMapPanePos()); - }, - - _getNewTopLeftPoint: function (center, zoom) { - var viewHalf = this.getSize()._divideBy(2); - // TODO round on display, not calculation to increase precision? - return this.project(center, zoom)._subtract(viewHalf)._round(); - }, - - _latLngToNewLayerPoint: function (latlng, newZoom, newCenter) { - var topLeft = this._getNewTopLeftPoint(newCenter, newZoom).add(this._getMapPanePos()); - return this.project(latlng, newZoom)._subtract(topLeft); - }, - - // layer point of the current center - _getCenterLayerPoint: function () { - return this.containerPointToLayerPoint(this.getSize()._divideBy(2)); - }, - - // offset of the specified place to the current center in pixels - _getCenterOffset: function (latlng) { - return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint()); - }, - - // adjust center for view to get inside bounds - _limitCenter: function (center, zoom, bounds) { - - if (!bounds) { return center; } - - var centerPoint = this.project(center, zoom), - viewHalf = this.getSize().divideBy(2), - viewBounds = new L.Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)), - offset = this._getBoundsOffset(viewBounds, bounds, zoom); - - return this.unproject(centerPoint.add(offset), zoom); - }, - - // adjust offset for view to get inside bounds - _limitOffset: function (offset, bounds) { - if (!bounds) { return offset; } - - var viewBounds = this.getPixelBounds(), - newBounds = new L.Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset)); - - return offset.add(this._getBoundsOffset(newBounds, bounds)); - }, - - // returns offset needed for pxBounds to get inside maxBounds at a specified zoom - _getBoundsOffset: function (pxBounds, maxBounds, zoom) { - var nwOffset = this.project(maxBounds.getNorthWest(), zoom).subtract(pxBounds.min), - seOffset = this.project(maxBounds.getSouthEast(), zoom).subtract(pxBounds.max), - - dx = this._rebound(nwOffset.x, -seOffset.x), - dy = this._rebound(nwOffset.y, -seOffset.y); - - return new L.Point(dx, dy); - }, - - _rebound: function (left, right) { - return left + right > 0 ? - Math.round(left - right) / 2 : - Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right)); - }, - - _limitZoom: function (zoom) { - var min = this.getMinZoom(), - max = this.getMaxZoom(); - - return Math.max(min, Math.min(max, zoom)); - } -}); - -L.map = function (id, options) { - return new L.Map(id, options); -}; - - -/* - * Mercator projection that takes into account that the Earth is not a perfect sphere. - * Less popular than spherical mercator; used by projections like EPSG:3395. - */ - -L.Projection.Mercator = { - MAX_LATITUDE: 85.0840591556, - - R_MINOR: 6356752.314245179, - R_MAJOR: 6378137, - - project: function (latlng) { // (LatLng) -> Point - var d = L.LatLng.DEG_TO_RAD, - max = this.MAX_LATITUDE, - lat = Math.max(Math.min(max, latlng.lat), -max), - r = this.R_MAJOR, - r2 = this.R_MINOR, - x = latlng.lng * d * r, - y = lat * d, - tmp = r2 / r, - eccent = Math.sqrt(1.0 - tmp * tmp), - con = eccent * Math.sin(y); - - con = Math.pow((1 - con) / (1 + con), eccent * 0.5); - - var ts = Math.tan(0.5 * ((Math.PI * 0.5) - y)) / con; - y = -r * Math.log(ts); - - return new L.Point(x, y); - }, - - unproject: function (point) { // (Point, Boolean) -> LatLng - var d = L.LatLng.RAD_TO_DEG, - r = this.R_MAJOR, - r2 = this.R_MINOR, - lng = point.x * d / r, - tmp = r2 / r, - eccent = Math.sqrt(1 - (tmp * tmp)), - ts = Math.exp(- point.y / r), - phi = (Math.PI / 2) - 2 * Math.atan(ts), - numIter = 15, - tol = 1e-7, - i = numIter, - dphi = 0.1, - con; - - while ((Math.abs(dphi) > tol) && (--i > 0)) { - con = eccent * Math.sin(phi); - dphi = (Math.PI / 2) - 2 * Math.atan(ts * - Math.pow((1.0 - con) / (1.0 + con), 0.5 * eccent)) - phi; - phi += dphi; - } - - return new L.LatLng(phi * d, lng); - } -}; - - - -L.CRS.EPSG3395 = L.extend({}, L.CRS, { - code: 'EPSG:3395', - - projection: L.Projection.Mercator, - - transformation: (function () { - var m = L.Projection.Mercator, - r = m.R_MAJOR, - scale = 0.5 / (Math.PI * r); - - return new L.Transformation(scale, 0.5, -scale, 0.5); - }()) -}); - - -/* - * L.TileLayer is used for standard xyz-numbered tile layers. - */ - -L.TileLayer = L.Class.extend({ - includes: L.Mixin.Events, - - options: { - minZoom: 0, - maxZoom: 18, - tileSize: 256, - subdomains: 'abc', - errorTileUrl: '', - attribution: '', - zoomOffset: 0, - opacity: 1, - /* - maxNativeZoom: null, - zIndex: null, - tms: false, - continuousWorld: false, - noWrap: false, - zoomReverse: false, - detectRetina: false, - reuseTiles: false, - bounds: false, - */ - unloadInvisibleTiles: L.Browser.mobile, - updateWhenIdle: L.Browser.mobile - }, - - initialize: function (url, options) { - options = L.setOptions(this, options); - - // detecting retina displays, adjusting tileSize and zoom levels - if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) { - - options.tileSize = Math.floor(options.tileSize / 2); - options.zoomOffset++; - - if (options.minZoom > 0) { - options.minZoom--; - } - this.options.maxZoom--; - } - - if (options.bounds) { - options.bounds = L.latLngBounds(options.bounds); - } - - this._url = url; - - var subdomains = this.options.subdomains; - - if (typeof subdomains === 'string') { - this.options.subdomains = subdomains.split(''); - } - }, - - onAdd: function (map) { - this._map = map; - this._animated = map._zoomAnimated; - - // create a container div for tiles - this._initContainer(); - - // set up events - map.on({ - 'viewreset': this._reset, - 'moveend': this._update - }, this); - - if (this._animated) { - map.on({ - 'zoomanim': this._animateZoom, - 'zoomend': this._endZoomAnim - }, this); - } - - if (!this.options.updateWhenIdle) { - this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this); - map.on('move', this._limitedUpdate, this); - } - - this._reset(); - this._update(); - }, - - addTo: function (map) { - map.addLayer(this); - return this; - }, - - onRemove: function (map) { - this._container.parentNode.removeChild(this._container); - - map.off({ - 'viewreset': this._reset, - 'moveend': this._update - }, this); - - if (this._animated) { - map.off({ - 'zoomanim': this._animateZoom, - 'zoomend': this._endZoomAnim - }, this); - } - - if (!this.options.updateWhenIdle) { - map.off('move', this._limitedUpdate, this); - } - - this._container = null; - this._map = null; - }, - - bringToFront: function () { - var pane = this._map._panes.tilePane; - - if (this._container) { - pane.appendChild(this._container); - this._setAutoZIndex(pane, Math.max); - } - - return this; - }, - - bringToBack: function () { - var pane = this._map._panes.tilePane; - - if (this._container) { - pane.insertBefore(this._container, pane.firstChild); - this._setAutoZIndex(pane, Math.min); - } - - return this; - }, - - getAttribution: function () { - return this.options.attribution; - }, - - getContainer: function () { - return this._container; - }, - - setOpacity: function (opacity) { - this.options.opacity = opacity; - - if (this._map) { - this._updateOpacity(); - } - - return this; - }, - - setZIndex: function (zIndex) { - this.options.zIndex = zIndex; - this._updateZIndex(); - - return this; - }, - - setUrl: function (url, noRedraw) { - this._url = url; - - if (!noRedraw) { - this.redraw(); - } - - return this; - }, - - redraw: function () { - if (this._map) { - this._reset({hard: true}); - this._update(); - } - return this; - }, - - _updateZIndex: function () { - if (this._container && this.options.zIndex !== undefined) { - this._container.style.zIndex = this.options.zIndex; - } - }, - - _setAutoZIndex: function (pane, compare) { - - var layers = pane.children, - edgeZIndex = -compare(Infinity, -Infinity), // -Infinity for max, Infinity for min - zIndex, i, len; - - for (i = 0, len = layers.length; i < len; i++) { - - if (layers[i] !== this._container) { - zIndex = parseInt(layers[i].style.zIndex, 10); - - if (!isNaN(zIndex)) { - edgeZIndex = compare(edgeZIndex, zIndex); - } - } - } - - this.options.zIndex = this._container.style.zIndex = - (isFinite(edgeZIndex) ? edgeZIndex : 0) + compare(1, -1); - }, - - _updateOpacity: function () { - var i, - tiles = this._tiles; - - if (L.Browser.ielt9) { - for (i in tiles) { - L.DomUtil.setOpacity(tiles[i], this.options.opacity); - } - } else { - L.DomUtil.setOpacity(this._container, this.options.opacity); - } - }, - - _initContainer: function () { - var tilePane = this._map._panes.tilePane; - - if (!this._container) { - this._container = L.DomUtil.create('div', 'leaflet-layer'); - - this._updateZIndex(); - - if (this._animated) { - var className = 'leaflet-tile-container'; - - this._bgBuffer = L.DomUtil.create('div', className, this._container); - this._tileContainer = L.DomUtil.create('div', className, this._container); - - } else { - this._tileContainer = this._container; - } - - tilePane.appendChild(this._container); - - if (this.options.opacity < 1) { - this._updateOpacity(); - } - } - }, - - _reset: function (e) { - for (var key in this._tiles) { - this.fire('tileunload', {tile: this._tiles[key]}); - } - - this._tiles = {}; - this._tilesToLoad = 0; - - if (this.options.reuseTiles) { - this._unusedTiles = []; - } - - this._tileContainer.innerHTML = ''; - - if (this._animated && e && e.hard) { - this._clearBgBuffer(); - } - - this._initContainer(); - }, - - _getTileSize: function () { - var map = this._map, - zoom = map.getZoom() + this.options.zoomOffset, - zoomN = this.options.maxNativeZoom, - tileSize = this.options.tileSize; - - if (zoomN && zoom > zoomN) { - tileSize = Math.round(map.getZoomScale(zoom) / map.getZoomScale(zoomN) * tileSize); - } - - return tileSize; - }, - - _update: function () { - - if (!this._map) { return; } - - var map = this._map, - bounds = map.getPixelBounds(), - zoom = map.getZoom(), - tileSize = this._getTileSize(); - - if (zoom > this.options.maxZoom || zoom < this.options.minZoom) { - return; - } - - var tileBounds = L.bounds( - bounds.min.divideBy(tileSize)._floor(), - bounds.max.divideBy(tileSize)._floor()); - - this._addTilesFromCenterOut(tileBounds); - - if (this.options.unloadInvisibleTiles || this.options.reuseTiles) { - this._removeOtherTiles(tileBounds); - } - }, - - _addTilesFromCenterOut: function (bounds) { - var queue = [], - center = bounds.getCenter(); - - var j, i, point; - - for (j = bounds.min.y; j <= bounds.max.y; j++) { - for (i = bounds.min.x; i <= bounds.max.x; i++) { - point = new L.Point(i, j); - - if (this._tileShouldBeLoaded(point)) { - queue.push(point); - } - } - } - - var tilesToLoad = queue.length; - - if (tilesToLoad === 0) { return; } - - // load tiles in order of their distance to center - queue.sort(function (a, b) { - return a.distanceTo(center) - b.distanceTo(center); - }); - - var fragment = document.createDocumentFragment(); - - // if its the first batch of tiles to load - if (!this._tilesToLoad) { - this.fire('loading'); - } - - this._tilesToLoad += tilesToLoad; - - for (i = 0; i < tilesToLoad; i++) { - this._addTile(queue[i], fragment); - } - - this._tileContainer.appendChild(fragment); - }, - - _tileShouldBeLoaded: function (tilePoint) { - if ((tilePoint.x + ':' + tilePoint.y) in this._tiles) { - return false; // already loaded - } - - var options = this.options; - - if (!options.continuousWorld) { - var limit = this._getWrapTileNum(); - - // don't load if exceeds world bounds - if ((options.noWrap && (tilePoint.x < 0 || tilePoint.x >= limit.x)) || - tilePoint.y < 0 || tilePoint.y >= limit.y) { return false; } - } - - if (options.bounds) { - var tileSize = this._getTileSize(), - nwPoint = tilePoint.multiplyBy(tileSize), - sePoint = nwPoint.add([tileSize, tileSize]), - nw = this._map.unproject(nwPoint), - se = this._map.unproject(sePoint); - - // TODO temporary hack, will be removed after refactoring projections - // https://github.com/Leaflet/Leaflet/issues/1618 - if (!options.continuousWorld && !options.noWrap) { - nw = nw.wrap(); - se = se.wrap(); - } - - if (!options.bounds.intersects([nw, se])) { return false; } - } - - return true; - }, - - _removeOtherTiles: function (bounds) { - var kArr, x, y, key; - - for (key in this._tiles) { - kArr = key.split(':'); - x = parseInt(kArr[0], 10); - y = parseInt(kArr[1], 10); - - // remove tile if it's out of bounds - if (x < bounds.min.x || x > bounds.max.x || y < bounds.min.y || y > bounds.max.y) { - this._removeTile(key); - } - } - }, - - _removeTile: function (key) { - var tile = this._tiles[key]; - - this.fire('tileunload', {tile: tile, url: tile.src}); - - if (this.options.reuseTiles) { - L.DomUtil.removeClass(tile, 'leaflet-tile-loaded'); - this._unusedTiles.push(tile); - - } else if (tile.parentNode === this._tileContainer) { - this._tileContainer.removeChild(tile); - } - - // for https://github.com/CloudMade/Leaflet/issues/137 - if (!L.Browser.android) { - tile.onload = null; - tile.src = L.Util.emptyImageUrl; - } - - delete this._tiles[key]; - }, - - _addTile: function (tilePoint, container) { - var tilePos = this._getTilePos(tilePoint); - - // get unused tile - or create a new tile - var tile = this._getTile(); - - /* - Chrome 20 layouts much faster with top/left (verify with timeline, frames) - Android 4 browser has display issues with top/left and requires transform instead - (other browsers don't currently care) - see debug/hacks/jitter.html for an example - */ - L.DomUtil.setPosition(tile, tilePos, L.Browser.chrome); - - this._tiles[tilePoint.x + ':' + tilePoint.y] = tile; - - this._loadTile(tile, tilePoint); - - if (tile.parentNode !== this._tileContainer) { - container.appendChild(tile); - } - }, - - _getZoomForUrl: function () { - - var options = this.options, - zoom = this._map.getZoom(); - - if (options.zoomReverse) { - zoom = options.maxZoom - zoom; - } - - zoom += options.zoomOffset; - - return options.maxNativeZoom ? Math.min(zoom, options.maxNativeZoom) : zoom; - }, - - _getTilePos: function (tilePoint) { - var origin = this._map.getPixelOrigin(), - tileSize = this._getTileSize(); - - return tilePoint.multiplyBy(tileSize).subtract(origin); - }, - - // image-specific code (override to implement e.g. Canvas or SVG tile layer) - - getTileUrl: function (tilePoint) { - return L.Util.template(this._url, L.extend({ - s: this._getSubdomain(tilePoint), - z: tilePoint.z, - x: tilePoint.x, - y: tilePoint.y - }, this.options)); - }, - - _getWrapTileNum: function () { - var crs = this._map.options.crs, - size = crs.getSize(this._map.getZoom()); - return size.divideBy(this._getTileSize())._floor(); - }, - - _adjustTilePoint: function (tilePoint) { - - var limit = this._getWrapTileNum(); - - // wrap tile coordinates - if (!this.options.continuousWorld && !this.options.noWrap) { - tilePoint.x = ((tilePoint.x % limit.x) + limit.x) % limit.x; - } - - if (this.options.tms) { - tilePoint.y = limit.y - tilePoint.y - 1; - } - - tilePoint.z = this._getZoomForUrl(); - }, - - _getSubdomain: function (tilePoint) { - var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length; - return this.options.subdomains[index]; - }, - - _getTile: function () { - if (this.options.reuseTiles && this._unusedTiles.length > 0) { - var tile = this._unusedTiles.pop(); - this._resetTile(tile); - return tile; - } - return this._createTile(); - }, - - // Override if data stored on a tile needs to be cleaned up before reuse - _resetTile: function (/*tile*/) {}, - - _createTile: function () { - var tile = L.DomUtil.create('img', 'leaflet-tile'); - tile.style.width = tile.style.height = this._getTileSize() + 'px'; - tile.galleryimg = 'no'; - - tile.onselectstart = tile.onmousemove = L.Util.falseFn; - - if (L.Browser.ielt9 && this.options.opacity !== undefined) { - L.DomUtil.setOpacity(tile, this.options.opacity); - } - // without this hack, tiles disappear after zoom on Chrome for Android - // https://github.com/Leaflet/Leaflet/issues/2078 - if (L.Browser.mobileWebkit3d) { - tile.style.WebkitBackfaceVisibility = 'hidden'; - } - return tile; - }, - - _loadTile: function (tile, tilePoint) { - tile._layer = this; - tile.onload = this._tileOnLoad; - tile.onerror = this._tileOnError; - - this._adjustTilePoint(tilePoint); - tile.src = this.getTileUrl(tilePoint); - - this.fire('tileloadstart', { - tile: tile, - url: tile.src - }); - }, - - _tileLoaded: function () { - this._tilesToLoad--; - - if (this._animated) { - L.DomUtil.addClass(this._tileContainer, 'leaflet-zoom-animated'); - } - - if (!this._tilesToLoad) { - this.fire('load'); - - if (this._animated) { - // clear scaled tiles after all new tiles are loaded (for performance) - clearTimeout(this._clearBgBufferTimer); - this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 500); - } - } - }, - - _tileOnLoad: function () { - var layer = this._layer; - - //Only if we are loading an actual image - if (this.src !== L.Util.emptyImageUrl) { - L.DomUtil.addClass(this, 'leaflet-tile-loaded'); - - layer.fire('tileload', { - tile: this, - url: this.src - }); - } - - layer._tileLoaded(); - }, - - _tileOnError: function () { - var layer = this._layer; - - layer.fire('tileerror', { - tile: this, - url: this.src - }); - - var newUrl = layer.options.errorTileUrl; - if (newUrl) { - this.src = newUrl; - } - - layer._tileLoaded(); - } -}); - -L.tileLayer = function (url, options) { - return new L.TileLayer(url, options); -}; - - -/* - * L.TileLayer.WMS is used for putting WMS tile layers on the map. - */ - -L.TileLayer.WMS = L.TileLayer.extend({ - - defaultWmsParams: { - service: 'WMS', - request: 'GetMap', - version: '1.1.1', - layers: '', - styles: '', - format: 'image/jpeg', - transparent: false - }, - - initialize: function (url, options) { // (String, Object) - - this._url = url; - - var wmsParams = L.extend({}, this.defaultWmsParams), - tileSize = options.tileSize || this.options.tileSize; - - if (options.detectRetina && L.Browser.retina) { - wmsParams.width = wmsParams.height = tileSize * 2; - } else { - wmsParams.width = wmsParams.height = tileSize; - } - - for (var i in options) { - // all keys that are not TileLayer options go to WMS params - if (!this.options.hasOwnProperty(i) && i !== 'crs') { - wmsParams[i] = options[i]; - } - } - - this.wmsParams = wmsParams; - - L.setOptions(this, options); - }, - - onAdd: function (map) { - - this._crs = this.options.crs || map.options.crs; - - this._wmsVersion = parseFloat(this.wmsParams.version); - - var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs'; - this.wmsParams[projectionKey] = this._crs.code; - - L.TileLayer.prototype.onAdd.call(this, map); - }, - - getTileUrl: function (tilePoint) { // (Point, Number) -> String - - var map = this._map, - tileSize = this.options.tileSize, - - nwPoint = tilePoint.multiplyBy(tileSize), - sePoint = nwPoint.add([tileSize, tileSize]), - - nw = this._crs.project(map.unproject(nwPoint, tilePoint.z)), - se = this._crs.project(map.unproject(sePoint, tilePoint.z)), - bbox = this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ? - [se.y, nw.x, nw.y, se.x].join(',') : - [nw.x, se.y, se.x, nw.y].join(','), - - url = L.Util.template(this._url, {s: this._getSubdomain(tilePoint)}); - - return url + L.Util.getParamString(this.wmsParams, url, true) + '&BBOX=' + bbox; - }, - - setParams: function (params, noRedraw) { - - L.extend(this.wmsParams, params); - - if (!noRedraw) { - this.redraw(); - } - - return this; - } -}); - -L.tileLayer.wms = function (url, options) { - return new L.TileLayer.WMS(url, options); -}; - - -/* - * L.TileLayer.Canvas is a class that you can use as a base for creating - * dynamically drawn Canvas-based tile layers. - */ - -L.TileLayer.Canvas = L.TileLayer.extend({ - options: { - async: false - }, - - initialize: function (options) { - L.setOptions(this, options); - }, - - redraw: function () { - if (this._map) { - this._reset({hard: true}); - this._update(); - } - - for (var i in this._tiles) { - this._redrawTile(this._tiles[i]); - } - return this; - }, - - _redrawTile: function (tile) { - this.drawTile(tile, tile._tilePoint, this._map._zoom); - }, - - _createTile: function () { - var tile = L.DomUtil.create('canvas', 'leaflet-tile'); - tile.width = tile.height = this.options.tileSize; - tile.onselectstart = tile.onmousemove = L.Util.falseFn; - return tile; - }, - - _loadTile: function (tile, tilePoint) { - tile._layer = this; - tile._tilePoint = tilePoint; - - this._redrawTile(tile); - - if (!this.options.async) { - this.tileDrawn(tile); - } - }, - - drawTile: function (/*tile, tilePoint*/) { - // override with rendering code - }, - - tileDrawn: function (tile) { - this._tileOnLoad.call(tile); - } -}); - - -L.tileLayer.canvas = function (options) { - return new L.TileLayer.Canvas(options); -}; - - -/* - * L.ImageOverlay is used to overlay images over the map (to specific geographical bounds). - */ - -L.ImageOverlay = L.Class.extend({ - includes: L.Mixin.Events, - - options: { - opacity: 1 - }, - - initialize: function (url, bounds, options) { // (String, LatLngBounds, Object) - this._url = url; - this._bounds = L.latLngBounds(bounds); - - L.setOptions(this, options); - }, - - onAdd: function (map) { - this._map = map; - - if (!this._image) { - this._initImage(); - } - - map._panes.overlayPane.appendChild(this._image); - - map.on('viewreset', this._reset, this); - - if (map.options.zoomAnimation && L.Browser.any3d) { - map.on('zoomanim', this._animateZoom, this); - } - - this._reset(); - }, - - onRemove: function (map) { - map.getPanes().overlayPane.removeChild(this._image); - - map.off('viewreset', this._reset, this); - - if (map.options.zoomAnimation) { - map.off('zoomanim', this._animateZoom, this); - } - }, - - addTo: function (map) { - map.addLayer(this); - return this; - }, - - setOpacity: function (opacity) { - this.options.opacity = opacity; - this._updateOpacity(); - return this; - }, - - // TODO remove bringToFront/bringToBack duplication from TileLayer/Path - bringToFront: function () { - if (this._image) { - this._map._panes.overlayPane.appendChild(this._image); - } - return this; - }, - - bringToBack: function () { - var pane = this._map._panes.overlayPane; - if (this._image) { - pane.insertBefore(this._image, pane.firstChild); - } - return this; - }, - - setUrl: function (url) { - this._url = url; - this._image.src = this._url; - }, - - getAttribution: function () { - return this.options.attribution; - }, - - _initImage: function () { - this._image = L.DomUtil.create('img', 'leaflet-image-layer'); - - if (this._map.options.zoomAnimation && L.Browser.any3d) { - L.DomUtil.addClass(this._image, 'leaflet-zoom-animated'); - } else { - L.DomUtil.addClass(this._image, 'leaflet-zoom-hide'); - } - - this._updateOpacity(); - - //TODO createImage util method to remove duplication - L.extend(this._image, { - galleryimg: 'no', - onselectstart: L.Util.falseFn, - onmousemove: L.Util.falseFn, - onload: L.bind(this._onImageLoad, this), - src: this._url - }); - }, - - _animateZoom: function (e) { - var map = this._map, - image = this._image, - scale = map.getZoomScale(e.zoom), - nw = this._bounds.getNorthWest(), - se = this._bounds.getSouthEast(), - - topLeft = map._latLngToNewLayerPoint(nw, e.zoom, e.center), - size = map._latLngToNewLayerPoint(se, e.zoom, e.center)._subtract(topLeft), - origin = topLeft._add(size._multiplyBy((1 / 2) * (1 - 1 / scale))); - - image.style[L.DomUtil.TRANSFORM] = - L.DomUtil.getTranslateString(origin) + ' scale(' + scale + ') '; - }, - - _reset: function () { - var image = this._image, - topLeft = this._map.latLngToLayerPoint(this._bounds.getNorthWest()), - size = this._map.latLngToLayerPoint(this._bounds.getSouthEast())._subtract(topLeft); - - L.DomUtil.setPosition(image, topLeft); - - image.style.width = size.x + 'px'; - image.style.height = size.y + 'px'; - }, - - _onImageLoad: function () { - this.fire('load'); - }, - - _updateOpacity: function () { - L.DomUtil.setOpacity(this._image, this.options.opacity); - } -}); - -L.imageOverlay = function (url, bounds, options) { - return new L.ImageOverlay(url, bounds, options); -}; - - -/* - * L.Icon is an image-based icon class that you can use with L.Marker for custom markers. - */ - -L.Icon = L.Class.extend({ - options: { - /* - iconUrl: (String) (required) - iconRetinaUrl: (String) (optional, used for retina devices if detected) - iconSize: (Point) (can be set through CSS) - iconAnchor: (Point) (centered by default, can be set in CSS with negative margins) - popupAnchor: (Point) (if not specified, popup opens in the anchor point) - shadowUrl: (String) (no shadow by default) - shadowRetinaUrl: (String) (optional, used for retina devices if detected) - shadowSize: (Point) - shadowAnchor: (Point) - */ - className: '' - }, - - initialize: function (options) { - L.setOptions(this, options); - }, - - createIcon: function (oldIcon) { - return this._createIcon('icon', oldIcon); - }, - - createShadow: function (oldIcon) { - return this._createIcon('shadow', oldIcon); - }, - - _createIcon: function (name, oldIcon) { - var src = this._getIconUrl(name); - - if (!src) { - if (name === 'icon') { - throw new Error('iconUrl not set in Icon options (see the docs).'); - } - return null; - } - - var img; - if (!oldIcon || oldIcon.tagName !== 'IMG') { - img = this._createImg(src); - } else { - img = this._createImg(src, oldIcon); - } - this._setIconStyles(img, name); - - return img; - }, - - _setIconStyles: function (img, name) { - var options = this.options, - size = L.point(options[name + 'Size']), - anchor; - - if (name === 'shadow') { - anchor = L.point(options.shadowAnchor || options.iconAnchor); - } else { - anchor = L.point(options.iconAnchor); - } - - if (!anchor && size) { - anchor = size.divideBy(2, true); - } - - img.className = 'leaflet-marker-' + name + ' ' + options.className; - - if (anchor) { - img.style.marginLeft = (-anchor.x) + 'px'; - img.style.marginTop = (-anchor.y) + 'px'; - } - - if (size) { - img.style.width = size.x + 'px'; - img.style.height = size.y + 'px'; - } - }, - - _createImg: function (src, el) { - el = el || document.createElement('img'); - el.src = src; - return el; - }, - - _getIconUrl: function (name) { - if (L.Browser.retina && this.options[name + 'RetinaUrl']) { - return this.options[name + 'RetinaUrl']; - } - return this.options[name + 'Url']; - } -}); - -L.icon = function (options) { - return new L.Icon(options); -}; - - -/* - * L.Icon.Default is the blue marker icon used by default in Leaflet. - */ - -L.Icon.Default = L.Icon.extend({ - - options: { - iconSize: [25, 41], - iconAnchor: [12, 41], - popupAnchor: [1, -34], - - shadowSize: [41, 41] - }, - - _getIconUrl: function (name) { - var key = name + 'Url'; - - if (this.options[key]) { - return this.options[key]; - } - - if (L.Browser.retina && name === 'icon') { - name += '-2x'; - } - - var path = L.Icon.Default.imagePath; - - if (!path) { - throw new Error('Couldn\'t autodetect L.Icon.Default.imagePath, set it manually.'); - } - - return path + '/marker-' + name + '.png'; - } -}); - -L.Icon.Default.imagePath = (function () { - var scripts = document.getElementsByTagName('script'), - leafletRe = /[\/^]leaflet[\-\._]?([\w\-\._]*)\.js\??/; - - var i, len, src, matches, path; - - for (i = 0, len = scripts.length; i < len; i++) { - src = scripts[i].src; - matches = src.match(leafletRe); - - if (matches) { - path = src.split(leafletRe)[0]; - return (path ? path + '/' : '') + 'images'; - } - } -}()); - - -/* - * L.Marker is used to display clickable/draggable icons on the map. - */ - -L.Marker = L.Class.extend({ - - includes: L.Mixin.Events, - - options: { - icon: new L.Icon.Default(), - title: '', - alt: '', - clickable: true, - draggable: false, - keyboard: true, - zIndexOffset: 0, - opacity: 1, - riseOnHover: false, - riseOffset: 250 - }, - - initialize: function (latlng, options) { - L.setOptions(this, options); - this._latlng = L.latLng(latlng); - }, - - onAdd: function (map) { - this._map = map; - - map.on('viewreset', this.update, this); - - this._initIcon(); - this.update(); - this.fire('add'); - - if (map.options.zoomAnimation && map.options.markerZoomAnimation) { - map.on('zoomanim', this._animateZoom, this); - } - }, - - addTo: function (map) { - map.addLayer(this); - return this; - }, - - onRemove: function (map) { - if (this.dragging) { - this.dragging.disable(); - } - - this._removeIcon(); - this._removeShadow(); - - this.fire('remove'); - - map.off({ - 'viewreset': this.update, - 'zoomanim': this._animateZoom - }, this); - - this._map = null; - }, - - getLatLng: function () { - return this._latlng; - }, - - setLatLng: function (latlng) { - this._latlng = L.latLng(latlng); - - this.update(); - - return this.fire('move', { latlng: this._latlng }); - }, - - setZIndexOffset: function (offset) { - this.options.zIndexOffset = offset; - this.update(); - - return this; - }, - - setIcon: function (icon) { - - this.options.icon = icon; - - if (this._map) { - this._initIcon(); - this.update(); - } - - if (this._popup) { - this.bindPopup(this._popup); - } - - return this; - }, - - update: function () { - if (this._icon) { - this._setPos(this._map.latLngToLayerPoint(this._latlng).round()); - } - return this; - }, - - _initIcon: function () { - var options = this.options, - map = this._map, - animation = (map.options.zoomAnimation && map.options.markerZoomAnimation), - classToAdd = animation ? 'leaflet-zoom-animated' : 'leaflet-zoom-hide'; - - var icon = options.icon.createIcon(this._icon), - addIcon = false; - - // if we're not reusing the icon, remove the old one and init new one - if (icon !== this._icon) { - if (this._icon) { - this._removeIcon(); - } - addIcon = true; - - if (options.title) { - icon.title = options.title; - } - - if (options.alt) { - icon.alt = options.alt; - } - } - - L.DomUtil.addClass(icon, classToAdd); - - if (options.keyboard) { - icon.tabIndex = '0'; - } - - this._icon = icon; - - this._initInteraction(); - - if (options.riseOnHover) { - L.DomEvent - .on(icon, 'mouseover', this._bringToFront, this) - .on(icon, 'mouseout', this._resetZIndex, this); - } - - var newShadow = options.icon.createShadow(this._shadow), - addShadow = false; - - if (newShadow !== this._shadow) { - this._removeShadow(); - addShadow = true; - } - - if (newShadow) { - L.DomUtil.addClass(newShadow, classToAdd); - } - this._shadow = newShadow; - - - if (options.opacity < 1) { - this._updateOpacity(); - } - - - var panes = this._map._panes; - - if (addIcon) { - panes.markerPane.appendChild(this._icon); - } - - if (newShadow && addShadow) { - panes.shadowPane.appendChild(this._shadow); - } - }, - - _removeIcon: function () { - if (this.options.riseOnHover) { - L.DomEvent - .off(this._icon, 'mouseover', this._bringToFront) - .off(this._icon, 'mouseout', this._resetZIndex); - } - - this._map._panes.markerPane.removeChild(this._icon); - - this._icon = null; - }, - - _removeShadow: function () { - if (this._shadow) { - this._map._panes.shadowPane.removeChild(this._shadow); - } - this._shadow = null; - }, - - _setPos: function (pos) { - L.DomUtil.setPosition(this._icon, pos); - - if (this._shadow) { - L.DomUtil.setPosition(this._shadow, pos); - } - - this._zIndex = pos.y + this.options.zIndexOffset; - - this._resetZIndex(); - }, - - _updateZIndex: function (offset) { - this._icon.style.zIndex = this._zIndex + offset; - }, - - _animateZoom: function (opt) { - var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round(); - - this._setPos(pos); - }, - - _initInteraction: function () { - - if (!this.options.clickable) { return; } - - // TODO refactor into something shared with Map/Path/etc. to DRY it up - - var icon = this._icon, - events = ['dblclick', 'mousedown', 'mouseover', 'mouseout', 'contextmenu']; - - L.DomUtil.addClass(icon, 'leaflet-clickable'); - L.DomEvent.on(icon, 'click', this._onMouseClick, this); - L.DomEvent.on(icon, 'keypress', this._onKeyPress, this); - - for (var i = 0; i < events.length; i++) { - L.DomEvent.on(icon, events[i], this._fireMouseEvent, this); - } - - if (L.Handler.MarkerDrag) { - this.dragging = new L.Handler.MarkerDrag(this); - - if (this.options.draggable) { - this.dragging.enable(); - } - } - }, - - _onMouseClick: function (e) { - var wasDragged = this.dragging && this.dragging.moved(); - - if (this.hasEventListeners(e.type) || wasDragged) { - L.DomEvent.stopPropagation(e); - } - - if (wasDragged) { return; } - - if ((!this.dragging || !this.dragging._enabled) && this._map.dragging && this._map.dragging.moved()) { return; } - - this.fire(e.type, { - originalEvent: e, - latlng: this._latlng - }); - }, - - _onKeyPress: function (e) { - if (e.keyCode === 13) { - this.fire('click', { - originalEvent: e, - latlng: this._latlng - }); - } - }, - - _fireMouseEvent: function (e) { - - this.fire(e.type, { - originalEvent: e, - latlng: this._latlng - }); - - // TODO proper custom event propagation - // this line will always be called if marker is in a FeatureGroup - if (e.type === 'contextmenu' && this.hasEventListeners(e.type)) { - L.DomEvent.preventDefault(e); - } - if (e.type !== 'mousedown') { - L.DomEvent.stopPropagation(e); - } else { - L.DomEvent.preventDefault(e); - } - }, - - setOpacity: function (opacity) { - this.options.opacity = opacity; - if (this._map) { - this._updateOpacity(); - } - - return this; - }, - - _updateOpacity: function () { - L.DomUtil.setOpacity(this._icon, this.options.opacity); - if (this._shadow) { - L.DomUtil.setOpacity(this._shadow, this.options.opacity); - } - }, - - _bringToFront: function () { - this._updateZIndex(this.options.riseOffset); - }, - - _resetZIndex: function () { - this._updateZIndex(0); - } -}); - -L.marker = function (latlng, options) { - return new L.Marker(latlng, options); -}; - - -/* - * L.DivIcon is a lightweight HTML-based icon class (as opposed to the image-based L.Icon) - * to use with L.Marker. - */ - -L.DivIcon = L.Icon.extend({ - options: { - iconSize: [12, 12], // also can be set through CSS - /* - iconAnchor: (Point) - popupAnchor: (Point) - html: (String) - bgPos: (Point) - */ - className: 'leaflet-div-icon', - html: false - }, - - createIcon: function (oldIcon) { - var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'), - options = this.options; - - if (options.html !== false) { - div.innerHTML = options.html; - } else { - div.innerHTML = ''; - } - - if (options.bgPos) { - div.style.backgroundPosition = - (-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px'; - } - - this._setIconStyles(div, 'icon'); - return div; - }, - - createShadow: function () { - return null; - } -}); - -L.divIcon = function (options) { - return new L.DivIcon(options); -}; - - -/* - * L.Popup is used for displaying popups on the map. - */ - -L.Map.mergeOptions({ - closePopupOnClick: true -}); - -L.Popup = L.Class.extend({ - includes: L.Mixin.Events, - - options: { - minWidth: 50, - maxWidth: 300, - // maxHeight: null, - autoPan: true, - closeButton: true, - offset: [0, 7], - autoPanPadding: [5, 5], - // autoPanPaddingTopLeft: null, - // autoPanPaddingBottomRight: null, - keepInView: false, - className: '', - zoomAnimation: true - }, - - initialize: function (options, source) { - L.setOptions(this, options); - - this._source = source; - this._animated = L.Browser.any3d && this.options.zoomAnimation; - this._isOpen = false; - }, - - onAdd: function (map) { - this._map = map; - - if (!this._container) { - this._initLayout(); - } - - var animFade = map.options.fadeAnimation; - - if (animFade) { - L.DomUtil.setOpacity(this._container, 0); - } - map._panes.popupPane.appendChild(this._container); - - map.on(this._getEvents(), this); - - this.update(); - - if (animFade) { - L.DomUtil.setOpacity(this._container, 1); - } - - this.fire('open'); - - map.fire('popupopen', {popup: this}); - - if (this._source) { - this._source.fire('popupopen', {popup: this}); - } - }, - - addTo: function (map) { - map.addLayer(this); - return this; - }, - - openOn: function (map) { - map.openPopup(this); - return this; - }, - - onRemove: function (map) { - map._panes.popupPane.removeChild(this._container); - - L.Util.falseFn(this._container.offsetWidth); // force reflow - - map.off(this._getEvents(), this); - - if (map.options.fadeAnimation) { - L.DomUtil.setOpacity(this._container, 0); - } - - this._map = null; - - this.fire('close'); - - map.fire('popupclose', {popup: this}); - - if (this._source) { - this._source.fire('popupclose', {popup: this}); - } - }, - - getLatLng: function () { - return this._latlng; - }, - - setLatLng: function (latlng) { - this._latlng = L.latLng(latlng); - if (this._map) { - this._updatePosition(); - this._adjustPan(); - } - return this; - }, - - getContent: function () { - return this._content; - }, - - setContent: function (content) { - this._content = content; - this.update(); - return this; - }, - - update: function () { - if (!this._map) { return; } - - this._container.style.visibility = 'hidden'; - - this._updateContent(); - this._updateLayout(); - this._updatePosition(); - - this._container.style.visibility = ''; - - this._adjustPan(); - }, - - _getEvents: function () { - var events = { - viewreset: this._updatePosition - }; - - if (this._animated) { - events.zoomanim = this._zoomAnimation; - } - if ('closeOnClick' in this.options ? this.options.closeOnClick : this._map.options.closePopupOnClick) { - events.preclick = this._close; - } - if (this.options.keepInView) { - events.moveend = this._adjustPan; - } - - return events; - }, - - _close: function () { - if (this._map) { - this._map.closePopup(this); - } - }, - - _initLayout: function () { - var prefix = 'leaflet-popup', - containerClass = prefix + ' ' + this.options.className + ' leaflet-zoom-' + - (this._animated ? 'animated' : 'hide'), - container = this._container = L.DomUtil.create('div', containerClass), - closeButton; - - if (this.options.closeButton) { - closeButton = this._closeButton = - L.DomUtil.create('a', prefix + '-close-button', container); - closeButton.href = '#close'; - closeButton.innerHTML = '×'; - L.DomEvent.disableClickPropagation(closeButton); - - L.DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this); - } - - var wrapper = this._wrapper = - L.DomUtil.create('div', prefix + '-content-wrapper', container); - L.DomEvent.disableClickPropagation(wrapper); - - this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper); - - L.DomEvent.disableScrollPropagation(this._contentNode); - L.DomEvent.on(wrapper, 'contextmenu', L.DomEvent.stopPropagation); - - this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container); - this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer); - }, - - _updateContent: function () { - if (!this._content) { return; } - - if (typeof this._content === 'string') { - this._contentNode.innerHTML = this._content; - } else { - while (this._contentNode.hasChildNodes()) { - this._contentNode.removeChild(this._contentNode.firstChild); - } - this._contentNode.appendChild(this._content); - } - this.fire('contentupdate'); - }, - - _updateLayout: function () { - var container = this._contentNode, - style = container.style; - - style.width = ''; - style.whiteSpace = 'nowrap'; - - var width = container.offsetWidth; - width = Math.min(width, this.options.maxWidth); - width = Math.max(width, this.options.minWidth); - - style.width = (width + 1) + 'px'; - style.whiteSpace = ''; - - style.height = ''; - - var height = container.offsetHeight, - maxHeight = this.options.maxHeight, - scrolledClass = 'leaflet-popup-scrolled'; - - if (maxHeight && height > maxHeight) { - style.height = maxHeight + 'px'; - L.DomUtil.addClass(container, scrolledClass); - } else { - L.DomUtil.removeClass(container, scrolledClass); - } - - this._containerWidth = this._container.offsetWidth; - }, - - _updatePosition: function () { - if (!this._map) { return; } - - var pos = this._map.latLngToLayerPoint(this._latlng), - animated = this._animated, - offset = L.point(this.options.offset); - - if (animated) { - L.DomUtil.setPosition(this._container, pos); - } - - this._containerBottom = -offset.y - (animated ? 0 : pos.y); - this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x + (animated ? 0 : pos.x); - - // bottom position the popup in case the height of the popup changes (images loading etc) - this._container.style.bottom = this._containerBottom + 'px'; - this._container.style.left = this._containerLeft + 'px'; - }, - - _zoomAnimation: function (opt) { - var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center); - - L.DomUtil.setPosition(this._container, pos); - }, - - _adjustPan: function () { - if (!this.options.autoPan) { return; } - - var map = this._map, - containerHeight = this._container.offsetHeight, - containerWidth = this._containerWidth, - - layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom); - - if (this._animated) { - layerPos._add(L.DomUtil.getPosition(this._container)); - } - - var containerPos = map.layerPointToContainerPoint(layerPos), - padding = L.point(this.options.autoPanPadding), - paddingTL = L.point(this.options.autoPanPaddingTopLeft || padding), - paddingBR = L.point(this.options.autoPanPaddingBottomRight || padding), - size = map.getSize(), - dx = 0, - dy = 0; - - if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right - dx = containerPos.x + containerWidth - size.x + paddingBR.x; - } - if (containerPos.x - dx - paddingTL.x < 0) { // left - dx = containerPos.x - paddingTL.x; - } - if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom - dy = containerPos.y + containerHeight - size.y + paddingBR.y; - } - if (containerPos.y - dy - paddingTL.y < 0) { // top - dy = containerPos.y - paddingTL.y; - } - - if (dx || dy) { - map - .fire('autopanstart') - .panBy([dx, dy]); - } - }, - - _onCloseButtonClick: function (e) { - this._close(); - L.DomEvent.stop(e); - } -}); - -L.popup = function (options, source) { - return new L.Popup(options, source); -}; - - -L.Map.include({ - openPopup: function (popup, latlng, options) { // (Popup) or (String || HTMLElement, LatLng[, Object]) - this.closePopup(); - - if (!(popup instanceof L.Popup)) { - var content = popup; - - popup = new L.Popup(options) - .setLatLng(latlng) - .setContent(content); - } - popup._isOpen = true; - - this._popup = popup; - return this.addLayer(popup); - }, - - closePopup: function (popup) { - if (!popup || popup === this._popup) { - popup = this._popup; - this._popup = null; - } - if (popup) { - this.removeLayer(popup); - popup._isOpen = false; - } - return this; - } -}); - - -/* - * Popup extension to L.Marker, adding popup-related methods. - */ - -L.Marker.include({ - openPopup: function () { - if (this._popup && this._map && !this._map.hasLayer(this._popup)) { - this._popup.setLatLng(this._latlng); - this._map.openPopup(this._popup); - } - - return this; - }, - - closePopup: function () { - if (this._popup) { - this._popup._close(); - } - return this; - }, - - togglePopup: function () { - if (this._popup) { - if (this._popup._isOpen) { - this.closePopup(); - } else { - this.openPopup(); - } - } - return this; - }, - - bindPopup: function (content, options) { - var anchor = L.point(this.options.icon.options.popupAnchor || [0, 0]); - - anchor = anchor.add(L.Popup.prototype.options.offset); - - if (options && options.offset) { - anchor = anchor.add(options.offset); - } - - options = L.extend({offset: anchor}, options); - - if (!this._popupHandlersAdded) { - this - .on('click', this.togglePopup, this) - .on('remove', this.closePopup, this) - .on('move', this._movePopup, this); - this._popupHandlersAdded = true; - } - - if (content instanceof L.Popup) { - L.setOptions(content, options); - this._popup = content; - content._source = this; - } else { - this._popup = new L.Popup(options, this) - .setContent(content); - } - - return this; - }, - - setPopupContent: function (content) { - if (this._popup) { - this._popup.setContent(content); - } - return this; - }, - - unbindPopup: function () { - if (this._popup) { - this._popup = null; - this - .off('click', this.togglePopup, this) - .off('remove', this.closePopup, this) - .off('move', this._movePopup, this); - this._popupHandlersAdded = false; - } - return this; - }, - - getPopup: function () { - return this._popup; - }, - - _movePopup: function (e) { - this._popup.setLatLng(e.latlng); - } -}); - - -/* - * L.LayerGroup is a class to combine several layers into one so that - * you can manipulate the group (e.g. add/remove it) as one layer. - */ - -L.LayerGroup = L.Class.extend({ - initialize: function (layers) { - this._layers = {}; - - var i, len; - - if (layers) { - for (i = 0, len = layers.length; i < len; i++) { - this.addLayer(layers[i]); - } - } - }, - - addLayer: function (layer) { - var id = this.getLayerId(layer); - - this._layers[id] = layer; - - if (this._map) { - this._map.addLayer(layer); - } - - return this; - }, - - removeLayer: function (layer) { - var id = layer in this._layers ? layer : this.getLayerId(layer); - - if (this._map && this._layers[id]) { - this._map.removeLayer(this._layers[id]); - } - - delete this._layers[id]; - - return this; - }, - - hasLayer: function (layer) { - if (!layer) { return false; } - - return (layer in this._layers || this.getLayerId(layer) in this._layers); - }, - - clearLayers: function () { - this.eachLayer(this.removeLayer, this); - return this; - }, - - invoke: function (methodName) { - var args = Array.prototype.slice.call(arguments, 1), - i, layer; - - for (i in this._layers) { - layer = this._layers[i]; - - if (layer[methodName]) { - layer[methodName].apply(layer, args); - } - } - - return this; - }, - - onAdd: function (map) { - this._map = map; - this.eachLayer(map.addLayer, map); - }, - - onRemove: function (map) { - this.eachLayer(map.removeLayer, map); - this._map = null; - }, - - addTo: function (map) { - map.addLayer(this); - return this; - }, - - eachLayer: function (method, context) { - for (var i in this._layers) { - method.call(context, this._layers[i]); - } - return this; - }, - - getLayer: function (id) { - return this._layers[id]; - }, - - getLayers: function () { - var layers = []; - - for (var i in this._layers) { - layers.push(this._layers[i]); - } - return layers; - }, - - setZIndex: function (zIndex) { - return this.invoke('setZIndex', zIndex); - }, - - getLayerId: function (layer) { - return L.stamp(layer); - } -}); - -L.layerGroup = function (layers) { - return new L.LayerGroup(layers); -}; - - -/* - * L.FeatureGroup extends L.LayerGroup by introducing mouse events and additional methods - * shared between a group of interactive layers (like vectors or markers). - */ - -L.FeatureGroup = L.LayerGroup.extend({ - includes: L.Mixin.Events, - - statics: { - EVENTS: 'click dblclick mouseover mouseout mousemove contextmenu popupopen popupclose' - }, - - addLayer: function (layer) { - if (this.hasLayer(layer)) { - return this; - } - - if ('on' in layer) { - layer.on(L.FeatureGroup.EVENTS, this._propagateEvent, this); - } - - L.LayerGroup.prototype.addLayer.call(this, layer); - - if (this._popupContent && layer.bindPopup) { - layer.bindPopup(this._popupContent, this._popupOptions); - } - - return this.fire('layeradd', {layer: layer}); - }, - - removeLayer: function (layer) { - if (!this.hasLayer(layer)) { - return this; - } - if (layer in this._layers) { - layer = this._layers[layer]; - } - - if ('off' in layer) { - layer.off(L.FeatureGroup.EVENTS, this._propagateEvent, this); - } - - L.LayerGroup.prototype.removeLayer.call(this, layer); - - if (this._popupContent) { - this.invoke('unbindPopup'); - } - - return this.fire('layerremove', {layer: layer}); - }, - - bindPopup: function (content, options) { - this._popupContent = content; - this._popupOptions = options; - return this.invoke('bindPopup', content, options); - }, - - openPopup: function (latlng) { - // open popup on the first layer - for (var id in this._layers) { - this._layers[id].openPopup(latlng); - break; - } - return this; - }, - - setStyle: function (style) { - return this.invoke('setStyle', style); - }, - - bringToFront: function () { - return this.invoke('bringToFront'); - }, - - bringToBack: function () { - return this.invoke('bringToBack'); - }, - - getBounds: function () { - var bounds = new L.LatLngBounds(); - - this.eachLayer(function (layer) { - bounds.extend(layer instanceof L.Marker ? layer.getLatLng() : layer.getBounds()); - }); - - return bounds; - }, - - _propagateEvent: function (e) { - e = L.extend({ - layer: e.target, - target: this - }, e); - this.fire(e.type, e); - } -}); - -L.featureGroup = function (layers) { - return new L.FeatureGroup(layers); -}; - - -/* - * L.Path is a base class for rendering vector paths on a map. Inherited by Polyline, Circle, etc. - */ - -L.Path = L.Class.extend({ - includes: [L.Mixin.Events], - - statics: { - // how much to extend the clip area around the map view - // (relative to its size, e.g. 0.5 is half the screen in each direction) - // set it so that SVG element doesn't exceed 1280px (vectors flicker on dragend if it is) - CLIP_PADDING: (function () { - var max = L.Browser.mobile ? 1280 : 2000, - target = (max / Math.max(window.outerWidth, window.outerHeight) - 1) / 2; - return Math.max(0, Math.min(0.5, target)); - })() - }, - - options: { - stroke: true, - color: '#0033ff', - dashArray: null, - lineCap: null, - lineJoin: null, - weight: 5, - opacity: 0.5, - - fill: false, - fillColor: null, //same as color by default - fillOpacity: 0.2, - - clickable: true - }, - - initialize: function (options) { - L.setOptions(this, options); - }, - - onAdd: function (map) { - this._map = map; - - if (!this._container) { - this._initElements(); - this._initEvents(); - } - - this.projectLatlngs(); - this._updatePath(); - - if (this._container) { - this._map._pathRoot.appendChild(this._container); - } - - this.fire('add'); - - map.on({ - 'viewreset': this.projectLatlngs, - 'moveend': this._updatePath - }, this); - }, - - addTo: function (map) { - map.addLayer(this); - return this; - }, - - onRemove: function (map) { - map._pathRoot.removeChild(this._container); - - // Need to fire remove event before we set _map to null as the event hooks might need the object - this.fire('remove'); - this._map = null; - - if (L.Browser.vml) { - this._container = null; - this._stroke = null; - this._fill = null; - } - - map.off({ - 'viewreset': this.projectLatlngs, - 'moveend': this._updatePath - }, this); - }, - - projectLatlngs: function () { - // do all projection stuff here - }, - - setStyle: function (style) { - L.setOptions(this, style); - - if (this._container) { - this._updateStyle(); - } - - return this; - }, - - redraw: function () { - if (this._map) { - this.projectLatlngs(); - this._updatePath(); - } - return this; - } -}); - -L.Map.include({ - _updatePathViewport: function () { - var p = L.Path.CLIP_PADDING, - size = this.getSize(), - panePos = L.DomUtil.getPosition(this._mapPane), - min = panePos.multiplyBy(-1)._subtract(size.multiplyBy(p)._round()), - max = min.add(size.multiplyBy(1 + p * 2)._round()); - - this._pathViewport = new L.Bounds(min, max); - } -}); - - -/* - * Extends L.Path with SVG-specific rendering code. - */ - -L.Path.SVG_NS = 'http://www.w3.org/2000/svg'; - -L.Browser.svg = !!(document.createElementNS && document.createElementNS(L.Path.SVG_NS, 'svg').createSVGRect); - -L.Path = L.Path.extend({ - statics: { - SVG: L.Browser.svg - }, - - bringToFront: function () { - var root = this._map._pathRoot, - path = this._container; - - if (path && root.lastChild !== path) { - root.appendChild(path); - } - return this; - }, - - bringToBack: function () { - var root = this._map._pathRoot, - path = this._container, - first = root.firstChild; - - if (path && first !== path) { - root.insertBefore(path, first); - } - return this; - }, - - getPathString: function () { - // form path string here - }, - - _createElement: function (name) { - return document.createElementNS(L.Path.SVG_NS, name); - }, - - _initElements: function () { - this._map._initPathRoot(); - this._initPath(); - this._initStyle(); - }, - - _initPath: function () { - this._container = this._createElement('g'); - - this._path = this._createElement('path'); - - if (this.options.className) { - L.DomUtil.addClass(this._path, this.options.className); - } - - this._container.appendChild(this._path); - }, - - _initStyle: function () { - if (this.options.stroke) { - this._path.setAttribute('stroke-linejoin', 'round'); - this._path.setAttribute('stroke-linecap', 'round'); - } - if (this.options.fill) { - this._path.setAttribute('fill-rule', 'evenodd'); - } - if (this.options.pointerEvents) { - this._path.setAttribute('pointer-events', this.options.pointerEvents); - } - if (!this.options.clickable && !this.options.pointerEvents) { - this._path.setAttribute('pointer-events', 'none'); - } - this._updateStyle(); - }, - - _updateStyle: function () { - if (this.options.stroke) { - this._path.setAttribute('stroke', this.options.color); - this._path.setAttribute('stroke-opacity', this.options.opacity); - this._path.setAttribute('stroke-width', this.options.weight); - if (this.options.dashArray) { - this._path.setAttribute('stroke-dasharray', this.options.dashArray); - } else { - this._path.removeAttribute('stroke-dasharray'); - } - if (this.options.lineCap) { - this._path.setAttribute('stroke-linecap', this.options.lineCap); - } - if (this.options.lineJoin) { - this._path.setAttribute('stroke-linejoin', this.options.lineJoin); - } - } else { - this._path.setAttribute('stroke', 'none'); - } - if (this.options.fill) { - this._path.setAttribute('fill', this.options.fillColor || this.options.color); - this._path.setAttribute('fill-opacity', this.options.fillOpacity); - } else { - this._path.setAttribute('fill', 'none'); - } - }, - - _updatePath: function () { - var str = this.getPathString(); - if (!str) { - // fix webkit empty string parsing bug - str = 'M0 0'; - } - this._path.setAttribute('d', str); - }, - - // TODO remove duplication with L.Map - _initEvents: function () { - if (this.options.clickable) { - if (L.Browser.svg || !L.Browser.vml) { - L.DomUtil.addClass(this._path, 'leaflet-clickable'); - } - - L.DomEvent.on(this._container, 'click', this._onMouseClick, this); - - var events = ['dblclick', 'mousedown', 'mouseover', - 'mouseout', 'mousemove', 'contextmenu']; - for (var i = 0; i < events.length; i++) { - L.DomEvent.on(this._container, events[i], this._fireMouseEvent, this); - } - } - }, - - _onMouseClick: function (e) { - if (this._map.dragging && this._map.dragging.moved()) { return; } - - this._fireMouseEvent(e); - }, - - _fireMouseEvent: function (e) { - if (!this._map || !this.hasEventListeners(e.type)) { return; } - - var map = this._map, - containerPoint = map.mouseEventToContainerPoint(e), - layerPoint = map.containerPointToLayerPoint(containerPoint), - latlng = map.layerPointToLatLng(layerPoint); - - this.fire(e.type, { - latlng: latlng, - layerPoint: layerPoint, - containerPoint: containerPoint, - originalEvent: e - }); - - if (e.type === 'contextmenu') { - L.DomEvent.preventDefault(e); - } - if (e.type !== 'mousemove') { - L.DomEvent.stopPropagation(e); - } - } -}); - -L.Map.include({ - _initPathRoot: function () { - if (!this._pathRoot) { - this._pathRoot = L.Path.prototype._createElement('svg'); - this._panes.overlayPane.appendChild(this._pathRoot); - - if (this.options.zoomAnimation && L.Browser.any3d) { - L.DomUtil.addClass(this._pathRoot, 'leaflet-zoom-animated'); - - this.on({ - 'zoomanim': this._animatePathZoom, - 'zoomend': this._endPathZoom - }); - } else { - L.DomUtil.addClass(this._pathRoot, 'leaflet-zoom-hide'); - } - - this.on('moveend', this._updateSvgViewport); - this._updateSvgViewport(); - } - }, - - _animatePathZoom: function (e) { - var scale = this.getZoomScale(e.zoom), - offset = this._getCenterOffset(e.center)._multiplyBy(-scale)._add(this._pathViewport.min); - - this._pathRoot.style[L.DomUtil.TRANSFORM] = - L.DomUtil.getTranslateString(offset) + ' scale(' + scale + ') '; - - this._pathZooming = true; - }, - - _endPathZoom: function () { - this._pathZooming = false; - }, - - _updateSvgViewport: function () { - - if (this._pathZooming) { - // Do not update SVGs while a zoom animation is going on otherwise the animation will break. - // When the zoom animation ends we will be updated again anyway - // This fixes the case where you do a momentum move and zoom while the move is still ongoing. - return; - } - - this._updatePathViewport(); - - var vp = this._pathViewport, - min = vp.min, - max = vp.max, - width = max.x - min.x, - height = max.y - min.y, - root = this._pathRoot, - pane = this._panes.overlayPane; - - // Hack to make flicker on drag end on mobile webkit less irritating - if (L.Browser.mobileWebkit) { - pane.removeChild(root); - } - - L.DomUtil.setPosition(root, min); - root.setAttribute('width', width); - root.setAttribute('height', height); - root.setAttribute('viewBox', [min.x, min.y, width, height].join(' ')); - - if (L.Browser.mobileWebkit) { - pane.appendChild(root); - } - } -}); - - -/* - * Popup extension to L.Path (polylines, polygons, circles), adding popup-related methods. - */ - -L.Path.include({ - - bindPopup: function (content, options) { - - if (content instanceof L.Popup) { - this._popup = content; - } else { - if (!this._popup || options) { - this._popup = new L.Popup(options, this); - } - this._popup.setContent(content); - } - - if (!this._popupHandlersAdded) { - this - .on('click', this._openPopup, this) - .on('remove', this.closePopup, this); - - this._popupHandlersAdded = true; - } - - return this; - }, - - unbindPopup: function () { - if (this._popup) { - this._popup = null; - this - .off('click', this._openPopup) - .off('remove', this.closePopup); - - this._popupHandlersAdded = false; - } - return this; - }, - - openPopup: function (latlng) { - - if (this._popup) { - // open the popup from one of the path's points if not specified - latlng = latlng || this._latlng || - this._latlngs[Math.floor(this._latlngs.length / 2)]; - - this._openPopup({latlng: latlng}); - } - - return this; - }, - - closePopup: function () { - if (this._popup) { - this._popup._close(); - } - return this; - }, - - _openPopup: function (e) { - this._popup.setLatLng(e.latlng); - this._map.openPopup(this._popup); - } -}); - - -/* - * Vector rendering for IE6-8 through VML. - * Thanks to Dmitry Baranovsky and his Raphael library for inspiration! - */ - -L.Browser.vml = !L.Browser.svg && (function () { - try { - var div = document.createElement('div'); - div.innerHTML = ''; - - var shape = div.firstChild; - shape.style.behavior = 'url(#default#VML)'; - - return shape && (typeof shape.adj === 'object'); - - } catch (e) { - return false; - } -}()); - -L.Path = L.Browser.svg || !L.Browser.vml ? L.Path : L.Path.extend({ - statics: { - VML: true, - CLIP_PADDING: 0.02 - }, - - _createElement: (function () { - try { - document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml'); - return function (name) { - return document.createElement(''); - }; - } catch (e) { - return function (name) { - return document.createElement( - '<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">'); - }; - } - }()), - - _initPath: function () { - var container = this._container = this._createElement('shape'); - - L.DomUtil.addClass(container, 'leaflet-vml-shape' + - (this.options.className ? ' ' + this.options.className : '')); - - if (this.options.clickable) { - L.DomUtil.addClass(container, 'leaflet-clickable'); - } - - container.coordsize = '1 1'; - - this._path = this._createElement('path'); - container.appendChild(this._path); - - this._map._pathRoot.appendChild(container); - }, - - _initStyle: function () { - this._updateStyle(); - }, - - _updateStyle: function () { - var stroke = this._stroke, - fill = this._fill, - options = this.options, - container = this._container; - - container.stroked = options.stroke; - container.filled = options.fill; - - if (options.stroke) { - if (!stroke) { - stroke = this._stroke = this._createElement('stroke'); - stroke.endcap = 'round'; - container.appendChild(stroke); - } - stroke.weight = options.weight + 'px'; - stroke.color = options.color; - stroke.opacity = options.opacity; - - if (options.dashArray) { - stroke.dashStyle = L.Util.isArray(options.dashArray) ? - options.dashArray.join(' ') : - options.dashArray.replace(/( *, *)/g, ' '); - } else { - stroke.dashStyle = ''; - } - if (options.lineCap) { - stroke.endcap = options.lineCap.replace('butt', 'flat'); - } - if (options.lineJoin) { - stroke.joinstyle = options.lineJoin; - } - - } else if (stroke) { - container.removeChild(stroke); - this._stroke = null; - } - - if (options.fill) { - if (!fill) { - fill = this._fill = this._createElement('fill'); - container.appendChild(fill); - } - fill.color = options.fillColor || options.color; - fill.opacity = options.fillOpacity; - - } else if (fill) { - container.removeChild(fill); - this._fill = null; - } - }, - - _updatePath: function () { - var style = this._container.style; - - style.display = 'none'; - this._path.v = this.getPathString() + ' '; // the space fixes IE empty path string bug - style.display = ''; - } -}); - -L.Map.include(L.Browser.svg || !L.Browser.vml ? {} : { - _initPathRoot: function () { - if (this._pathRoot) { return; } - - var root = this._pathRoot = document.createElement('div'); - root.className = 'leaflet-vml-container'; - this._panes.overlayPane.appendChild(root); - - this.on('moveend', this._updatePathViewport); - this._updatePathViewport(); - } -}); - - -/* - * Vector rendering for all browsers that support canvas. - */ - -L.Browser.canvas = (function () { - return !!document.createElement('canvas').getContext; -}()); - -L.Path = (L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? L.Path : L.Path.extend({ - statics: { - //CLIP_PADDING: 0.02, // not sure if there's a need to set it to a small value - CANVAS: true, - SVG: false - }, - - redraw: function () { - if (this._map) { - this.projectLatlngs(); - this._requestUpdate(); - } - return this; - }, - - setStyle: function (style) { - L.setOptions(this, style); - - if (this._map) { - this._updateStyle(); - this._requestUpdate(); - } - return this; - }, - - onRemove: function (map) { - map - .off('viewreset', this.projectLatlngs, this) - .off('moveend', this._updatePath, this); - - if (this.options.clickable) { - this._map.off('click', this._onClick, this); - this._map.off('mousemove', this._onMouseMove, this); - } - - this._requestUpdate(); - - this.fire('remove'); - this._map = null; - }, - - _requestUpdate: function () { - if (this._map && !L.Path._updateRequest) { - L.Path._updateRequest = L.Util.requestAnimFrame(this._fireMapMoveEnd, this._map); - } - }, - - _fireMapMoveEnd: function () { - L.Path._updateRequest = null; - this.fire('moveend'); - }, - - _initElements: function () { - this._map._initPathRoot(); - this._ctx = this._map._canvasCtx; - }, - - _updateStyle: function () { - var options = this.options; - - if (options.stroke) { - this._ctx.lineWidth = options.weight; - this._ctx.strokeStyle = options.color; - } - if (options.fill) { - this._ctx.fillStyle = options.fillColor || options.color; - } - - if (options.lineCap) { - this._ctx.lineCap = options.lineCap; - } - if (options.lineJoin) { - this._ctx.lineJoin = options.lineJoin; - } - }, - - _drawPath: function () { - var i, j, len, len2, point, drawMethod; - - this._ctx.beginPath(); - - for (i = 0, len = this._parts.length; i < len; i++) { - for (j = 0, len2 = this._parts[i].length; j < len2; j++) { - point = this._parts[i][j]; - drawMethod = (j === 0 ? 'move' : 'line') + 'To'; - - this._ctx[drawMethod](point.x, point.y); - } - // TODO refactor ugly hack - if (this instanceof L.Polygon) { - this._ctx.closePath(); - } - } - }, - - _checkIfEmpty: function () { - return !this._parts.length; - }, - - _updatePath: function () { - if (this._checkIfEmpty()) { return; } - - var ctx = this._ctx, - options = this.options; - - this._drawPath(); - ctx.save(); - this._updateStyle(); - - if (options.fill) { - ctx.globalAlpha = options.fillOpacity; - ctx.fill(options.fillRule || 'evenodd'); - } - - if (options.stroke) { - ctx.globalAlpha = options.opacity; - ctx.stroke(); - } - - ctx.restore(); - - // TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature - }, - - _initEvents: function () { - if (this.options.clickable) { - this._map.on('mousemove', this._onMouseMove, this); - this._map.on('click dblclick contextmenu', this._fireMouseEvent, this); - } - }, - - _fireMouseEvent: function (e) { - if (this._containsPoint(e.layerPoint)) { - this.fire(e.type, e); - } - }, - - _onMouseMove: function (e) { - if (!this._map || this._map._animatingZoom) { return; } - - // TODO don't do on each move - if (this._containsPoint(e.layerPoint)) { - this._ctx.canvas.style.cursor = 'pointer'; - this._mouseInside = true; - this.fire('mouseover', e); - - } else if (this._mouseInside) { - this._ctx.canvas.style.cursor = ''; - this._mouseInside = false; - this.fire('mouseout', e); - } - } -}); - -L.Map.include((L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? {} : { - _initPathRoot: function () { - var root = this._pathRoot, - ctx; - - if (!root) { - root = this._pathRoot = document.createElement('canvas'); - root.style.position = 'absolute'; - ctx = this._canvasCtx = root.getContext('2d'); - - ctx.lineCap = 'round'; - ctx.lineJoin = 'round'; - - this._panes.overlayPane.appendChild(root); - - if (this.options.zoomAnimation) { - this._pathRoot.className = 'leaflet-zoom-animated'; - this.on('zoomanim', this._animatePathZoom); - this.on('zoomend', this._endPathZoom); - } - this.on('moveend', this._updateCanvasViewport); - this._updateCanvasViewport(); - } - }, - - _updateCanvasViewport: function () { - // don't redraw while zooming. See _updateSvgViewport for more details - if (this._pathZooming) { return; } - this._updatePathViewport(); - - var vp = this._pathViewport, - min = vp.min, - size = vp.max.subtract(min), - root = this._pathRoot; - - //TODO check if this works properly on mobile webkit - L.DomUtil.setPosition(root, min); - root.width = size.x; - root.height = size.y; - root.getContext('2d').translate(-min.x, -min.y); - } -}); - - -/* - * L.LineUtil contains different utility functions for line segments - * and polylines (clipping, simplification, distances, etc.) - */ - -/*jshint bitwise:false */ // allow bitwise operations for this file - -L.LineUtil = { - - // Simplify polyline with vertex reduction and Douglas-Peucker simplification. - // Improves rendering performance dramatically by lessening the number of points to draw. - - simplify: function (/*Point[]*/ points, /*Number*/ tolerance) { - if (!tolerance || !points.length) { - return points.slice(); - } - - var sqTolerance = tolerance * tolerance; - - // stage 1: vertex reduction - points = this._reducePoints(points, sqTolerance); - - // stage 2: Douglas-Peucker simplification - points = this._simplifyDP(points, sqTolerance); - - return points; - }, - - // distance from a point to a segment between two points - pointToSegmentDistance: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { - return Math.sqrt(this._sqClosestPointOnSegment(p, p1, p2, true)); - }, - - closestPointOnSegment: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { - return this._sqClosestPointOnSegment(p, p1, p2); - }, - - // Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm - _simplifyDP: function (points, sqTolerance) { - - var len = points.length, - ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array, - markers = new ArrayConstructor(len); - - markers[0] = markers[len - 1] = 1; - - this._simplifyDPStep(points, markers, sqTolerance, 0, len - 1); - - var i, - newPoints = []; - - for (i = 0; i < len; i++) { - if (markers[i]) { - newPoints.push(points[i]); - } - } - - return newPoints; - }, - - _simplifyDPStep: function (points, markers, sqTolerance, first, last) { - - var maxSqDist = 0, - index, i, sqDist; - - for (i = first + 1; i <= last - 1; i++) { - sqDist = this._sqClosestPointOnSegment(points[i], points[first], points[last], true); - - if (sqDist > maxSqDist) { - index = i; - maxSqDist = sqDist; - } - } - - if (maxSqDist > sqTolerance) { - markers[index] = 1; - - this._simplifyDPStep(points, markers, sqTolerance, first, index); - this._simplifyDPStep(points, markers, sqTolerance, index, last); - } - }, - - // reduce points that are too close to each other to a single point - _reducePoints: function (points, sqTolerance) { - var reducedPoints = [points[0]]; - - for (var i = 1, prev = 0, len = points.length; i < len; i++) { - if (this._sqDist(points[i], points[prev]) > sqTolerance) { - reducedPoints.push(points[i]); - prev = i; - } - } - if (prev < len - 1) { - reducedPoints.push(points[len - 1]); - } - return reducedPoints; - }, - - // Cohen-Sutherland line clipping algorithm. - // Used to avoid rendering parts of a polyline that are not currently visible. - - clipSegment: function (a, b, bounds, useLastCode) { - var codeA = useLastCode ? this._lastCode : this._getBitCode(a, bounds), - codeB = this._getBitCode(b, bounds), - - codeOut, p, newCode; - - // save 2nd code to avoid calculating it on the next segment - this._lastCode = codeB; - - while (true) { - // if a,b is inside the clip window (trivial accept) - if (!(codeA | codeB)) { - return [a, b]; - // if a,b is outside the clip window (trivial reject) - } else if (codeA & codeB) { - return false; - // other cases - } else { - codeOut = codeA || codeB; - p = this._getEdgeIntersection(a, b, codeOut, bounds); - newCode = this._getBitCode(p, bounds); - - if (codeOut === codeA) { - a = p; - codeA = newCode; - } else { - b = p; - codeB = newCode; - } - } - } - }, - - _getEdgeIntersection: function (a, b, code, bounds) { - var dx = b.x - a.x, - dy = b.y - a.y, - min = bounds.min, - max = bounds.max; - - if (code & 8) { // top - return new L.Point(a.x + dx * (max.y - a.y) / dy, max.y); - } else if (code & 4) { // bottom - return new L.Point(a.x + dx * (min.y - a.y) / dy, min.y); - } else if (code & 2) { // right - return new L.Point(max.x, a.y + dy * (max.x - a.x) / dx); - } else if (code & 1) { // left - return new L.Point(min.x, a.y + dy * (min.x - a.x) / dx); - } - }, - - _getBitCode: function (/*Point*/ p, bounds) { - var code = 0; - - if (p.x < bounds.min.x) { // left - code |= 1; - } else if (p.x > bounds.max.x) { // right - code |= 2; - } - if (p.y < bounds.min.y) { // bottom - code |= 4; - } else if (p.y > bounds.max.y) { // top - code |= 8; - } - - return code; - }, - - // square distance (to avoid unnecessary Math.sqrt calls) - _sqDist: function (p1, p2) { - var dx = p2.x - p1.x, - dy = p2.y - p1.y; - return dx * dx + dy * dy; - }, - - // return closest point on segment or distance to that point - _sqClosestPointOnSegment: function (p, p1, p2, sqDist) { - var x = p1.x, - y = p1.y, - dx = p2.x - x, - dy = p2.y - y, - dot = dx * dx + dy * dy, - t; - - if (dot > 0) { - t = ((p.x - x) * dx + (p.y - y) * dy) / dot; - - if (t > 1) { - x = p2.x; - y = p2.y; - } else if (t > 0) { - x += dx * t; - y += dy * t; - } - } - - dx = p.x - x; - dy = p.y - y; - - return sqDist ? dx * dx + dy * dy : new L.Point(x, y); - } -}; - - -/* - * L.Polyline is used to display polylines on a map. - */ - -L.Polyline = L.Path.extend({ - initialize: function (latlngs, options) { - L.Path.prototype.initialize.call(this, options); - - this._latlngs = this._convertLatLngs(latlngs); - }, - - options: { - // how much to simplify the polyline on each zoom level - // more = better performance and smoother look, less = more accurate - smoothFactor: 1.0, - noClip: false - }, - - projectLatlngs: function () { - this._originalPoints = []; - - for (var i = 0, len = this._latlngs.length; i < len; i++) { - this._originalPoints[i] = this._map.latLngToLayerPoint(this._latlngs[i]); - } - }, - - getPathString: function () { - for (var i = 0, len = this._parts.length, str = ''; i < len; i++) { - str += this._getPathPartStr(this._parts[i]); - } - return str; - }, - - getLatLngs: function () { - return this._latlngs; - }, - - setLatLngs: function (latlngs) { - this._latlngs = this._convertLatLngs(latlngs); - return this.redraw(); - }, - - addLatLng: function (latlng) { - this._latlngs.push(L.latLng(latlng)); - return this.redraw(); - }, - - spliceLatLngs: function () { // (Number index, Number howMany) - var removed = [].splice.apply(this._latlngs, arguments); - this._convertLatLngs(this._latlngs, true); - this.redraw(); - return removed; - }, - - closestLayerPoint: function (p) { - var minDistance = Infinity, parts = this._parts, p1, p2, minPoint = null; - - for (var j = 0, jLen = parts.length; j < jLen; j++) { - var points = parts[j]; - for (var i = 1, len = points.length; i < len; i++) { - p1 = points[i - 1]; - p2 = points[i]; - var sqDist = L.LineUtil._sqClosestPointOnSegment(p, p1, p2, true); - if (sqDist < minDistance) { - minDistance = sqDist; - minPoint = L.LineUtil._sqClosestPointOnSegment(p, p1, p2); - } - } - } - if (minPoint) { - minPoint.distance = Math.sqrt(minDistance); - } - return minPoint; - }, - - getBounds: function () { - return new L.LatLngBounds(this.getLatLngs()); - }, - - _convertLatLngs: function (latlngs, overwrite) { - var i, len, target = overwrite ? latlngs : []; - - for (i = 0, len = latlngs.length; i < len; i++) { - if (L.Util.isArray(latlngs[i]) && typeof latlngs[i][0] !== 'number') { - return; - } - target[i] = L.latLng(latlngs[i]); - } - return target; - }, - - _initEvents: function () { - L.Path.prototype._initEvents.call(this); - }, - - _getPathPartStr: function (points) { - var round = L.Path.VML; - - for (var j = 0, len2 = points.length, str = '', p; j < len2; j++) { - p = points[j]; - if (round) { - p._round(); - } - str += (j ? 'L' : 'M') + p.x + ' ' + p.y; - } - return str; - }, - - _clipPoints: function () { - var points = this._originalPoints, - len = points.length, - i, k, segment; - - if (this.options.noClip) { - this._parts = [points]; - return; - } - - this._parts = []; - - var parts = this._parts, - vp = this._map._pathViewport, - lu = L.LineUtil; - - for (i = 0, k = 0; i < len - 1; i++) { - segment = lu.clipSegment(points[i], points[i + 1], vp, i); - if (!segment) { - continue; - } - - parts[k] = parts[k] || []; - parts[k].push(segment[0]); - - // if segment goes out of screen, or it's the last one, it's the end of the line part - if ((segment[1] !== points[i + 1]) || (i === len - 2)) { - parts[k].push(segment[1]); - k++; - } - } - }, - - // simplify each clipped part of the polyline - _simplifyPoints: function () { - var parts = this._parts, - lu = L.LineUtil; - - for (var i = 0, len = parts.length; i < len; i++) { - parts[i] = lu.simplify(parts[i], this.options.smoothFactor); - } - }, - - _updatePath: function () { - if (!this._map) { return; } - - this._clipPoints(); - this._simplifyPoints(); - - L.Path.prototype._updatePath.call(this); - } -}); - -L.polyline = function (latlngs, options) { - return new L.Polyline(latlngs, options); -}; - - -/* - * L.PolyUtil contains utility functions for polygons (clipping, etc.). - */ - -/*jshint bitwise:false */ // allow bitwise operations here - -L.PolyUtil = {}; - -/* - * Sutherland-Hodgeman polygon clipping algorithm. - * Used to avoid rendering parts of a polygon that are not currently visible. - */ -L.PolyUtil.clipPolygon = function (points, bounds) { - var clippedPoints, - edges = [1, 4, 2, 8], - i, j, k, - a, b, - len, edge, p, - lu = L.LineUtil; - - for (i = 0, len = points.length; i < len; i++) { - points[i]._code = lu._getBitCode(points[i], bounds); - } - - // for each edge (left, bottom, right, top) - for (k = 0; k < 4; k++) { - edge = edges[k]; - clippedPoints = []; - - for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { - a = points[i]; - b = points[j]; - - // if a is inside the clip window - if (!(a._code & edge)) { - // if b is outside the clip window (a->b goes out of screen) - if (b._code & edge) { - p = lu._getEdgeIntersection(b, a, edge, bounds); - p._code = lu._getBitCode(p, bounds); - clippedPoints.push(p); - } - clippedPoints.push(a); - - // else if b is inside the clip window (a->b enters the screen) - } else if (!(b._code & edge)) { - p = lu._getEdgeIntersection(b, a, edge, bounds); - p._code = lu._getBitCode(p, bounds); - clippedPoints.push(p); - } - } - points = clippedPoints; - } - - return points; -}; - - -/* - * L.Polygon is used to display polygons on a map. - */ - -L.Polygon = L.Polyline.extend({ - options: { - fill: true - }, - - initialize: function (latlngs, options) { - L.Polyline.prototype.initialize.call(this, latlngs, options); - this._initWithHoles(latlngs); - }, - - _initWithHoles: function (latlngs) { - var i, len, hole; - if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) { - this._latlngs = this._convertLatLngs(latlngs[0]); - this._holes = latlngs.slice(1); - - for (i = 0, len = this._holes.length; i < len; i++) { - hole = this._holes[i] = this._convertLatLngs(this._holes[i]); - if (hole[0].equals(hole[hole.length - 1])) { - hole.pop(); - } - } - } - - // filter out last point if its equal to the first one - latlngs = this._latlngs; - - if (latlngs.length >= 2 && latlngs[0].equals(latlngs[latlngs.length - 1])) { - latlngs.pop(); - } - }, - - projectLatlngs: function () { - L.Polyline.prototype.projectLatlngs.call(this); - - // project polygon holes points - // TODO move this logic to Polyline to get rid of duplication - this._holePoints = []; - - if (!this._holes) { return; } - - var i, j, len, len2; - - for (i = 0, len = this._holes.length; i < len; i++) { - this._holePoints[i] = []; - - for (j = 0, len2 = this._holes[i].length; j < len2; j++) { - this._holePoints[i][j] = this._map.latLngToLayerPoint(this._holes[i][j]); - } - } - }, - - setLatLngs: function (latlngs) { - if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) { - this._initWithHoles(latlngs); - return this.redraw(); - } else { - return L.Polyline.prototype.setLatLngs.call(this, latlngs); - } - }, - - _clipPoints: function () { - var points = this._originalPoints, - newParts = []; - - this._parts = [points].concat(this._holePoints); - - if (this.options.noClip) { return; } - - for (var i = 0, len = this._parts.length; i < len; i++) { - var clipped = L.PolyUtil.clipPolygon(this._parts[i], this._map._pathViewport); - if (clipped.length) { - newParts.push(clipped); - } - } - - this._parts = newParts; - }, - - _getPathPartStr: function (points) { - var str = L.Polyline.prototype._getPathPartStr.call(this, points); - return str + (L.Browser.svg ? 'z' : 'x'); - } -}); - -L.polygon = function (latlngs, options) { - return new L.Polygon(latlngs, options); -}; - - -/* - * Contains L.MultiPolyline and L.MultiPolygon layers. - */ - -(function () { - function createMulti(Klass) { - - return L.FeatureGroup.extend({ - - initialize: function (latlngs, options) { - this._layers = {}; - this._options = options; - this.setLatLngs(latlngs); - }, - - setLatLngs: function (latlngs) { - var i = 0, - len = latlngs.length; - - this.eachLayer(function (layer) { - if (i < len) { - layer.setLatLngs(latlngs[i++]); - } else { - this.removeLayer(layer); - } - }, this); - - while (i < len) { - this.addLayer(new Klass(latlngs[i++], this._options)); - } - - return this; - }, - - getLatLngs: function () { - var latlngs = []; - - this.eachLayer(function (layer) { - latlngs.push(layer.getLatLngs()); - }); - - return latlngs; - } - }); - } - - L.MultiPolyline = createMulti(L.Polyline); - L.MultiPolygon = createMulti(L.Polygon); - - L.multiPolyline = function (latlngs, options) { - return new L.MultiPolyline(latlngs, options); - }; - - L.multiPolygon = function (latlngs, options) { - return new L.MultiPolygon(latlngs, options); - }; -}()); - - -/* - * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object. - */ - -L.Rectangle = L.Polygon.extend({ - initialize: function (latLngBounds, options) { - L.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options); - }, - - setBounds: function (latLngBounds) { - this.setLatLngs(this._boundsToLatLngs(latLngBounds)); - }, - - _boundsToLatLngs: function (latLngBounds) { - latLngBounds = L.latLngBounds(latLngBounds); - return [ - latLngBounds.getSouthWest(), - latLngBounds.getNorthWest(), - latLngBounds.getNorthEast(), - latLngBounds.getSouthEast() - ]; - } -}); - -L.rectangle = function (latLngBounds, options) { - return new L.Rectangle(latLngBounds, options); -}; - - -/* - * L.Circle is a circle overlay (with a certain radius in meters). - */ - -L.Circle = L.Path.extend({ - initialize: function (latlng, radius, options) { - L.Path.prototype.initialize.call(this, options); - - this._latlng = L.latLng(latlng); - this._mRadius = radius; - }, - - options: { - fill: true - }, - - setLatLng: function (latlng) { - this._latlng = L.latLng(latlng); - return this.redraw(); - }, - - setRadius: function (radius) { - this._mRadius = radius; - return this.redraw(); - }, - - projectLatlngs: function () { - var lngRadius = this._getLngRadius(), - latlng = this._latlng, - pointLeft = this._map.latLngToLayerPoint([latlng.lat, latlng.lng - lngRadius]); - - this._point = this._map.latLngToLayerPoint(latlng); - this._radius = Math.max(this._point.x - pointLeft.x, 1); - }, - - getBounds: function () { - var lngRadius = this._getLngRadius(), - latRadius = (this._mRadius / 40075017) * 360, - latlng = this._latlng; - - return new L.LatLngBounds( - [latlng.lat - latRadius, latlng.lng - lngRadius], - [latlng.lat + latRadius, latlng.lng + lngRadius]); - }, - - getLatLng: function () { - return this._latlng; - }, - - getPathString: function () { - var p = this._point, - r = this._radius; - - if (this._checkIfEmpty()) { - return ''; - } - - if (L.Browser.svg) { - return 'M' + p.x + ',' + (p.y - r) + - 'A' + r + ',' + r + ',0,1,1,' + - (p.x - 0.1) + ',' + (p.y - r) + ' z'; - } else { - p._round(); - r = Math.round(r); - return 'AL ' + p.x + ',' + p.y + ' ' + r + ',' + r + ' 0,' + (65535 * 360); - } - }, - - getRadius: function () { - return this._mRadius; - }, - - // TODO Earth hardcoded, move into projection code! - - _getLatRadius: function () { - return (this._mRadius / 40075017) * 360; - }, - - _getLngRadius: function () { - return this._getLatRadius() / Math.cos(L.LatLng.DEG_TO_RAD * this._latlng.lat); - }, - - _checkIfEmpty: function () { - if (!this._map) { - return false; - } - var vp = this._map._pathViewport, - r = this._radius, - p = this._point; - - return p.x - r > vp.max.x || p.y - r > vp.max.y || - p.x + r < vp.min.x || p.y + r < vp.min.y; - } -}); - -L.circle = function (latlng, radius, options) { - return new L.Circle(latlng, radius, options); -}; - - -/* - * L.CircleMarker is a circle overlay with a permanent pixel radius. - */ - -L.CircleMarker = L.Circle.extend({ - options: { - radius: 10, - weight: 2 - }, - - initialize: function (latlng, options) { - L.Circle.prototype.initialize.call(this, latlng, null, options); - this._radius = this.options.radius; - }, - - projectLatlngs: function () { - this._point = this._map.latLngToLayerPoint(this._latlng); - }, - - _updateStyle : function () { - L.Circle.prototype._updateStyle.call(this); - this.setRadius(this.options.radius); - }, - - setLatLng: function (latlng) { - L.Circle.prototype.setLatLng.call(this, latlng); - if (this._popup && this._popup._isOpen) { - this._popup.setLatLng(latlng); - } - return this; - }, - - setRadius: function (radius) { - this.options.radius = this._radius = radius; - return this.redraw(); - }, - - getRadius: function () { - return this._radius; - } -}); - -L.circleMarker = function (latlng, options) { - return new L.CircleMarker(latlng, options); -}; - - -/* - * Extends L.Polyline to be able to manually detect clicks on Canvas-rendered polylines. - */ - -L.Polyline.include(!L.Path.CANVAS ? {} : { - _containsPoint: function (p, closed) { - var i, j, k, len, len2, dist, part, - w = this.options.weight / 2; - - if (L.Browser.touch) { - w += 10; // polyline click tolerance on touch devices - } - - for (i = 0, len = this._parts.length; i < len; i++) { - part = this._parts[i]; - for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { - if (!closed && (j === 0)) { - continue; - } - - dist = L.LineUtil.pointToSegmentDistance(p, part[k], part[j]); - - if (dist <= w) { - return true; - } - } - } - return false; - } -}); - - -/* - * Extends L.Polygon to be able to manually detect clicks on Canvas-rendered polygons. - */ - -L.Polygon.include(!L.Path.CANVAS ? {} : { - _containsPoint: function (p) { - var inside = false, - part, p1, p2, - i, j, k, - len, len2; - - // TODO optimization: check if within bounds first - - if (L.Polyline.prototype._containsPoint.call(this, p, true)) { - // click on polygon border - return true; - } - - // ray casting algorithm for detecting if point is in polygon - - for (i = 0, len = this._parts.length; i < len; i++) { - part = this._parts[i]; - - for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { - p1 = part[j]; - p2 = part[k]; - - if (((p1.y > p.y) !== (p2.y > p.y)) && - (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { - inside = !inside; - } - } - } - - return inside; - } -}); - - -/* - * Extends L.Circle with Canvas-specific code. - */ - -L.Circle.include(!L.Path.CANVAS ? {} : { - _drawPath: function () { - var p = this._point; - this._ctx.beginPath(); - this._ctx.arc(p.x, p.y, this._radius, 0, Math.PI * 2, false); - }, - - _containsPoint: function (p) { - var center = this._point, - w2 = this.options.stroke ? this.options.weight / 2 : 0; - - return (p.distanceTo(center) <= this._radius + w2); - } -}); - - -/* - * CircleMarker canvas specific drawing parts. - */ - -L.CircleMarker.include(!L.Path.CANVAS ? {} : { - _updateStyle: function () { - L.Path.prototype._updateStyle.call(this); - } -}); - - -/* - * L.GeoJSON turns any GeoJSON data into a Leaflet layer. - */ - -L.GeoJSON = L.FeatureGroup.extend({ - - initialize: function (geojson, options) { - L.setOptions(this, options); - - this._layers = {}; - - if (geojson) { - this.addData(geojson); - } - }, - - addData: function (geojson) { - var features = L.Util.isArray(geojson) ? geojson : geojson.features, - i, len, feature; - - if (features) { - for (i = 0, len = features.length; i < len; i++) { - // Only add this if geometry or geometries are set and not null - feature = features[i]; - if (feature.geometries || feature.geometry || feature.features || feature.coordinates) { - this.addData(features[i]); - } - } - return this; - } - - var options = this.options; - - if (options.filter && !options.filter(geojson)) { return; } - - var layer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng, options); - layer.feature = L.GeoJSON.asFeature(geojson); - - layer.defaultOptions = layer.options; - this.resetStyle(layer); - - if (options.onEachFeature) { - options.onEachFeature(geojson, layer); - } - - return this.addLayer(layer); - }, - - resetStyle: function (layer) { - var style = this.options.style; - if (style) { - // reset any custom styles - L.Util.extend(layer.options, layer.defaultOptions); - - this._setLayerStyle(layer, style); - } - }, - - setStyle: function (style) { - this.eachLayer(function (layer) { - this._setLayerStyle(layer, style); - }, this); - }, - - _setLayerStyle: function (layer, style) { - if (typeof style === 'function') { - style = style(layer.feature); - } - if (layer.setStyle) { - layer.setStyle(style); - } - } -}); - -L.extend(L.GeoJSON, { - geometryToLayer: function (geojson, pointToLayer, coordsToLatLng, vectorOptions) { - var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson, - coords = geometry.coordinates, - layers = [], - latlng, latlngs, i, len; - - coordsToLatLng = coordsToLatLng || this.coordsToLatLng; - - switch (geometry.type) { - case 'Point': - latlng = coordsToLatLng(coords); - return pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng); - - case 'MultiPoint': - for (i = 0, len = coords.length; i < len; i++) { - latlng = coordsToLatLng(coords[i]); - layers.push(pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng)); - } - return new L.FeatureGroup(layers); - - case 'LineString': - latlngs = this.coordsToLatLngs(coords, 0, coordsToLatLng); - return new L.Polyline(latlngs, vectorOptions); - - case 'Polygon': - if (coords.length === 2 && !coords[1].length) { - throw new Error('Invalid GeoJSON object.'); - } - latlngs = this.coordsToLatLngs(coords, 1, coordsToLatLng); - return new L.Polygon(latlngs, vectorOptions); - - case 'MultiLineString': - latlngs = this.coordsToLatLngs(coords, 1, coordsToLatLng); - return new L.MultiPolyline(latlngs, vectorOptions); - - case 'MultiPolygon': - latlngs = this.coordsToLatLngs(coords, 2, coordsToLatLng); - return new L.MultiPolygon(latlngs, vectorOptions); - - case 'GeometryCollection': - for (i = 0, len = geometry.geometries.length; i < len; i++) { - - layers.push(this.geometryToLayer({ - geometry: geometry.geometries[i], - type: 'Feature', - properties: geojson.properties - }, pointToLayer, coordsToLatLng, vectorOptions)); - } - return new L.FeatureGroup(layers); - - default: - throw new Error('Invalid GeoJSON object.'); - } - }, - - coordsToLatLng: function (coords) { // (Array[, Boolean]) -> LatLng - return new L.LatLng(coords[1], coords[0], coords[2]); - }, - - coordsToLatLngs: function (coords, levelsDeep, coordsToLatLng) { // (Array[, Number, Function]) -> Array - var latlng, i, len, - latlngs = []; - - for (i = 0, len = coords.length; i < len; i++) { - latlng = levelsDeep ? - this.coordsToLatLngs(coords[i], levelsDeep - 1, coordsToLatLng) : - (coordsToLatLng || this.coordsToLatLng)(coords[i]); - - latlngs.push(latlng); - } - - return latlngs; - }, - - latLngToCoords: function (latlng) { - var coords = [latlng.lng, latlng.lat]; - - if (latlng.alt !== undefined) { - coords.push(latlng.alt); - } - return coords; - }, - - latLngsToCoords: function (latLngs) { - var coords = []; - - for (var i = 0, len = latLngs.length; i < len; i++) { - coords.push(L.GeoJSON.latLngToCoords(latLngs[i])); - } - - return coords; - }, - - getFeature: function (layer, newGeometry) { - return layer.feature ? L.extend({}, layer.feature, {geometry: newGeometry}) : L.GeoJSON.asFeature(newGeometry); - }, - - asFeature: function (geoJSON) { - if (geoJSON.type === 'Feature') { - return geoJSON; - } - - return { - type: 'Feature', - properties: {}, - geometry: geoJSON - }; - } -}); - -var PointToGeoJSON = { - toGeoJSON: function () { - return L.GeoJSON.getFeature(this, { - type: 'Point', - coordinates: L.GeoJSON.latLngToCoords(this.getLatLng()) - }); - } -}; - -L.Marker.include(PointToGeoJSON); -L.Circle.include(PointToGeoJSON); -L.CircleMarker.include(PointToGeoJSON); - -L.Polyline.include({ - toGeoJSON: function () { - return L.GeoJSON.getFeature(this, { - type: 'LineString', - coordinates: L.GeoJSON.latLngsToCoords(this.getLatLngs()) - }); - } -}); - -L.Polygon.include({ - toGeoJSON: function () { - var coords = [L.GeoJSON.latLngsToCoords(this.getLatLngs())], - i, len, hole; - - coords[0].push(coords[0][0]); - - if (this._holes) { - for (i = 0, len = this._holes.length; i < len; i++) { - hole = L.GeoJSON.latLngsToCoords(this._holes[i]); - hole.push(hole[0]); - coords.push(hole); - } - } - - return L.GeoJSON.getFeature(this, { - type: 'Polygon', - coordinates: coords - }); - } -}); - -(function () { - function multiToGeoJSON(type) { - return function () { - var coords = []; - - this.eachLayer(function (layer) { - coords.push(layer.toGeoJSON().geometry.coordinates); - }); - - return L.GeoJSON.getFeature(this, { - type: type, - coordinates: coords - }); - }; - } - - L.MultiPolyline.include({toGeoJSON: multiToGeoJSON('MultiLineString')}); - L.MultiPolygon.include({toGeoJSON: multiToGeoJSON('MultiPolygon')}); - - L.LayerGroup.include({ - toGeoJSON: function () { - - var geometry = this.feature && this.feature.geometry, - jsons = [], - json; - - if (geometry && geometry.type === 'MultiPoint') { - return multiToGeoJSON('MultiPoint').call(this); - } - - var isGeometryCollection = geometry && geometry.type === 'GeometryCollection'; - - this.eachLayer(function (layer) { - if (layer.toGeoJSON) { - json = layer.toGeoJSON(); - jsons.push(isGeometryCollection ? json.geometry : L.GeoJSON.asFeature(json)); - } - }); - - if (isGeometryCollection) { - return L.GeoJSON.getFeature(this, { - geometries: jsons, - type: 'GeometryCollection' - }); - } - - return { - type: 'FeatureCollection', - features: jsons - }; - } - }); -}()); - -L.geoJson = function (geojson, options) { - return new L.GeoJSON(geojson, options); -}; - - -/* - * L.DomEvent contains functions for working with DOM events. - */ - -L.DomEvent = { - /* inspired by John Resig, Dean Edwards and YUI addEvent implementations */ - addListener: function (obj, type, fn, context) { // (HTMLElement, String, Function[, Object]) - - var id = L.stamp(fn), - key = '_leaflet_' + type + id, - handler, originalHandler, newType; - - if (obj[key]) { return this; } - - handler = function (e) { - return fn.call(context || obj, e || L.DomEvent._getEvent()); - }; - - if (L.Browser.pointer && type.indexOf('touch') === 0) { - return this.addPointerListener(obj, type, handler, id); - } - if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener) { - this.addDoubleTapListener(obj, handler, id); - } - - if ('addEventListener' in obj) { - - if (type === 'mousewheel') { - obj.addEventListener('DOMMouseScroll', handler, false); - obj.addEventListener(type, handler, false); - - } else if ((type === 'mouseenter') || (type === 'mouseleave')) { - - originalHandler = handler; - newType = (type === 'mouseenter' ? 'mouseover' : 'mouseout'); - - handler = function (e) { - if (!L.DomEvent._checkMouse(obj, e)) { return; } - return originalHandler(e); - }; - - obj.addEventListener(newType, handler, false); - - } else if (type === 'click' && L.Browser.android) { - originalHandler = handler; - handler = function (e) { - return L.DomEvent._filterClick(e, originalHandler); - }; - - obj.addEventListener(type, handler, false); - } else { - obj.addEventListener(type, handler, false); - } - - } else if ('attachEvent' in obj) { - obj.attachEvent('on' + type, handler); - } - - obj[key] = handler; - - return this; - }, - - removeListener: function (obj, type, fn) { // (HTMLElement, String, Function) - - var id = L.stamp(fn), - key = '_leaflet_' + type + id, - handler = obj[key]; - - if (!handler) { return this; } - - if (L.Browser.pointer && type.indexOf('touch') === 0) { - this.removePointerListener(obj, type, id); - } else if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) { - this.removeDoubleTapListener(obj, id); - - } else if ('removeEventListener' in obj) { - - if (type === 'mousewheel') { - obj.removeEventListener('DOMMouseScroll', handler, false); - obj.removeEventListener(type, handler, false); - - } else if ((type === 'mouseenter') || (type === 'mouseleave')) { - obj.removeEventListener((type === 'mouseenter' ? 'mouseover' : 'mouseout'), handler, false); - } else { - obj.removeEventListener(type, handler, false); - } - } else if ('detachEvent' in obj) { - obj.detachEvent('on' + type, handler); - } - - obj[key] = null; - - return this; - }, - - stopPropagation: function (e) { - - if (e.stopPropagation) { - e.stopPropagation(); - } else { - e.cancelBubble = true; - } - L.DomEvent._skipped(e); - - return this; - }, - - disableScrollPropagation: function (el) { - var stop = L.DomEvent.stopPropagation; - - return L.DomEvent - .on(el, 'mousewheel', stop) - .on(el, 'MozMousePixelScroll', stop); - }, - - disableClickPropagation: function (el) { - var stop = L.DomEvent.stopPropagation; - - for (var i = L.Draggable.START.length - 1; i >= 0; i--) { - L.DomEvent.on(el, L.Draggable.START[i], stop); - } - - return L.DomEvent - .on(el, 'click', L.DomEvent._fakeStop) - .on(el, 'dblclick', stop); - }, - - preventDefault: function (e) { - - if (e.preventDefault) { - e.preventDefault(); - } else { - e.returnValue = false; - } - return this; - }, - - stop: function (e) { - return L.DomEvent - .preventDefault(e) - .stopPropagation(e); - }, - - getMousePosition: function (e, container) { - if (!container) { - return new L.Point(e.clientX, e.clientY); - } - - var rect = container.getBoundingClientRect(); - - return new L.Point( - e.clientX - rect.left - container.clientLeft, - e.clientY - rect.top - container.clientTop); - }, - - getWheelDelta: function (e) { - - var delta = 0; - - if (e.wheelDelta) { - delta = e.wheelDelta / 120; - } - if (e.detail) { - delta = -e.detail / 3; - } - return delta; - }, - - _skipEvents: {}, - - _fakeStop: function (e) { - // fakes stopPropagation by setting a special event flag, checked/reset with L.DomEvent._skipped(e) - L.DomEvent._skipEvents[e.type] = true; - }, - - _skipped: function (e) { - var skipped = this._skipEvents[e.type]; - // reset when checking, as it's only used in map container and propagates outside of the map - this._skipEvents[e.type] = false; - return skipped; - }, - - // check if element really left/entered the event target (for mouseenter/mouseleave) - _checkMouse: function (el, e) { - - var related = e.relatedTarget; - - if (!related) { return true; } - - try { - while (related && (related !== el)) { - related = related.parentNode; - } - } catch (err) { - return false; - } - return (related !== el); - }, - - _getEvent: function () { // evil magic for IE - /*jshint noarg:false */ - var e = window.event; - if (!e) { - var caller = arguments.callee.caller; - while (caller) { - e = caller['arguments'][0]; - if (e && window.Event === e.constructor) { - break; - } - caller = caller.caller; - } - } - return e; - }, - - // this is a horrible workaround for a bug in Android where a single touch triggers two click events - _filterClick: function (e, handler) { - var timeStamp = (e.timeStamp || e.originalEvent.timeStamp), - elapsed = L.DomEvent._lastClick && (timeStamp - L.DomEvent._lastClick); - - // are they closer together than 500ms yet more than 100ms? - // Android typically triggers them ~300ms apart while multiple listeners - // on the same event should be triggered far faster; - // or check if click is simulated on the element, and if it is, reject any non-simulated events - - if ((elapsed && elapsed > 100 && elapsed < 500) || (e.target._simulatedClick && !e._simulated)) { - L.DomEvent.stop(e); - return; - } - L.DomEvent._lastClick = timeStamp; - - return handler(e); - } -}; - -L.DomEvent.on = L.DomEvent.addListener; -L.DomEvent.off = L.DomEvent.removeListener; - - -/* - * L.Draggable allows you to add dragging capabilities to any element. Supports mobile devices too. - */ - -L.Draggable = L.Class.extend({ - includes: L.Mixin.Events, - - statics: { - START: L.Browser.touch ? ['touchstart', 'mousedown'] : ['mousedown'], - END: { - mousedown: 'mouseup', - touchstart: 'touchend', - pointerdown: 'touchend', - MSPointerDown: 'touchend' - }, - MOVE: { - mousedown: 'mousemove', - touchstart: 'touchmove', - pointerdown: 'touchmove', - MSPointerDown: 'touchmove' - } - }, - - initialize: function (element, dragStartTarget) { - this._element = element; - this._dragStartTarget = dragStartTarget || element; - }, - - enable: function () { - if (this._enabled) { return; } - - for (var i = L.Draggable.START.length - 1; i >= 0; i--) { - L.DomEvent.on(this._dragStartTarget, L.Draggable.START[i], this._onDown, this); - } - - this._enabled = true; - }, - - disable: function () { - if (!this._enabled) { return; } - - for (var i = L.Draggable.START.length - 1; i >= 0; i--) { - L.DomEvent.off(this._dragStartTarget, L.Draggable.START[i], this._onDown, this); - } - - this._enabled = false; - this._moved = false; - }, - - _onDown: function (e) { - this._moved = false; - - if (e.shiftKey || ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; } - - L.DomEvent.stopPropagation(e); - - if (L.Draggable._disabled) { return; } - - L.DomUtil.disableImageDrag(); - L.DomUtil.disableTextSelection(); - - if (this._moving) { return; } - - var first = e.touches ? e.touches[0] : e; - - this._startPoint = new L.Point(first.clientX, first.clientY); - this._startPos = this._newPos = L.DomUtil.getPosition(this._element); - - L.DomEvent - .on(document, L.Draggable.MOVE[e.type], this._onMove, this) - .on(document, L.Draggable.END[e.type], this._onUp, this); - }, - - _onMove: function (e) { - if (e.touches && e.touches.length > 1) { - this._moved = true; - return; - } - - var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e), - newPoint = new L.Point(first.clientX, first.clientY), - offset = newPoint.subtract(this._startPoint); - - if (!offset.x && !offset.y) { return; } - if (L.Browser.touch && Math.abs(offset.x) + Math.abs(offset.y) < 3) { return; } - - L.DomEvent.preventDefault(e); - - if (!this._moved) { - this.fire('dragstart'); - - this._moved = true; - this._startPos = L.DomUtil.getPosition(this._element).subtract(offset); - - L.DomUtil.addClass(document.body, 'leaflet-dragging'); - this._lastTarget = e.target || e.srcElement; - L.DomUtil.addClass(this._lastTarget, 'leaflet-drag-target'); - } - - this._newPos = this._startPos.add(offset); - this._moving = true; - - L.Util.cancelAnimFrame(this._animRequest); - this._animRequest = L.Util.requestAnimFrame(this._updatePosition, this, true, this._dragStartTarget); - }, - - _updatePosition: function () { - this.fire('predrag'); - L.DomUtil.setPosition(this._element, this._newPos); - this.fire('drag'); - }, - - _onUp: function () { - L.DomUtil.removeClass(document.body, 'leaflet-dragging'); - - if (this._lastTarget) { - L.DomUtil.removeClass(this._lastTarget, 'leaflet-drag-target'); - this._lastTarget = null; - } - - for (var i in L.Draggable.MOVE) { - L.DomEvent - .off(document, L.Draggable.MOVE[i], this._onMove) - .off(document, L.Draggable.END[i], this._onUp); - } - - L.DomUtil.enableImageDrag(); - L.DomUtil.enableTextSelection(); - - if (this._moved && this._moving) { - // ensure drag is not fired after dragend - L.Util.cancelAnimFrame(this._animRequest); - - this.fire('dragend', { - distance: this._newPos.distanceTo(this._startPos) - }); - } - - this._moving = false; - } -}); - - -/* - L.Handler is a base class for handler classes that are used internally to inject - interaction features like dragging to classes like Map and Marker. -*/ - -L.Handler = L.Class.extend({ - initialize: function (map) { - this._map = map; - }, - - enable: function () { - if (this._enabled) { return; } - - this._enabled = true; - this.addHooks(); - }, - - disable: function () { - if (!this._enabled) { return; } - - this._enabled = false; - this.removeHooks(); - }, - - enabled: function () { - return !!this._enabled; - } -}); - - -/* - * L.Handler.MapDrag is used to make the map draggable (with panning inertia), enabled by default. - */ - -L.Map.mergeOptions({ - dragging: true, - - inertia: !L.Browser.android23, - inertiaDeceleration: 3400, // px/s^2 - inertiaMaxSpeed: Infinity, // px/s - inertiaThreshold: L.Browser.touch ? 32 : 18, // ms - easeLinearity: 0.25, - - // TODO refactor, move to CRS - worldCopyJump: false -}); - -L.Map.Drag = L.Handler.extend({ - addHooks: function () { - if (!this._draggable) { - var map = this._map; - - this._draggable = new L.Draggable(map._mapPane, map._container); - - this._draggable.on({ - 'dragstart': this._onDragStart, - 'drag': this._onDrag, - 'dragend': this._onDragEnd - }, this); - - if (map.options.worldCopyJump) { - this._draggable.on('predrag', this._onPreDrag, this); - map.on('viewreset', this._onViewReset, this); - - map.whenReady(this._onViewReset, this); - } - } - this._draggable.enable(); - }, - - removeHooks: function () { - this._draggable.disable(); - }, - - moved: function () { - return this._draggable && this._draggable._moved; - }, - - _onDragStart: function () { - var map = this._map; - - if (map._panAnim) { - map._panAnim.stop(); - } - - map - .fire('movestart') - .fire('dragstart'); - - if (map.options.inertia) { - this._positions = []; - this._times = []; - } - }, - - _onDrag: function () { - if (this._map.options.inertia) { - var time = this._lastTime = +new Date(), - pos = this._lastPos = this._draggable._newPos; - - this._positions.push(pos); - this._times.push(time); - - if (time - this._times[0] > 200) { - this._positions.shift(); - this._times.shift(); - } - } - - this._map - .fire('move') - .fire('drag'); - }, - - _onViewReset: function () { - // TODO fix hardcoded Earth values - var pxCenter = this._map.getSize()._divideBy(2), - pxWorldCenter = this._map.latLngToLayerPoint([0, 0]); - - this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x; - this._worldWidth = this._map.project([0, 180]).x; - }, - - _onPreDrag: function () { - // TODO refactor to be able to adjust map pane position after zoom - var worldWidth = this._worldWidth, - halfWidth = Math.round(worldWidth / 2), - dx = this._initialWorldOffset, - x = this._draggable._newPos.x, - newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx, - newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx, - newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2; - - this._draggable._newPos.x = newX; - }, - - _onDragEnd: function (e) { - var map = this._map, - options = map.options, - delay = +new Date() - this._lastTime, - - noInertia = !options.inertia || delay > options.inertiaThreshold || !this._positions[0]; - - map.fire('dragend', e); - - if (noInertia) { - map.fire('moveend'); - - } else { - - var direction = this._lastPos.subtract(this._positions[0]), - duration = (this._lastTime + delay - this._times[0]) / 1000, - ease = options.easeLinearity, - - speedVector = direction.multiplyBy(ease / duration), - speed = speedVector.distanceTo([0, 0]), - - limitedSpeed = Math.min(options.inertiaMaxSpeed, speed), - limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed), - - decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease), - offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round(); - - if (!offset.x || !offset.y) { - map.fire('moveend'); - - } else { - offset = map._limitOffset(offset, map.options.maxBounds); - - L.Util.requestAnimFrame(function () { - map.panBy(offset, { - duration: decelerationDuration, - easeLinearity: ease, - noMoveStart: true - }); - }); - } - } - } -}); - -L.Map.addInitHook('addHandler', 'dragging', L.Map.Drag); - - -/* - * L.Handler.DoubleClickZoom is used to handle double-click zoom on the map, enabled by default. - */ - -L.Map.mergeOptions({ - doubleClickZoom: true -}); - -L.Map.DoubleClickZoom = L.Handler.extend({ - addHooks: function () { - this._map.on('dblclick', this._onDoubleClick, this); - }, - - removeHooks: function () { - this._map.off('dblclick', this._onDoubleClick, this); - }, - - _onDoubleClick: function (e) { - var map = this._map, - zoom = map.getZoom() + (e.originalEvent.shiftKey ? -1 : 1); - - if (map.options.doubleClickZoom === 'center') { - map.setZoom(zoom); - } else { - map.setZoomAround(e.containerPoint, zoom); - } - } -}); - -L.Map.addInitHook('addHandler', 'doubleClickZoom', L.Map.DoubleClickZoom); - - -/* - * L.Handler.ScrollWheelZoom is used by L.Map to enable mouse scroll wheel zoom on the map. - */ - -L.Map.mergeOptions({ - scrollWheelZoom: true -}); - -L.Map.ScrollWheelZoom = L.Handler.extend({ - addHooks: function () { - L.DomEvent.on(this._map._container, 'mousewheel', this._onWheelScroll, this); - L.DomEvent.on(this._map._container, 'MozMousePixelScroll', L.DomEvent.preventDefault); - this._delta = 0; - }, - - removeHooks: function () { - L.DomEvent.off(this._map._container, 'mousewheel', this._onWheelScroll); - L.DomEvent.off(this._map._container, 'MozMousePixelScroll', L.DomEvent.preventDefault); - }, - - _onWheelScroll: function (e) { - var delta = L.DomEvent.getWheelDelta(e); - - this._delta += delta; - this._lastMousePos = this._map.mouseEventToContainerPoint(e); - - if (!this._startTime) { - this._startTime = +new Date(); - } - - var left = Math.max(40 - (+new Date() - this._startTime), 0); - - clearTimeout(this._timer); - this._timer = setTimeout(L.bind(this._performZoom, this), left); - - L.DomEvent.preventDefault(e); - L.DomEvent.stopPropagation(e); - }, - - _performZoom: function () { - var map = this._map, - delta = this._delta, - zoom = map.getZoom(); - - delta = delta > 0 ? Math.ceil(delta) : Math.floor(delta); - delta = Math.max(Math.min(delta, 4), -4); - delta = map._limitZoom(zoom + delta) - zoom; - - this._delta = 0; - this._startTime = null; - - if (!delta) { return; } - - if (map.options.scrollWheelZoom === 'center') { - map.setZoom(zoom + delta); - } else { - map.setZoomAround(this._lastMousePos, zoom + delta); - } - } -}); - -L.Map.addInitHook('addHandler', 'scrollWheelZoom', L.Map.ScrollWheelZoom); - - -/* - * Extends the event handling code with double tap support for mobile browsers. - */ - -L.extend(L.DomEvent, { - - _touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart', - _touchend: L.Browser.msPointer ? 'MSPointerUp' : L.Browser.pointer ? 'pointerup' : 'touchend', - - // inspired by Zepto touch code by Thomas Fuchs - addDoubleTapListener: function (obj, handler, id) { - var last, - doubleTap = false, - delay = 250, - touch, - pre = '_leaflet_', - touchstart = this._touchstart, - touchend = this._touchend, - trackedTouches = []; - - function onTouchStart(e) { - var count; - - if (L.Browser.pointer) { - trackedTouches.push(e.pointerId); - count = trackedTouches.length; - } else { - count = e.touches.length; - } - if (count > 1) { - return; - } - - var now = Date.now(), - delta = now - (last || now); - - touch = e.touches ? e.touches[0] : e; - doubleTap = (delta > 0 && delta <= delay); - last = now; - } - - function onTouchEnd(e) { - if (L.Browser.pointer) { - var idx = trackedTouches.indexOf(e.pointerId); - if (idx === -1) { - return; - } - trackedTouches.splice(idx, 1); - } - - if (doubleTap) { - if (L.Browser.pointer) { - // work around .type being readonly with MSPointer* events - var newTouch = { }, - prop; - - // jshint forin:false - for (var i in touch) { - prop = touch[i]; - if (typeof prop === 'function') { - newTouch[i] = prop.bind(touch); - } else { - newTouch[i] = prop; - } - } - touch = newTouch; - } - touch.type = 'dblclick'; - handler(touch); - last = null; - } - } - obj[pre + touchstart + id] = onTouchStart; - obj[pre + touchend + id] = onTouchEnd; - - // on pointer we need to listen on the document, otherwise a drag starting on the map and moving off screen - // will not come through to us, so we will lose track of how many touches are ongoing - var endElement = L.Browser.pointer ? document.documentElement : obj; - - obj.addEventListener(touchstart, onTouchStart, false); - endElement.addEventListener(touchend, onTouchEnd, false); - - if (L.Browser.pointer) { - endElement.addEventListener(L.DomEvent.POINTER_CANCEL, onTouchEnd, false); - } - - return this; - }, - - removeDoubleTapListener: function (obj, id) { - var pre = '_leaflet_'; - - obj.removeEventListener(this._touchstart, obj[pre + this._touchstart + id], false); - (L.Browser.pointer ? document.documentElement : obj).removeEventListener( - this._touchend, obj[pre + this._touchend + id], false); - - if (L.Browser.pointer) { - document.documentElement.removeEventListener(L.DomEvent.POINTER_CANCEL, obj[pre + this._touchend + id], - false); - } - - return this; - } -}); - - -/* - * Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices. - */ - -L.extend(L.DomEvent, { - - //static - POINTER_DOWN: L.Browser.msPointer ? 'MSPointerDown' : 'pointerdown', - POINTER_MOVE: L.Browser.msPointer ? 'MSPointerMove' : 'pointermove', - POINTER_UP: L.Browser.msPointer ? 'MSPointerUp' : 'pointerup', - POINTER_CANCEL: L.Browser.msPointer ? 'MSPointerCancel' : 'pointercancel', - - _pointers: [], - _pointerDocumentListener: false, - - // Provides a touch events wrapper for (ms)pointer events. - // Based on changes by veproza https://github.com/CloudMade/Leaflet/pull/1019 - //ref http://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890 - - addPointerListener: function (obj, type, handler, id) { - - switch (type) { - case 'touchstart': - return this.addPointerListenerStart(obj, type, handler, id); - case 'touchend': - return this.addPointerListenerEnd(obj, type, handler, id); - case 'touchmove': - return this.addPointerListenerMove(obj, type, handler, id); - default: - throw 'Unknown touch event type'; - } - }, - - addPointerListenerStart: function (obj, type, handler, id) { - var pre = '_leaflet_', - pointers = this._pointers; - - var cb = function (e) { - if (e.pointerType !== 'mouse' && e.pointerType !== e.MSPOINTER_TYPE_MOUSE) { - L.DomEvent.preventDefault(e); - } - - var alreadyInArray = false; - for (var i = 0; i < pointers.length; i++) { - if (pointers[i].pointerId === e.pointerId) { - alreadyInArray = true; - break; - } - } - if (!alreadyInArray) { - pointers.push(e); - } - - e.touches = pointers.slice(); - e.changedTouches = [e]; - - handler(e); - }; - - obj[pre + 'touchstart' + id] = cb; - obj.addEventListener(this.POINTER_DOWN, cb, false); - - // need to also listen for end events to keep the _pointers list accurate - // this needs to be on the body and never go away - if (!this._pointerDocumentListener) { - var internalCb = function (e) { - for (var i = 0; i < pointers.length; i++) { - if (pointers[i].pointerId === e.pointerId) { - pointers.splice(i, 1); - break; - } - } - }; - //We listen on the documentElement as any drags that end by moving the touch off the screen get fired there - document.documentElement.addEventListener(this.POINTER_UP, internalCb, false); - document.documentElement.addEventListener(this.POINTER_CANCEL, internalCb, false); - - this._pointerDocumentListener = true; - } - - return this; - }, - - addPointerListenerMove: function (obj, type, handler, id) { - var pre = '_leaflet_', - touches = this._pointers; - - function cb(e) { - - // don't fire touch moves when mouse isn't down - if ((e.pointerType === e.MSPOINTER_TYPE_MOUSE || e.pointerType === 'mouse') && e.buttons === 0) { return; } - - for (var i = 0; i < touches.length; i++) { - if (touches[i].pointerId === e.pointerId) { - touches[i] = e; - break; - } - } - - e.touches = touches.slice(); - e.changedTouches = [e]; - - handler(e); - } - - obj[pre + 'touchmove' + id] = cb; - obj.addEventListener(this.POINTER_MOVE, cb, false); - - return this; - }, - - addPointerListenerEnd: function (obj, type, handler, id) { - var pre = '_leaflet_', - touches = this._pointers; - - var cb = function (e) { - for (var i = 0; i < touches.length; i++) { - if (touches[i].pointerId === e.pointerId) { - touches.splice(i, 1); - break; - } - } - - e.touches = touches.slice(); - e.changedTouches = [e]; - - handler(e); - }; - - obj[pre + 'touchend' + id] = cb; - obj.addEventListener(this.POINTER_UP, cb, false); - obj.addEventListener(this.POINTER_CANCEL, cb, false); - - return this; - }, - - removePointerListener: function (obj, type, id) { - var pre = '_leaflet_', - cb = obj[pre + type + id]; - - switch (type) { - case 'touchstart': - obj.removeEventListener(this.POINTER_DOWN, cb, false); - break; - case 'touchmove': - obj.removeEventListener(this.POINTER_MOVE, cb, false); - break; - case 'touchend': - obj.removeEventListener(this.POINTER_UP, cb, false); - obj.removeEventListener(this.POINTER_CANCEL, cb, false); - break; - } - - return this; - } -}); - - -/* - * L.Handler.TouchZoom is used by L.Map to add pinch zoom on supported mobile browsers. - */ - -L.Map.mergeOptions({ - touchZoom: L.Browser.touch && !L.Browser.android23, - bounceAtZoomLimits: true -}); - -L.Map.TouchZoom = L.Handler.extend({ - addHooks: function () { - L.DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this); - }, - - removeHooks: function () { - L.DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this); - }, - - _onTouchStart: function (e) { - var map = this._map; - - if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; } - - var p1 = map.mouseEventToLayerPoint(e.touches[0]), - p2 = map.mouseEventToLayerPoint(e.touches[1]), - viewCenter = map._getCenterLayerPoint(); - - this._startCenter = p1.add(p2)._divideBy(2); - this._startDist = p1.distanceTo(p2); - - this._moved = false; - this._zooming = true; - - this._centerOffset = viewCenter.subtract(this._startCenter); - - if (map._panAnim) { - map._panAnim.stop(); - } - - L.DomEvent - .on(document, 'touchmove', this._onTouchMove, this) - .on(document, 'touchend', this._onTouchEnd, this); - - L.DomEvent.preventDefault(e); - }, - - _onTouchMove: function (e) { - var map = this._map; - - if (!e.touches || e.touches.length !== 2 || !this._zooming) { return; } - - var p1 = map.mouseEventToLayerPoint(e.touches[0]), - p2 = map.mouseEventToLayerPoint(e.touches[1]); - - this._scale = p1.distanceTo(p2) / this._startDist; - this._delta = p1._add(p2)._divideBy(2)._subtract(this._startCenter); - - if (this._scale === 1) { return; } - - if (!map.options.bounceAtZoomLimits) { - if ((map.getZoom() === map.getMinZoom() && this._scale < 1) || - (map.getZoom() === map.getMaxZoom() && this._scale > 1)) { return; } - } - - if (!this._moved) { - L.DomUtil.addClass(map._mapPane, 'leaflet-touching'); - - map - .fire('movestart') - .fire('zoomstart'); - - this._moved = true; - } - - L.Util.cancelAnimFrame(this._animRequest); - this._animRequest = L.Util.requestAnimFrame( - this._updateOnMove, this, true, this._map._container); - - L.DomEvent.preventDefault(e); - }, - - _updateOnMove: function () { - var map = this._map, - origin = this._getScaleOrigin(), - center = map.layerPointToLatLng(origin), - zoom = map.getScaleZoom(this._scale); - - map._animateZoom(center, zoom, this._startCenter, this._scale, this._delta, false, true); - }, - - _onTouchEnd: function () { - if (!this._moved || !this._zooming) { - this._zooming = false; - return; - } - - var map = this._map; - - this._zooming = false; - L.DomUtil.removeClass(map._mapPane, 'leaflet-touching'); - L.Util.cancelAnimFrame(this._animRequest); - - L.DomEvent - .off(document, 'touchmove', this._onTouchMove) - .off(document, 'touchend', this._onTouchEnd); - - var origin = this._getScaleOrigin(), - center = map.layerPointToLatLng(origin), - - oldZoom = map.getZoom(), - floatZoomDelta = map.getScaleZoom(this._scale) - oldZoom, - roundZoomDelta = (floatZoomDelta > 0 ? - Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)), - - zoom = map._limitZoom(oldZoom + roundZoomDelta), - scale = map.getZoomScale(zoom) / this._scale; - - map._animateZoom(center, zoom, origin, scale); - }, - - _getScaleOrigin: function () { - var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale); - return this._startCenter.add(centerOffset); - } -}); - -L.Map.addInitHook('addHandler', 'touchZoom', L.Map.TouchZoom); - - -/* - * L.Map.Tap is used to enable mobile hacks like quick taps and long hold. - */ - -L.Map.mergeOptions({ - tap: true, - tapTolerance: 15 -}); - -L.Map.Tap = L.Handler.extend({ - addHooks: function () { - L.DomEvent.on(this._map._container, 'touchstart', this._onDown, this); - }, - - removeHooks: function () { - L.DomEvent.off(this._map._container, 'touchstart', this._onDown, this); - }, - - _onDown: function (e) { - if (!e.touches) { return; } - - L.DomEvent.preventDefault(e); - - this._fireClick = true; - - // don't simulate click or track longpress if more than 1 touch - if (e.touches.length > 1) { - this._fireClick = false; - clearTimeout(this._holdTimeout); - return; - } - - var first = e.touches[0], - el = first.target; - - this._startPos = this._newPos = new L.Point(first.clientX, first.clientY); - - // if touching a link, highlight it - if (el.tagName && el.tagName.toLowerCase() === 'a') { - L.DomUtil.addClass(el, 'leaflet-active'); - } - - // simulate long hold but setting a timeout - this._holdTimeout = setTimeout(L.bind(function () { - if (this._isTapValid()) { - this._fireClick = false; - this._onUp(); - this._simulateEvent('contextmenu', first); - } - }, this), 1000); - - L.DomEvent - .on(document, 'touchmove', this._onMove, this) - .on(document, 'touchend', this._onUp, this); - }, - - _onUp: function (e) { - clearTimeout(this._holdTimeout); - - L.DomEvent - .off(document, 'touchmove', this._onMove, this) - .off(document, 'touchend', this._onUp, this); - - if (this._fireClick && e && e.changedTouches) { - - var first = e.changedTouches[0], - el = first.target; - - if (el && el.tagName && el.tagName.toLowerCase() === 'a') { - L.DomUtil.removeClass(el, 'leaflet-active'); - } - - // simulate click if the touch didn't move too much - if (this._isTapValid()) { - this._simulateEvent('click', first); - } - } - }, - - _isTapValid: function () { - return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance; - }, - - _onMove: function (e) { - var first = e.touches[0]; - this._newPos = new L.Point(first.clientX, first.clientY); - }, - - _simulateEvent: function (type, e) { - var simulatedEvent = document.createEvent('MouseEvents'); - - simulatedEvent._simulated = true; - e.target._simulatedClick = true; - - simulatedEvent.initMouseEvent( - type, true, true, window, 1, - e.screenX, e.screenY, - e.clientX, e.clientY, - false, false, false, false, 0, null); - - e.target.dispatchEvent(simulatedEvent); - } -}); - -if (L.Browser.touch && !L.Browser.pointer) { - L.Map.addInitHook('addHandler', 'tap', L.Map.Tap); -} - - -/* - * L.Handler.ShiftDragZoom is used to add shift-drag zoom interaction to the map - * (zoom to a selected bounding box), enabled by default. - */ - -L.Map.mergeOptions({ - boxZoom: true -}); - -L.Map.BoxZoom = L.Handler.extend({ - initialize: function (map) { - this._map = map; - this._container = map._container; - this._pane = map._panes.overlayPane; - this._moved = false; - }, - - addHooks: function () { - L.DomEvent.on(this._container, 'mousedown', this._onMouseDown, this); - }, - - removeHooks: function () { - L.DomEvent.off(this._container, 'mousedown', this._onMouseDown); - this._moved = false; - }, - - moved: function () { - return this._moved; - }, - - _onMouseDown: function (e) { - this._moved = false; - - if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; } - - L.DomUtil.disableTextSelection(); - L.DomUtil.disableImageDrag(); - - this._startLayerPoint = this._map.mouseEventToLayerPoint(e); - - L.DomEvent - .on(document, 'mousemove', this._onMouseMove, this) - .on(document, 'mouseup', this._onMouseUp, this) - .on(document, 'keydown', this._onKeyDown, this); - }, - - _onMouseMove: function (e) { - if (!this._moved) { - this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._pane); - L.DomUtil.setPosition(this._box, this._startLayerPoint); - - //TODO refactor: move cursor to styles - this._container.style.cursor = 'crosshair'; - this._map.fire('boxzoomstart'); - } - - var startPoint = this._startLayerPoint, - box = this._box, - - layerPoint = this._map.mouseEventToLayerPoint(e), - offset = layerPoint.subtract(startPoint), - - newPos = new L.Point( - Math.min(layerPoint.x, startPoint.x), - Math.min(layerPoint.y, startPoint.y)); - - L.DomUtil.setPosition(box, newPos); - - this._moved = true; - - // TODO refactor: remove hardcoded 4 pixels - box.style.width = (Math.max(0, Math.abs(offset.x) - 4)) + 'px'; - box.style.height = (Math.max(0, Math.abs(offset.y) - 4)) + 'px'; - }, - - _finish: function () { - if (this._moved) { - this._pane.removeChild(this._box); - this._container.style.cursor = ''; - } - - L.DomUtil.enableTextSelection(); - L.DomUtil.enableImageDrag(); - - L.DomEvent - .off(document, 'mousemove', this._onMouseMove) - .off(document, 'mouseup', this._onMouseUp) - .off(document, 'keydown', this._onKeyDown); - }, - - _onMouseUp: function (e) { - - this._finish(); - - var map = this._map, - layerPoint = map.mouseEventToLayerPoint(e); - - if (this._startLayerPoint.equals(layerPoint)) { return; } - - var bounds = new L.LatLngBounds( - map.layerPointToLatLng(this._startLayerPoint), - map.layerPointToLatLng(layerPoint)); - - map.fitBounds(bounds); - - map.fire('boxzoomend', { - boxZoomBounds: bounds - }); - }, - - _onKeyDown: function (e) { - if (e.keyCode === 27) { - this._finish(); - } - } -}); - -L.Map.addInitHook('addHandler', 'boxZoom', L.Map.BoxZoom); - - -/* - * L.Map.Keyboard is handling keyboard interaction with the map, enabled by default. - */ - -L.Map.mergeOptions({ - keyboard: true, - keyboardPanOffset: 80, - keyboardZoomOffset: 1 -}); - -L.Map.Keyboard = L.Handler.extend({ - - keyCodes: { - left: [37], - right: [39], - down: [40], - up: [38], - zoomIn: [187, 107, 61, 171], - zoomOut: [189, 109, 173] - }, - - initialize: function (map) { - this._map = map; - - this._setPanOffset(map.options.keyboardPanOffset); - this._setZoomOffset(map.options.keyboardZoomOffset); - }, - - addHooks: function () { - var container = this._map._container; - - // make the container focusable by tabbing - if (container.tabIndex === -1) { - container.tabIndex = '0'; - } - - L.DomEvent - .on(container, 'focus', this._onFocus, this) - .on(container, 'blur', this._onBlur, this) - .on(container, 'mousedown', this._onMouseDown, this); - - this._map - .on('focus', this._addHooks, this) - .on('blur', this._removeHooks, this); - }, - - removeHooks: function () { - this._removeHooks(); - - var container = this._map._container; - - L.DomEvent - .off(container, 'focus', this._onFocus, this) - .off(container, 'blur', this._onBlur, this) - .off(container, 'mousedown', this._onMouseDown, this); - - this._map - .off('focus', this._addHooks, this) - .off('blur', this._removeHooks, this); - }, - - _onMouseDown: function () { - if (this._focused) { return; } - - var body = document.body, - docEl = document.documentElement, - top = body.scrollTop || docEl.scrollTop, - left = body.scrollLeft || docEl.scrollLeft; - - this._map._container.focus(); - - window.scrollTo(left, top); - }, - - _onFocus: function () { - this._focused = true; - this._map.fire('focus'); - }, - - _onBlur: function () { - this._focused = false; - this._map.fire('blur'); - }, - - _setPanOffset: function (pan) { - var keys = this._panKeys = {}, - codes = this.keyCodes, - i, len; - - for (i = 0, len = codes.left.length; i < len; i++) { - keys[codes.left[i]] = [-1 * pan, 0]; - } - for (i = 0, len = codes.right.length; i < len; i++) { - keys[codes.right[i]] = [pan, 0]; - } - for (i = 0, len = codes.down.length; i < len; i++) { - keys[codes.down[i]] = [0, pan]; - } - for (i = 0, len = codes.up.length; i < len; i++) { - keys[codes.up[i]] = [0, -1 * pan]; - } - }, - - _setZoomOffset: function (zoom) { - var keys = this._zoomKeys = {}, - codes = this.keyCodes, - i, len; - - for (i = 0, len = codes.zoomIn.length; i < len; i++) { - keys[codes.zoomIn[i]] = zoom; - } - for (i = 0, len = codes.zoomOut.length; i < len; i++) { - keys[codes.zoomOut[i]] = -zoom; - } - }, - - _addHooks: function () { - L.DomEvent.on(document, 'keydown', this._onKeyDown, this); - }, - - _removeHooks: function () { - L.DomEvent.off(document, 'keydown', this._onKeyDown, this); - }, - - _onKeyDown: function (e) { - var key = e.keyCode, - map = this._map; - - if (key in this._panKeys) { - - if (map._panAnim && map._panAnim._inProgress) { return; } - - map.panBy(this._panKeys[key]); - - if (map.options.maxBounds) { - map.panInsideBounds(map.options.maxBounds); - } - - } else if (key in this._zoomKeys) { - map.setZoom(map.getZoom() + this._zoomKeys[key]); - - } else { - return; - } - - L.DomEvent.stop(e); - } -}); - -L.Map.addInitHook('addHandler', 'keyboard', L.Map.Keyboard); - - -/* - * L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable. - */ - -L.Handler.MarkerDrag = L.Handler.extend({ - initialize: function (marker) { - this._marker = marker; - }, - - addHooks: function () { - var icon = this._marker._icon; - if (!this._draggable) { - this._draggable = new L.Draggable(icon, icon); - } - - this._draggable - .on('dragstart', this._onDragStart, this) - .on('drag', this._onDrag, this) - .on('dragend', this._onDragEnd, this); - this._draggable.enable(); - L.DomUtil.addClass(this._marker._icon, 'leaflet-marker-draggable'); - }, - - removeHooks: function () { - this._draggable - .off('dragstart', this._onDragStart, this) - .off('drag', this._onDrag, this) - .off('dragend', this._onDragEnd, this); - - this._draggable.disable(); - L.DomUtil.removeClass(this._marker._icon, 'leaflet-marker-draggable'); - }, - - moved: function () { - return this._draggable && this._draggable._moved; - }, - - _onDragStart: function () { - this._marker - .closePopup() - .fire('movestart') - .fire('dragstart'); - }, - - _onDrag: function () { - var marker = this._marker, - shadow = marker._shadow, - iconPos = L.DomUtil.getPosition(marker._icon), - latlng = marker._map.layerPointToLatLng(iconPos); - - // update shadow position - if (shadow) { - L.DomUtil.setPosition(shadow, iconPos); - } - - marker._latlng = latlng; - - marker - .fire('move', {latlng: latlng}) - .fire('drag'); - }, - - _onDragEnd: function (e) { - this._marker - .fire('moveend') - .fire('dragend', e); - } -}); - - -/* - * L.Control is a base class for implementing map controls. Handles positioning. - * All other controls extend from this class. - */ - -L.Control = L.Class.extend({ - options: { - position: 'topright' - }, - - initialize: function (options) { - L.setOptions(this, options); - }, - - getPosition: function () { - return this.options.position; - }, - - setPosition: function (position) { - var map = this._map; - - if (map) { - map.removeControl(this); - } - - this.options.position = position; - - if (map) { - map.addControl(this); - } - - return this; - }, - - getContainer: function () { - return this._container; - }, - - addTo: function (map) { - this._map = map; - - var container = this._container = this.onAdd(map), - pos = this.getPosition(), - corner = map._controlCorners[pos]; - - L.DomUtil.addClass(container, 'leaflet-control'); - - if (pos.indexOf('bottom') !== -1) { - corner.insertBefore(container, corner.firstChild); - } else { - corner.appendChild(container); - } - - return this; - }, - - removeFrom: function (map) { - var pos = this.getPosition(), - corner = map._controlCorners[pos]; - - corner.removeChild(this._container); - this._map = null; - - if (this.onRemove) { - this.onRemove(map); - } - - return this; - }, - - _refocusOnMap: function () { - if (this._map) { - this._map.getContainer().focus(); - } - } -}); - -L.control = function (options) { - return new L.Control(options); -}; - - -// adds control-related methods to L.Map - -L.Map.include({ - addControl: function (control) { - control.addTo(this); - return this; - }, - - removeControl: function (control) { - control.removeFrom(this); - return this; - }, - - _initControlPos: function () { - var corners = this._controlCorners = {}, - l = 'leaflet-', - container = this._controlContainer = - L.DomUtil.create('div', l + 'control-container', this._container); - - function createCorner(vSide, hSide) { - var className = l + vSide + ' ' + l + hSide; - - corners[vSide + hSide] = L.DomUtil.create('div', className, container); - } - - createCorner('top', 'left'); - createCorner('top', 'right'); - createCorner('bottom', 'left'); - createCorner('bottom', 'right'); - }, - - _clearControlPos: function () { - this._container.removeChild(this._controlContainer); - } -}); - - -/* - * L.Control.Zoom is used for the default zoom buttons on the map. - */ - -L.Control.Zoom = L.Control.extend({ - options: { - position: 'topleft', - zoomInText: '+', - zoomInTitle: 'Zoom in', - zoomOutText: '-', - zoomOutTitle: 'Zoom out' - }, - - onAdd: function (map) { - var zoomName = 'leaflet-control-zoom', - container = L.DomUtil.create('div', zoomName + ' leaflet-bar'); - - this._map = map; - - this._zoomInButton = this._createButton( - this.options.zoomInText, this.options.zoomInTitle, - zoomName + '-in', container, this._zoomIn, this); - this._zoomOutButton = this._createButton( - this.options.zoomOutText, this.options.zoomOutTitle, - zoomName + '-out', container, this._zoomOut, this); - - this._updateDisabled(); - map.on('zoomend zoomlevelschange', this._updateDisabled, this); - - return container; - }, - - onRemove: function (map) { - map.off('zoomend zoomlevelschange', this._updateDisabled, this); - }, - - _zoomIn: function (e) { - this._map.zoomIn(e.shiftKey ? 3 : 1); - }, - - _zoomOut: function (e) { - this._map.zoomOut(e.shiftKey ? 3 : 1); - }, - - _createButton: function (html, title, className, container, fn, context) { - var link = L.DomUtil.create('a', className, container); - link.innerHTML = html; - link.href = '#'; - link.title = title; - - var stop = L.DomEvent.stopPropagation; - - L.DomEvent - .on(link, 'click', stop) - .on(link, 'mousedown', stop) - .on(link, 'dblclick', stop) - .on(link, 'click', L.DomEvent.preventDefault) - .on(link, 'click', fn, context) - .on(link, 'click', this._refocusOnMap, context); - - return link; - }, - - _updateDisabled: function () { - var map = this._map, - className = 'leaflet-disabled'; - - L.DomUtil.removeClass(this._zoomInButton, className); - L.DomUtil.removeClass(this._zoomOutButton, className); - - if (map._zoom === map.getMinZoom()) { - L.DomUtil.addClass(this._zoomOutButton, className); - } - if (map._zoom === map.getMaxZoom()) { - L.DomUtil.addClass(this._zoomInButton, className); - } - } -}); - -L.Map.mergeOptions({ - zoomControl: true -}); - -L.Map.addInitHook(function () { - if (this.options.zoomControl) { - this.zoomControl = new L.Control.Zoom(); - this.addControl(this.zoomControl); - } -}); - -L.control.zoom = function (options) { - return new L.Control.Zoom(options); -}; - - - -/* - * L.Control.Attribution is used for displaying attribution on the map (added by default). - */ - -L.Control.Attribution = L.Control.extend({ - options: { - position: 'bottomright', - prefix: 'Leaflet' - }, - - initialize: function (options) { - L.setOptions(this, options); - - this._attributions = {}; - }, - - onAdd: function (map) { - this._container = L.DomUtil.create('div', 'leaflet-control-attribution'); - L.DomEvent.disableClickPropagation(this._container); - - for (var i in map._layers) { - if (map._layers[i].getAttribution) { - this.addAttribution(map._layers[i].getAttribution()); - } - } - - map - .on('layeradd', this._onLayerAdd, this) - .on('layerremove', this._onLayerRemove, this); - - this._update(); - - return this._container; - }, - - onRemove: function (map) { - map - .off('layeradd', this._onLayerAdd) - .off('layerremove', this._onLayerRemove); - - }, - - setPrefix: function (prefix) { - this.options.prefix = prefix; - this._update(); - return this; - }, - - addAttribution: function (text) { - if (!text) { return; } - - if (!this._attributions[text]) { - this._attributions[text] = 0; - } - this._attributions[text]++; - - this._update(); - - return this; - }, - - removeAttribution: function (text) { - if (!text) { return; } - - if (this._attributions[text]) { - this._attributions[text]--; - this._update(); - } - - return this; - }, - - _update: function () { - if (!this._map) { return; } - - var attribs = []; - - for (var i in this._attributions) { - if (this._attributions[i]) { - attribs.push(i); - } - } - - var prefixAndAttribs = []; - - if (this.options.prefix) { - prefixAndAttribs.push(this.options.prefix); - } - if (attribs.length) { - prefixAndAttribs.push(attribs.join(', ')); - } - - this._container.innerHTML = prefixAndAttribs.join(' | '); - }, - - _onLayerAdd: function (e) { - if (e.layer.getAttribution) { - this.addAttribution(e.layer.getAttribution()); - } - }, - - _onLayerRemove: function (e) { - if (e.layer.getAttribution) { - this.removeAttribution(e.layer.getAttribution()); - } - } -}); - -L.Map.mergeOptions({ - attributionControl: true -}); - -L.Map.addInitHook(function () { - if (this.options.attributionControl) { - this.attributionControl = (new L.Control.Attribution()).addTo(this); - } -}); - -L.control.attribution = function (options) { - return new L.Control.Attribution(options); -}; - - -/* - * L.Control.Scale is used for displaying metric/imperial scale on the map. - */ - -L.Control.Scale = L.Control.extend({ - options: { - position: 'bottomleft', - maxWidth: 100, - metric: true, - imperial: true, - updateWhenIdle: false - }, - - onAdd: function (map) { - this._map = map; - - var className = 'leaflet-control-scale', - container = L.DomUtil.create('div', className), - options = this.options; - - this._addScales(options, className, container); - - map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this); - map.whenReady(this._update, this); - - return container; - }, - - onRemove: function (map) { - map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this); - }, - - _addScales: function (options, className, container) { - if (options.metric) { - this._mScale = L.DomUtil.create('div', className + '-line', container); - } - if (options.imperial) { - this._iScale = L.DomUtil.create('div', className + '-line', container); - } - }, - - _update: function () { - var bounds = this._map.getBounds(), - centerLat = bounds.getCenter().lat, - halfWorldMeters = 6378137 * Math.PI * Math.cos(centerLat * Math.PI / 180), - dist = halfWorldMeters * (bounds.getNorthEast().lng - bounds.getSouthWest().lng) / 180, - - size = this._map.getSize(), - options = this.options, - maxMeters = 0; - - if (size.x > 0) { - maxMeters = dist * (options.maxWidth / size.x); - } - - this._updateScales(options, maxMeters); - }, - - _updateScales: function (options, maxMeters) { - if (options.metric && maxMeters) { - this._updateMetric(maxMeters); - } - - if (options.imperial && maxMeters) { - this._updateImperial(maxMeters); - } - }, - - _updateMetric: function (maxMeters) { - var meters = this._getRoundNum(maxMeters); - - this._mScale.style.width = this._getScaleWidth(meters / maxMeters) + 'px'; - this._mScale.innerHTML = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km'; - }, - - _updateImperial: function (maxMeters) { - var maxFeet = maxMeters * 3.2808399, - scale = this._iScale, - maxMiles, miles, feet; - - if (maxFeet > 5280) { - maxMiles = maxFeet / 5280; - miles = this._getRoundNum(maxMiles); - - scale.style.width = this._getScaleWidth(miles / maxMiles) + 'px'; - scale.innerHTML = miles + ' mi'; - - } else { - feet = this._getRoundNum(maxFeet); - - scale.style.width = this._getScaleWidth(feet / maxFeet) + 'px'; - scale.innerHTML = feet + ' ft'; - } - }, - - _getScaleWidth: function (ratio) { - return Math.round(this.options.maxWidth * ratio) - 10; - }, - - _getRoundNum: function (num) { - var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1), - d = num / pow10; - - d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1; - - return pow10 * d; - } -}); - -L.control.scale = function (options) { - return new L.Control.Scale(options); -}; - - -/* - * L.Control.Layers is a control to allow users to switch between different layers on the map. - */ - -L.Control.Layers = L.Control.extend({ - options: { - collapsed: true, - position: 'topright', - autoZIndex: true - }, - - initialize: function (baseLayers, overlays, options) { - L.setOptions(this, options); - - this._layers = {}; - this._lastZIndex = 0; - this._handlingClick = false; - - for (var i in baseLayers) { - this._addLayer(baseLayers[i], i); - } - - for (i in overlays) { - this._addLayer(overlays[i], i, true); - } - }, - - onAdd: function (map) { - this._initLayout(); - this._update(); - - map - .on('layeradd', this._onLayerChange, this) - .on('layerremove', this._onLayerChange, this); - - return this._container; - }, - - onRemove: function (map) { - map - .off('layeradd', this._onLayerChange, this) - .off('layerremove', this._onLayerChange, this); - }, - - addBaseLayer: function (layer, name) { - this._addLayer(layer, name); - this._update(); - return this; - }, - - addOverlay: function (layer, name) { - this._addLayer(layer, name, true); - this._update(); - return this; - }, - - removeLayer: function (layer) { - var id = L.stamp(layer); - delete this._layers[id]; - this._update(); - return this; - }, - - _initLayout: function () { - var className = 'leaflet-control-layers', - container = this._container = L.DomUtil.create('div', className); - - //Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released - container.setAttribute('aria-haspopup', true); - - if (!L.Browser.touch) { - L.DomEvent - .disableClickPropagation(container) - .disableScrollPropagation(container); - } else { - L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation); - } - - var form = this._form = L.DomUtil.create('form', className + '-list'); - - if (this.options.collapsed) { - if (!L.Browser.android) { - L.DomEvent - .on(container, 'mouseover', this._expand, this) - .on(container, 'mouseout', this._collapse, this); - } - var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container); - link.href = '#'; - link.title = 'Layers'; - - if (L.Browser.touch) { - L.DomEvent - .on(link, 'click', L.DomEvent.stop) - .on(link, 'click', this._expand, this); - } - else { - L.DomEvent.on(link, 'focus', this._expand, this); - } - //Work around for Firefox android issue https://github.com/Leaflet/Leaflet/issues/2033 - L.DomEvent.on(form, 'click', function () { - setTimeout(L.bind(this._onInputClick, this), 0); - }, this); - - this._map.on('click', this._collapse, this); - // TODO keyboard accessibility - } else { - this._expand(); - } - - this._baseLayersList = L.DomUtil.create('div', className + '-base', form); - this._separator = L.DomUtil.create('div', className + '-separator', form); - this._overlaysList = L.DomUtil.create('div', className + '-overlays', form); - - container.appendChild(form); - }, - - _addLayer: function (layer, name, overlay) { - var id = L.stamp(layer); - - this._layers[id] = { - layer: layer, - name: name, - overlay: overlay - }; - - if (this.options.autoZIndex && layer.setZIndex) { - this._lastZIndex++; - layer.setZIndex(this._lastZIndex); - } - }, - - _update: function () { - if (!this._container) { - return; - } - - this._baseLayersList.innerHTML = ''; - this._overlaysList.innerHTML = ''; - - var baseLayersPresent = false, - overlaysPresent = false, - i, obj; - - for (i in this._layers) { - obj = this._layers[i]; - this._addItem(obj); - overlaysPresent = overlaysPresent || obj.overlay; - baseLayersPresent = baseLayersPresent || !obj.overlay; - } - - this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none'; - }, - - _onLayerChange: function (e) { - var obj = this._layers[L.stamp(e.layer)]; - - if (!obj) { return; } - - if (!this._handlingClick) { - this._update(); - } - - var type = obj.overlay ? - (e.type === 'layeradd' ? 'overlayadd' : 'overlayremove') : - (e.type === 'layeradd' ? 'baselayerchange' : null); - - if (type) { - this._map.fire(type, obj); - } - }, - - // IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe) - _createRadioElement: function (name, checked) { - - var radioHtml = '= 0) { - this._onZoomTransitionEnd(); - } - }, - - _nothingToAnimate: function () { - return !this._container.getElementsByClassName('leaflet-zoom-animated').length; - }, - - _tryAnimatedZoom: function (center, zoom, options) { - - if (this._animatingZoom) { return true; } - - options = options || {}; - - // don't animate if disabled, not supported or zoom difference is too large - if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() || - Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; } - - // offset is the pixel coords of the zoom origin relative to the current center - var scale = this.getZoomScale(zoom), - offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale), - origin = this._getCenterLayerPoint()._add(offset); - - // don't animate if the zoom origin isn't within one screen from the current center, unless forced - if (options.animate !== true && !this.getSize().contains(offset)) { return false; } - - this - .fire('movestart') - .fire('zoomstart'); - - this._animateZoom(center, zoom, origin, scale, null, true); - - return true; - }, - - _animateZoom: function (center, zoom, origin, scale, delta, backwards, forTouchZoom) { - - if (!forTouchZoom) { - this._animatingZoom = true; - } - - // put transform transition on all layers with leaflet-zoom-animated class - L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim'); - - // remember what center/zoom to set after animation - this._animateToCenter = center; - this._animateToZoom = zoom; - - // disable any dragging during animation - if (L.Draggable) { - L.Draggable._disabled = true; - } - - L.Util.requestAnimFrame(function () { - this.fire('zoomanim', { - center: center, - zoom: zoom, - origin: origin, - scale: scale, - delta: delta, - backwards: backwards - }); - // horrible hack to work around a Chrome bug https://github.com/Leaflet/Leaflet/issues/3689 - setTimeout(L.bind(this._onZoomTransitionEnd, this), 250); - }, this); - }, - - _onZoomTransitionEnd: function () { - if (!this._animatingZoom) { return; } - - this._animatingZoom = false; - - L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim'); - - L.Util.requestAnimFrame(function () { - this._resetView(this._animateToCenter, this._animateToZoom, true, true); - - if (L.Draggable) { - L.Draggable._disabled = false; - } - }, this); - } -}); - - -/* - Zoom animation logic for L.TileLayer. -*/ - -L.TileLayer.include({ - _animateZoom: function (e) { - if (!this._animating) { - this._animating = true; - this._prepareBgBuffer(); - } - - var bg = this._bgBuffer, - transform = L.DomUtil.TRANSFORM, - initialTransform = e.delta ? L.DomUtil.getTranslateString(e.delta) : bg.style[transform], - scaleStr = L.DomUtil.getScaleString(e.scale, e.origin); - - bg.style[transform] = e.backwards ? - scaleStr + ' ' + initialTransform : - initialTransform + ' ' + scaleStr; - }, - - _endZoomAnim: function () { - var front = this._tileContainer, - bg = this._bgBuffer; - - front.style.visibility = ''; - front.parentNode.appendChild(front); // Bring to fore - - // force reflow - L.Util.falseFn(bg.offsetWidth); - - var zoom = this._map.getZoom(); - if (zoom > this.options.maxZoom || zoom < this.options.minZoom) { - this._clearBgBuffer(); - } - - this._animating = false; - }, - - _clearBgBuffer: function () { - var map = this._map; - - if (map && !map._animatingZoom && !map.touchZoom._zooming) { - this._bgBuffer.innerHTML = ''; - this._bgBuffer.style[L.DomUtil.TRANSFORM] = ''; - } - }, - - _prepareBgBuffer: function () { - - var front = this._tileContainer, - bg = this._bgBuffer; - - // if foreground layer doesn't have many tiles but bg layer does, - // keep the existing bg layer and just zoom it some more - - var bgLoaded = this._getLoadedTilesPercentage(bg), - frontLoaded = this._getLoadedTilesPercentage(front); - - if (bg && bgLoaded > 0.5 && frontLoaded < 0.5) { - - front.style.visibility = 'hidden'; - this._stopLoadingImages(front); - return; - } - - // prepare the buffer to become the front tile pane - bg.style.visibility = 'hidden'; - bg.style[L.DomUtil.TRANSFORM] = ''; - - // switch out the current layer to be the new bg layer (and vice-versa) - this._tileContainer = bg; - bg = this._bgBuffer = front; - - this._stopLoadingImages(bg); - - //prevent bg buffer from clearing right after zoom - clearTimeout(this._clearBgBufferTimer); - }, - - _getLoadedTilesPercentage: function (container) { - var tiles = container.getElementsByTagName('img'), - i, len, count = 0; - - for (i = 0, len = tiles.length; i < len; i++) { - if (tiles[i].complete) { - count++; - } - } - return count / len; - }, - - // stops loading all tiles in the background layer - _stopLoadingImages: function (container) { - var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')), - i, len, tile; - - for (i = 0, len = tiles.length; i < len; i++) { - tile = tiles[i]; - - if (!tile.complete) { - tile.onload = L.Util.falseFn; - tile.onerror = L.Util.falseFn; - tile.src = L.Util.emptyImageUrl; - - tile.parentNode.removeChild(tile); - } - } - } -}); - - -/* - * Provides L.Map with convenient shortcuts for using browser geolocation features. - */ - -L.Map.include({ - _defaultLocateOptions: { - watch: false, - setView: false, - maxZoom: Infinity, - timeout: 10000, - maximumAge: 0, - enableHighAccuracy: false - }, - - locate: function (/*Object*/ options) { - - options = this._locateOptions = L.extend(this._defaultLocateOptions, options); - - if (!navigator.geolocation) { - this._handleGeolocationError({ - code: 0, - message: 'Geolocation not supported.' - }); - return this; - } - - var onResponse = L.bind(this._handleGeolocationResponse, this), - onError = L.bind(this._handleGeolocationError, this); - - if (options.watch) { - this._locationWatchId = - navigator.geolocation.watchPosition(onResponse, onError, options); - } else { - navigator.geolocation.getCurrentPosition(onResponse, onError, options); - } - return this; - }, - - stopLocate: function () { - if (navigator.geolocation) { - navigator.geolocation.clearWatch(this._locationWatchId); - } - if (this._locateOptions) { - this._locateOptions.setView = false; - } - return this; - }, - - _handleGeolocationError: function (error) { - var c = error.code, - message = error.message || - (c === 1 ? 'permission denied' : - (c === 2 ? 'position unavailable' : 'timeout')); - - if (this._locateOptions.setView && !this._loaded) { - this.fitWorld(); - } - - this.fire('locationerror', { - code: c, - message: 'Geolocation error: ' + message + '.' - }); - }, - - _handleGeolocationResponse: function (pos) { - var lat = pos.coords.latitude, - lng = pos.coords.longitude, - latlng = new L.LatLng(lat, lng), - - latAccuracy = 180 * pos.coords.accuracy / 40075017, - lngAccuracy = latAccuracy / Math.cos(L.LatLng.DEG_TO_RAD * lat), - - bounds = L.latLngBounds( - [lat - latAccuracy, lng - lngAccuracy], - [lat + latAccuracy, lng + lngAccuracy]), - - options = this._locateOptions; - - if (options.setView) { - var zoom = Math.min(this.getBoundsZoom(bounds), options.maxZoom); - this.setView(latlng, zoom); - } - - var data = { - latlng: latlng, - bounds: bounds, - timestamp: pos.timestamp - }; - - for (var i in pos.coords) { - if (typeof pos.coords[i] === 'number') { - data[i] = pos.coords[i]; - } - } - - this.fire('locationfound', data); - } -}); - - -}(window, document)); \ No newline at end of file