mirror of
https://github.com/openvk/openvk
synced 2024-12-23 17:12:01 +03:00
Гео-метки
This commit is contained in:
parent
a2384cc231
commit
da863c25f3
14 changed files with 297 additions and 4 deletions
|
@ -246,5 +246,22 @@ class Post extends Postable
|
|||
$this->save();
|
||||
}
|
||||
|
||||
function getGeo(): ?object
|
||||
{
|
||||
if (!$this->getRecord()->geo) return NULL;
|
||||
|
||||
return (object) json_decode($this->getRecord()->geo, true, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
function getLat(): ?float
|
||||
{
|
||||
return (float) $this->getRecord()->geo_lat ?? NULL;
|
||||
}
|
||||
|
||||
function getLon(): ?float
|
||||
{
|
||||
return (float) $this->getRecord()->geo_lon ?? NULL;
|
||||
}
|
||||
|
||||
use Traits\TRichText;
|
||||
}
|
||||
|
|
10
Web/Models/sql/get-nearest-posts.tsql
Normal file
10
Web/Models/sql/get-nearest-posts.tsql
Normal file
|
@ -0,0 +1,10 @@
|
|||
SELECT *,
|
||||
SQRT(
|
||||
POW(69.1 * (? - geo_lat), 2) +
|
||||
POW(69.1 * (? - geo_lon) * COS(RADIANS(geo_lat)), 2)
|
||||
) AS distance
|
||||
FROM Posts
|
||||
WHERE id <> ?
|
||||
HAVING distance < 1 AND distance IS NOT NULL
|
||||
ORDER BY distance
|
||||
LIMIT 25;
|
|
@ -293,7 +293,16 @@ final class WallPresenter extends OpenVKPresenter
|
|||
}
|
||||
}
|
||||
|
||||
if(empty($this->postParam("text")) && !$photo && !$video && !$poll && !$note)
|
||||
$geo = NULL;
|
||||
|
||||
if (!is_null($this->postParam("geo")) && $this->postParam("geo") != "none") {
|
||||
$geo = json_decode($this->postParam("geo"), true, JSON_UNESCAPED_UNICODE);
|
||||
if (!$geo["lat"] || !$geo["lng"] || !$geo["name"]) {
|
||||
$this->flashFail("err", tr("error"), "Ошибка при прикреплении геометки");
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($this->postParam("text")) && !$photo && !$video && !$poll && !$note && !$geo)
|
||||
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
|
||||
|
||||
try {
|
||||
|
@ -305,6 +314,11 @@ final class WallPresenter extends OpenVKPresenter
|
|||
$post->setAnonymous($anon);
|
||||
$post->setFlags($flags);
|
||||
$post->setNsfw($this->postParam("nsfw") === "on");
|
||||
if ($geo) {
|
||||
$post->setGeo(json_encode($geo));
|
||||
$post->setGeo_Lat($geo["lat"]);
|
||||
$post->setGeo_Lon($geo["lng"]);
|
||||
}
|
||||
$post->save();
|
||||
} catch (\LengthException $ex) {
|
||||
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big"));
|
||||
|
@ -480,4 +494,49 @@ final class WallPresenter extends OpenVKPresenter
|
|||
# TODO localize message based on language and ?act=(un)pin
|
||||
$this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment"));
|
||||
}
|
||||
|
||||
function renderNearest(int $wall, int $post_id): void
|
||||
{
|
||||
if ($_SERVER["REQUEST_METHOD"] !== "POST") $this->notFound();
|
||||
$this->assertUserLoggedIn();
|
||||
|
||||
$post = $this->posts->getPostById($wall, $post_id);
|
||||
if(!$post)
|
||||
$this->notFound();
|
||||
|
||||
$lat = $post->getLat();
|
||||
$lon = $post->getLon();
|
||||
|
||||
if (!$lat || !$lon)
|
||||
$this->returnJson(["success" => false, "error" => "У поста не указана гео-метка"]);
|
||||
|
||||
$query = file_get_contents(__DIR__ . "/../Models/sql/get-nearest-posts.tsql");
|
||||
$_posts = DatabaseConnection::i()->getContext()->query($query, $lat, $lon, $post->getId())->fetchAll();
|
||||
$posts = [];
|
||||
foreach ($_posts as $post) {
|
||||
$distance = $post["distance"];
|
||||
$post = (new Posts)->get($post["id"]);
|
||||
if (!$post || $post->isDeleted()) continue;
|
||||
|
||||
$owner = $post->getOwner();
|
||||
|
||||
$preview = mb_substr($post->getText(), 0, 50) . (strlen($post->getText()) > 50 ? "..." : "");
|
||||
$posts[] = [
|
||||
"preview" => strlen($preview) > 0 ? $preview : "(нет текста)",
|
||||
"url" => "/wall" . $post->getPrettyId(),
|
||||
"time" => $post->getPublicationTime()->html(),
|
||||
"owner" => [
|
||||
"url" => $owner->getURL(),
|
||||
"avatar_url" => $owner->getAvatarURL(),
|
||||
"name" => $owner->getCanonicalName(),
|
||||
"verified" => $owner->isVerified(),
|
||||
"writes" => ($owner instanceof User) ? ($owner->isFemale() ? tr("post_writes_f") : tr("post_writes_m")) : tr("post_writes_m")
|
||||
],
|
||||
"geo" => $post->getGeo(),
|
||||
"distance" => $distance
|
||||
];
|
||||
}
|
||||
|
||||
$this->returnJson(["success" => true, "posts" => $posts, "need_count" => count($posts) === 25]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
|
||||
<div n:class="postFeedWrapper, $thisUser->hasMicroblogEnabled() ? postFeedWrapperMicroblog">
|
||||
{include "../components/textArea.xml", route => "/wall" . $thisUser->getId() . "/makePost", graffiti => true, polls => true, notes => true}
|
||||
{include "../components/textArea.xml", route => "/wall" . $thisUser->getId() . "/makePost", graffiti => true, polls => true, notes => true, geo => true}
|
||||
</div>
|
||||
|
||||
{foreach $posts as $post}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div class="content_divider">
|
||||
<div>
|
||||
<div n:if="$canPost" class="content_subtitle">
|
||||
{include "../components/textArea.xml", route => "/wall$owner/makePost"}
|
||||
{include "../components/textArea.xml", route => "/wall$owner/makePost", geo => true}
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
|
|
@ -73,6 +73,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div n:if="$post->getGeo()" style="padding: 4px;">
|
||||
<div style="border-bottom: #ECECEC solid 1px;" />
|
||||
<div style="cursor: pointer; padding: 4px;" onclick="javascript:openGeo({$post->getGeo()}, {$post->getOwner()->getId()}, {$post->getVirtualId()})"><b>Геометка</b>: {$post->getGeo()->name}</div>
|
||||
</div>
|
||||
<div n:if="$post->isAd()" style="color:grey;">
|
||||
<br/>
|
||||
! Этот пост был размещён за взятку.
|
||||
|
|
|
@ -67,6 +67,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div n:if="$post->getGeo()" style="padding: 4px;">
|
||||
<div style="border-bottom: #ECECEC solid 1px;" />
|
||||
<div style="cursor: pointer; padding: 4px;" onclick="javascript:openGeo({$post->getGeo()}, {$post->getOwner()->getId()}, {$post->getVirtualId()})"><b>Геометка</b>: {$post->getGeo()->name}</div>
|
||||
</div>
|
||||
<div n:if="$post->isAd()" style="color:grey;">
|
||||
<br/>
|
||||
! Этот пост был размещён за взятку.
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
</div>
|
||||
<div class="post-has-note">
|
||||
|
||||
</div>
|
||||
<div class="post-has-geo">
|
||||
|
||||
</div>
|
||||
<div n:if="$postOpts ?? true" class="post-opts">
|
||||
{var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']}
|
||||
|
@ -58,6 +61,7 @@
|
|||
<input n:if="!OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']" type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display:none;" />
|
||||
<input type="hidden" name="poll" value="none" />
|
||||
<input type="hidden" id="note" name="note" value="none" />
|
||||
<input type="hidden" id="geo" name="geo" value="none" />
|
||||
<input type="hidden" name="type" value="1" />
|
||||
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||
<br/>
|
||||
|
@ -91,6 +95,10 @@
|
|||
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/office-chart-bar-stacked.png" />
|
||||
{_poll}
|
||||
</a>
|
||||
<a n:if="$geo ?? false" href="javascript:initGeo({$textAreaId})">
|
||||
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/apps/amarok.png" />
|
||||
Гео-метка
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -113,3 +121,10 @@
|
|||
{script "js/vnd_literallycanvas.js"}
|
||||
{css "js/node_modules/literallycanvas/lib/css/literallycanvas.css"}
|
||||
{/if}
|
||||
|
||||
{if $geo}
|
||||
{script "js/node_modules/leaflet/dist/leaflet.js"}
|
||||
{css "js/node_modules/leaflet/dist/leaflet.css"}
|
||||
{script "js/node_modules/leaflet-control-geocoder/dist/Control.Geocoder.js"}
|
||||
{css "js/node_modules/leaflet-control-geocoder/dist/Control.Geocoder.css"}
|
||||
{/if}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<div n:if="$canPost" class="content_subtitle">
|
||||
{include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true, polls => true, notes => true}
|
||||
{include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true, polls => true, notes => true, geo => true}
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
|
|
@ -137,6 +137,8 @@ routes:
|
|||
handler: "Wall->delete"
|
||||
- url: "/wall{num}_{num}/pin"
|
||||
handler: "Wall->pin"
|
||||
- url: "/wall{num}_{num}/nearest"
|
||||
handler: "Wall->nearest"
|
||||
- url: "/blob_{text}/{?path}.{text}"
|
||||
handler: "Blob->file"
|
||||
placeholders:
|
||||
|
|
|
@ -263,3 +263,162 @@ async function showArticle(note_id) {
|
|||
u("body").removeClass("dimmed");
|
||||
u("body").addClass("article");
|
||||
}
|
||||
|
||||
async function initGeo(tid) {
|
||||
MessageBox("Прикрепить геометку", "<div id=\"osm-map\"></div>", ["Прикрепить", "Отмена"], [(function () {
|
||||
let marker = {
|
||||
lat: currentMarker._latlng.lat,
|
||||
lng: currentMarker._latlng.lng,
|
||||
name: currentMarker._popup._content
|
||||
};
|
||||
$(`#post-buttons${tid} #geo`).val(JSON.stringify(marker));
|
||||
$(`#post-buttons${tid} .post-has-geo`).text(`Геометка: ${marker.name}`);
|
||||
$(`#post-buttons${tid} .post-has-geo`).show();
|
||||
}), Function.noop]);
|
||||
|
||||
const element = document.getElementById('osm-map');
|
||||
element.style = 'height: 600px;';
|
||||
|
||||
let markerLayers = L.layerGroup();
|
||||
|
||||
let map = L.map(element, {
|
||||
center: [55.322978, 38.673362],
|
||||
zoom: 10,
|
||||
attributionControl: false,
|
||||
width: 800
|
||||
});
|
||||
let currentMarker = null;
|
||||
markerLayers.addTo(map);
|
||||
|
||||
map.on('click', (e) => {
|
||||
let lat = e.latlng.lat;
|
||||
let lng = e.latlng.lng;
|
||||
|
||||
if (currentMarker) map.removeLayer(currentMarker);
|
||||
|
||||
$.get({
|
||||
url: `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=jsonv2`,
|
||||
success: (response) => {
|
||||
markerLayers.clearLayers();
|
||||
|
||||
currentMarker = L.marker([lat, lng]).addTo(map);
|
||||
currentMarker.bindPopup(response?.name ?? response?.display_name).openPopup();
|
||||
markerLayers.addLayer(currentMarker);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const geocoderControl = L.Control.geocoder({
|
||||
defaultMarkGeocode: false,
|
||||
}).addTo(map);
|
||||
|
||||
geocoderControl.on('markgeocode', function (e) {
|
||||
console.log(e);
|
||||
let lat = e.geocode.properties.lat;
|
||||
let lng = e.geocode.properties.lon;
|
||||
let name = (e.geocode.properties?.name ?? e.geocode.properties.display_name);
|
||||
|
||||
if (currentMarker) map.removeLayer(currentMarker);
|
||||
|
||||
currentMarker = L.marker([lat, lng]).addTo(map);
|
||||
currentMarker.bindPopup(name).openPopup();
|
||||
|
||||
console.log("Широта: " + lat + ", Долгота: " + lng);
|
||||
console.log("Название места: " + name);
|
||||
|
||||
let marker = {
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
name: name
|
||||
};
|
||||
map.setView([lat, lng], 15);
|
||||
});
|
||||
|
||||
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
$(".ovk-diag-cont").width('50%');
|
||||
setTimeout(function(){ map.invalidateSize()}, 100);
|
||||
}
|
||||
|
||||
function openGeo(data, owner_id, virtual_id) {
|
||||
MessageBox("Геометка", "<div id=\"osm-map\"></div>", ["Ближайшие посты", "OK"], [(function () {
|
||||
getNearPosts(owner_id, virtual_id);
|
||||
}), Function.noop]);
|
||||
|
||||
let element = document.getElementById('osm-map');
|
||||
element.style = 'height: 600px;';
|
||||
|
||||
let map = L.map(element, {attributionControl: false});
|
||||
let target = L.latLng(data.lat, data.lng);
|
||||
map.setView(target, 15);
|
||||
let marker = L.marker(target).addTo(map);
|
||||
marker.bindPopup(data.name).openPopup();
|
||||
|
||||
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
$(".ovk-diag-cont").width('50%');
|
||||
setTimeout(function(){ map.invalidateSize()}, 100);
|
||||
}
|
||||
|
||||
function getNearPosts(owner_id, virtual_id) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `/wall${owner_id}_${virtual_id}/nearest`,
|
||||
success: (response) => {
|
||||
if (response.success) {
|
||||
openNearPosts(response);
|
||||
} else {
|
||||
MessageBox("Ошибка", "Произошла ошибка в ходе запроса:" + (response?.error ?? "Неизвестная ошибка"), ["OK"], [Function.noop]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openNearPosts(posts) {
|
||||
console.log(posts);
|
||||
let MsgBody = "";
|
||||
|
||||
posts.posts.forEach((post) => {
|
||||
MsgBody += `<a style="color: inherit; display: block; margin-bottom: 8px;" href="${post.url}" target="_blank">
|
||||
<table border="0" style="font-size: 11px;" class="post">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="54" valign="top">
|
||||
<a href="${post.owner.url}">
|
||||
<img src="${post.owner.avatar_url}" width="50">
|
||||
</a>
|
||||
</td>
|
||||
<td width="100%" valign="top">
|
||||
<div class="post-author">
|
||||
<a href="${post.owner.url}"><b>${post.owner.name}</b></a>
|
||||
${post.owner.verified ? `<img class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png">` : ""}
|
||||
${post.owner.writes}
|
||||
<br>
|
||||
<a href="${post.url}" class="date">
|
||||
${post.time}
|
||||
</a>
|
||||
</div>
|
||||
<div class="post-content" id="2_28">
|
||||
<div class="text" id="text2_28">
|
||||
${post.preview}
|
||||
</div>
|
||||
<div style="padding: 4px;">
|
||||
<div style="border-bottom: #ECECEC solid 1px;"></div>
|
||||
<div style="cursor: pointer; padding: 4px;"><b>Геометка</b>: ${post.geo.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</a>`;
|
||||
});
|
||||
|
||||
if (posts.need_count) MsgBody += "<br/><br/><center style='color: grey;'>Показаны первые 25 постов</center>"
|
||||
|
||||
MessageBox("Ближайшие посты", MsgBody, ["OK"], [Function.noop]);
|
||||
}
|
|
@ -6,6 +6,8 @@
|
|||
"jquery": "^3.0.0",
|
||||
"knockout": "^3.5.1",
|
||||
"ky": "^0.19.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-control-geocoder": "^2.4.0",
|
||||
"literallycanvas": "^0.5.2",
|
||||
"monaco-editor": "^0.20.0",
|
||||
"msgpack-lite": "^0.1.26",
|
||||
|
|
|
@ -173,6 +173,18 @@ ky@^0.19.0:
|
|||
resolved "https://registry.yarnpkg.com/ky/-/ky-0.19.0.tgz#d6ad117e89efe2d85a1c2e91462d48ca1cda1f7a"
|
||||
integrity sha512-RkDgbg5ahMv1MjHfJI2WJA2+Qbxq0iNSLWhreYiCHeHry9Q12sedCnP5KYGPt7sydDvsyH+8UcG6Kanq5mpsyw==
|
||||
|
||||
leaflet-control-geocoder@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet-control-geocoder/-/leaflet-control-geocoder-2.4.0.tgz#f6c00ae00b53d2ac5908e874a9aefd414f615f22"
|
||||
integrity sha512-b2QlxuFd40uIDbnoUI3U9fzfnB4yKUYlmsXjquJ2d2YjoJqnyVYcIJeErAVv3kPvX3nI0gzvBq1XHMgSVFrGkQ==
|
||||
optionalDependencies:
|
||||
open-location-code "^1.0.0"
|
||||
|
||||
leaflet@^1.9.4:
|
||||
version "1.9.4"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d"
|
||||
integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
|
||||
|
||||
literallycanvas@^0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/literallycanvas/-/literallycanvas-0.5.2.tgz#7d4800a8d9c4b38a593e91695d52466689586abd"
|
||||
|
@ -225,6 +237,11 @@ object-assign@^4.1.0, object-assign@^4.1.1:
|
|||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||
|
||||
open-location-code@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/open-location-code/-/open-location-code-1.0.3.tgz#5ea1a34ee5221c6cafa04392e1bd906fd7488f7e"
|
||||
integrity sha512-DBm14BSn40Ee241n80zIFXIT6+y8Tb0I+jTdosLJ8Sidvr2qONvymwqymVbHV2nS+1gkDZ5eTNpnOIVV0Kn2fw==
|
||||
|
||||
plotly.js-dist@^1.52.3:
|
||||
version "1.52.3"
|
||||
resolved "https://registry.yarnpkg.com/plotly.js-dist/-/plotly.js-dist-1.52.3.tgz#4c16c6da6adab6cdba169087b5005bdddbf10834"
|
||||
|
|
4
install/sqls/00038-posts-geo.sql
Normal file
4
install/sqls/00038-posts-geo.sql
Normal file
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE `Posts`
|
||||
ADD `geo` LONGTEXT NULL DEFAULT NULL AFTER `deleted`,
|
||||
ADD `geo_lat` DECIMAL(10, 8) NULL DEFAULT NULL AFTER `geo`,
|
||||
ADD `geo_lon` DECIMAL(10, 8) NULL DEFAULT NULL AFTER `geo_lat`;
|
Loading…
Reference in a new issue