Skip to content

Latest commit

 

History

History
executable file
·
856 lines (681 loc) · 26.3 KB

README.md

File metadata and controls

executable file
·
856 lines (681 loc) · 26.3 KB

济·忆 | TJ-Memory 🚣‍♀️🚣🚣‍♂️

🎥 介绍视频 | Intro Video

📦tj-memory-0.0.1-SNAPSHOT


背景介绍

疫情突然而至,打破了原本的学校生活,也打破了我们记录生活的方式

从blog到vlog,人们记录生活的方式在不断丰富

济·忆 —— 尝试从全新视角发现身边的美

📸 用全景记录生活


主要界面

主界面

image-20200618085228615

同济日历

image-20200618085303035

同济印记

image-20200618085345999

注册 / 登陆

image-20200618085048717

全景漫游

image-20200618085506054

image-20200618085632781

image-20200618085648694

loading

image-20200618085125606

404 Not Found

image-20200618085403934

分享

image-20200618090119324


功能介绍

  • 同济日历
    • 查看某年某月日历信息
    • 单击鼠标切换日期
    • 双击日期上传图片并添加回忆
  • 全景拼接:可以上传1~3张照片,后台会进行全景拼接(全景拼接算法需要满足图片本身位于同一平面等,可能不能完美拼接)
  • 同济印记:以百叶窗的形式展示所有回忆
  • 全景漫游
    • 拖动鼠标进行场景漫游
    • 双击添加回忆
    • 单击鼠标右键切换文字标签和图标标签
    • 鼠标滚轮进行场景缩放
    • 适配手机端触摸事件
  • 界面导航
    • 其中完善了「同济商城」并作为子系统
  • 注册 / 登陆
  • 媒体信息(github,邮箱,视频,文档)
  • 分享
    • qq
    • 微信(需要用浏览器扫码)
    • qq空间
    • 微博
  • 特效
    • svg波浪特效
    • 动态加载流体特效
  • CMS动态修改网站内容(不需要再次部署)
  • 数据持久化
  • 远程部署:tj-memory.doublez.site
  • 适配PC 平板 手机等不同平台

开发环境

  • 操作系统

    • 开发环境:macOS Catalina 10.15.4
    • 部署环境:Ubuntu 16.04.6 LTS
  • IDE

    • Visual Studio Code 1.45.1
    • IntelliJ IDEA 2019.3.2
    • PyCharm 2019.3.2
  • 开发语言

    • HTML5
    • CSS3
    • JavaScript
    • Java
    • python
  • **数据库 **: mysql 8.0.20

  • 框架技术

    • 前端:

      • 原生HTML + CSS + JavaScript
      • three.js

      ​ 仅有一处使用Vue框架,主要为了全方面学习

    • 后端:SpringBoot + Maven + Mysql + MyBatis

  • 主要依赖: opencv-python

    其中涉及到版本问题,环境需要进行如下配置

    pip uninstall opencv-python
    pip install opencv-python==3.4.2.16 -i "https://pypi.doubanio.com/simple/"
    pip install opencv-contrib-python==3.4.2.16 -i "https://pypi.doubanio.com/simple/"
  • CMS: Contentful

    部分图片资源来自网络,侵删


关于作者


核心实现

前端

Contentful

function getEntries_by_ContentType(client, content_type){
     return new Promise(resolve => {
         client.getEntries({
             content_type: content_type
           })
             .then((response) => {
                 resolve(response);
             })
             .catch(console.error);
     });
 }

let response = await getEntries_by_ContentType(client, 'tjMemoryHero');
let heroObj = response.items[0].fields;
this.styleObject.backgroundImage = "url(" + "https:" + heroObj.bg.fields.file.url + ")";

日历

日历核心部分全部由JavaScript进行动态生成

<div id="CalendarGroup" class="CalendarGroup">
  <div class="awesome-calendar">
    <div class="header" style="grid-area: header;">
      <div class="left-part">
        <div id="year-left" class="nev">&lt;&lt;</div>
        <div id="month-left" class="nev">&lt;</div>
      </div>
      <h1 id="current-year-month">November<br />2017</h1>
      <div class="right-part">
        <div id="month-right" class="nev">&gt;</div>
        <div id="year-right" class="nev">&gt;&gt;</div>
      </div>
    </div>
    <div id="weeks" class="weeks" style="grid-area: weeks;">
      <!-- <div class="week" style="grid-area: Monday;">Monday</div> -->
    </div>
    <div id="days" class="days" style="grid-area: days;">
    </div>
  </div>
</div>
function printDays(year, month) {
  refresh();

  isExistArray = [];

  var nowMonthStartDay = new Date(year, month - 1, 1).getDay();   //当前月第一天是周几
  if(nowMonthStartDay == 0) {
    nowMonthStartDay = 7;
  }
  var numberOfDaysInMonth = new Date(year, month, 0).getDate();   //当前月有多少天
  now.innerHTML = " ";
  now.innerHTML = monthLetter[month - 1] + '<br />' + year;        //改变title的内容

  if(sessionStorage.getItem("isLogin") === "true"){
    startLoader();	// 启动流体loader
    let calendarInfo = {
      "username": sessionStorage.getItem("username"),
      "picyear": changeYear,
      "picmonth": changeMonth + 1,
    };
    connectToBackEnd(calendarInfo, "calendar")
      .then(result => {
      stopLoad();	// 停止流体loader
      obj = [];
      if(result['state'] === 'true'){
        obj = result["photos"];
        
        initHistory(result["photos"]);

        for(var i = 1, j = nowMonthStartDay; i <= numberOfDaysInMonth; i++, j++) {  //判断变色的日期
          var DayText = $("day" + j);
          var DayBgColor = DayText;
          if(isAwesome){
            DayText = $("day-text-" + j);
            DayBgColor = $("day-bgcolor-" + j);
          }

          let existItem = isExist(obj, changeYear, changeMonth+1, j);
          if(existItem != false){
            isExistArray.push(j.toString());
            $("day-description-" + j).innerHTML = existItem.description;

            setStyle(DayBgColor, {
              background: "url(" + existItem.imgbase.replace(/[\r\n]/g,"") + ")",
              backgroundSize: "cover",
              backgroundPosition: "center",
            });
          }

          DayText.innerHTML = "" + i;
          if(year == thisYear && month == thisMonth && i.toString() == dayNum) {   //当前日期一直是红色
            setStyle(DayBgColor, {
              background: '#E96D71'
            });
          }
          if(i.toString() == changeDay) {     //选中的符合要求的日期显示绿色
            DayBgColor.style = "background:#1abc9c";
            let freshDay = new Date(changeYear, changeMonth-1, changeDay);
            let weekNum = (freshDay.getDay() + 7 - 1) % 7;

            if(weekNum === 6) {
              $("week-bg").style.left = "calc(calc(calc(var(--button-width) + var(--week-padding))*" + weekNum + ") - calc(var(--week-padding)*0.5))";
            } else {
              $("week-bg").style.left = "calc(calc(var(--button-width) + var(--week-padding))*" + weekNum + ")";
            }
          }
        }
      } else {
        alert(result['msg'] + "加载同济日历,请刷新尝试");
      }
    })
      .catch(error => console.log(error));
  }

上传图片

myinput.onchange = function (){
  // TODO 用户选择好文件点击确定
  let imageType = /^image\//;

  if(this.files.length){
    var _description = prompt("请输入你的回忆描述");
    if(_description==null || _description==""){
      alert("输入的回忆不能为空");
      return;
    }
    event.target.parentElement.nextElementSibling.firstElementChild.innerHTML = _description;
    isExistArray.push(now.day.toString());

    /******* 多张图片处理逻辑 *********/
    let fileappidx = null;
    let total = this.files.length;
    counter = 0;

    for(var i=0; i<this.files.length; ++i){
      let file = this.files[i];
      fileappidx = file.type.substring(file.type.indexOf("/")+1);
      let reader = new FileReader();
      if (!imageType.test(file.type)) {
        alert("请选择图片, 该类型的文件不受支持!");
        return;
      }

      let imgbase64 = reader.result;

      //新建 FileReader 对象
      reader.onload = function(){
        counter ++;
        let _imgname = getIndexStr(now.year) + "-" +
            getIndexStr(now.month) + "-" +
            getIndexStr(now.day) + "." + fileappidx;

        let data_from_front = {
          username: sessionStorage.getItem("username"),
          picyear: now.year,
          picmonth: now.month,
          picday: now.day,
          description: _description,
          imgbase: this.result,
          imgurl: sessionStorage.getItem("username") + "/" + _imgname,
          index: counter,
          length: total,
        };

        let itemBg = event.target.previousElementSibling.firstElementChild;
        setStyle(itemBg, {
          background: "url(" + this.result + ")",
          backgroundSize: "cover",
          backgroundPosition: "center",
        });

        connectToBackEnd(data_from_front, "add_picture")
          .then(result => {
          if(result['state'] === 'true'){
            obj.push({
              description: data_from_front.description,
              picyear: data_from_front.picyear,
              picmonth: data_from_front.picmonth,
              picday: parseInt(data_from_front.picday),
              username: data_from_front.username,
              imgbase: this.result,
              picurl: data_from_front.imgurl
            });
          } else {
            alert(result['msg'] + "添加图片失败,请刷新尝试");
          }
        })
          .catch(error => console.log(error));
      };
      reader.readAsDataURL(file); //以base64方式读取文件
    }
    /******* END 多张图片处理逻辑 *********/
  }
};

全景漫游

(这里体量比较庞大,可以参看tpanorama.js)

opt = {
  container: 'panoramaConianer',//容器
  url: '../db/' + imgurl ,
  lables: _lables,
  widthSegments: 60,      //水平切段数
  heightSegments: 40,     //垂直切段数(值小粗糙速度快,值大精细速度慢)
  pRadius: 1000,          //全景球的半径,推荐使用默认值
  minFocalLength: 6,      //镜头最小拉近距离
  maxFocalLength: 100,    //镜头最大拉近距离
  sprite: 'label',         // label,icon
  onClick: (e) => {
    console.log(e.object.name);
  }
};
tp = new tpanorama(opt);
tpanorama.prototype = {
  constructor: this,
  def: {},
  render: function (opt) {
    this.def = extend(options, opt, true);
    document.getElementById(this.def.container).innerHTML = '';
    _lables = [];
    initContainer(this.def.container);
    initCamera();
    initRaycaster();
    makePanorama(this.def.pRadius, this.def.widthSegments, this.def.heightSegments, this.def.url);
    initRenderer();
    initLable(this.def.lables, this.def.sprite);

    _container.addEventListener('dblclick', mydblClickHandler, false);
    _container.addEventListener('touchstart', mydblTouchHandler, false);
    _container.addEventListener('mousedown', onRightClickHandler, false);
    document.oncontextmenu = function(e){
      e.preventDefault();
      return false;
    };
    _container.addEventListener('mousedown', onDocumentMouseDown, false);
    _container.addEventListener('mousemove', onDocumentMouseMove, false);
    _container.addEventListener('mouseup', onDocumentMouseUp, false);
    _container.addEventListener('touchstart', onDocumentTouchStart, false);
    _container.addEventListener('touchmove', onDocumentTouchMove, false);
    _container.addEventListener('touchend', onDocumentTouchEnd, false);

    _container.addEventListener('mousewheel', (e) => {
      onDocumentMouseWheel(e, this.def.minFocalLength, this.def.maxFocalLength);
    }, false);
    _container.addEventListener('DOMMouseScroll', (e) => {
      onDocumentMouseWheel(e, this.def.minFocalLength, this.def.maxFocalLength);
    }, false);
    _container.addEventListener('click', onDocumentMouseClick.bind(this), false);
    global.addEventListener('resize', onWindowResize, false);

    animate();
  }
};

后端

Controller

@Controller
@ComponentScan({"site.doublez.tjmemory.service"})
@MapperScan("site.doublez.tjmemory.dao")
public class CalendarController {

    @Resource
    private PythonConnectorService pythonConnectorService;

    @Resource
    private PhotoService photoService;

    @PostMapping("/calendar")
    @ResponseBody
    public Map<String, Object> History(@RequestBody Map<String,Object> map) throws ParseException {
        String username = map.get("username").toString();
        int picyear = Integer.parseInt(map.get("picyear").toString());
        int picmonth = Integer.parseInt(map.get("picmonth").toString());
        Map<String, Object> result_map = new HashMap<>();
        try {
            ArrayList<Photo> photoArrayList = photoService.select_photos_by_year_month(new DayInfo(username, picyear, picmonth));
            result_map.put("photos", photoArrayList);
            result_map.put("state", "true");
        } catch (Exception e){
            e.printStackTrace();
            result_map.put("state", "false");
            result_map.put("msg", "数据库错误");
        }

        return result_map;
    }
}

Dao

@Mapper
public interface PhotoDao {
    void insert_photo(Photo photo);

    ArrayList<Photo> select_photos_by_year_month(DayInfo dayInfo);

    Photo select_photo_by_date(DayInfo dayInfo);
}

Entity

@Data
public class Photo {
    private int id;
    private String username;
    private int picyear;
    private int picmonth;
    private int picday;
    private String imgbase;
    private String description;
    private String picurl;
}

Service

@Service("labelService")
@ComponentScan({"site.doublez.tjmemory.dao"})
public class LabelService {
    @Resource
    private LabelDao labelDao;

    public void insert_label(Label label){
        labelDao.insert_label(label);
    }

    public ArrayList<Label> select_label(String username, int picyear, int picmonth, int picday){
        return labelDao.select_label(new DayInfo(username, picyear, picmonth, picday));
    }
}

全景拼接

def full_view(filename1, filename2, dirname):
    leftgray, rightgray = cv2.imread(dirname + filename1), cv2.imread(dirname + filename2)

    hessian = 400
    surf = cv2.xfeatures2d.SURF_create(hessian)  # 将Hessian Threshold设置为400,阈值越大能检测的特征就越少
    kp1, des1 = surf.detectAndCompute(leftgray, None)  # 查找关键点和描述符
    kp2, des2 = surf.detectAndCompute(rightgray, None)

    FLANN_INDEX_KDTREE = 0  # 建立FLANN匹配器的参数
    indexParams = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)  # 配置索引,密度树的数量为5
    searchParams = dict(checks=50)  # 指定递归次数
    # FlannBasedMatcher:是目前最快的特征匹配算法(最近邻搜索)
    flann = cv2.FlannBasedMatcher(indexParams, searchParams)  # 建立匹配器
    matches = flann.knnMatch(des1, des2, k=2)  # 得出匹配的关键点

    good = []
    # 提取优秀的特征点
    for m, n in matches:
        # if m.distance < 0.7 * n.distance:  # 如果第一个邻近距离比第二个邻近距离的0.7倍小,则保留
        if m.distance < 0.3 * n.distance:
            good.append(m)
    src_pts = np.array([kp1[m.queryIdx].pt for m in good])  # 查询图像的特征描述子索引
    dst_pts = np.array([kp2[m.trainIdx].pt for m in good])  # 训练(模板)图像的特征描述子索引
    H = cv2.findHomography(src_pts, dst_pts)  # 生成变换矩阵

    h, w = leftgray.shape[:2]
    h1, w1 = rightgray.shape[:2]
    shft = np.array([[1.0, 0, w], [0, 1.0, 0], [0, 0, 1.0]])
    M = np.dot(shft, H[0])  # 获取左边图像到右边图像的投影映射关系

    dst_corners = cv2.warpPerspective(leftgray, M, (w * 2, h))  # 透视变换,新图像可容纳完整的两幅图
    # cv2.imshow('before add right', dst_corners)
    # dst_corners[0:h, 0:w] = leftgray
    dst_corners[0:h, w:w + w1] = rightgray  # 将第二幅图放在右侧

    # 删除空白列
    sum_col = np.sum(np.sum(dst_corners, axis=0), axis=1)

    result_img = np.zeros(shape=(dst_corners.shape[0], 1, 3))

    for i in range(len(sum_col)):
        if sum_col[i] != 0:
            result_img = np.hstack([result_img, dst_corners[:, i:i + 1, :]])

    result_img = result_img[:, 1:]

    # cv2.imshow('dest', dst_corners)
    result_name = get_full_view_result_name(filename1, filename2)

    cv2.imwrite(dirname + result_name, result_img)

    cv2.waitKey()
    cv2.destroyAllWindows()

    return result_name

图片处理

图片编码

public String encode_img_to_base64(String imgurl){
  String basepath = "src/main/resources/static/db/";
  byte[] data = null;
  try{
    InputStream in = new FileInputStream(basepath + imgurl);
    data = new byte[in.available()];
    in.read(data);
    in.close();
  } catch (IOException e){
    e.printStackTrace();
  }

  BASE64Encoder encoder = new BASE64Encoder();
  return encoder.encode(data);
}

图片解码

public void saveImg(String baseImg, String imgurl){
  //正则表达式的筛选规则,为了获取图片的类型
  String rgex = "data:image/(.*?);base64";
  String type = getSubUtilSimple(baseImg, rgex);
  //去除base64图片的前缀
  baseImg = baseImg.replaceFirst("data:(.+?);base64,", "");
  byte[] b;
  byte[] bs;
  OutputStream os = null;

  //把图片转换成二进制
  b = Base64.decode(baseImg.replaceAll(" ", "+"));

  File imageFile = new File(imgurl);
  BASE64Decoder d = new BASE64Decoder();
  // 保存
  try {
    bs = d.decodeBuffer(Base64.encode(b));
    os = new FileOutputStream(imageFile);
    os.write(bs);
  } catch (IOException e) {
    e.printStackTrace();

  }finally {
    if (os != null) {
      try {
        os.close();
      } catch (IOException e) {
        e.getLocalizedMessage();
      }
    }
  }
}

图片压缩

Thumbnails.of(basepath + imgurl).scale(0.2).toFile(basepath + newimgurl);

数据库

使用MediumText字段进行存储

创建用户表

CREATE TABLE `TJ-Memory`.`User` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(45) NOT NULL,
  `password` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`id`));

照片相关sql

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="site.doublez.tjmemory.dao.PhotoDao">

    <insert id = "insert_photo" parameterType="site.doublez.tjmemory.entity.Photo">
        INSERT INTO
            Photo(ID,USERNAME,PICYEAR,PICMONTH,PICDAY,IMGBASE,DESCRIPTION,PICURL)
        VALUES
            (#{id},#{username},#{picyear},#{picmonth},#{picday},#{imgbase},#{description},#{picurl});
    </insert>

    <select id="select_photos_by_year_month" parameterType="site.doublez.tjmemory.entity.DayInfo" resultType="site.doublez.tjmemory.entity.Photo">
        SELECT *
        FROM Photo
        WHERE (USERNAME=#{username} AND PICYEAR=#{picyear} AND PICMONTH=#{picmonth});
    </select>

    <select id="select_photo_by_date" parameterType="site.doublez.tjmemory.entity.DayInfo" resultType="site.doublez.tjmemory.entity.Photo">
        SELECT *
        FROM Photo
        WHERE (USERNAME=#{username} AND PICYEAR=#{picyear} AND PICMONTH=#{picmonth} AND PICDAY=#{picday});
    </select>
</mapper>

项目结构
  • 后端

    .
    ├── HELP.md
    ├── README.md
    ├── TJ-Memory.iml
    ├── mvnw
    ├── mvnw.cmd
    ├── pom.xml
    └── src
        ├── main
        │   ├── java
        │   │   └── site
        │   │       └── doublez
        │   │           └── tjmemory
        │   │               ├── TjMemoryApplication.java
        │   │               ├── controller
        │   │               │   ├── CalendarController.java
        │   │               │   ├── HistoryController.java
        │   │               │   ├── IndexController.java
        │   │               │   ├── LoginController.java
        │   │               │   ├── MyErrorController.java
        │   │               │   └── PanoramaController.java
        │   │               ├── dao
        │   │               │   ├── LabelDao.java
        │   │               │   ├── PhotoDao.java
        │   │               │   └── UserDao.java
        │   │               ├── entity
        │   │               │   ├── CalendarInfo.java
        │   │               │   ├── CrossConfig.java
        │   │               │   ├── DayInfo.java
        │   │               │   ├── Label.java
        │   │               │   ├── Photo.java
        │   │               │   └── User.java
        │   │               └── service
        │   │                   ├── LabelService.java
        │   │                   ├── PhotoService.java
        │   │                   ├── PythonConnectorService.java
        │   │                   └── UserService.java
        │   └── resources
        │       ├── application.yml
        │       ├── mapper
        │       │   ├── LabelMapper.xml
        │       │   ├── PhotoMapper.xml
        │       │   └── UserMapper.xml
        │       ├── static
        │       └── templates
        └── test
            └── java
                └── site
                    └── doublez
                        └── tjmemory
                            └── TjMemoryApplicationTests.java
    
  • 前端

    .
    ├── static
    │   ├── css
    │   │   ├── calendar.css
    │   │   ├── error.css
    │   │   ├── footer.css
    │   │   ├── global.css
    │   │   ├── hero.css
    │   │   ├── history.css
    │   │   ├── loader.css
    │   │   ├── login.css
    │   │   ├── myshare.css
    │   │   ├── navigator.css
    │   │   ├── panorama.css
    │   │   ├── sectioncaption.css
    │   │   ├── socialmedia.css
    │   │   └── wavesection.css
    │   ├── db
    │   ├── fonts
    │   │   ├── iconfont.eot
    │   │   ├── iconfont.svg
    │   │   ├── iconfont.ttf
    │   │   └── iconfont.woff
    │   ├── img
    │   │   └── favicon.ico
    │   ├── js
    │   │   ├── awesome-calendar.js
    │   │   ├── calendar.js
    │   │   ├── contentful.api.js
    │   │   ├── error.js
    │   │   ├── hero.js
    │   │   ├── history.js
    │   │   ├── index.js
    │   │   ├── loader.js
    │   │   ├── login.js
    │   │   ├── myshare.js
    │   │   ├── navigator.js
    │   │   ├── panorama.js
    │   │   ├── socialmedia.js
    │   │   ├── util.js
    │   │   ├── waterripple.js
    │   │   └── wavesection.js
    │   ├── json
    │   │   ├── navigator.json
    │   │   ├── socialmedia.json
    │   │   ├── testcalendar.json
    │   │   └── tongji.json
    │   ├── lib
    │   │   ├── share.min.css
    │   │   ├── social-share.min.js
    │   │   ├── three.js
    │   │   ├── tpanorama.js
    │   │   └── vue.min.js
    │   └── python
    │       ├── panorama.py
    │       └── test.py
    └── templates
        ├── calendar.html
        ├── error.html
        ├── index.html
        ├── login.html
        └── panorama.html